1use std::collections::BTreeMap;
40
41use super::{AggRule, Aggregated, ScaleLadder, Section};
42
43#[derive(Debug, Clone, Copy)]
52#[non_exhaustive]
53pub struct CtprofMetricDef {
54 pub name: &'static str,
55 pub rule: AggRule,
56 pub sched_class: Option<&'static str>,
80 pub config_gates: &'static [&'static str],
114 pub is_dead: bool,
129 pub description: &'static str,
139 pub section: Section,
152}
153
154pub static CTPROF_METRICS: &[CtprofMetricDef] = &[
172 CtprofMetricDef {
174 name: "thread_count",
175 rule: AggRule::SumCount(|_| crate::metric_types::MonotonicCount(1)),
176 sched_class: None,
177 config_gates: &[],
178 is_dead: false,
179 description: "Number of threads in this group. Each thread contributes 1; the sum is the group population. Useful for --sort-by thread_count:desc to find groups where thread count changed the most.",
180 section: Section::Primary,
181 },
182 CtprofMetricDef {
184 name: "policy",
185 rule: AggRule::Mode(|t| t.policy.clone()),
186 sched_class: None,
187 config_gates: &[],
188 is_dead: false,
189 description: "Scheduling policy (SCHED_OTHER, SCHED_FIFO, SCHED_RR, SCHED_BATCH, SCHED_IDLE, SCHED_DEADLINE, SCHED_EXT).",
190 section: Section::Primary,
191 },
192 CtprofMetricDef {
193 name: "nice",
194 rule: AggRule::RangeI32(|t| t.nice),
195 sched_class: None,
196 config_gates: &[],
197 is_dead: false,
198 description: "Nice value (-20..19); CFS priority knob.",
199 section: Section::Primary,
200 },
201 CtprofMetricDef {
208 name: "priority",
209 rule: AggRule::RangeI32(|t| t.priority),
210 sched_class: None,
211 config_gates: &[],
212 is_dead: false,
213 description: "Kernel task priority from /proc/<tid>/stat field 18 (CFS=[0..39], RT=[-2..-100], DL=-101).",
214 section: Section::Primary,
215 },
216 CtprofMetricDef {
222 name: "rt_priority",
223 rule: AggRule::RangeU32(|t| t.rt_priority),
224 sched_class: None,
225 config_gates: &[],
226 is_dead: false,
227 description: "Real-time scheduler priority (0..99); 0 for non-RT tasks.",
228 section: Section::Primary,
229 },
230 CtprofMetricDef {
231 name: "cpu_affinity",
232 rule: AggRule::Affinity(|t| t.cpu_affinity.clone()),
233 sched_class: None,
234 config_gates: &[],
235 is_dead: false,
236 description: "Set of CPUs the task is allowed to run on (sched_getaffinity result).",
237 section: Section::Primary,
238 },
239 CtprofMetricDef {
240 name: "processor",
241 rule: AggRule::RangeI32(|t| t.processor),
242 sched_class: None,
243 config_gates: &[],
244 is_dead: false,
245 description: "Last CPU the task ran on.",
246 section: Section::Primary,
247 },
248 CtprofMetricDef {
249 name: "state",
250 rule: AggRule::ModeChar(|t| t.state),
251 sched_class: None,
252 config_gates: &[],
253 is_dead: false,
254 description: "Task state letter (R running, S sleeping, D uninterruptible, Z zombie, T stopped).",
255 section: Section::Primary,
256 },
257 CtprofMetricDef {
262 name: "ext_enabled",
263 rule: AggRule::ModeBool(|t| t.ext_enabled),
264 sched_class: None,
265 config_gates: &["CONFIG_SCHED_CLASS_EXT"],
266 is_dead: false,
267 description: "Whether the task is currently dispatched on the sched_ext class.",
268 section: Section::Primary,
269 },
270 CtprofMetricDef {
282 name: "nr_threads",
283 rule: AggRule::MaxGaugeCount(|t| t.nr_threads),
284 sched_class: None,
285 config_gates: &[],
286 is_dead: false,
287 description: "Process-wide thread count (signal_struct->nr_threads); leader-only.",
288 section: Section::Primary,
289 },
290 CtprofMetricDef {
295 name: "run_time_ns",
296 rule: AggRule::SumNs(|t| t.run_time_ns),
297 sched_class: None,
298 config_gates: &["CONFIG_SCHED_INFO"],
299 is_dead: false,
300 description: "Cumulative on-CPU time, ns; /proc/<tid>/schedstat field 1.",
301 section: Section::Primary,
302 },
303 CtprofMetricDef {
307 name: "wait_time_ns",
308 rule: AggRule::SumNs(|t| t.wait_time_ns),
309 sched_class: None,
310 config_gates: &["CONFIG_SCHED_INFO"],
311 is_dead: false,
312 description: "Cumulative time waiting on the runqueue, ns; schedstat field 2.",
313 section: Section::Primary,
314 },
315 CtprofMetricDef {
318 name: "timeslices",
319 rule: AggRule::SumCount(|t| t.timeslices),
320 sched_class: None,
321 config_gates: &["CONFIG_SCHED_INFO"],
322 is_dead: false,
323 description: "Number of times the task was run on a CPU; schedstat field 3.",
324 section: Section::Primary,
325 },
326 CtprofMetricDef {
327 name: "voluntary_csw",
328 rule: AggRule::SumCount(|t| t.voluntary_csw),
329 sched_class: None,
330 config_gates: &[],
331 is_dead: false,
332 description: "Voluntary context switches (task gave up the CPU itself).",
333 section: Section::Primary,
334 },
335 CtprofMetricDef {
336 name: "nonvoluntary_csw",
337 rule: AggRule::SumCount(|t| t.nonvoluntary_csw),
338 sched_class: None,
339 config_gates: &[],
340 is_dead: false,
341 description: "Involuntary context switches (task was preempted).",
342 section: Section::Primary,
343 },
344 CtprofMetricDef {
351 name: "nr_wakeups",
352 rule: AggRule::SumCount(|t| t.nr_wakeups),
353 sched_class: None,
354 config_gates: &["CONFIG_SCHEDSTATS"],
355 is_dead: false,
356 description: "Total wakeups via try_to_wake_up().",
357 section: Section::Primary,
358 },
359 CtprofMetricDef {
360 name: "nr_wakeups_local",
361 rule: AggRule::SumCount(|t| t.nr_wakeups_local),
362 sched_class: None,
363 config_gates: &["CONFIG_SCHEDSTATS"],
364 is_dead: false,
365 description: "Wakeups landed on the same CPU as the waker.",
366 section: Section::Primary,
367 },
368 CtprofMetricDef {
369 name: "nr_wakeups_remote",
370 rule: AggRule::SumCount(|t| t.nr_wakeups_remote),
371 sched_class: None,
372 config_gates: &["CONFIG_SCHEDSTATS"],
373 is_dead: false,
374 description: "Wakeups landed on a different CPU than the waker.",
375 section: Section::Primary,
376 },
377 CtprofMetricDef {
378 name: "nr_wakeups_sync",
379 rule: AggRule::SumCount(|t| t.nr_wakeups_sync),
380 sched_class: None,
381 config_gates: &["CONFIG_SCHEDSTATS"],
382 is_dead: false,
383 description: "WF_SYNC wakeups (synchronous wakeup hint to scheduler).",
384 section: Section::Primary,
385 },
386 CtprofMetricDef {
387 name: "nr_wakeups_migrate",
388 rule: AggRule::SumCount(|t| t.nr_wakeups_migrate),
389 sched_class: None,
390 config_gates: &["CONFIG_SCHEDSTATS"],
391 is_dead: false,
392 description: "Wakeups where the task migrated to a different CPU than its prior one (WF_MIGRATED); distinct from nr_wakeups_remote (waker CPU != target CPU).",
393 section: Section::Primary,
394 },
395 CtprofMetricDef {
403 name: "nr_wakeups_affine",
404 rule: AggRule::SumCount(|t| t.nr_wakeups_affine),
405 sched_class: Some("cfs-only"),
406 config_gates: &["CONFIG_SCHEDSTATS"],
407 is_dead: false,
408 description: "Wakeups that succeeded under the wake_affine() heuristic.",
409 section: Section::Primary,
410 },
411 CtprofMetricDef {
412 name: "nr_wakeups_affine_attempts",
413 rule: AggRule::SumCount(|t| t.nr_wakeups_affine_attempts),
414 sched_class: Some("cfs-only"),
415 config_gates: &["CONFIG_SCHEDSTATS"],
416 is_dead: false,
417 description: "wake_affine() attempts; success rate = nr_wakeups_affine / attempts.",
418 section: Section::Primary,
419 },
420 CtprofMetricDef {
424 name: "nr_migrations",
425 rule: AggRule::SumCount(|t| t.nr_migrations),
426 sched_class: None,
427 config_gates: &[],
428 is_dead: false,
429 description: "Cumulative cross-CPU migrations of the task.",
430 section: Section::Primary,
431 },
432 CtprofMetricDef {
436 name: "nr_forced_migrations",
437 rule: AggRule::SumCount(|t| t.nr_forced_migrations),
438 sched_class: Some("cfs-only"),
439 config_gates: &["CONFIG_SCHEDSTATS"],
440 is_dead: false,
441 description: "Migrations forced by the CFS load balancer.",
442 section: Section::Primary,
443 },
444 CtprofMetricDef {
448 name: "nr_failed_migrations_affine",
449 rule: AggRule::SumCount(|t| t.nr_failed_migrations_affine),
450 sched_class: Some("cfs-only"),
451 config_gates: &["CONFIG_SCHEDSTATS"],
452 is_dead: false,
453 description: "Load-balancer migrations rejected for cpu-affinity reasons.",
454 section: Section::Primary,
455 },
456 CtprofMetricDef {
457 name: "nr_failed_migrations_running",
458 rule: AggRule::SumCount(|t| t.nr_failed_migrations_running),
459 sched_class: Some("cfs-only"),
460 config_gates: &["CONFIG_SCHEDSTATS"],
461 is_dead: false,
462 description: "Load-balancer migrations rejected because the task was running.",
463 section: Section::Primary,
464 },
465 CtprofMetricDef {
466 name: "nr_failed_migrations_hot",
467 rule: AggRule::SumCount(|t| t.nr_failed_migrations_hot),
468 sched_class: Some("cfs-only"),
469 config_gates: &["CONFIG_SCHEDSTATS"],
470 is_dead: false,
471 description: "Load-balancer migrations rejected because the task was cache-hot.",
472 section: Section::Primary,
473 },
474 CtprofMetricDef {
486 name: "wait_sum",
487 rule: AggRule::SumNs(|t| t.wait_sum),
488 sched_class: Some("non-ext"),
489 config_gates: &["CONFIG_SCHEDSTATS"],
490 is_dead: false,
491 description: "Cumulative time the task waited on the runqueue, ns.",
492 section: Section::Primary,
493 },
494 CtprofMetricDef {
495 name: "wait_count",
496 rule: AggRule::SumCount(|t| t.wait_count),
497 sched_class: Some("non-ext"),
498 config_gates: &["CONFIG_SCHEDSTATS"],
499 is_dead: false,
500 description: "Number of distinct runqueue-wait intervals the task accumulated.",
501 section: Section::Primary,
502 },
503 CtprofMetricDef {
504 name: "wait_max",
505 rule: AggRule::MaxPeak(|t| t.wait_max),
506 sched_class: Some("non-ext"),
507 config_gates: &["CONFIG_SCHEDSTATS"],
508 is_dead: false,
509 description: "Longest single runqueue-wait interval observed, ns.",
510 section: Section::Primary,
511 },
512 CtprofMetricDef {
530 name: "voluntary_sleep_ns",
531 rule: AggRule::SumNs(|t| t.voluntary_sleep_ns),
532 sched_class: Some("non-ext"),
533 config_gates: &["CONFIG_SCHEDSTATS"],
534 is_dead: false,
535 description: "Pure voluntary sleep time (TASK_INTERRUPTIBLE only), ns; capture-side normalized as sum_sleep_runtime - sum_block_runtime so the kernel's sleep/block double-count is stripped before delta math.",
536 section: Section::Primary,
537 },
538 CtprofMetricDef {
539 name: "sleep_max",
540 rule: AggRule::MaxPeak(|t| t.sleep_max),
541 sched_class: Some("non-ext"),
542 config_gates: &["CONFIG_SCHEDSTATS"],
543 is_dead: false,
544 description: "Longest single sleep interval observed, ns.",
545 section: Section::Primary,
546 },
547 CtprofMetricDef {
551 name: "block_sum",
552 rule: AggRule::SumNs(|t| t.block_sum),
553 sched_class: Some("non-ext"),
554 config_gates: &["CONFIG_SCHEDSTATS"],
555 is_dead: false,
556 description: "Cumulative time the task spent blocked (TASK_UNINTERRUPTIBLE), ns.",
557 section: Section::Primary,
558 },
559 CtprofMetricDef {
560 name: "block_max",
561 rule: AggRule::MaxPeak(|t| t.block_max),
562 sched_class: Some("non-ext"),
563 config_gates: &["CONFIG_SCHEDSTATS"],
564 is_dead: false,
565 description: "Longest single uninterruptible-block interval observed, ns.",
566 section: Section::Primary,
567 },
568 CtprofMetricDef {
572 name: "iowait_sum",
573 rule: AggRule::SumNs(|t| t.iowait_sum),
574 sched_class: Some("non-ext"),
575 config_gates: &["CONFIG_SCHEDSTATS"],
576 is_dead: false,
577 description: "Cumulative time the task spent in iowait, ns.",
578 section: Section::Primary,
579 },
580 CtprofMetricDef {
581 name: "iowait_count",
582 rule: AggRule::SumCount(|t| t.iowait_count),
583 sched_class: Some("non-ext"),
584 config_gates: &["CONFIG_SCHEDSTATS"],
585 is_dead: false,
586 description: "Number of distinct iowait intervals the task accumulated.",
587 section: Section::Primary,
588 },
589 CtprofMetricDef {
604 name: "exec_max",
605 rule: AggRule::MaxPeak(|t| t.exec_max),
606 sched_class: None,
607 config_gates: &["CONFIG_SCHEDSTATS"],
608 is_dead: false,
609 description: "Longest single uninterrupted on-CPU run observed, ns.",
610 section: Section::Primary,
611 },
612 CtprofMetricDef {
617 name: "slice_max",
618 rule: AggRule::MaxPeak(|t| t.slice_max),
619 sched_class: Some("cfs-only"),
620 config_gates: &["CONFIG_SCHEDSTATS"],
621 is_dead: false,
622 description: "Longest CFS slice the task was granted, ns.",
623 section: Section::Primary,
624 },
625 CtprofMetricDef {
639 name: "core_forceidle_sum",
640 rule: AggRule::SumNs(|t| t.core_forceidle_sum),
641 sched_class: None,
642 config_gates: &["CONFIG_SCHED_CORE", "CONFIG_SCHEDSTATS"],
643 is_dead: false,
644 description: "Cumulative time this task forced its SMT sibling idle, ns (core scheduling).",
645 section: Section::Primary,
646 },
647 CtprofMetricDef {
660 name: "fair_slice_ns",
661 rule: AggRule::MaxGaugeNs(|t| t.fair_slice_ns),
662 sched_class: Some("fair-policy"),
663 config_gates: &[],
664 is_dead: false,
665 description: "Current scheduler slice, ns; snapshot from /proc/<tid>/sched (stale under sched_ext).",
666 section: Section::Primary,
667 },
668 CtprofMetricDef {
670 name: "allocated_bytes",
671 rule: AggRule::SumBytes(|t| t.allocated_bytes),
672 sched_class: None,
673 config_gates: &[],
674 is_dead: false,
675 description: "jemalloc per-thread allocated bytes (TSD thread_allocated counter).",
676 section: Section::Primary,
677 },
678 CtprofMetricDef {
679 name: "deallocated_bytes",
680 rule: AggRule::SumBytes(|t| t.deallocated_bytes),
681 sched_class: None,
682 config_gates: &[],
683 is_dead: false,
684 description: "jemalloc per-thread deallocated bytes (TSD thread_deallocated counter).",
685 section: Section::Primary,
686 },
687 CtprofMetricDef {
688 name: "minflt",
689 rule: AggRule::SumCount(|t| t.minflt),
690 sched_class: None,
691 config_gates: &[],
692 is_dead: false,
693 description: "Minor page faults (resolved without I/O).",
694 section: Section::Primary,
695 },
696 CtprofMetricDef {
697 name: "majflt",
698 rule: AggRule::SumCount(|t| t.majflt),
699 sched_class: None,
700 config_gates: &[],
701 is_dead: false,
702 description: "Major page faults (required disk I/O to resolve).",
703 section: Section::Primary,
704 },
705 CtprofMetricDef {
706 name: "utime_clock_ticks",
707 rule: AggRule::SumTicks(|t| t.utime_clock_ticks),
708 sched_class: None,
709 config_gates: &[],
710 is_dead: false,
711 description: "User-mode CPU time, USER_HZ ticks; /proc/<tid>/stat field 14.",
712 section: Section::Primary,
713 },
714 CtprofMetricDef {
715 name: "stime_clock_ticks",
716 rule: AggRule::SumTicks(|t| t.stime_clock_ticks),
717 sched_class: None,
718 config_gates: &[],
719 is_dead: false,
720 description: "Kernel-mode CPU time, USER_HZ ticks; /proc/<tid>/stat field 15.",
721 section: Section::Primary,
722 },
723 CtprofMetricDef {
731 name: "rchar",
732 rule: AggRule::SumBytes(|t| t.rchar),
733 sched_class: None,
734 config_gates: &["CONFIG_TASK_IO_ACCOUNTING"],
735 is_dead: false,
736 description: "Bytes read at the read syscall layer (incl. cached / pagecache hits).",
737 section: Section::Primary,
738 },
739 CtprofMetricDef {
740 name: "wchar",
741 rule: AggRule::SumBytes(|t| t.wchar),
742 sched_class: None,
743 config_gates: &["CONFIG_TASK_IO_ACCOUNTING"],
744 is_dead: false,
745 description: "Bytes written at the write syscall layer (incl. pagecache / writeback).",
746 section: Section::Primary,
747 },
748 CtprofMetricDef {
749 name: "syscr",
750 rule: AggRule::SumCount(|t| t.syscr),
751 sched_class: None,
752 config_gates: &["CONFIG_TASK_IO_ACCOUNTING"],
753 is_dead: false,
754 description: "Number of read syscalls.",
755 section: Section::Primary,
756 },
757 CtprofMetricDef {
758 name: "syscw",
759 rule: AggRule::SumCount(|t| t.syscw),
760 sched_class: None,
761 config_gates: &["CONFIG_TASK_IO_ACCOUNTING"],
762 is_dead: false,
763 description: "Number of write syscalls.",
764 section: Section::Primary,
765 },
766 CtprofMetricDef {
767 name: "read_bytes",
768 rule: AggRule::SumBytes(|t| t.read_bytes),
769 sched_class: None,
770 config_gates: &["CONFIG_TASK_IO_ACCOUNTING"],
771 is_dead: false,
772 description: "Bytes that hit the storage device on read (excludes pagecache hits).",
773 section: Section::Primary,
774 },
775 CtprofMetricDef {
776 name: "write_bytes",
777 rule: AggRule::SumBytes(|t| t.write_bytes),
778 sched_class: None,
779 config_gates: &["CONFIG_TASK_IO_ACCOUNTING"],
780 is_dead: false,
781 description: "Bytes that hit the storage device on write (post-writeback).",
782 section: Section::Primary,
783 },
784 CtprofMetricDef {
798 name: "cancelled_write_bytes",
799 rule: AggRule::SumBytes(|t| t.cancelled_write_bytes),
800 sched_class: None,
801 config_gates: &["CONFIG_TASK_IO_ACCOUNTING"],
802 is_dead: false,
803 description: "Bytes the kernel deaccounted from a prior dirty-write because the page was reclaimed without writeback (truncate / inode invalidation); recorded on the truncating task, not the writer. Per-thread `write_bytes - cancelled_write_bytes` is NOT a valid derivation — see field doc.",
804 section: Section::Primary,
805 },
806 CtprofMetricDef {
838 name: "cpu_delay_count",
839 rule: AggRule::SumCount(|t| t.cpu_delay_count),
840 sched_class: None,
841 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
842 is_dead: false,
843 description: "Number of off-CPU windows the task waited for the runqueue to schedule it (taskstats cpu_count). RACY: count + total are not updated atomically.",
844 section: Section::TaskstatsDelay,
845 },
846 CtprofMetricDef {
847 name: "cpu_delay_total_ns",
848 rule: AggRule::SumNs(|t| t.cpu_delay_total_ns),
849 sched_class: None,
850 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
851 is_dead: false,
852 description: "Cumulative ns the task waited on the runqueue (taskstats cpu_delay_total). Distinct from `wait_sum` (schedstat) which captures the same wait-for-CPU bucket via a different code path. RACY (see cpu_delay_count).",
853 section: Section::TaskstatsDelay,
854 },
855 CtprofMetricDef {
856 name: "cpu_delay_max_ns",
857 rule: AggRule::MaxPeak(|t| t.cpu_delay_max_ns),
858 sched_class: None,
859 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
860 is_dead: false,
861 description: "Longest single CPU-wait window observed, ns (taskstats cpu_delay_max).",
862 section: Section::TaskstatsDelay,
863 },
864 CtprofMetricDef {
865 name: "cpu_delay_min_ns",
866 rule: AggRule::MaxPeak(|t| t.cpu_delay_min_ns),
867 sched_class: None,
868 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
869 is_dead: false,
870 description: "Shortest non-zero CPU-wait window observed, ns (taskstats cpu_delay_min). Sentinel 0 means \"no events observed\" — compare against cpu_delay_count.",
871 section: Section::TaskstatsDelay,
872 },
873 CtprofMetricDef {
876 name: "blkio_delay_count",
877 rule: AggRule::SumCount(|t| t.blkio_delay_count),
878 sched_class: None,
879 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
880 is_dead: false,
881 description: "Number of synchronous block-I/O wait windows (taskstats blkio_count).",
882 section: Section::TaskstatsDelay,
883 },
884 CtprofMetricDef {
885 name: "blkio_delay_total_ns",
886 rule: AggRule::SumNs(|t| t.blkio_delay_total_ns),
887 sched_class: None,
888 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
889 is_dead: false,
890 description: "Cumulative ns waiting on synchronous block I/O (taskstats blkio_delay_total). Distinct from `iowait_sum` (schedstat).",
891 section: Section::TaskstatsDelay,
892 },
893 CtprofMetricDef {
894 name: "blkio_delay_max_ns",
895 rule: AggRule::MaxPeak(|t| t.blkio_delay_max_ns),
896 sched_class: None,
897 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
898 is_dead: false,
899 description: "Longest single block-I/O wait observed, ns (taskstats blkio_delay_max).",
900 section: Section::TaskstatsDelay,
901 },
902 CtprofMetricDef {
903 name: "blkio_delay_min_ns",
904 rule: AggRule::MaxPeak(|t| t.blkio_delay_min_ns),
905 sched_class: None,
906 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
907 is_dead: false,
908 description: "Shortest non-zero block-I/O wait observed, ns (taskstats blkio_delay_min). Sentinel 0 means \"no events observed\".",
909 section: Section::TaskstatsDelay,
910 },
911 CtprofMetricDef {
915 name: "swapin_delay_count",
916 rule: AggRule::SumCount(|t| t.swapin_delay_count),
917 sched_class: None,
918 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
919 is_dead: false,
920 description: "Number of swap-in wait windows (taskstats swapin_count). OVERLAPS with thrashing_delay_count — do not sum.",
921 section: Section::TaskstatsDelay,
922 },
923 CtprofMetricDef {
924 name: "swapin_delay_total_ns",
925 rule: AggRule::SumNs(|t| t.swapin_delay_total_ns),
926 sched_class: None,
927 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
928 is_dead: false,
929 description: "Cumulative ns waiting for swap-in to complete (taskstats swapin_delay_total).",
930 section: Section::TaskstatsDelay,
931 },
932 CtprofMetricDef {
933 name: "swapin_delay_max_ns",
934 rule: AggRule::MaxPeak(|t| t.swapin_delay_max_ns),
935 sched_class: None,
936 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
937 is_dead: false,
938 description: "Longest single swap-in wait observed, ns (taskstats swapin_delay_max).",
939 section: Section::TaskstatsDelay,
940 },
941 CtprofMetricDef {
942 name: "swapin_delay_min_ns",
943 rule: AggRule::MaxPeak(|t| t.swapin_delay_min_ns),
944 sched_class: None,
945 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
946 is_dead: false,
947 description: "Shortest non-zero swap-in wait observed, ns (taskstats swapin_delay_min). Sentinel 0 means \"no events observed\".",
948 section: Section::TaskstatsDelay,
949 },
950 CtprofMetricDef {
952 name: "freepages_delay_count",
953 rule: AggRule::SumCount(|t| t.freepages_delay_count),
954 sched_class: None,
955 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
956 is_dead: false,
957 description: "Number of direct-reclaim wait windows (taskstats freepages_count).",
958 section: Section::TaskstatsDelay,
959 },
960 CtprofMetricDef {
961 name: "freepages_delay_total_ns",
962 rule: AggRule::SumNs(|t| t.freepages_delay_total_ns),
963 sched_class: None,
964 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
965 is_dead: false,
966 description: "Cumulative ns waiting in direct memory reclaim (taskstats freepages_delay_total).",
967 section: Section::TaskstatsDelay,
968 },
969 CtprofMetricDef {
970 name: "freepages_delay_max_ns",
971 rule: AggRule::MaxPeak(|t| t.freepages_delay_max_ns),
972 sched_class: None,
973 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
974 is_dead: false,
975 description: "Longest single direct-reclaim wait observed, ns (taskstats freepages_delay_max).",
976 section: Section::TaskstatsDelay,
977 },
978 CtprofMetricDef {
979 name: "freepages_delay_min_ns",
980 rule: AggRule::MaxPeak(|t| t.freepages_delay_min_ns),
981 sched_class: None,
982 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
983 is_dead: false,
984 description: "Shortest non-zero direct-reclaim wait observed, ns (taskstats freepages_delay_min). Sentinel 0 means \"no events observed\".",
985 section: Section::TaskstatsDelay,
986 },
987 CtprofMetricDef {
989 name: "thrashing_delay_count",
990 rule: AggRule::SumCount(|t| t.thrashing_delay_count),
991 sched_class: None,
992 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
993 is_dead: false,
994 description: "Number of thrashing wait windows (taskstats thrashing_count). OVERLAPS with swapin_delay_count — do not sum.",
995 section: Section::TaskstatsDelay,
996 },
997 CtprofMetricDef {
998 name: "thrashing_delay_total_ns",
999 rule: AggRule::SumNs(|t| t.thrashing_delay_total_ns),
1000 sched_class: None,
1001 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
1002 is_dead: false,
1003 description: "Cumulative ns waiting under thrashing pressure (taskstats thrashing_delay_total).",
1004 section: Section::TaskstatsDelay,
1005 },
1006 CtprofMetricDef {
1007 name: "thrashing_delay_max_ns",
1008 rule: AggRule::MaxPeak(|t| t.thrashing_delay_max_ns),
1009 sched_class: None,
1010 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
1011 is_dead: false,
1012 description: "Longest single thrashing wait observed, ns (taskstats thrashing_delay_max).",
1013 section: Section::TaskstatsDelay,
1014 },
1015 CtprofMetricDef {
1016 name: "thrashing_delay_min_ns",
1017 rule: AggRule::MaxPeak(|t| t.thrashing_delay_min_ns),
1018 sched_class: None,
1019 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
1020 is_dead: false,
1021 description: "Shortest non-zero thrashing wait observed, ns (taskstats thrashing_delay_min). Sentinel 0 means \"no events observed\".",
1022 section: Section::TaskstatsDelay,
1023 },
1024 CtprofMetricDef {
1026 name: "compact_delay_count",
1027 rule: AggRule::SumCount(|t| t.compact_delay_count),
1028 sched_class: None,
1029 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
1030 is_dead: false,
1031 description: "Number of memory-compaction wait windows (taskstats compact_count).",
1032 section: Section::TaskstatsDelay,
1033 },
1034 CtprofMetricDef {
1035 name: "compact_delay_total_ns",
1036 rule: AggRule::SumNs(|t| t.compact_delay_total_ns),
1037 sched_class: None,
1038 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
1039 is_dead: false,
1040 description: "Cumulative ns waiting on memory compaction (taskstats compact_delay_total).",
1041 section: Section::TaskstatsDelay,
1042 },
1043 CtprofMetricDef {
1044 name: "compact_delay_max_ns",
1045 rule: AggRule::MaxPeak(|t| t.compact_delay_max_ns),
1046 sched_class: None,
1047 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
1048 is_dead: false,
1049 description: "Longest single compaction wait observed, ns (taskstats compact_delay_max).",
1050 section: Section::TaskstatsDelay,
1051 },
1052 CtprofMetricDef {
1053 name: "compact_delay_min_ns",
1054 rule: AggRule::MaxPeak(|t| t.compact_delay_min_ns),
1055 sched_class: None,
1056 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
1057 is_dead: false,
1058 description: "Shortest non-zero compaction wait observed, ns (taskstats compact_delay_min). Sentinel 0 means \"no events observed\".",
1059 section: Section::TaskstatsDelay,
1060 },
1061 CtprofMetricDef {
1063 name: "wpcopy_delay_count",
1064 rule: AggRule::SumCount(|t| t.wpcopy_delay_count),
1065 sched_class: None,
1066 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
1067 is_dead: false,
1068 description: "Number of write-protect-copy (CoW) fault wait windows (taskstats wpcopy_count).",
1069 section: Section::TaskstatsDelay,
1070 },
1071 CtprofMetricDef {
1072 name: "wpcopy_delay_total_ns",
1073 rule: AggRule::SumNs(|t| t.wpcopy_delay_total_ns),
1074 sched_class: None,
1075 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
1076 is_dead: false,
1077 description: "Cumulative ns waiting on write-protect-copy faults (taskstats wpcopy_delay_total).",
1078 section: Section::TaskstatsDelay,
1079 },
1080 CtprofMetricDef {
1081 name: "wpcopy_delay_max_ns",
1082 rule: AggRule::MaxPeak(|t| t.wpcopy_delay_max_ns),
1083 sched_class: None,
1084 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
1085 is_dead: false,
1086 description: "Longest single write-protect-copy fault wait observed, ns (taskstats wpcopy_delay_max).",
1087 section: Section::TaskstatsDelay,
1088 },
1089 CtprofMetricDef {
1090 name: "wpcopy_delay_min_ns",
1091 rule: AggRule::MaxPeak(|t| t.wpcopy_delay_min_ns),
1092 sched_class: None,
1093 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
1094 is_dead: false,
1095 description: "Shortest non-zero write-protect-copy fault wait observed, ns (taskstats wpcopy_delay_min). Sentinel 0 means \"no events observed\".",
1096 section: Section::TaskstatsDelay,
1097 },
1098 CtprofMetricDef {
1102 name: "irq_delay_count",
1103 rule: AggRule::SumCount(|t| t.irq_delay_count),
1104 sched_class: None,
1105 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
1106 is_dead: false,
1107 description: "Number of IRQ-handler windows charged to the task (taskstats irq_count).",
1108 section: Section::TaskstatsDelay,
1109 },
1110 CtprofMetricDef {
1111 name: "irq_delay_total_ns",
1112 rule: AggRule::SumNs(|t| t.irq_delay_total_ns),
1113 sched_class: None,
1114 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
1115 is_dead: false,
1116 description: "Cumulative ns of IRQ handling charged to the task (taskstats irq_delay_total).",
1117 section: Section::TaskstatsDelay,
1118 },
1119 CtprofMetricDef {
1120 name: "irq_delay_max_ns",
1121 rule: AggRule::MaxPeak(|t| t.irq_delay_max_ns),
1122 sched_class: None,
1123 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
1124 is_dead: false,
1125 description: "Longest single IRQ-handler window observed, ns (taskstats irq_delay_max).",
1126 section: Section::TaskstatsDelay,
1127 },
1128 CtprofMetricDef {
1129 name: "irq_delay_min_ns",
1130 rule: AggRule::MaxPeak(|t| t.irq_delay_min_ns),
1131 sched_class: None,
1132 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_DELAY_ACCT"],
1133 is_dead: false,
1134 description: "Shortest non-zero IRQ-handler window observed, ns (taskstats irq_delay_min). Sentinel 0 means \"no events observed\".",
1135 section: Section::TaskstatsDelay,
1136 },
1137 CtprofMetricDef {
1146 name: "hiwater_rss_bytes",
1147 rule: AggRule::MaxPeakBytes(|t| t.hiwater_rss_bytes),
1148 sched_class: None,
1149 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_XACCT"],
1150 is_dead: false,
1151 description: "Lifetime high-watermark of resident-set size, bytes (taskstats hiwater_rss). Distinct from smaps_rollup_kib[\"Rss\"] which is the CURRENT RSS.",
1152 section: Section::TaskstatsDelay,
1153 },
1154 CtprofMetricDef {
1155 name: "hiwater_vm_bytes",
1156 rule: AggRule::MaxPeakBytes(|t| t.hiwater_vm_bytes),
1157 sched_class: None,
1158 config_gates: &["CONFIG_TASKSTATS", "CONFIG_TASK_XACCT"],
1159 is_dead: false,
1160 description: "Lifetime high-watermark of virtual-memory size, bytes (taskstats hiwater_vm).",
1161 section: Section::TaskstatsDelay,
1162 },
1163];
1164
1165#[derive(Debug, Clone, Copy, PartialEq)]
1205#[non_exhaustive]
1206pub enum DerivedValue {
1207 Scalar(f64),
1215}
1216
1217impl DerivedValue {
1218 pub fn as_f64(&self) -> f64 {
1221 match self {
1222 DerivedValue::Scalar(v) => *v,
1223 }
1224 }
1225}
1226
1227#[derive(Debug, Clone, Copy)]
1239#[non_exhaustive]
1240pub struct DerivedMetricDef {
1241 pub name: &'static str,
1242 pub ladder: ScaleLadder,
1248 pub description: &'static str,
1251 pub inputs: &'static [&'static str],
1255 pub is_ratio: bool,
1282 pub compute: fn(&BTreeMap<String, Aggregated>) -> Option<DerivedValue>,
1286 pub section: Section,
1301}
1302
1303fn input_scalar(metrics: &BTreeMap<String, Aggregated>, name: &str) -> Option<f64> {
1306 metrics.get(name).and_then(|a| a.numeric())
1307}
1308
1309fn ratio_compute(
1315 metrics: &BTreeMap<String, Aggregated>,
1316 numerator: &str,
1317 denominator: &str,
1318) -> Option<DerivedValue> {
1319 let num = input_scalar(metrics, numerator)?;
1320 let den = input_scalar(metrics, denominator)?;
1321 if den == 0.0 {
1322 return None;
1323 }
1324 Some(DerivedValue::Scalar(num / den))
1325}
1326
1327fn ratio_of_sum_compute(
1333 metrics: &BTreeMap<String, Aggregated>,
1334 numerator: &str,
1335 addend: &str,
1336) -> Option<DerivedValue> {
1337 let num = input_scalar(metrics, numerator)?;
1338 let other = input_scalar(metrics, addend)?;
1339 let den = num + other;
1340 if den == 0.0 {
1341 return None;
1342 }
1343 Some(DerivedValue::Scalar(num / den))
1344}
1345
1346pub static CTPROF_DERIVED_METRICS: &[DerivedMetricDef] = &[
1352 DerivedMetricDef {
1353 name: "affine_success_ratio",
1354 ladder: ScaleLadder::None,
1355 description: "wake_affine() success ratio: nr_wakeups_affine / nr_wakeups_affine_attempts.",
1356 inputs: &["nr_wakeups_affine", "nr_wakeups_affine_attempts"],
1357 is_ratio: true,
1358 compute: |m| ratio_compute(m, "nr_wakeups_affine", "nr_wakeups_affine_attempts"),
1359 section: Section::Derived,
1360 },
1361 DerivedMetricDef {
1362 name: "avg_wait_ns",
1363 ladder: ScaleLadder::Ns,
1364 description: "Average runqueue-wait duration per scheduling event: wait_sum / wait_count (ns/event).",
1365 inputs: &["wait_sum", "wait_count"],
1366 is_ratio: false,
1367 compute: |m| ratio_compute(m, "wait_sum", "wait_count"),
1368 section: Section::Derived,
1369 },
1370 DerivedMetricDef {
1377 name: "cpu_efficiency",
1378 ladder: ScaleLadder::None,
1379 description: "Fraction of total scheduler-tracked time spent on-CPU: run_time_ns / (run_time_ns + wait_time_ns).",
1380 inputs: &["run_time_ns", "wait_time_ns"],
1381 is_ratio: true,
1382 compute: |m| ratio_of_sum_compute(m, "run_time_ns", "wait_time_ns"),
1383 section: Section::Derived,
1384 },
1385 DerivedMetricDef {
1386 name: "avg_slice_ns",
1387 ladder: ScaleLadder::Ns,
1388 description: "Average on-CPU slice length per timeslice: run_time_ns / timeslices (ns/timeslice).",
1389 inputs: &["run_time_ns", "timeslices"],
1390 is_ratio: false,
1391 compute: |m| ratio_compute(m, "run_time_ns", "timeslices"),
1392 section: Section::Derived,
1393 },
1394 DerivedMetricDef {
1395 name: "involuntary_csw_ratio",
1396 ladder: ScaleLadder::None,
1397 description: "Fraction of context switches that were preemptions: nonvoluntary_csw / (voluntary_csw + nonvoluntary_csw).",
1398 inputs: &["nonvoluntary_csw", "voluntary_csw"],
1399 is_ratio: true,
1400 compute: |m| ratio_of_sum_compute(m, "nonvoluntary_csw", "voluntary_csw"),
1401 section: Section::Derived,
1402 },
1403 DerivedMetricDef {
1404 name: "disk_io_fraction",
1405 ladder: ScaleLadder::None,
1406 description: "Fraction of read syscall bytes that hit storage: read_bytes / rchar. Typically <= 1.0 but can exceed when readahead pulls more block-device bytes than the syscall requested.",
1407 inputs: &["read_bytes", "rchar"],
1408 is_ratio: true,
1409 compute: |m| ratio_compute(m, "read_bytes", "rchar"),
1410 section: Section::Derived,
1411 },
1412 DerivedMetricDef {
1413 name: "live_heap_estimate",
1414 ladder: ScaleLadder::Bytes,
1415 description: "jemalloc live-heap estimate: allocated_bytes - deallocated_bytes. Signed: negative when deallocations dominate (freelist drains memory allocated before capture, or sampled mid-update on a thread that just released a large arena). Renders a negative value with an explicit minus and the IEC binary suffix (e.g. `-1.907MiB`). Absent (rendered `-`, no value) when the jemalloc family was not captured for the group — a non-jemalloc process, or the TSD probe could not attach — distinct from a measured zero.",
1416 inputs: &["allocated_bytes", "deallocated_bytes"],
1417 is_ratio: false,
1418 compute: |m| {
1419 let alloc = input_scalar(m, "allocated_bytes")?;
1420 let dealloc = input_scalar(m, "deallocated_bytes")?;
1421 Some(DerivedValue::Scalar(alloc - dealloc))
1422 },
1423 section: Section::Derived,
1424 },
1425 DerivedMetricDef {
1426 name: "avg_iowait_ns",
1427 ladder: ScaleLadder::Ns,
1428 description: "Average iowait interval per iowait event: iowait_sum / iowait_count (ns/event).",
1429 inputs: &["iowait_sum", "iowait_count"],
1430 is_ratio: false,
1431 compute: |m| ratio_compute(m, "iowait_sum", "iowait_count"),
1432 section: Section::Derived,
1433 },
1434 DerivedMetricDef {
1444 name: "avg_cpu_delay_ns",
1445 ladder: ScaleLadder::Ns,
1446 description: "Average CPU-wait per scheduling event: cpu_delay_total_ns / cpu_delay_count (ns/event). RACY: the kernel updates count + total via the lockless sched_info path, so a concurrent reader may observe one ahead of the other; the quotient is approximate at the sub-event scale and stable at the integrated scale.",
1447 inputs: &["cpu_delay_total_ns", "cpu_delay_count"],
1448 is_ratio: false,
1449 compute: |m| ratio_compute(m, "cpu_delay_total_ns", "cpu_delay_count"),
1450 section: Section::TaskstatsDelay,
1451 },
1452 DerivedMetricDef {
1453 name: "avg_blkio_delay_ns",
1454 ladder: ScaleLadder::Ns,
1455 description: "Average synchronous block-I/O wait per event: blkio_delay_total_ns / blkio_delay_count (ns/event). Distinct from avg_iowait_ns (schedstat) — this travels through the delayacct path and is the canonical delay-accounting block-I/O reading.",
1456 inputs: &["blkio_delay_total_ns", "blkio_delay_count"],
1457 is_ratio: false,
1458 compute: |m| ratio_compute(m, "blkio_delay_total_ns", "blkio_delay_count"),
1459 section: Section::TaskstatsDelay,
1460 },
1461 DerivedMetricDef {
1462 name: "avg_swapin_delay_ns",
1463 ladder: ScaleLadder::Ns,
1464 description: "Average swap-in wait per event: swapin_delay_total_ns / swapin_delay_count (ns/event). OVERLAPS with thrashing — every thrashing event is also a swapin event from the syscall layer; do not sum the two averages or the underlying totals directly.",
1465 inputs: &["swapin_delay_total_ns", "swapin_delay_count"],
1466 is_ratio: false,
1467 compute: |m| ratio_compute(m, "swapin_delay_total_ns", "swapin_delay_count"),
1468 section: Section::TaskstatsDelay,
1469 },
1470 DerivedMetricDef {
1471 name: "avg_freepages_delay_ns",
1472 ladder: ScaleLadder::Ns,
1473 description: "Average direct-reclaim wait per event: freepages_delay_total_ns / freepages_delay_count (ns/event).",
1474 inputs: &["freepages_delay_total_ns", "freepages_delay_count"],
1475 is_ratio: false,
1476 compute: |m| ratio_compute(m, "freepages_delay_total_ns", "freepages_delay_count"),
1477 section: Section::TaskstatsDelay,
1478 },
1479 DerivedMetricDef {
1480 name: "avg_thrashing_delay_ns",
1481 ladder: ScaleLadder::Ns,
1482 description: "Average thrashing wait per event: thrashing_delay_total_ns / thrashing_delay_count (ns/event). OVERLAPS with swapin (see avg_swapin_delay_ns).",
1483 inputs: &["thrashing_delay_total_ns", "thrashing_delay_count"],
1484 is_ratio: false,
1485 compute: |m| ratio_compute(m, "thrashing_delay_total_ns", "thrashing_delay_count"),
1486 section: Section::TaskstatsDelay,
1487 },
1488 DerivedMetricDef {
1489 name: "avg_compact_delay_ns",
1490 ladder: ScaleLadder::Ns,
1491 description: "Average memory-compaction wait per event: compact_delay_total_ns / compact_delay_count (ns/event).",
1492 inputs: &["compact_delay_total_ns", "compact_delay_count"],
1493 is_ratio: false,
1494 compute: |m| ratio_compute(m, "compact_delay_total_ns", "compact_delay_count"),
1495 section: Section::TaskstatsDelay,
1496 },
1497 DerivedMetricDef {
1498 name: "avg_wpcopy_delay_ns",
1499 ladder: ScaleLadder::Ns,
1500 description: "Average write-protect-copy fault wait per event: wpcopy_delay_total_ns / wpcopy_delay_count (ns/event).",
1501 inputs: &["wpcopy_delay_total_ns", "wpcopy_delay_count"],
1502 is_ratio: false,
1503 compute: |m| ratio_compute(m, "wpcopy_delay_total_ns", "wpcopy_delay_count"),
1504 section: Section::TaskstatsDelay,
1505 },
1506 DerivedMetricDef {
1507 name: "avg_irq_delay_ns",
1508 ladder: ScaleLadder::Ns,
1509 description: "Average IRQ-handler window per event: irq_delay_total_ns / irq_delay_count (ns/event).",
1510 inputs: &["irq_delay_total_ns", "irq_delay_count"],
1511 is_ratio: false,
1512 compute: |m| ratio_compute(m, "irq_delay_total_ns", "irq_delay_count"),
1513 section: Section::TaskstatsDelay,
1514 },
1515 DerivedMetricDef {
1527 name: "total_offcpu_delay_ns",
1528 ladder: ScaleLadder::Ns,
1529 description: "Sum of all off-CPU delay-accounting buckets, ns: cpu + blkio + freepages + compact + wpcopy + irq + max(swapin, thrashing). The swapin/thrashing pair is OR'd with .max() rather than summed because the two share syscall-layer events (every thrashing event is also a swapin). Returns `-` when any input is missing (CONFIG_TASK_DELAY_ACCT off, runtime toggle off, or kernel older than the bucket's introduction version).",
1530 inputs: &[
1531 "cpu_delay_total_ns",
1532 "blkio_delay_total_ns",
1533 "swapin_delay_total_ns",
1534 "freepages_delay_total_ns",
1535 "thrashing_delay_total_ns",
1536 "compact_delay_total_ns",
1537 "wpcopy_delay_total_ns",
1538 "irq_delay_total_ns",
1539 ],
1540 is_ratio: false,
1541 compute: |m| {
1542 let cpu = input_scalar(m, "cpu_delay_total_ns")?;
1543 let blkio = input_scalar(m, "blkio_delay_total_ns")?;
1544 let swapin = input_scalar(m, "swapin_delay_total_ns")?;
1545 let freepages = input_scalar(m, "freepages_delay_total_ns")?;
1546 let thrashing = input_scalar(m, "thrashing_delay_total_ns")?;
1547 let compact = input_scalar(m, "compact_delay_total_ns")?;
1548 let wpcopy = input_scalar(m, "wpcopy_delay_total_ns")?;
1549 let irq = input_scalar(m, "irq_delay_total_ns")?;
1550 let mem_overlap = swapin.max(thrashing);
1551 Some(DerivedValue::Scalar(
1552 cpu + blkio + freepages + compact + wpcopy + irq + mem_overlap,
1553 ))
1554 },
1555 section: Section::TaskstatsDelay,
1556 },
1557];
1558
1559pub fn metric_display_name(metric: &CtprofMetricDef) -> &'static str {
1572 metric.name
1573}
1574
1575pub fn metric_tags(metric: &CtprofMetricDef) -> String {
1600 let mut out = String::new();
1601 if let Some(class) = metric.sched_class {
1602 out.push('[');
1603 out.push_str(class);
1604 out.push(']');
1605 }
1606 if metric.is_dead {
1607 if !out.is_empty() {
1608 out.push(' ');
1609 }
1610 out.push_str("[dead]");
1611 }
1612 for gate in metric.config_gates {
1613 if !out.is_empty() {
1614 out.push(' ');
1615 }
1616 out.push('[');
1617 let short = gate.strip_prefix("CONFIG_").unwrap_or(gate);
1618 out.push_str(short);
1619 out.push(']');
1620 }
1621 out
1622}