1use std::{
13 collections::BTreeMap,
14 ffi::{CString, OsStr},
15 os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd},
16 ptr::NonNull,
17 sync::{
18 Arc,
19 atomic::{AtomicBool, Ordering},
20 },
21 time::{Duration, Instant},
22};
23
24pub use anyhow::Result;
25use anyhow::{Context, bail, ensure};
26use libbpf_rs::{
27 Btf, Link, MapCore, MapFlags, ObjectBuilder, OpenObject, ProgramType,
28 btf::{
29 BtfType, HasSize, ReferencesType,
30 types::{
31 Composite, Enum, Enum64, Func, FuncProto, Int, IntEncoding, MemberAttr, Ptr, Struct,
32 },
33 },
34};
35use log::{info, warn};
36pub use scx_rustsched_abi as config;
37pub use scx_rustsched_macros::Opts;
38pub use scx_utils::compat;
41use scx_utils::{UserExitInfo, libbpf_clap_opts::LibbpfOpts};
42
43#[path = "../../scx_rustsched/src/edq.rs"]
46pub mod edq;
47mod kfunc_protos;
48mod queues;
49pub mod tiers;
50
51pub use libbpf_rs::Object;
52pub use queues::{ArenaView, CidTableReader, QueueSampler, QueueStats};
53pub use tiers::Tiers;
54
55#[derive(Clone)]
58pub struct Loaded(Arc<Object>);
59
60impl std::ops::Deref for Loaded {
61 type Target = Object;
62
63 fn deref(&self) -> &Object {
64 &self.0
65 }
66}
67
68#[derive(Clone, Copy, Debug, Default)]
71pub struct OpStats {
72 pub calls: u64,
73 pub ns: u64,
74 pub misses: u64,
75}
76
77pub fn cgroup_ops() -> Result<Option<&'static str>> {
82 let btf = Btf::from_vmlinux().context("read running kernel BTF")?;
83 let ops = btf
84 .type_by_name::<Struct<'_>>("sched_ext_ops_cid")
85 .context("find sched_ext_ops_cid")?;
86 let has = |name: &str| {
87 Composite::from(ops)
88 .iter()
89 .any(|member| member.name == Some(OsStr::new(name)))
90 };
91 Ok(if has("cpuctl_init") {
92 Some("rust_ops")
93 } else if has("cgroup_init") {
94 Some("rust_ops_cgroup")
95 } else {
96 None
97 })
98}
99
100pub fn ops_member(
103 object_name: &str,
104 object: &[u8],
105 ops: &str,
106 name: &str,
107) -> Result<std::ops::Range<usize>> {
108 let btf = Btf::from_raw(object_name, object)?.context("read BPF object BTF")?;
109 let local = btf
110 .type_by_name::<Struct<'_>>(ops)
111 .with_context(|| format!("find the BPF object's {ops}"))?;
112 let (field, offset) = member(&btf, local, name)?;
113 let size = Int::try_from(field)
114 .map_err(|_| anyhow::anyhow!("{ops}.{name} is not an integer"))?
115 .size();
116 let start = offset as usize;
117 Ok(start..start + size)
118}
119
120pub fn missing_callbacks(
126 object_name: &str,
127 object: &[u8],
128 ops: &str,
129) -> Result<Vec<std::ops::Range<usize>>> {
130 let vmlinux = Btf::from_vmlinux().context("read running kernel BTF")?;
131 let kernel = vmlinux
132 .type_by_name::<Struct<'_>>("sched_ext_ops_cid")
133 .context("find sched_ext_ops_cid")?;
134 let kernel = Composite::from(kernel);
135 let kernel: Vec<_> = kernel.iter().filter_map(|member| member.name).collect();
136 let btf = Btf::from_raw(object_name, object)?.context("read BPF object BTF")?;
137 let local = btf
138 .type_by_name::<Struct<'_>>(ops)
139 .with_context(|| format!("find the BPF object's {ops}"))?;
140 Composite::from(local)
141 .iter()
142 .filter(|member| {
143 member.name.is_some_and(|name| {
144 let bytes = name.as_encoded_bytes();
145 (bytes.starts_with(b"cpuctl_") || bytes.starts_with(b"cgroup_"))
146 && !kernel.contains(&name)
147 })
148 })
149 .map(|member| match member.attr {
150 MemberAttr::Normal { offset } if offset % 8 == 0 => {
151 let start = offset as usize / 8;
152 Ok(start..start + 8)
153 }
154 _ => bail!("unexpected cgroup callback member"),
155 })
156 .collect()
157}
158
159pub fn enumerator(btf: &Btf<'_>, name: &str) -> Option<u64> {
161 btf.type_by_kind::<Enum<'_>>()
162 .find_map(|kind| {
163 kind.iter()
164 .find(|value| value.name == Some(OsStr::new(name)))
165 .map(|value| value.value as u64)
166 })
167 .or_else(|| {
168 btf.type_by_kind::<Enum64<'_>>().find_map(|kind| {
169 kind.iter()
170 .find(|value| value.name == Some(OsStr::new(name)))
171 .map(|value| value.value as u64)
172 })
173 })
174}
175
176pub fn sized_topology() -> Result<bool> {
181 let btf = Btf::from_vmlinux().context("read running kernel BTF")?;
182 let function = btf
183 .type_by_name::<Func<'_>>("scx_bpf_cid_topo")
184 .context("find scx_bpf_cid_topo")?;
185 let prototype = FuncProto::try_from(function.referenced_type())
186 .map_err(|_| anyhow::anyhow!("scx_bpf_cid_topo has no function prototype"))?;
187 let topology = btf
188 .type_by_name::<Struct<'_>>("scx_cid_topo")
189 .context("find scx_cid_topo")?;
190 match prototype.len() {
191 2 => {
192 ensure!(
193 matches!(topology.size(), 24 | 32),
194 "unexpected legacy CID topology size"
195 );
196 Ok(false)
197 }
198 3 => {
199 ensure!(
200 matches!(topology.size(), 24 | 32 | 40),
201 "unexpected sized CID topology size"
202 );
203 Ok(true)
204 }
205 count => bail!("unsupported scx_bpf_cid_topo argument count: {count}"),
206 }
207}
208
209pub fn member<'btf>(
210 btf: &'btf Btf<'_>,
211 structure: Struct<'btf>,
212 name: &str,
213) -> Result<(BtfType<'btf>, u64)> {
214 let composite = Composite::from(structure);
215 let member = composite
216 .iter()
217 .find(|member| member.name == Some(OsStr::new(name)))
218 .with_context(|| format!("kernel BTF is missing field {name}"))?;
219 let MemberAttr::Normal { offset } = member.attr else {
220 bail!("kernel field {name} is a bitfield");
221 };
222 ensure!(offset % 8 == 0, "kernel field {name} is not byte aligned");
223 let field = btf
224 .type_by_id::<BtfType<'_>>(member.ty)
225 .context("resolve field type")?
226 .skip_mods_and_typedefs();
227 Ok((field, u64::from(offset / 8)))
228}
229
230pub const POOL_PAGES: u32 = 512;
232
233pub fn configuration(dump_len: u32, runtime_options: u64) -> Result<[u64; config::WORDS]> {
238 let btf = Btf::from_vmlinux().context("read running kernel BTF")?;
239 let task = btf
240 .type_by_name::<Struct<'_>>("task_struct")
241 .context("find task_struct")?;
242 let (entity, entity_offset) = member(&btf, task, "scx")?;
243 let entity =
244 Struct::try_from(entity).map_err(|_| anyhow::anyhow!("task_struct.scx is not a struct"))?;
245 let mut values = [0; config::WORDS];
246 values[config::ARENA_PAGES] = u64::from(POOL_PAGES);
247 values[config::RUNTIME_OPTIONS] = runtime_options;
248 values[config::TICK_NS] = tick_ns();
249 for (index, name, size) in [
250 (config::SCX_SLICE, "slice", 8),
251 (config::SCX_FLAGS, "flags", 4),
252 (config::SCX_TID, "tid", 8),
253 (config::DSQ_VTIME, "dsq_vtime", 8),
254 ] {
255 let (field, offset) = member(&btf, entity, name)?;
256 let integer =
257 Int::try_from(field).map_err(|_| anyhow::anyhow!("scx.{name} is not an integer"))?;
258 ensure!(
259 integer.size() == size
260 && integer.bits as usize == size * 8
261 && integer.offset == 0
262 && matches!(integer.encoding, IntEncoding::None),
263 "unexpected type for scx.{name}"
264 );
265 let offset = entity_offset + offset;
266 ensure!(
267 offset % size as u64 == 0 && offset + size as u64 <= task.size() as u64,
268 "invalid task offset for scx.{name}"
269 );
270 values[index] = offset;
271 }
272 let (cpus, cpus_offset) = member(&btf, task, "cpus_ptr")?;
273 Ptr::try_from(cpus).map_err(|_| anyhow::anyhow!("task_struct.cpus_ptr is not a pointer"))?;
274 ensure!(
275 cpus_offset % 8 == 0 && cpus_offset + 8 <= task.size() as u64,
276 "invalid cpus_ptr offset"
277 );
278 values[config::CPUS_PTR] = cpus_offset;
279 let (migration, migration_offset) = member(&btf, task, "migration_disabled")?;
280 let migration = Int::try_from(migration)
281 .map_err(|_| anyhow::anyhow!("task_struct.migration_disabled is not an integer"))?;
282 ensure!(
283 migration.size() == 2
284 && migration.bits == 16
285 && migration.offset == 0
286 && matches!(migration.encoding, IntEncoding::None)
287 && migration_offset % 2 == 0
288 && migration_offset + 2 <= task.size() as u64,
289 "invalid migration_disabled field"
290 );
291 values[config::MIGRATION_DISABLED] = migration_offset;
292 let (flags, flags_offset) = member(&btf, task, "flags")?;
293 let flags =
294 Int::try_from(flags).map_err(|_| anyhow::anyhow!("task_struct.flags is not an integer"))?;
295 ensure!(
296 flags.size() == 4
297 && flags.bits == 32
298 && flags.offset == 0
299 && matches!(flags.encoding, IntEncoding::None)
300 && flags_offset % 4 == 0
301 && flags_offset + 4 <= task.size() as u64,
302 "invalid task flags field"
303 );
304 values[config::TASK_FLAGS] = flags_offset;
305 let (fair, fair_offset) = member(&btf, task, "se")?;
307 let fair =
308 Struct::try_from(fair).map_err(|_| anyhow::anyhow!("task_struct.se is not a struct"))?;
309 for (index, name, size) in [
310 (config::CUSTOM_SLICE, "custom_slice", 1),
311 (config::SE_SLICE, "slice", 8),
312 ] {
313 let (field, offset) = member(&btf, fair, name)?;
314 let integer =
315 Int::try_from(field).map_err(|_| anyhow::anyhow!("se.{name} is not an integer"))?;
316 ensure!(
317 integer.size() == size && integer.bits as usize == size * 8 && integer.offset == 0,
318 "unexpected type for se.{name}"
319 );
320 let offset = fair_offset + offset;
321 ensure!(
322 offset % size as u64 == 0 && offset + size as u64 <= task.size() as u64,
323 "invalid task offset for se.{name}"
324 );
325 values[index] = offset;
326 }
327 let (allowed, allowed_offset) = member(&btf, task, "nr_cpus_allowed")?;
330 let allowed = Int::try_from(allowed)
331 .map_err(|_| anyhow::anyhow!("task_struct.nr_cpus_allowed is not an integer"))?;
332 ensure!(
333 allowed.size() == 4 && allowed.offset == 0 && allowed_offset % 4 == 0,
334 "invalid nr_cpus_allowed field"
335 );
336 values[config::NR_CPUS_ALLOWED] = allowed_offset;
337 let runqueue = btf
340 .type_by_name::<Struct<'_>>("rq")
341 .context("find struct rq")?;
342 let (clock, clock_offset) = member(&btf, runqueue, "clock_task")?;
343 let clock =
344 Int::try_from(clock).map_err(|_| anyhow::anyhow!("rq.clock_task is not an integer"))?;
345 ensure!(
346 clock.size() == 8 && clock_offset % 8 == 0 && clock_offset + 8 <= runqueue.size() as u64,
347 "invalid rq.clock_task field"
348 );
349 values[config::RQ_CLOCK_TASK] = clock_offset;
350 if let Some(args) = btf.type_by_name::<Struct<'_>>("scx_cgroup_init_args") {
354 let cgroup = btf
355 .type_by_name::<Struct<'_>>("cgroup")
356 .context("find struct cgroup")?;
357 let (level, level_offset) = member(&btf, cgroup, "level")?;
358 let level =
359 Int::try_from(level).map_err(|_| anyhow::anyhow!("cgroup.level is not an integer"))?;
360 ensure!(
361 level.size() == 4 && level_offset % 4 == 0,
362 "invalid cgroup.level field"
363 );
364 values[config::TG_LEVEL] = level_offset;
365 let (state, state_offset) = member(&btf, cgroup, "self")?;
366 let state =
367 Struct::try_from(state).map_err(|_| anyhow::anyhow!("cgroup.self is not a struct"))?;
368 let (flags, flags_offset) = member(&btf, state, "flags")?;
369 let flags = Int::try_from(flags)
370 .map_err(|_| anyhow::anyhow!("cgroup.self.flags is not an integer"))?;
371 ensure!(
372 flags.size() == 4 && (state_offset + flags_offset) % 4 == 0,
373 "invalid cgroup.self.flags field"
374 );
375 values[config::TG_FLAGS] = state_offset + flags_offset;
376 values[config::TG_ONLINE] = btf
377 .type_by_kind::<Enum<'_>>()
378 .find_map(|kind| {
379 kind.iter()
380 .find(|value| value.name == Some(OsStr::new("CSS_ONLINE")))
381 .map(|value| value.value as u64)
382 })
383 .context("find CSS_ONLINE")?;
384 let (weight, weight_offset) = member(&btf, args, "weight")?;
385 let weight = Int::try_from(weight)
386 .map_err(|_| anyhow::anyhow!("scx_cgroup_init_args.weight is not an integer"))?;
387 ensure!(
388 weight.size() == 4 && weight_offset % 4 == 0,
389 "invalid scx_cgroup_init_args.weight field"
390 );
391 values[config::TG_WEIGHT] = weight_offset;
392 for (index, name) in [
395 (config::BW_PERIOD, "bw_period_us"),
396 (config::BW_QUOTA, "bw_quota_us"),
397 (config::BW_BURST, "bw_burst_us"),
398 ] {
399 if let Ok((field, offset)) = member(&btf, args, name)
400 && Int::try_from(field).is_ok_and(|field| field.size() == 8)
401 && offset % 8 == 0
402 && offset != 0
403 {
404 values[index] = offset;
405 }
406 }
407 let (_, subsys_offset) = member(&btf, cgroup, "subsys")?;
411 let cpu = btf
412 .type_by_kind::<Enum<'_>>()
413 .find_map(|kind| {
414 kind.iter()
415 .find(|value| value.name == Some(OsStr::new("cpu_cgrp_id")))
416 .map(|value| value.value as u64)
417 })
418 .context("find cpu_cgrp_id")?;
419 let pointer = btf.ptr_size()?.get() as u64;
420 values[config::CPU_CSS] = subsys_offset + cpu * pointer;
421 if let Some(group) = btf.type_by_name::<Struct<'_>>("task_group")
422 && let Ok((_, css_offset)) = member(&btf, group, "css")
423 && let Ok((idle, idle_offset)) = member(&btf, group, "idle")
424 && css_offset == 0
425 && Int::try_from(idle).is_ok_and(|idle| idle.size() == 4)
426 {
427 values[config::TG_IDLE] = idle_offset;
428 }
429 if let Ok((_, node_offset)) = member(&btf, cgroup, "kn")
432 && let Some(node) = btf.type_by_name::<Struct<'_>>("kernfs_node")
433 && let Ok((id, id_offset)) = member(&btf, node, "id")
434 && Int::try_from(id).is_ok_and(|id| id.size() == 8)
435 {
436 values[config::TG_KN] = node_offset;
437 values[config::KN_ID] = id_offset;
438 }
439 }
440 let exit = btf
443 .type_by_name::<Struct<'_>>("scx_exit_info")
444 .context("find struct scx_exit_info")?;
445 values[config::EXIT_CPU] = member(&btf, exit, "exit_cpu").map_or(0, |(_, offset)| offset + 1);
446 for (index, name) in [
447 (config::EXIT_CODE, "exit_code"),
448 (config::EXIT_REASON, "reason"),
449 (config::EXIT_MSG, "msg"),
450 (config::EXIT_DUMP, "dump"),
451 (config::EXIT_KIND, "kind"),
452 ] {
453 values[index] = member(&btf, exit, name)?.1;
454 }
455 values[config::DUMP_LEN] = u64::from(dump_len);
456 let dump = btf
457 .type_by_name::<Struct<'_>>("scx_dump_ctx")
458 .context("find struct scx_dump_ctx")?;
459 values[config::DUMP_AT_JIFFIES] = member(&btf, dump, "at_jiffies")?.1;
460 values[config::DUMP_AT_NS] = member(&btf, dump, "at_ns")?.1;
461 values[config::RUNNABLE_AT] = entity_offset + member(&btf, entity, "runnable_at")?.1;
462 values[config::PID] = member(&btf, task, "pid")?.1;
463 let (se, se_offset) = member(&btf, task, "se")?;
464 let se = Struct::try_from(se).map_err(|_| anyhow::anyhow!("task_struct.se is not a struct"))?;
465 values[config::SUM_EXEC_RUNTIME] = se_offset + member(&btf, se, "sum_exec_runtime")?.1;
466 for (index, name) in [
470 (config::ENQ_CPU_SELECTED, "SCX_ENQ_CPU_SELECTED"),
471 (config::ENQ_IMMED, "SCX_ENQ_IMMED"),
472 (config::ENQ_REENQ, "SCX_ENQ_REENQ"),
473 (config::REENQ_MASK, "SCX_TASK_REENQ_REASON_MASK"),
474 (config::REENQ_IMMED, "SCX_TASK_REENQ_IMMED"),
475 (config::REENQ_PREEMPTED, "SCX_TASK_REENQ_PREEMPTED"),
476 (config::ENQ_LAST, "SCX_ENQ_LAST"),
477 (config::ENQ_PREEMPT_LAZY, "SCX_ENQ_PREEMPT_LAZY"),
478 (config::ENQ_BLOCKED, "SCX_ENQ_BLOCKED"),
479 ] {
480 values[index] = enumerator(&btf, name).unwrap_or(0);
481 }
482 if let Ok((blocked, offset)) = member(&btf, task, "is_blocked")
484 && Int::try_from(blocked).is_ok_and(|blocked| blocked.size() == 1)
485 {
486 values[config::IS_BLOCKED] = offset;
487 }
488 if let (Some(domain), Some(group), Some(cluster)) = (
492 btf.type_by_name::<Struct<'_>>("sched_domain"),
493 btf.type_by_name::<Struct<'_>>("sched_group"),
494 enumerator(&btf, "SD_CLUSTER"),
495 ) && let (Ok((groups, groups_offset)), Ok((flags, flags_offset))) =
496 (member(&btf, domain, "groups"), member(&btf, group, "flags"))
497 && Ptr::try_from(groups).is_ok()
498 && Int::try_from(flags).is_ok_and(|flags| flags.size() == 4)
499 && groups_offset % 8 == 0
500 && flags_offset % 4 == 0
501 {
502 values[config::SD_GROUPS] = groups_offset + 1;
503 values[config::SG_FLAGS] = flags_offset;
504 values[config::SD_CLUSTER] = cluster;
505 }
506 for (index, name) in [
509 (config::STATIC_PRIO, "static_prio"),
510 (config::POLICY, "policy"),
511 ] {
512 let (field, offset) = member(&btf, task, name)?;
513 let integer = Int::try_from(field)
514 .map_err(|_| anyhow::anyhow!("task_struct.{name} is not an integer"))?;
515 ensure!(
516 integer.size() == 4
517 && integer.bits == 32
518 && integer.offset == 0
519 && offset % 4 == 0
520 && offset + 4 <= task.size() as u64,
521 "invalid task_struct.{name} field"
522 );
523 values[index] = offset;
524 }
525 Ok(values)
526}
527
528const ARENA_PAGES: u32 = if cfg!(target_arch = "aarch64") {
532 1 << 16
533} else {
534 1 << 20
535};
536
537pub fn open(name: &str, object: &[u8], libbpf: &LibbpfOpts, verbose: bool) -> Result<OpenObject> {
541 ensure!(
544 !object.is_empty(),
545 "built without the Rust BPF toolchain (rust-src, bpf-linker and \
546 llvm-objcopy): install it and rebuild {name}"
547 );
548 let (object, adapted) = kfunc_protos::adapt(object).context("match kfunc prototypes")?;
549 let object = object.as_slice();
550 for kfunc in adapted {
551 info!("{kfunc}() returns differently on this kernel: prototype matched");
552 }
553 let mut builder = ObjectBuilder::default();
554 builder.name(name)?;
555 builder.debug(verbose);
556 let name = CString::new(name)?;
557 Ok(match libbpf.clone().into_bpf_open_opts() {
558 Some(mut opts) => {
559 opts.object_name = name.as_ptr();
560 let pointer = unsafe {
561 libbpf_rs::libbpf_sys::bpf_object__open_mem(
562 object.as_ptr().cast(),
563 object.len() as libbpf_rs::libbpf_sys::size_t,
564 &opts,
565 )
566 };
567 let pointer = NonNull::new(pointer).ok_or_else(|| {
568 anyhow::anyhow!("open Rust BPF object: {}", std::io::Error::last_os_error())
569 })?;
570 unsafe { OpenObject::from_ptr(pointer) }
571 }
572 None => builder
573 .open_memory(object)
574 .context("open Rust BPF object")?,
575 })
576}
577
578pub fn select_ops(
584 name: &str,
585 open: &mut OpenObject,
586 object: &[u8],
587 cgroup_ops: Option<&'static str>,
588 dump_len: u32,
589) -> Result<&'static str> {
590 let ops = cgroup_ops.unwrap_or("rust_ops");
591 let ops_type = ops_type(ops);
592 let callbacks = missing_callbacks(name, object, ops_type)?;
593 let dump_length = ops_member(name, object, ops_type, "exit_dump_len")?;
594 open.maps_mut()
597 .find(|map| map.name() == OsStr::new(".data.uei_dump"))
598 .context("find the exit dump")?
599 .set_value_size(dump_len)?;
600 for mut map in open.maps_mut() {
601 let name = map.name().to_owned();
602 if name.as_encoded_bytes().starts_with(b"rust_ops") {
603 map.set_autocreate(name == OsStr::new(ops))?;
604 if name == OsStr::new(ops) {
605 let data = map
606 .initial_value_mut()
607 .context("initialize the struct_ops map")?;
608 for range in &callbacks {
609 data.get_mut(range.clone())
610 .context("cgroup callback beyond the struct_ops map")?
611 .fill(0);
612 }
613 let length = dump_length.clone();
614 data.get_mut(length)
615 .context("exit_dump_len beyond the struct_ops map")?
616 .copy_from_slice(&dump_len.to_ne_bytes());
617 }
618 }
619 }
620 Ok(ops)
621}
622
623fn ops_type(ops: &str) -> &'static str {
625 if ops == "rust_ops" {
626 "sched_ext_ops_cid"
627 } else {
628 "sched_ext_ops_cid___cgroup"
629 }
630}
631
632pub fn ops_flags(name: &str, open: &mut OpenObject, object: &[u8], ops: &str) -> Result<u64> {
635 let range = ops_member(name, object, ops_type(ops), "flags")?;
636 let map = open
637 .maps_mut()
638 .find(|map| map.name() == OsStr::new(ops))
639 .context("find the struct_ops map")?;
640 let bytes = map
641 .initial_value()
642 .and_then(|data| data.get(range))
643 .context("flags beyond the struct_ops map")?;
644 Ok(u64::from_ne_bytes(bytes.try_into()?))
645}
646
647pub fn set_ops_flags(
649 name: &str,
650 open: &mut OpenObject,
651 object: &[u8],
652 ops: &str,
653 flags: u64,
654) -> Result<()> {
655 let range = ops_member(name, object, ops_type(ops), "flags")?;
656 let mut map = open
657 .maps_mut()
658 .find(|map| map.name() == OsStr::new(ops))
659 .context("find the struct_ops map")?;
660 map.initial_value_mut()
661 .and_then(|data| data.get_mut(range))
662 .context("flags beyond the struct_ops map")?
663 .copy_from_slice(&flags.to_ne_bytes());
664 Ok(())
665}
666
667pub const WATCHDOG_MAX_TIMEOUT_MS: u32 = 30_000;
670
671pub fn set_ops_timeout(
674 name: &str,
675 open: &mut OpenObject,
676 object: &[u8],
677 ops: &str,
678 timeout_ms: u32,
679) -> Result<()> {
680 ensure!(
682 timeout_ms <= WATCHDOG_MAX_TIMEOUT_MS,
683 "the watchdog timeout is at most {WATCHDOG_MAX_TIMEOUT_MS} ms, not {timeout_ms}"
684 );
685 let range = ops_member(name, object, ops_type(ops), "timeout_ms")?;
686 let mut map = open
687 .maps_mut()
688 .find(|map| map.name() == OsStr::new(ops))
689 .context("find the struct_ops map")?;
690 map.initial_value_mut()
691 .and_then(|data| data.get_mut(range))
692 .context("timeout_ms beyond the struct_ops map")?
693 .copy_from_slice(&timeout_ms.to_ne_bytes());
694 Ok(())
695}
696
697pub fn declared_dump_len(name: &str, open: &mut OpenObject, object: &[u8]) -> Result<u32> {
700 let range = ops_member(name, object, "sched_ext_ops_cid", "exit_dump_len")?;
701 let mut map = open
702 .maps_mut()
703 .find(|map| map.name() == OsStr::new("rust_ops"))
704 .context("find the struct_ops map")?;
705 let bytes = map
706 .initial_value_mut()
707 .context("read the struct_ops map")?
708 .get(range)
709 .context("exit_dump_len beyond the struct_ops map")?;
710 Ok(match u32::from_ne_bytes(bytes.try_into()?) {
712 0 => EXIT_DUMP_DEFAULT,
713 length => length,
714 })
715}
716
717pub fn size_maps(open: &mut OpenObject) -> Result<()> {
720 open.maps_mut()
721 .find(|map| map.name() == OsStr::new("arena"))
722 .context("find arena map")?
723 .set_max_entries(ARENA_PAGES)?;
724 for mut map in open
725 .maps_mut()
726 .filter(|map| map.name() == OsStr::new("hrticks"))
727 {
728 map.set_max_entries(libbpf_rs::num_possible_cpus()? as u32)?;
729 }
730 Ok(())
731}
732
733pub fn set_config(open: &mut OpenObject, config: &[u64]) -> Result<()> {
735 let mut map = open
736 .maps_mut()
737 .find(|map| map.name().as_encoded_bytes().ends_with(b".rodata.config"))
738 .context("find scheduler configuration")?;
739 let data = map
740 .initial_value_mut()
741 .context("initialize scheduler configuration")?;
742 ensure!(
743 data.len() == std::mem::size_of_val(config),
744 "unexpected configuration size"
745 );
746 for (destination, value) in data.as_chunks_mut::<8>().0.iter_mut().zip(config) {
747 *destination = value.to_ne_bytes();
748 }
749 Ok(())
750}
751
752pub fn attach(object: &mut Object, ops: &str) -> Result<Link> {
754 object
755 .maps_mut()
756 .find(|map| map.name() == OsStr::new(ops))
757 .context("find the struct_ops map")?
758 .attach_struct_ops()
759 .context("attach sched_ext scheduler")
760}
761
762pub fn arena_stats(object: &Object) -> Result<[u64; 6]> {
766 let stats = object
767 .maps()
768 .find(|map| map.name() == OsStr::new(".data.astats"))
769 .context("find arena statistics")?;
770 let mut data = [0_u8; 48];
771 ensure!(
772 stats.lookup_into(&0_u32.to_ne_bytes(), &mut data, MapFlags::ANY)?,
773 "read arena statistics"
774 );
775 let mut counters = [0; 6];
776 for (counter, bytes) in counters.iter_mut().zip(data.as_chunks::<8>().0) {
777 *counter = u64::from_ne_bytes(*bytes);
778 }
779 Ok(counters)
780}
781
782pub fn check_arena_stats(counters: &[u64; 6]) -> Result<()> {
784 ensure!(
785 counters[0] == counters[1],
786 "arena task contexts were not all released"
787 );
788 ensure!(counters[2] == 0, "arena task context validation failed");
789 ensure!(counters[5] == 0, "idle state CAS retries exhausted");
790 Ok(())
791}
792
793pub struct OpsProfiler {
798 pub enabled: Option<OwnedFd>,
800 last_read: Instant,
801 warned: bool,
802}
803
804impl Default for OpsProfiler {
805 fn default() -> Self {
806 Self::new()
807 }
808}
809
810impl OpsProfiler {
811 const IDLE: Duration = Duration::from_secs(5);
813
814 pub fn new() -> Self {
815 Self {
816 enabled: None,
817 last_read: Instant::now(),
818 warned: false,
819 }
820 }
821
822 fn enable(&mut self) {
823 if self.enabled.is_some() {
824 return;
825 }
826 let fd = unsafe {
827 libbpf_rs::libbpf_sys::bpf_enable_stats(libbpf_rs::libbpf_sys::BPF_STATS_RUN_TIME)
828 };
829 if fd < 0 {
830 if !self.warned {
831 eprintln!(
832 "BPF run-time stats unavailable: {}",
833 std::io::Error::last_os_error()
834 );
835 self.warned = true;
836 }
837 return;
838 }
839 self.enabled = Some(unsafe { OwnedFd::from_raw_fd(fd) });
840 }
841
842 pub fn expire(&mut self) {
844 if self.enabled.is_some() && self.last_read.elapsed() > Self::IDLE {
845 self.enabled = None;
846 }
847 }
848
849 pub fn sample(&mut self, object: &Object) -> BTreeMap<String, OpStats> {
853 self.last_read = Instant::now();
854 self.enable();
855 let mut ops = BTreeMap::new();
856 if self.enabled.is_none() {
857 return ops;
858 }
859 for program in object.progs() {
860 if program.prog_type() != ProgramType::StructOps {
861 continue;
862 }
863 let mut info: libbpf_rs::libbpf_sys::bpf_prog_info = unsafe { std::mem::zeroed() };
864 let mut len = std::mem::size_of_val(&info) as u32;
865 let result = unsafe {
866 libbpf_rs::libbpf_sys::bpf_prog_get_info_by_fd(
867 program.as_fd().as_raw_fd(),
868 (&raw mut info).cast(),
869 &mut len,
870 )
871 };
872 if result != 0 {
873 continue;
874 }
875 let name = program.name().to_string_lossy();
876 let name = name
879 .strip_prefix("scx_")
880 .or_else(|| name.strip_prefix("rust_"))
881 .unwrap_or(&name)
882 .to_string();
883 ops.insert(
884 name,
885 OpStats {
886 calls: info.run_cnt,
887 ns: info.run_time_ns,
888 misses: info.recursion_misses,
889 },
890 );
891 }
892 ops
893 }
894}
895
896pub fn arena_usage(object: &Object) -> (Option<u64>, u64) {
899 let Some(map) = object.maps().find(|map| map.name() == OsStr::new("arena")) else {
900 return (None, 0);
901 };
902 let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }.max(1) as u64;
903 let max = u64::from(map.max_entries()) * page;
904 let fd = map.as_fd().as_raw_fd();
905 let memlock: u64 = std::fs::read_to_string(format!("/proc/self/fdinfo/{fd}"))
906 .ok()
907 .and_then(|info| {
908 info.lines()
909 .find_map(|line| line.strip_prefix("memlock:"))
910 .and_then(|value| value.trim().parse().ok())
911 })
912 .unwrap_or(0);
913 ((memlock > 0).then_some(memlock), max)
914}
915
916const EXIT_UNREG_KERN: i32 = 66;
918
919pub const EXIT_DUMP_DEFAULT: u32 = 32768;
921const UEI_BYTES: usize = 4 + 4 + 8 + 128 + 1024;
924
925pub fn exit_report(object: &Object, dump_len: u32) -> Result<Option<UserExitInfo>> {
928 let uei = object
929 .maps()
930 .find(|map| map.name() == OsStr::new(".data.uei"))
931 .context("find the exit record")?;
932 let mut record = vec![0_u8; UEI_BYTES];
933 ensure!(
934 uei.lookup_into(&0_u32.to_ne_bytes(), &mut record, MapFlags::ANY)?,
935 "read the exit record"
936 );
937 if i32::from_ne_bytes(record[0..4].try_into()?) == 0 {
938 return Ok(None);
939 }
940 let dump = object
941 .maps()
942 .find(|map| map.name() == OsStr::new(".data.uei_dump"))
943 .context("find the exit dump")?;
944 let mut text = vec![0_u8; dump_len as usize + 1];
945 dump.lookup_into(
946 &0_u32.to_ne_bytes(),
947 &mut text[..dump_len as usize],
948 MapFlags::ANY,
949 )?;
950 record[UEI_BYTES - 1] = 0;
951 record[16 + 127] = 0;
952 Ok(Some(UserExitInfo::new(
953 record[0..4].as_ptr().cast(),
954 record[8..16].as_ptr().cast(),
955 record[4..8].as_ptr().cast(),
956 record[16..].as_ptr().cast(),
957 record[16 + 128..].as_ptr().cast(),
958 text.as_ptr().cast(),
959 )))
960}
961
962fn tick_ns() -> u64 {
965 let mut time = libc::timespec {
966 tv_sec: 0,
967 tv_nsec: 0,
968 };
969 let result = unsafe { libc::clock_getres(libc::CLOCK_MONOTONIC_COARSE, &mut time) };
970 if result == 0 && time.tv_sec == 0 && time.tv_nsec > 0 {
971 time.tv_nsec as u64
972 } else {
973 1_000_000
974 }
975}
976
977const PARAM_PREFIX: &str = ".rodata.param.";
980
981fn param_name(name: &str) -> String {
983 name.to_ascii_lowercase().replace('-', "_")
984}
985
986fn param_of(section: &OsStr) -> Option<(String, String)> {
989 let (name, ty) = section
990 .to_str()?
991 .strip_prefix(PARAM_PREFIX)?
992 .rsplit_once('.')?;
993 Some((param_name(name), ty.to_owned()))
994}
995
996fn param_parse(ty: &str, size: usize, text: &str) -> Option<u64> {
1000 let bits = 8 * u32::try_from(size).ok()?;
1001 if !(8..=64).contains(&bits) {
1002 return None;
1003 }
1004 match (ty, text) {
1005 ("bool", "true" | "1") => Some(1),
1006 ("bool", "false" | "0") => Some(0),
1007 ("bool", _) => None,
1008 _ if ty.starts_with('i') => text
1009 .parse::<i64>()
1010 .ok()
1011 .filter(|value| (value << (64 - bits)) >> (64 - bits) == *value)
1012 .map(|value| value as u64),
1013 _ => match text.strip_prefix("0x") {
1014 Some(digits) => u64::from_str_radix(digits, 16).ok(),
1015 None => text.parse::<u64>().ok(),
1016 }
1017 .filter(|value| bits == 64 || value >> bits == 0),
1018 }
1019}
1020
1021fn param_element(ty: &str) -> Option<&str> {
1024 ty.strip_prefix('[')?.strip_suffix(']')
1025}
1026
1027pub trait ParamValue: Copy {
1030 const TYPE: &'static str;
1032 fn append(self, bytes: &mut Vec<u8>);
1034}
1035
1036macro_rules! param_values {
1037 ($($type:ident),*) => {
1038 $(impl ParamValue for $type {
1039 const TYPE: &'static str = stringify!($type);
1040 fn append(self, bytes: &mut Vec<u8>) {
1041 bytes.extend_from_slice(&self.to_ne_bytes());
1042 }
1043 })*
1044 };
1045}
1046param_values!(u8, u16, u32, u64, i8, i16, i32, i64);
1047
1048impl ParamValue for bool {
1049 const TYPE: &'static str = "bool";
1050 fn append(self, bytes: &mut Vec<u8>) {
1051 bytes.push(u8::from(self));
1052 }
1053}
1054
1055pub struct Params<'a> {
1060 open: &'a mut OpenObject,
1061 set: std::collections::BTreeSet<String>,
1062 flags: u64,
1063}
1064
1065impl<'a> Params<'a> {
1066 pub fn new(open: &'a mut OpenObject, flags: u64) -> Self {
1068 Self {
1069 open,
1070 set: Default::default(),
1071 flags,
1072 }
1073 }
1074
1075 pub fn flags(&self) -> u64 {
1078 self.flags
1079 }
1080
1081 pub fn add_flags(&mut self, flags: u64) {
1084 self.flags |= flags;
1085 }
1086
1087 pub fn clear_flags(&mut self, flags: u64) {
1089 self.flags &= !flags;
1090 }
1091
1092 pub fn type_of(&mut self, name: &str) -> Option<String> {
1094 let name = param_name(name);
1095 self.open
1096 .maps_mut()
1097 .filter_map(|map| param_of(map.name()))
1098 .find_map(|(found, ty)| (found == name).then_some(ty))
1099 }
1100
1101 fn write(
1104 &mut self,
1105 name: &str,
1106 bytes: impl FnOnce(&str, usize) -> Result<Vec<u8>>,
1107 ) -> Result<()> {
1108 let name = param_name(name);
1109 let (mut map, ty) = self
1110 .open
1111 .maps_mut()
1112 .find_map(|map| match param_of(map.name()) {
1113 Some((found, ty)) if found == name => Some((map, ty)),
1114 _ => None,
1115 })
1116 .with_context(|| format!("the scheduler has no parameter {name}"))?;
1117 let data = map
1118 .initial_value_mut()
1119 .with_context(|| format!("parameter {name} has no value"))?;
1120 let bytes = bytes(&ty, data.len()).with_context(|| format!("parameter {name}"))?;
1121 data.copy_from_slice(&bytes);
1122 self.set.insert(name);
1123 Ok(())
1124 }
1125
1126 pub fn set_text(&mut self, name: &str, text: &str) -> Result<()> {
1130 self.write(name, |ty, size| {
1131 let value = param_parse(ty, size, text)
1132 .with_context(|| format!("{text:?} is not a valid {ty}"))?;
1133 Ok(value.to_ne_bytes()[..size].to_vec())
1134 })
1135 }
1136
1137 pub fn set<T: ParamValue>(&mut self, name: &str, values: &[T]) -> Result<()> {
1141 self.write(name, |ty, size| {
1142 let array = param_element(ty);
1143 ensure!(
1144 array.unwrap_or(ty) == T::TYPE,
1145 "it is {ty}, not {}",
1146 T::TYPE
1147 );
1148 let mut bytes = Vec::new();
1149 values.iter().for_each(|value| value.append(&mut bytes));
1150 ensure!(
1151 bytes.len() == size || array.is_some() && bytes.len() < size,
1152 "{} values do not fit {ty} of {size} bytes",
1153 values.len()
1154 );
1155 bytes.resize(size, 0);
1156 Ok(bytes)
1157 })
1158 }
1159
1160 pub fn set_options<A: clap::Args>(&mut self, matches: &clap::ArgMatches) -> Result<()> {
1169 for (name, value) in named::<A>(matches) {
1170 let scalar = |ty: String| param_element(&ty).is_none();
1171 if let Some(ty) = self.type_of(&name) {
1172 if scalar(ty) {
1173 self.set_text(&name, &value)?;
1174 }
1175 continue;
1176 }
1177 let Some((tunable, factor)) = time_unit(&name) else {
1178 continue;
1179 };
1180 if self.type_of(&tunable).is_some_and(scalar) {
1181 let scaled = value
1182 .parse::<i128>()
1183 .ok()
1184 .and_then(|value| value.checked_mul(factor))
1185 .with_context(|| {
1186 format!("--{}: {value:?} is not a time", name.replace('_', "-"))
1187 })?;
1188 self.set_text(&tunable, &scaled.to_string())
1189 .with_context(|| format!("--{} {value}", name.replace('_', "-")))?;
1190 }
1191 }
1192 Ok(())
1193 }
1194
1195 pub fn check(self) -> Result<()> {
1199 let unset: Vec<_> = self
1200 .open
1201 .maps_mut()
1202 .filter_map(|map| Some(param_of(map.name())?.0))
1203 .filter(|name| !self.set.contains(name))
1204 .collect();
1205 ensure!(
1206 unset.is_empty(),
1207 "no option sets the scheduler's parameters: {}",
1208 unset.join(", ")
1209 );
1210 Ok(())
1211 }
1212}
1213
1214#[derive(Default)]
1216pub struct RunOptions {
1217 pub libbpf: LibbpfOpts,
1219 pub verbose: bool,
1221 pub verify: bool,
1223 pub duration: Option<u64>,
1225 pub exit_dump_len: u32,
1228 pub timeout_ms: u32,
1231 pub stats: Option<f64>,
1233 pub version: &'static str,
1235}
1236
1237pub struct Capacity {
1240 pub mode: tiers::CapacityMode,
1241 pub tolerance_pct: Option<u32>,
1243 pub no_asym_packing: bool,
1245}
1246
1247impl Default for Capacity {
1248 fn default() -> Self {
1249 Self {
1250 mode: tiers::CapacityMode::Kernel,
1251 tolerance_pct: None,
1252 no_asym_packing: false,
1253 }
1254 }
1255}
1256
1257type Idle = Option<Box<dyn FnMut(&Loaded)>>;
1259
1260pub struct Stats {
1264 describe: Box<dyn Fn() -> Result<()>>,
1265 #[allow(clippy::type_complexity)]
1266 serve: Box<dyn FnOnce(Idle) -> Result<Box<dyn FnMut(&Loaded, Duration) -> Result<()>>>>,
1267 idle: Idle,
1268 monitor: Box<dyn FnOnce(Duration, Arc<AtomicBool>) -> Result<()> + Send>,
1269}
1270
1271impl Stats {
1272 pub fn new<M>(
1276 data: fn() -> scx_stats::StatsServerData<(), M>,
1277 mut metrics: impl FnMut(&Loaded) -> M + 'static,
1278 mut show: impl FnMut(M, Duration) -> Result<()> + Send + 'static,
1279 ) -> Self
1280 where
1281 M: Send + 'static + for<'a> serde::Deserialize<'a>,
1282 {
1283 Self {
1284 describe: Box::new(move || data().describe_meta(&mut std::io::stdout(), None)),
1285 idle: None,
1286 serve: Box::new(move |mut idle: Idle| {
1287 let server = scx_stats::StatsServer::new(data()).launch()?;
1288 let (responses, requests) = server.channels();
1289 Ok(Box::new(move |object: &Loaded, timeout: Duration| {
1290 let _server = &server;
1292 match requests.recv_timeout(timeout) {
1293 Ok(()) => responses
1294 .send(metrics(object))
1295 .map_err(|_| anyhow::anyhow!("stats client went away"))?,
1296 Err(error) if error.is_timeout() => {
1297 if let Some(idle) = idle.as_mut() {
1298 idle(object);
1299 }
1300 }
1301 Err(error) => Err(error)?,
1302 }
1303 Ok(())
1304 }))
1305 }),
1306 monitor: Box::new(move |interval, shutdown| {
1307 scx_utils::monitor_stats::<M>(
1308 &[],
1309 interval,
1310 || shutdown.load(Ordering::Relaxed),
1311 |metrics| show(metrics, interval),
1312 )
1313 }),
1314 }
1315 }
1316}
1317
1318impl Stats {
1319 pub fn idle(mut self, idle: impl FnMut(&Loaded) + 'static) -> Self {
1324 self.idle = Some(Box::new(idle));
1325 self
1326 }
1327}
1328
1329pub trait Scheduler: clap::Args {
1333 fn settings(&self, _params: &mut Params) -> Result<()> {
1335 Ok(())
1336 }
1337
1338 fn runtime(&self) -> u64 {
1341 0
1342 }
1343
1344 fn capacity(&self) -> Capacity {
1346 Capacity::default()
1347 }
1348
1349 fn stats(&self) -> Option<Stats> {
1352 None
1353 }
1354
1355 fn attached(&self, attached: &Attached<'_>) {
1359 info!("{} attached", attached.name());
1360 }
1361
1362 fn stopped(&self, name: &str, why: &str) {
1366 info!("{name} stopped: {why}");
1367 }
1368}
1369
1370pub struct Attached<'a> {
1373 name: &'a str,
1374 version: &'a str,
1375 object: &'a Object,
1376 flags: u64,
1377 runtime: u64,
1378 tiers: Option<&'a Tiers>,
1379 topology: std::cell::OnceCell<Option<Topology>>,
1380}
1381
1382struct Topology {
1384 nr_cpus: usize,
1385 nr_nodes: usize,
1386 smt: bool,
1387}
1388
1389impl Attached<'_> {
1390 pub fn name(&self) -> &str {
1392 self.name
1393 }
1394
1395 pub fn version(&self) -> &str {
1397 self.version
1398 }
1399
1400 pub fn build(&self) -> String {
1403 scx_utils::build_id::full_version(self.version)
1404 }
1405
1406 pub fn object(&self) -> &Object {
1408 self.object
1409 }
1410
1411 pub fn kernel_release(&self) -> String {
1413 let mut uts: libc::utsname = unsafe { std::mem::zeroed() };
1414 if unsafe { libc::uname(&mut uts) } != 0 {
1415 return "unknown".to_string();
1416 }
1417 unsafe { std::ffi::CStr::from_ptr(uts.release.as_ptr()) }
1418 .to_string_lossy()
1419 .into_owned()
1420 }
1421
1422 pub fn tick_ns(&self) -> u64 {
1424 tick_ns()
1425 }
1426
1427 fn topology(&self) -> Option<&Topology> {
1428 self.topology
1429 .get_or_init(|| {
1430 let topology = scx_utils::Topology::new().ok()?;
1431 Some(Topology {
1432 nr_cpus: topology.all_cpus.len(),
1433 nr_nodes: topology
1434 .nodes
1435 .values()
1436 .filter(|node| !node.all_cpus.is_empty())
1437 .count(),
1438 smt: topology.smt_enabled,
1439 })
1440 })
1441 .as_ref()
1442 }
1443
1444 pub fn nr_cpus(&self) -> Option<usize> {
1446 Some(self.topology()?.nr_cpus)
1447 }
1448
1449 pub fn nr_nodes(&self) -> Option<usize> {
1452 Some(self.topology()?.nr_nodes)
1453 }
1454
1455 pub fn numa_disabled(&self) -> bool {
1457 self.runtime & config::runtime::DISABLE_NUMA != 0
1458 }
1459
1460 pub fn smt(&self) -> Option<bool> {
1464 Some(self.topology()?.smt && self.runtime & config::runtime::DISABLE_SMT == 0)
1465 }
1466
1467 pub fn ops_flags(&self) -> u64 {
1469 self.flags
1470 }
1471
1472 pub fn tiers(&self) -> Option<&Tiers> {
1474 self.tiers
1475 }
1476
1477 pub fn arena_usage(&self) -> (Option<u64>, u64) {
1480 arena_usage(self.object)
1481 }
1482}
1483
1484fn publish_tiers(object: &mut Object, capacity: &Capacity) -> Result<Tiers> {
1487 let mut tiers = tiers::Tiers::new(capacity.mode, capacity.tolerance_pct)?;
1488 tiers.read_priorities(object, capacity.no_asym_packing)?;
1489 tiers.publish(object)?;
1490 Ok(tiers)
1491}
1492
1493fn spawn_monitor(
1496 monitor: Box<dyn FnOnce(Duration, Arc<AtomicBool>) -> Result<()> + Send>,
1497 interval: f64,
1498 shutdown: Arc<AtomicBool>,
1499) -> std::thread::JoinHandle<()> {
1500 std::thread::spawn(move || {
1501 if let Err(error) = monitor(Duration::from_secs_f64(interval), shutdown) {
1502 eprintln!("stats monitor thread finished because of an error {error}");
1503 }
1504 })
1505}
1506
1507pub fn run<S: Scheduler>(
1514 name: &str,
1515 legacy: &[u8],
1516 sized: &[u8],
1517 options: &RunOptions,
1518 shutdown: &Arc<AtomicBool>,
1519 matches: &clap::ArgMatches,
1520 scheduler: &S,
1521) -> Result<()> {
1522 let bytes = if sized_topology()? { sized } else { legacy };
1523 let mut open = open(name, bytes, &options.libbpf, options.verbose)?;
1524 let dump_len = match options.exit_dump_len {
1525 0 => declared_dump_len(name, &mut open, bytes)?,
1526 len => len,
1527 };
1528 let config = configuration(dump_len, scheduler.runtime())?;
1529 let cgroup = if open
1532 .maps_mut()
1533 .any(|map| map.name() == OsStr::new("rust_ops_cgroup"))
1534 {
1535 cgroup_ops()?
1536 } else {
1537 None
1538 };
1539 let ops = select_ops(name, &mut open, bytes, cgroup, dump_len)?;
1540 if options.timeout_ms != 0 {
1541 set_ops_timeout(name, &mut open, bytes, ops, options.timeout_ms)?;
1542 }
1543 let declared = ops_flags(name, &mut open, bytes, ops)?;
1544 size_maps(&mut open)?;
1545 let mut config = config;
1546 let mut params = Params::new(&mut open, declared);
1547 params.set_options::<S>(matches)?;
1548 if params.type_of("stats").as_deref() == Some("bool") {
1551 params.set("stats", &[options.stats.is_some()])?;
1552 }
1553 scheduler.settings(&mut params)?;
1554 let flags = params.flags();
1555 params.check()?;
1556 set_ops_flags(name, &mut open, bytes, ops, flags)?;
1557 config[config::OPS_FLAGS] = flags;
1558 set_config(&mut open, &config)?;
1559 let mut object = open.load().context("load Rust BPF object")?;
1560 if options.verify {
1561 println!("Rust BPF object verified successfully");
1562 return Ok(());
1563 }
1564 let tiers = publish_tiers(&mut object, &scheduler.capacity())
1568 .inspect_err(|error| warn!("CPU capacities and ranks not read: {error:#}"))
1569 .ok();
1570 let link = attach(&mut object, ops)?;
1571 let object = Loaded(Arc::new(object));
1572 scheduler.attached(&Attached {
1573 name,
1574 version: options.version,
1575 object: &object,
1576 flags,
1577 runtime: scheduler.runtime(),
1578 tiers: tiers.as_ref(),
1579 topology: std::cell::OnceCell::new(),
1580 });
1581 let start = Instant::now();
1582 let (mut serve, mut monitor) = match scheduler.stats() {
1584 Some(stats) => {
1585 let serve = (stats.serve)(stats.idle)?;
1586 let monitor = options
1587 .stats
1588 .map(|interval| spawn_monitor(stats.monitor, interval, shutdown.clone()));
1589 (Some(serve), monitor)
1590 }
1591 None => (None, None),
1592 };
1593 while !shutdown.load(Ordering::Relaxed)
1594 && options
1595 .duration
1596 .is_none_or(|seconds| start.elapsed() < Duration::from_secs(seconds))
1597 && exit_report(&object, dump_len)?.is_none()
1598 {
1599 match serve.as_mut() {
1600 Some(serve) => serve(&object, Duration::from_millis(100))?,
1601 None => std::thread::sleep(Duration::from_millis(100)),
1602 }
1603 }
1604 shutdown.store(true, Ordering::Relaxed);
1605 if let Some(monitor) = monitor.take() {
1606 let _ = monitor.join();
1607 }
1608 drop(serve);
1609 drop(link);
1610 let counters = arena_stats(&object)?;
1613 let clean = check_arena_stats(&counters);
1614 if options.verbose || clean.is_err() {
1615 info!(
1616 "arena: allocated={} freed={} errors={} failures={} pages={}",
1617 counters[0], counters[1], counters[2], counters[3], counters[4]
1618 );
1619 info!("idle: cas_failures={}", counters[5]);
1620 }
1621 clean?;
1622 if let Some(why) = exit_summary(&object)? {
1625 scheduler.stopped(name, &why);
1626 return Ok(());
1627 }
1628 match exit_report(&object, dump_len)? {
1629 Some(info) => info.report(),
1630 None => Ok(()),
1631 }
1632}
1633
1634fn exit_summary(object: &Object) -> Result<Option<String>> {
1638 let uei = object
1639 .maps()
1640 .find(|map| map.name() == OsStr::new(".data.uei"))
1641 .context("find the exit record")?;
1642 let mut record = vec![0_u8; UEI_BYTES];
1643 ensure!(
1644 uei.lookup_into(&0_u32.to_ne_bytes(), &mut record, MapFlags::ANY)?,
1645 "read the exit record"
1646 );
1647 let kind = i32::from_ne_bytes(record[0..4].try_into()?);
1648 if kind == 0 || kind > EXIT_UNREG_KERN {
1649 return Ok(None);
1650 }
1651 let text = |bytes: &[u8]| {
1652 let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
1653 String::from_utf8_lossy(&bytes[..end]).into_owned()
1654 };
1655 let reason = text(&record[16..16 + 128]);
1656 let msg = text(&record[16 + 128..]);
1657 Ok(Some(match (reason.is_empty(), msg.is_empty()) {
1658 (true, _) => "unknown reason".to_string(),
1659 (false, true) => reason,
1660 (false, false) => format!("{reason} ({msg})"),
1661 }))
1662}
1663
1664#[derive(clap::Parser)]
1666struct Cli {
1667 #[arg(short = 'v', long, help = "Enable libbpf debug output")]
1668 verbose: bool,
1669 #[arg(short = 'V', long, help = "Print the scheduler version and exit")]
1670 version: bool,
1671 #[arg(long, help = "Load the BPF object and exit")]
1672 verify: bool,
1673 #[arg(long, help = "Run for this many seconds")]
1674 duration: Option<u64>,
1675 #[arg(
1676 long,
1677 default_value_t = 0,
1678 help = "Exit debug dump buffer length. 0 indicates default"
1679 )]
1680 exit_dump_len: u32,
1681 #[arg(
1682 long,
1683 default_value_t = 0,
1684 help = "Watchdog timeout in milliseconds, at most 30000. 0 keeps the scheduler's own"
1685 )]
1686 timeout_ms: u32,
1687 #[arg(long, help = "Enable stats monitoring with the specified interval")]
1688 stats: Option<f64>,
1689 #[arg(
1690 long,
1691 help = "Run in stats monitoring mode with the specified interval. Scheduler is not launched"
1692 )]
1693 monitor: Option<f64>,
1694 #[arg(long, help = "Show descriptions for statistics")]
1695 help_stats: bool,
1696 #[clap(flatten, next_help_heading = "Libbpf Options")]
1697 libbpf: LibbpfOpts,
1698}
1699
1700#[derive(clap::Args)]
1702pub struct NoOpts {}
1703
1704impl Scheduler for NoOpts {}
1705
1706fn time_unit(option: &str) -> Option<(String, i128)> {
1709 [("_us", 1_000), ("_ms", 1_000_000)]
1710 .into_iter()
1711 .find_map(|(suffix, factor)| Some((format!("{}_ns", option.strip_suffix(suffix)?), factor)))
1712}
1713
1714fn named<A: clap::Args>(matches: &clap::ArgMatches) -> Vec<(String, String)> {
1716 A::augment_args(clap::Command::new(""))
1717 .get_arguments()
1718 .filter_map(|arg| {
1719 let name = arg.get_id().as_str();
1720 let value = matches.get_raw(name)?.next()?;
1721 Some((name.to_owned(), value.to_string_lossy().into_owned()))
1722 })
1723 .collect()
1724}
1725
1726fn init_logger() -> Result<()> {
1729 let mut config = simplelog::ConfigBuilder::new();
1730 let _ = config.set_time_offset_to_local();
1733 config
1734 .set_time_level(simplelog::LevelFilter::Error)
1735 .set_location_level(simplelog::LevelFilter::Off)
1736 .set_target_level(simplelog::LevelFilter::Off)
1737 .set_thread_level(simplelog::LevelFilter::Off);
1738 simplelog::TermLogger::init(
1739 simplelog::LevelFilter::Info,
1740 config.build(),
1741 simplelog::TerminalMode::Stderr,
1742 simplelog::ColorChoice::Auto,
1743 )?;
1744 Ok(())
1745}
1746
1747pub fn cli<S: Scheduler>(
1751 name: &'static str,
1752 version: &'static str,
1753 about: &'static str,
1754 legacy: &[u8],
1755 sized: &[u8],
1756) -> Result<()> {
1757 use clap::{Args, FromArgMatches};
1758
1759 let command = Cli::augment_args(S::augment_args(clap::Command::new(name)));
1760 let matches = command.about(about).get_matches();
1761 let options = Cli::from_arg_matches(&matches)?;
1762 let scheduler = S::from_arg_matches(&matches)?;
1763 if options.version {
1764 println!("{name} {}", scx_utils::build_id::full_version(version));
1765 return Ok(());
1766 }
1767 init_logger()?;
1768 let shutdown = Arc::new(AtomicBool::new(false));
1769 let signal = shutdown.clone();
1770 ctrlc::set_handler(move || signal.store(true, Ordering::Relaxed))?;
1771 if options.help_stats || options.monitor.is_some() || options.stats.is_some() {
1772 let stats = scheduler
1773 .stats()
1774 .with_context(|| format!("{name} has no statistics"))?;
1775 if options.help_stats {
1776 return (stats.describe)();
1777 }
1778 if let Some(interval) = options.monitor {
1779 return (stats.monitor)(Duration::from_secs_f64(interval), shutdown);
1780 }
1781 }
1782 run(
1783 name,
1784 legacy,
1785 sized,
1786 &RunOptions {
1787 libbpf: options.libbpf,
1788 verbose: options.verbose,
1789 verify: options.verify,
1790 duration: options.duration,
1791 exit_dump_len: options.exit_dump_len,
1792 timeout_ms: options.timeout_ms,
1793 stats: options.stats,
1794 version,
1795 },
1796 &shutdown,
1797 &matches,
1798 &scheduler,
1799 )
1800}
1801
1802#[macro_export]
1838macro_rules! main {
1839 ($about:expr) => {
1840 $crate::main!($about, $crate::NoOpts);
1841 };
1842 ($about:expr, $opts:ty) => {
1843 fn main() -> $crate::Result<()> {
1844 $crate::cli::<$opts>(
1845 env!("CARGO_PKG_NAME"),
1846 env!("CARGO_PKG_VERSION"),
1847 $about,
1848 include_bytes!(concat!(env!("OUT_DIR"), "/scheduler-legacy.bpf.o")),
1849 include_bytes!(concat!(env!("OUT_DIR"), "/scheduler-sized.bpf.o")),
1850 )
1851 }
1852 };
1853}
1854
1855#[cfg(test)]
1856mod tests {
1857 use super::*;
1858
1859 #[test]
1860 fn param_sections() {
1861 let of = |section: &str| param_of(OsStr::new(section));
1862 assert_eq!(
1863 of(".rodata.param.SLICE_NS.u64"),
1864 Some(("slice_ns".into(), "u64".into()))
1865 );
1866 assert_eq!(
1867 of(".rodata.param.STEAL.bool"),
1868 Some(("steal".into(), "bool".into()))
1869 );
1870 assert_eq!(of(".rodata.config"), None);
1871 assert_eq!(of(".rodata.param.NOTYPE"), None);
1872 assert_eq!(param_name("Slice-NS"), "slice_ns");
1873 }
1874
1875 #[test]
1876 fn params_from_options() {
1877 use clap::{Args, FromArgMatches};
1878
1879 #[derive(clap::Args)]
1880 struct Opts {
1881 #[clap(short = 's', long, default_value = "5000000")]
1882 slice_ns: u64,
1883 #[clap(long, action = clap::ArgAction::Set, default_value = "true")]
1884 steal: bool,
1885 #[clap(long)]
1886 fast: bool,
1887 }
1888
1889 let parse = |line: &[&str]| {
1890 let command = Cli::augment_args(Opts::augment_args(clap::Command::new("test")));
1891 let matches = command.get_matches_from(line);
1892 assert!(Cli::from_arg_matches(&matches).is_ok());
1893 named::<Opts>(&matches)
1894 .iter()
1895 .map(|(name, value)| format!("{name}={value}"))
1896 .collect::<Vec<_>>()
1897 };
1898 assert_eq!(
1899 parse(&["test"]),
1900 ["slice_ns=5000000", "steal=true", "fast=false"]
1901 );
1902 assert_eq!(
1903 parse(&["test", "-s", "1000", "--steal", "false", "--fast", "-v"]),
1904 ["slice_ns=1000", "steal=false", "fast=true"]
1905 );
1906 }
1907
1908 #[test]
1909 fn param_arrays() {
1910 assert_eq!(
1911 param_of(OsStr::new(".rodata.param.PRIMARY_CPUS.[u64]")),
1912 Some(("primary_cpus".into(), "[u64]".into()))
1913 );
1914 assert_eq!(param_element("[u64]"), Some("u64"));
1915 assert_eq!(param_element("u64"), None);
1916 let mut bytes = Vec::new();
1917 [1_u16, 0x0203]
1918 .iter()
1919 .for_each(|value| value.append(&mut bytes));
1920 true.append(&mut bytes);
1921 assert_eq!(bytes.len(), 5);
1922 assert_eq!(bytes[..2], 1_u16.to_ne_bytes());
1923 assert_eq!(bytes[4], 1);
1924 assert_eq!(<i32 as ParamValue>::TYPE, "i32");
1925 }
1926
1927 #[test]
1928 fn param_values() {
1929 assert_eq!(param_parse("u64", 8, "5000000"), Some(5_000_000));
1930 assert_eq!(param_parse("u64", 8, "0x10"), Some(16));
1931 assert_eq!(param_parse("u64", 8, "-1"), None);
1932 assert_eq!(
1933 param_parse("u32", 4, "4294967295"),
1934 Some(u64::from(u32::MAX))
1935 );
1936 assert_eq!(param_parse("u32", 4, "4294967296"), None);
1937 assert_eq!(param_parse("u8", 1, "256"), None);
1938 assert_eq!(param_parse("i32", 4, "-1"), Some(u64::MAX));
1939 assert_eq!(param_parse("i32", 4, "2147483648"), None);
1940 assert_eq!(param_parse("i8", 1, "-128"), Some(-128_i64 as u64));
1941 assert_eq!(param_parse("i8", 1, "-129"), None);
1942 assert_eq!(param_parse("bool", 1, "true"), Some(1));
1943 assert_eq!(param_parse("bool", 1, "0"), Some(0));
1944 assert_eq!(param_parse("bool", 1, "7"), None);
1945 assert_eq!(param_parse("u64", 16, "1"), None);
1946 assert_eq!(param_parse("u64", 8, "many"), None);
1947 }
1948
1949 #[test]
1950 fn time_units() {
1951 assert_eq!(time_unit("slice_us"), Some(("slice_ns".into(), 1_000)));
1952 assert_eq!(
1953 time_unit("timeout_ms"),
1954 Some(("timeout_ns".into(), 1_000_000))
1955 );
1956 assert_eq!(time_unit("slice_ns"), None);
1957 assert_eq!(time_unit("bonus"), None);
1958 }
1959}