pub struct WorkloadConfig {Show 13 fields
pub num_workers: usize,
pub affinity: AffinityIntent,
pub work_type: WorkType,
pub sched_policy: SchedPolicy,
pub mem_policy: MemPolicy,
pub mpol_flags: MpolFlags,
pub nice: Option<i32>,
pub clone_mode: CloneMode,
pub comm: Option<Cow<'static, str>>,
pub uid: Option<u32>,
pub gid: Option<u32>,
pub numa_node: Option<u32>,
pub composed: Vec<WorkSpec>,
}Expand description
Configuration for spawning a group of worker processes.
Fields§
§num_workers: usizeNumber of worker processes to fork. Concrete usize (not
Option): WorkloadConfig is the spawn-time configuration
passed to WorkloadHandle::spawn, by which point the worker
count must be known. The Option-to-usize coalescing happens
upstream at resolve_num_workers, which
reads crate::workload::WorkSpec::num_workers
(Option<usize> — None falls back to
Ctx::workers_per_cgroup) and produces the resolved value
passed to Self::for_scenario_engine. The type asymmetry
is deliberate: WorkSpec is the user-facing declarative
spec where None means “inherit the cgroup-level default”,
WorkloadConfig is the spawn-time concrete config where
usize is the only sensible type.
affinity: AffinityIntentPer-worker affinity intent. Resolved at spawn time via the
same gate as composed entries (see Self::composed):
AffinityIntent::Inherit (resolved to
ResolvedAffinity::None),
AffinityIntent::Exact (resolved to
ResolvedAffinity::Fixed), and
AffinityIntent::RandomSubset (resolved to
ResolvedAffinity::Random — sampling deferred per-worker
at spawn time) are accepted at WorkloadHandle::spawn.
Topology-aware variants (SingleCpu, LlcAligned,
CrossCgroup, SmtSiblingPair) require scenario context
and are rejected with an actionable diagnostic.
Type-unified with WorkSpec::affinity so a test author
writes the same affinity expression at the top level and
inside composed entries.
work_type: WorkTypeWhat each worker does.
sched_policy: SchedPolicyLinux scheduling policy.
mem_policy: MemPolicyNUMA memory placement policy.
mpol_flags: MpolFlagsOptional mode flags for set_mempolicy(2).
nice: Option<i32>Per-worker nice value applied via setpriority(2) after
fork, before the work loop. Range -20..=19 per MIN_NICE
/ MAX_NICE in kernel/sys.c’s setpriority syscall;
values outside this window are clamped kernel-side. None
(the default) skips the syscall entirely so the worker
inherits the parent’s nice value; Some(n) invokes
setpriority(PRIO_PROCESS, 0, n) unconditionally — a user
who wants the worker to land on nice 0 regardless of the
parent’s nice (or a cgroup-level default stored at
CgroupDef::default_nice)
writes Some(0), distinct from None.
Values below the calling task’s current nice require
CAP_SYS_NICE (the kernel’s can_nice check fires on
niceval < task_nice(p), not only on negatives — the
set_one_prio gate at kernel/sys.c returns EACCES to
unprivileged callers when is_nice_reduction rejects the
requested value). With Some(0) on a parent at nice=5,
setpriority returns EACCES without the capability.
None (inherit) is always safe. Failures are logged once
via stderr and do not abort the worker — the
scheduling-policy and affinity sites use the same idiom.
clone_mode: CloneModeHow to create each worker. Defaults to CloneMode::Fork.
comm: Option<Cow<'static, str>>Worker process name set via prctl(PR_SET_NAME) after fork.
The setter rejects > 15 bytes (TASK_COMM_LEN-1) at
construction so the operator sees the cap at the call site
instead of debugging a truncated comm. None inherits the
binary name. Mirrors WorkSpec::comm so the primary group
exposes the same scheduler-matcher knob composed entries
already do.
uid: Option<u32>Effective UID set via setresuid(uid, uid, uid) after fork.
None inherits the parent’s euid. Mirrors WorkSpec::uid.
gid: Option<u32>Effective GID set via setresgid(gid, gid, gid) after fork.
None inherits the parent’s egid. Mirrors WorkSpec::gid.
numa_node: Option<u32>Restrict worker affinity to the CPUs of this NUMA node.
Applied via sched_setaffinity after fork. Mirrors
WorkSpec::numa_node.
composed: Vec<WorkSpec>Secondary worker groups spawned alongside the primary group
described by the top-level fields. Each entry is a
WorkSpec with its own work_type, num_workers,
sched_policy, affinity, etc. Composed groups are spawned
in declaration order after the primary group; their workers
run concurrently with the primary’s for the lifetime of the
WorkloadHandle. The default (an empty vec) skips the
composed pass and behaves exactly as the pre-composition
spawn.
All groups share the same stop signal —
WorkloadHandle::stop_and_collect terminates primary plus
every composed group atomically. Per-group stop is not
supported.
Reports carry WorkerReport::group_idx = 0 for the primary
group and 1..=N for composed entries in declaration order.
§Worked example
Build a multi-group workload — primary SpinWait(2) plus
one PipeIo(2) composed group plus one YieldHeavy(1)
composed group — using either the replacing
composed setter or the appending
push_composed chain:
use ktstr::workload::{WorkSpec, WorkType, WorkloadConfig};
// Append style: each call adds one group to the existing list.
let cfg = WorkloadConfig::default()
.work_type(WorkType::SpinWait)
.workers(2)
.push_composed(
WorkSpec::default()
.work_type(WorkType::pipe_io(64))
.workers(2),
)
.push_composed(
WorkSpec::default()
.work_type(WorkType::YieldHeavy)
.workers(1),
);
assert_eq!(cfg.composed.len(), 2);
// Replace style: one call passes every composed group at once.
let cfg2 = WorkloadConfig::default()
.work_type(WorkType::SpinWait)
.workers(2)
.composed([
WorkSpec::default().work_type(WorkType::pipe_io(64)).workers(2),
WorkSpec::default().work_type(WorkType::YieldHeavy).workers(1),
]);
assert_eq!(cfg2.composed.len(), 2);§Resolution rules at spawn time
Composed WorkSpec entries must specify
WorkSpec::num_workers (Some(n)); the None default
resolved by the scenario engine via
Ctx::workers_per_cgroup is unreachable from
WorkloadHandle::spawn and is rejected with an actionable
diagnostic.
Composed WorkSpec::affinity accepts the no-context
variants AffinityIntent::Inherit (resolved to
ResolvedAffinity::None), AffinityIntent::Exact
(resolved to ResolvedAffinity::Fixed), and
AffinityIntent::RandomSubset (resolved to
ResolvedAffinity::Random — sampling deferred per-worker
at spawn time). The topology-aware variants (SingleCpu,
LlcAligned, CrossCgroup, SmtSiblingPair) are rejected
because spawn() has no access to the
crate::topology::TestTopology / cpuset state that the
scenario engine threads in.
Composed entries inherit the parent
WorkloadConfig::clone_mode — the dispatch path
(fork vs thread) is a workload-wide property, so
WorkSpec carries no clone_mode field of its own.
Composition is single-level — a WorkSpec inside
composed has no composed field of its own.
Implementations§
Source§impl WorkloadConfig
impl WorkloadConfig
Sourcepub fn validate(&self) -> Result<()>
pub fn validate(&self) -> Result<()>
Validate the config before spawn. Fails loud on invariants
that the worker-spawn path otherwise handles by silent
degradation — in particular mem_policy variants that
require a non-empty nodemask (Bind / Interleave / PreferredMany /
WeightedInterleave with an empty BTreeSet).
§Why a config-layer gate
apply_mempolicy_with_flags (called from the worker’s hot
path in BOTH forked-child and thread-mode contexts) currently
handles an empty node-set by logging to stderr and
returning — the worker silently proceeds with default kernel
placement instead of the requested NUMA binding. That
silent-skip is a silent-drop bug (the test reports success
while the actual workload ran with the wrong placement).
A hypothetical fix-it-in-the-worker design — libc::_exit(1)
on an empty node-set inside the worker — was rejected because
it is unsound for thread-mode workers: _exit invokes
exit_group(2) (verified at kernel/exit.c::do_group_exit →
zap_other_threads) which terminates EVERY thread in the
caller’s tgid. A thread-mode worker shares its tgid with the
test runner, so an inner _exit(1) would kill the runner.
Rejecting at the config layer keeps the failure visible as a
returnable Result BEFORE any worker context exists,
regardless of clone-mode dispatch, and avoids the exit_group
hazard entirely.
§What is validated
Two gates, in order:
num_workers > 0on the primary group and on every composedWorkSpecentry — zero workers emit noWorkerReports and downstream assertions would vacuously pass. Composed entries also route throughWorkloadHandle::spawn(viaGroupParams::from_composed) directly, bypassing the scenario-engine’sresolve_num_workersresolver, so the gate must live here to catchcomposed[i].num_workers=0before the spawn cascade forks anything.mem_policyon the primary group and on every composedWorkSpecentry.
Per-entry errors name the offending slot ("primary" or
"composed[N] (group_idx M)") so the test author can
locate the misconfigured group. Gate (1) runs first so the
more-fundamental “no workers” diagnostic surfaces before a
secondary mem_policy failure (which becomes moot when no
worker exists to bind).
§Scope
Validates mem_policy and num_workers > 0. Other field
invariants are validated at their own use sites:
workers_pct via WorkSpec::resolve_workers_pct,
WorkType payloads via per-variant constructors and
validate_workload_admission, AffinityIntent topology
rules at the scenario-engine resolve_affinity_for_cgroup
resolver. This method is the home for invariants that must
hold BEFORE any worker context (threads, forks, cgroups)
exists — mem_policy qualifies because of the silent-skip +
exit_group hazard noted above; num_workers == 0
qualifies because every downstream gate becomes
vacuous-pass. Future fields with the same
“must-fail-before-spawn” shape belong here too.
§Return type
Returns anyhow::Result (composite-layer convention used
by sibling composite validators
crate::test_support::entry::KtstrTestEntry::validate and
crate::test_support::entry::TopologyConstraints::validate
— they wrap leaf validators that return
Result<(), String> with slot-context). The leaf validator
MemPolicy::validate returns Result<(), String> to match
the leaf convention used by every per-spec validator in the
project.
Sourcepub fn affinity(self, a: AffinityIntent) -> Self
pub fn affinity(self, a: AffinityIntent) -> Self
Set the per-worker affinity intent.
At WorkloadHandle::spawn, AffinityIntent::Inherit,
AffinityIntent::Exact, and AffinityIntent::RandomSubset
are accepted; topology-aware variants (SingleCpu,
LlcAligned, CrossCgroup, SmtSiblingPair) require
scenario context and are rejected.
Idiomatic short form for an exact CPU set:
cfg.affinity(AffinityIntent::exact([0, 1])).
Sourcepub fn sched_policy(self, p: SchedPolicy) -> Self
pub fn sched_policy(self, p: SchedPolicy) -> Self
Set the Linux scheduling policy.
Sourcepub fn mem_policy(self, p: MemPolicy) -> Self
pub fn mem_policy(self, p: MemPolicy) -> Self
Set the NUMA memory placement policy.
Sourcepub fn mpol_flags(self, f: MpolFlags) -> Self
pub fn mpol_flags(self, f: MpolFlags) -> Self
Set the NUMA memory policy mode flags.
Sourcepub fn nice(self, n: i32) -> Self
pub fn nice(self, n: i32) -> Self
Set the per-worker nice value applied via setpriority(2).
Stores Some(n) on the config; the spawn pipeline calls
setpriority(PRIO_PROCESS, 0, n) unconditionally (including
n == 0). The “skip the syscall, inherit the parent’s nice”
state is the type-level default None — set the field via
..Default::default() (or leave the builder unchained) when
you want inherit semantics. Values below the calling task’s
current nice require CAP_SYS_NICE; see
WorkloadConfig::nice for the full can_nice rule.
Sourcepub fn clone_mode(self, m: CloneMode) -> Self
pub fn clone_mode(self, m: CloneMode) -> Self
Set the clone mode used when spawning each worker.
CloneMode::Fork (the default) preserves historical
behavior. See CloneMode for the full menu and dispatch
status.
Sourcepub fn comm(self, name: impl Into<Cow<'static, str>>) -> Self
pub fn comm(self, name: impl Into<Cow<'static, str>>) -> Self
Set the worker process name via prctl(PR_SET_NAME).
§Panics
Panics on programmer-error inputs — mirrors
crate::workload::WorkSpec::pcomm’s # Panics:
- Empty string.
- Interior NUL byte (prctl C-string truncation).
- More than 15 bytes (
TASK_COMM_LEN - 1cap).
See
validate_task_comm_string
for the centralized rationale; name.len() is the BYTE
length (UTF-8 multi-byte chars count as their byte width).
Sourcepub fn composed(self, specs: impl IntoIterator<Item = WorkSpec>) -> Self
pub fn composed(self, specs: impl IntoIterator<Item = WorkSpec>) -> Self
Replace the composed worker groups (replacing setter).
Pass an iterator of WorkSpec entries; the existing
composed vec is REPLACED with the supplied entries. Each
will be spawned as an independent group alongside the
primary described by the top-level fields. Pass an empty
iterator to clear any previously-set composed groups.
Use this when you have all groups in hand at once. To add
one group at a time to an existing list, use the appending
push_composed instead.
See Self::composed for the resolution rules applied to
each entry’s num_workers / affinity fields at spawn time.
Sourcepub fn push_composed(self, spec: WorkSpec) -> Self
pub fn push_composed(self, spec: WorkSpec) -> Self
Append a single composed worker group to the existing list (appending setter).
The supplied WorkSpec is PUSHED onto the existing
composed vec; previously-set groups are preserved.
Convenience for chained construction:
cfg.push_composed(a).push_composed(b) produces
composed: [a, b].
Use this when building the group list incrementally. To
replace the entire list in one call, use the replacing
composed instead.
Trait Implementations§
Source§impl Clone for WorkloadConfig
impl Clone for WorkloadConfig
Source§fn clone(&self) -> WorkloadConfig
fn clone(&self) -> WorkloadConfig
1.0.0 · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreSource§impl Debug for WorkloadConfig
impl Debug for WorkloadConfig
Source§impl Default for WorkloadConfig
impl Default for WorkloadConfig
Source§impl<'de> Deserialize<'de> for WorkloadConfig
impl<'de> Deserialize<'de> for WorkloadConfig
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
Source§impl PartialEq for WorkloadConfig
impl PartialEq for WorkloadConfig
Source§impl Serialize for WorkloadConfig
impl Serialize for WorkloadConfig
impl StructuralPartialEq for WorkloadConfig
Auto Trait Implementations§
impl Freeze for WorkloadConfig
impl RefUnwindSafe for WorkloadConfig
impl Send for WorkloadConfig
impl Sync for WorkloadConfig
impl Unpin for WorkloadConfig
impl UnwindSafe for WorkloadConfig
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
§impl<T> Instrument for T
impl<T> Instrument for T
§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more