1pub mod stats;
6use stats::Metrics;
7
8use std::mem::MaybeUninit;
9use std::sync::Arc;
10use std::sync::atomic::AtomicBool;
11use std::sync::atomic::Ordering;
12use std::time::Duration;
13
14use anyhow::Context;
15use anyhow::Result;
16use anyhow::bail;
17use clap::Parser;
18use crossbeam::channel::RecvTimeoutError;
19use libbpf_rs::AsRawLibbpf;
20use libbpf_rs::MapCore as _;
21use libbpf_rs::OpenObject;
22use libbpf_rs::ProgramInput;
23use libbpf_rs::skel::Skel;
24use scx_arena::ArenaLib;
25use scx_stats::prelude::*;
26use scx_utils::NR_CPU_IDS;
27use scx_utils::Topology;
28use scx_utils::UserExitInfo;
29use scx_utils::build_id;
30use scx_utils::compat;
31use scx_utils::init_libbpf_logging;
32use scx_utils::libbpf_clap_opts::LibbpfOpts;
33use scx_utils::pm::{
34 cpu_idle_resume_latency_supported, epp_supported, for_each_uncore_domain, get_epp,
35 get_turbo_enabled, get_uncore_max_freq_khz, get_uncore_min_freq_khz, set_epp,
36 set_turbo_enabled, set_uncore_max_freq_khz, turbo_supported, uncore_freq_supported,
37 update_cpu_idle_resume_latency,
38};
39use scx_utils::scx_ops_attach;
40use scx_utils::scx_ops_load;
41use scx_utils::scx_ops_open;
42use scx_utils::uei_exited;
43use scx_utils::uei_report;
44use tracing::{debug, info, warn};
45use tracing_subscriber::filter::EnvFilter;
46
47use bpf_intf::stat_idx_P2DQ_NR_STATS;
48use bpf_intf::stat_idx_P2DQ_STAT_ATQ_ENQ;
49use bpf_intf::stat_idx_P2DQ_STAT_ATQ_REENQ;
50use bpf_intf::stat_idx_P2DQ_STAT_DIRECT;
51use bpf_intf::stat_idx_P2DQ_STAT_DISPATCH_PICK2;
52use bpf_intf::stat_idx_P2DQ_STAT_DSQ_CHANGE;
53use bpf_intf::stat_idx_P2DQ_STAT_DSQ_SAME;
54use bpf_intf::stat_idx_P2DQ_STAT_EAS_BIG_SELECT;
55use bpf_intf::stat_idx_P2DQ_STAT_EAS_FALLBACK;
56use bpf_intf::stat_idx_P2DQ_STAT_EAS_LITTLE_SELECT;
57use bpf_intf::stat_idx_P2DQ_STAT_ENQ_CPU;
58use bpf_intf::stat_idx_P2DQ_STAT_ENQ_INTR;
59use bpf_intf::stat_idx_P2DQ_STAT_ENQ_LLC;
60use bpf_intf::stat_idx_P2DQ_STAT_ENQ_MIG;
61use bpf_intf::stat_idx_P2DQ_STAT_EXEC_BALANCE;
62use bpf_intf::stat_idx_P2DQ_STAT_EXEC_SAME_LLC;
63use bpf_intf::stat_idx_P2DQ_STAT_FORK_BALANCE;
64use bpf_intf::stat_idx_P2DQ_STAT_FORK_SAME_LLC;
65use bpf_intf::stat_idx_P2DQ_STAT_IDLE;
66use bpf_intf::stat_idx_P2DQ_STAT_KEEP;
67use bpf_intf::stat_idx_P2DQ_STAT_LLC_MIGRATION;
68use bpf_intf::stat_idx_P2DQ_STAT_NODE_MIGRATION;
69use bpf_intf::stat_idx_P2DQ_STAT_SELECT_PICK2;
70use bpf_intf::stat_idx_P2DQ_STAT_THERMAL_AVOID;
71use bpf_intf::stat_idx_P2DQ_STAT_THERMAL_KICK;
72use bpf_intf::stat_idx_P2DQ_STAT_WAKE_LLC;
73use bpf_intf::stat_idx_P2DQ_STAT_WAKE_MIG;
74use bpf_intf::stat_idx_P2DQ_STAT_WAKE_PREV;
75use scx_p2dq::SchedulerOpts;
76use scx_p2dq::TOPO;
77use scx_p2dq::bpf_intf;
78use scx_p2dq::bpf_skel::*;
79
80const SCHEDULER_NAME: &str = "scx_p2dq";
81#[derive(Debug, Parser)]
87struct CliOpts {
88 #[clap(short = 'v', long, action = clap::ArgAction::Count)]
90 verbose: u8,
91
92 #[clap(long, default_value = "info")]
95 pub log_level: String,
96
97 #[clap(long)]
99 pub stats: Option<f64>,
100
101 #[clap(long)]
104 pub monitor: Option<f64>,
105
106 #[clap(long)]
108 pub version: bool,
109
110 #[clap(long)]
112 pub run_id: Option<u64>,
113
114 #[clap(flatten)]
115 pub sched: SchedulerOpts,
116
117 #[clap(flatten, next_help_heading = "Libbpf Options")]
118 pub libbpf: LibbpfOpts,
119}
120
121struct Scheduler<'a> {
122 skel: BpfSkel<'a>,
123 struct_ops: Option<libbpf_rs::Link>,
124 debug_level: u8,
125
126 stats_server: StatsServer<(), Metrics>,
127}
128
129impl<'a> Scheduler<'a> {
130 fn init(
131 opts: &SchedulerOpts,
132 libbpf_ops: &LibbpfOpts,
133 open_object: &'a mut MaybeUninit<OpenObject>,
134 log_level: &str,
135 ) -> Result<Self> {
136 let debug_level = if log_level.contains("trace") {
138 2
139 } else if log_level.contains("debug") {
140 1
141 } else {
142 0
143 };
144 let mut skel_builder = BpfSkelBuilder::default();
145 skel_builder.obj_builder.debug(debug_level > 1);
146 init_libbpf_logging(None);
147 info!(
148 "Running scx_p2dq (build ID: {})",
149 build_id::full_version(env!("CARGO_PKG_VERSION"))
150 );
151 let topo = if opts.virt_llc_enabled {
152 Topology::with_args(&opts.topo)?
153 } else {
154 Topology::new()?
155 };
156 let open_opts = libbpf_ops.clone().into_bpf_open_opts();
157 let mut open_skel = scx_ops_open!(skel_builder, open_object, p2dq, open_opts).context(
158 "Failed to open BPF object. This can be caused by a mismatch between the kernel \
159 version and the BPF object, permission or other libbpf issues. Try running `dmesg \
160 | grep bpf` to see if there are any error messages related to the BPF object. See \
161 the LibbpfOptions section in the help for more information on configuration related \
162 to this issue or file an issue on the scx repo if the problem persists. \
163 https://github.com/sched-ext/scx/issues/new?labels=scx_p2dq&title=scx_p2dq:%20New%20Issue&assignees=hodgesds&body=Kernel%20version:%20(fill%20me%20out)%0ADistribution:%20(fill%20me%20out)%0AHardware:%20(fill%20me%20out)%0A%0AIssue:%20(fill%20me%20out)"
164 )?;
165
166 #[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
170 open_skel.progs.on_thermal_pressure.set_autoload(false);
171
172 let hw_profile = scx_p2dq::HardwareProfile::detect();
174 let mut opts_optimized = opts.clone();
175 if opts.hw_auto_optimize {
176 hw_profile.optimize_scheduler_opts(&mut opts_optimized);
177 }
178
179 scx_p2dq::init_open_skel!(
180 &mut open_skel,
181 topo,
182 &opts_optimized,
183 debug_level,
184 &hw_profile
185 )?;
186
187 #[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
189 {
190 let thermal_enabled = std::path::Path::new(
191 "/sys/kernel/tracing/events/thermal_pressure/hw_pressure_update",
192 )
193 .exists()
194 || std::path::Path::new(
195 "/sys/kernel/debug/tracing/events/thermal_pressure/hw_pressure_update",
196 )
197 .exists();
198
199 if thermal_enabled {
200 debug!(
201 "Kernel supports thermal pressure tracking, enabling hw_pressure_update tracepoint"
202 );
203 open_skel.progs.on_thermal_pressure.set_autoload(true);
204 stats::set_thermal_tracking_enabled(true);
205
206 open_skel
207 .maps
208 .rodata_data
209 .as_mut()
210 .unwrap()
211 .p2dq_config
212 .thermal_enabled = std::mem::MaybeUninit::new(true);
213 } else {
214 debug!(
215 "Kernel does not support thermal pressure tracking (CONFIG_SCHED_HW_PRESSURE not enabled)"
216 );
217 }
218 }
219
220 if opts_optimized.enable_eas {
221 stats::set_eas_enabled(true);
222 }
223
224 if opts_optimized.atq_enabled && compat::ksym_exists("bpf_spin_unlock").unwrap_or(false) {
225 stats::set_atq_enabled(true);
226 }
227
228 if opts.queued_wakeup {
229 open_skel.struct_ops.p2dq_mut().flags |= *compat::SCX_OPS_ALLOW_QUEUED_WAKEUP;
230 }
231 open_skel.struct_ops.p2dq_mut().flags |= *compat::SCX_OPS_KEEP_BUILTIN_IDLE;
232
233 unsafe {
237 libbpf_rs::libbpf_sys::bpf_map__set_autoattach(
238 open_skel.maps.p2dq.as_libbpf_object().as_ptr(),
239 false,
240 );
241 }
242
243 let mut skel = scx_ops_load!(open_skel, p2dq, uei)?;
244 scx_p2dq::init_skel!(&mut skel, topo);
245
246 let stats_server = StatsServer::new(stats::server_data()).launch()?;
247
248 Ok(Self {
249 skel,
250 struct_ops: None,
251 debug_level,
252 stats_server,
253 })
254 }
255
256 fn get_metrics(&self) -> Metrics {
257 let mut stats = vec![0u64; stat_idx_P2DQ_NR_STATS as usize];
258 let stats_map = &self.skel.maps.stats;
259 for stat in 0..stat_idx_P2DQ_NR_STATS {
260 let cpu_stat_vec: Vec<Vec<u8>> = stats_map
261 .lookup_percpu(&stat.to_ne_bytes(), libbpf_rs::MapFlags::ANY)
262 .unwrap()
263 .unwrap();
264 let sum: u64 = cpu_stat_vec
265 .iter()
266 .map(|val| u64::from_ne_bytes(val.as_slice().try_into().unwrap()))
267 .sum();
268 stats[stat as usize] = sum;
269 }
270 Metrics {
271 atq_enq: stats[stat_idx_P2DQ_STAT_ATQ_ENQ as usize],
272 atq_reenq: stats[stat_idx_P2DQ_STAT_ATQ_REENQ as usize],
273 direct: stats[stat_idx_P2DQ_STAT_DIRECT as usize],
274 idle: stats[stat_idx_P2DQ_STAT_IDLE as usize],
275 dsq_change: stats[stat_idx_P2DQ_STAT_DSQ_CHANGE as usize],
276 same_dsq: stats[stat_idx_P2DQ_STAT_DSQ_SAME as usize],
277 keep: stats[stat_idx_P2DQ_STAT_KEEP as usize],
278 enq_cpu: stats[stat_idx_P2DQ_STAT_ENQ_CPU as usize],
279 enq_intr: stats[stat_idx_P2DQ_STAT_ENQ_INTR as usize],
280 enq_llc: stats[stat_idx_P2DQ_STAT_ENQ_LLC as usize],
281 enq_mig: stats[stat_idx_P2DQ_STAT_ENQ_MIG as usize],
282 select_pick2: stats[stat_idx_P2DQ_STAT_SELECT_PICK2 as usize],
283 dispatch_pick2: stats[stat_idx_P2DQ_STAT_DISPATCH_PICK2 as usize],
284 llc_migrations: stats[stat_idx_P2DQ_STAT_LLC_MIGRATION as usize],
285 node_migrations: stats[stat_idx_P2DQ_STAT_NODE_MIGRATION as usize],
286 wake_prev: stats[stat_idx_P2DQ_STAT_WAKE_PREV as usize],
287 wake_llc: stats[stat_idx_P2DQ_STAT_WAKE_LLC as usize],
288 wake_mig: stats[stat_idx_P2DQ_STAT_WAKE_MIG as usize],
289 fork_balance: stats[stat_idx_P2DQ_STAT_FORK_BALANCE as usize],
290 exec_balance: stats[stat_idx_P2DQ_STAT_EXEC_BALANCE as usize],
291 fork_same_llc: stats[stat_idx_P2DQ_STAT_FORK_SAME_LLC as usize],
292 exec_same_llc: stats[stat_idx_P2DQ_STAT_EXEC_SAME_LLC as usize],
293 thermal_kick: stats[stat_idx_P2DQ_STAT_THERMAL_KICK as usize],
294 thermal_avoid: stats[stat_idx_P2DQ_STAT_THERMAL_AVOID as usize],
295 eas_little_select: stats[stat_idx_P2DQ_STAT_EAS_LITTLE_SELECT as usize],
296 eas_big_select: stats[stat_idx_P2DQ_STAT_EAS_BIG_SELECT as usize],
297 eas_fallback: stats[stat_idx_P2DQ_STAT_EAS_FALLBACK as usize],
298 }
299 }
300
301 fn run(&mut self, shutdown: Arc<AtomicBool>) -> Result<UserExitInfo> {
302 let (res_ch, req_ch) = self.stats_server.channels();
303
304 while !shutdown.load(Ordering::Relaxed) && !uei_exited!(&self.skel, uei) {
305 match req_ch.recv_timeout(Duration::from_secs(1)) {
306 Ok(()) => res_ch.send(self.get_metrics())?,
307 Err(RecvTimeoutError::Timeout) => {}
308 Err(e) => Err(e)?,
309 }
310 }
311
312 let _ = self.struct_ops.take();
313 uei_report!(&self.skel, uei)
314 }
315
316 fn print_topology(&mut self) -> Result<()> {
317 let input = ProgramInput {
318 ..Default::default()
319 };
320
321 let output = self.skel.progs.arena_topology_print.test_run(input)?;
322 if output.return_value != 0 {
323 bail!(
324 "Could not initialize arenas, topo_print returned {}",
325 output.return_value as i32
326 );
327 }
328
329 Ok(())
330 }
331
332 fn start(&mut self) -> Result<()> {
333 self.struct_ops = Some(scx_ops_attach!(self.skel, p2dq)?);
334
335 if self.debug_level > 0 {
336 self.print_topology()?;
337 }
338
339 info!("P2DQ scheduler started! Run `scx_p2dq --monitor` for metrics.");
340
341 Ok(())
342 }
343}
344
345impl Drop for Scheduler<'_> {
346 fn drop(&mut self) {
347 info!("Unregister {SCHEDULER_NAME} scheduler");
348
349 if let Some(struct_ops) = self.struct_ops.take() {
350 drop(struct_ops);
351 }
352 }
353}
354
355#[clap_main::clap_main]
356fn main(opts: CliOpts) -> Result<()> {
357 if opts.version {
358 println!(
359 "scx_p2dq: {}",
360 build_id::full_version(env!("CARGO_PKG_VERSION"))
361 );
362 return Ok(());
363 }
364
365 let env_filter = EnvFilter::try_from_default_env()
366 .or_else(|_| match EnvFilter::try_new(&opts.log_level) {
367 Ok(filter) => Ok(filter),
368 Err(e) => {
369 eprintln!(
370 "invalid log envvar: {}, using info, err is: {}",
371 opts.log_level, e
372 );
373 EnvFilter::try_new("info")
374 }
375 })
376 .unwrap_or_else(|_| EnvFilter::new("info"));
377
378 match tracing_subscriber::fmt()
379 .with_env_filter(env_filter)
380 .with_target(true)
381 .with_thread_ids(true)
382 .with_file(true)
383 .with_line_number(true)
384 .try_init()
385 {
386 Ok(()) => {}
387 Err(e) => eprintln!("failed to init logger: {}", e),
388 }
389
390 if opts.verbose > 0 {
391 warn!("Setting verbose via -v is deprecated and will be an error in future releases.");
392 }
393
394 if let Some(run_id) = opts.run_id {
395 info!("scx_p2dq run_id: {}", run_id);
396 }
397
398 let shutdown = Arc::new(AtomicBool::new(false));
399 let shutdown_clone = shutdown.clone();
400 ctrlc::set_handler(move || {
401 shutdown_clone.store(true, Ordering::Relaxed);
402 })
403 .context("Error setting Ctrl-C handler")?;
404
405 if let Some(intv) = opts.monitor.or(opts.stats) {
406 let shutdown_copy = shutdown.clone();
407 let jh = std::thread::spawn(move || {
408 match stats::monitor(Duration::from_secs_f64(intv), shutdown_copy) {
409 Ok(_) => {
410 debug!("stats monitor thread finished successfully")
411 }
412 Err(error_object) => {
413 warn!("stats monitor thread finished because of an error {error_object}")
414 }
415 }
416 });
417 if opts.monitor.is_some() {
418 let _ = jh.join();
419 return Ok(());
420 }
421 }
422
423 if let Some(idle_resume_us) = opts.sched.idle_resume_us {
424 if !cpu_idle_resume_latency_supported() {
425 warn!("idle resume latency not supported");
426 } else if idle_resume_us > 0 {
427 info!("Setting idle QoS to {idle_resume_us}us");
428 for cpu in TOPO.all_cpus.values() {
429 update_cpu_idle_resume_latency(cpu.id, idle_resume_us.try_into().unwrap())?;
430 }
431 }
432 }
433
434 let is_efficiency = opts.sched.sched_mode == scx_p2dq::SchedMode::Efficiency;
435 let is_performance = opts.sched.sched_mode == scx_p2dq::SchedMode::Performance;
436
437 let mut orig_uncore_freqs: Vec<(u32, u32, u32)> = Vec::new();
438 if opts.sched.uncore_max_freq_mhz.is_some() || is_efficiency || is_performance {
439 if !uncore_freq_supported() {
440 if opts.sched.uncore_max_freq_mhz.is_some() {
441 warn!("uncore frequency control not supported");
442 }
443 } else {
444 let _ = for_each_uncore_domain(|pkg, die| {
445 let freq_khz = if let Some(mhz) = opts.sched.uncore_max_freq_mhz {
446 mhz * 1000
447 } else if is_efficiency {
448 get_uncore_min_freq_khz(pkg, die)?
449 } else {
450 get_uncore_max_freq_khz(pkg, die)?
451 };
452 if let Ok(orig) = get_uncore_max_freq_khz(pkg, die)
453 && orig != freq_khz
454 {
455 info!(
456 "Setting max uncore frequency for package {} die {} to {} MHz",
457 pkg,
458 die,
459 freq_khz / 1000
460 );
461 orig_uncore_freqs.push((pkg, die, orig));
462 set_uncore_max_freq_khz(pkg, die, freq_khz)?;
463 }
464 Ok(())
465 });
466 }
467 }
468
469 let mut orig_epps: Vec<(usize, String)> = Vec::new();
470 if (is_efficiency || is_performance) && epp_supported() {
471 let target_epp = if is_efficiency {
472 "power"
473 } else {
474 "performance"
475 };
476 for cpu in TOPO.all_cpus.values() {
477 if let Ok(orig) = get_epp(cpu.id)
478 && orig != target_epp
479 {
480 if orig_epps.is_empty() {
481 info!("Setting EPP to {} for all CPUs", target_epp);
482 }
483 orig_epps.push((cpu.id, orig));
484 let _ = set_epp(cpu.id, target_epp);
485 }
486 }
487 }
488
489 let orig_turbo = if turbo_supported() {
490 let target_turbo = opts.sched.turbo.or(if is_efficiency {
491 Some(false)
492 } else if is_performance {
493 Some(true)
494 } else {
495 None
496 });
497 if let Some(want_enabled) = target_turbo {
498 if let Ok(current) = get_turbo_enabled() {
499 if current != want_enabled {
500 let mode_suffix = if opts.sched.turbo.is_none() {
501 if is_efficiency {
502 " for efficiency mode"
503 } else {
504 " for performance mode"
505 }
506 } else {
507 ""
508 };
509 info!(
510 "{} turbo{}",
511 if want_enabled {
512 "Enabling"
513 } else {
514 "Disabling"
515 },
516 mode_suffix
517 );
518 let _ = set_turbo_enabled(want_enabled);
519 Some(current)
520 } else {
521 None
522 }
523 } else {
524 None
525 }
526 } else {
527 None
528 }
529 } else {
530 if opts.sched.turbo.is_some() {
531 warn!("turbo control not supported");
532 }
533 None
534 };
535
536 let mut open_object = MaybeUninit::uninit();
537 loop {
538 let _arenalib;
540 let mut sched =
541 Scheduler::init(&opts.sched, &opts.libbpf, &mut open_object, &opts.log_level)?;
542 let task_size = std::mem::size_of::<types::task_p2dq>();
543 _arenalib = ArenaLib::setup(sched.skel.object_mut(), task_size, 0, *NR_CPU_IDS)?;
544
545 sched.start()?;
546
547 if !sched.run(shutdown.clone())?.should_restart() {
548 break;
549 }
550 }
551
552 if let Some(was_enabled) = orig_turbo {
553 info!(
554 "Restoring turbo to {}",
555 if was_enabled { "enabled" } else { "disabled" }
556 );
557 let _ = set_turbo_enabled(was_enabled);
558 }
559
560 if !orig_epps.is_empty() {
561 info!("Restoring EPP settings");
562 for (cpu, epp) in orig_epps {
563 let _ = set_epp(cpu, &epp);
564 }
565 }
566
567 for (pkg, die, orig_khz) in orig_uncore_freqs {
568 info!(
569 "Restoring uncore frequency for package {} die {} to {} MHz",
570 pkg,
571 die,
572 orig_khz / 1000
573 );
574 let _ = set_uncore_max_freq_khz(pkg, die, orig_khz);
575 }
576
577 Ok(())
578}