1use crate::collector::{CollectorAvailability, CollectorCapability, SnapshotCollector};
11use crate::schema_v2::{Evidence, EvidenceGap, SpanRecordV2};
12use serde::{Deserialize, Serialize};
13
14#[cfg(all(feature = "hardware-counters", target_os = "linux"))]
15mod linux;
16#[cfg(all(feature = "hardware-counters", target_os = "macos"))]
17mod macos;
18#[cfg(all(feature = "hardware-counters", windows))]
19mod windows;
20
21#[cfg(all(feature = "hardware-counters", target_os = "linux"))]
22use linux as platform;
23#[cfg(all(feature = "hardware-counters", target_os = "macos"))]
24use macos as platform;
25#[cfg(any(
26 not(feature = "hardware-counters"),
27 all(
28 feature = "hardware-counters",
29 not(any(target_os = "linux", target_os = "macos", windows))
30 )
31))]
32use stubs as platform;
33#[cfg(all(feature = "hardware-counters", windows))]
34use windows as platform;
35
36pub const HARDWARE_EVIDENCE_V2_VERSION: u16 = 1;
37pub const SPAN_HARDWARE_V2_VERSION: u16 = 1;
38pub const MAX_UTILIZATION_SAMPLES: usize = 256;
40pub const MAX_SAMPLE_THREADS: usize = 1024;
42
43fn gap<T>(reason: EvidenceGap) -> Evidence<T> {
44 Evidence::unavailable(reason)
45}
46
47const fn legacy_component_version() -> u16 {
48 1
49}
50
51#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
53#[serde(rename_all = "kebab-case")]
54pub enum HardwareFieldSourceV2 {
55 PerfEventOpen,
56 ProcSelfSched,
57 ProcSelfSchedstat,
58 ProcSelfTaskStat,
59 ProcSelfStatus,
60 ProcSelfStat,
61 ProcSelfIo,
62 ProcPressure,
63 SysfsCpu,
64 SysfsCgroup,
65 SysfsThermal,
66 SystemCall,
67 WindowsApi,
68 MacOsApi,
69}
70
71#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
73pub struct SourcedEvidenceV2<T> {
74 pub value: Evidence<T>,
75 pub source: HardwareFieldSourceV2,
76}
77
78impl<T> SourcedEvidenceV2<T> {
79 pub fn recorded(value: T, source: HardwareFieldSourceV2) -> Self {
80 Self {
81 value: Evidence::recorded(value),
82 source,
83 }
84 }
85
86 pub const fn gapped(source: HardwareFieldSourceV2, reason: EvidenceGap) -> Self {
87 Self {
88 value: Evidence::unavailable(reason),
89 source,
90 }
91 }
92}
93
94pub(crate) fn sourced_delta(
95 end: &SourcedEvidenceV2<u64>,
96 start: &SourcedEvidenceV2<u64>,
97) -> SourcedEvidenceV2<u64> {
98 match (&end.value, &start.value) {
99 (Evidence::Recorded { value: end_value }, Evidence::Recorded { value: start_value }) => {
100 SourcedEvidenceV2::recorded(end_value.saturating_sub(*start_value), end.source)
101 }
102 (Evidence::Unavailable { reason }, _) => SourcedEvidenceV2::gapped(end.source, *reason),
103 (_, Evidence::Unavailable { reason }) => SourcedEvidenceV2::gapped(end.source, *reason),
104 }
105}
106
107pub fn milli_ratio(numerator: u64, denominator: u64) -> Option<u64> {
109 if denominator == 0 {
110 return None;
111 }
112 Some(u64::try_from(u128::from(numerator) * 1_000 / u128::from(denominator)).unwrap_or(u64::MAX))
113}
114
115fn milli_ratio_evidence(numerator: &Evidence<u64>, denominator: &Evidence<u64>) -> Evidence<u64> {
116 match (numerator, denominator) {
117 (Evidence::Recorded { value: top }, Evidence::Recorded { value: bottom }) => {
118 milli_ratio(*top, *bottom)
119 .map_or_else(|| gap(EvidenceGap::Unavailable), Evidence::recorded)
120 }
121 (Evidence::Unavailable { reason }, _) => gap(*reason),
122 (_, Evidence::Unavailable { reason }) => gap(*reason),
123 }
124}
125
126#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
128pub struct SpanHardwareV2 {
129 #[serde(default = "legacy_component_version")]
130 pub version: u16,
131 pub cycles_begin: Evidence<u64>,
132 pub cycles_end: Evidence<u64>,
133 pub instructions_begin: Evidence<u64>,
134 pub instructions_end: Evidence<u64>,
135}
136
137impl SpanHardwareV2 {
138 pub fn cycles(&self) -> Evidence<u64> {
140 match (&self.cycles_end, &self.cycles_begin) {
141 (Evidence::Recorded { value: end }, Evidence::Recorded { value: begin }) => {
142 Evidence::recorded(end.saturating_sub(*begin))
143 }
144 (Evidence::Unavailable { reason }, _) => gap(*reason),
145 (_, Evidence::Unavailable { reason }) => gap(*reason),
146 }
147 }
148
149 pub fn instructions(&self) -> Evidence<u64> {
151 match (&self.instructions_end, &self.instructions_begin) {
152 (Evidence::Recorded { value: end }, Evidence::Recorded { value: begin }) => {
153 Evidence::recorded(end.saturating_sub(*begin))
154 }
155 (Evidence::Unavailable { reason }, _) => gap(*reason),
156 (_, Evidence::Unavailable { reason }) => gap(*reason),
157 }
158 }
159
160 pub fn cpi_milli(&self) -> Evidence<u64> {
162 milli_ratio_evidence(&self.cycles(), &self.instructions())
163 }
164}
165
166#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
168pub struct HardwareCounterSampleV2 {
169 #[serde(default = "legacy_component_version")]
170 pub version: u16,
171 pub elapsed_ns: u64,
172 pub cycles: SourcedEvidenceV2<u64>,
173 pub instructions: SourcedEvidenceV2<u64>,
174 pub cache_references: SourcedEvidenceV2<u64>,
175 pub cache_misses: SourcedEvidenceV2<u64>,
176 pub branch_instructions: SourcedEvidenceV2<u64>,
177 pub branch_misses: SourcedEvidenceV2<u64>,
178 pub stalled_cycles_frontend: SourcedEvidenceV2<u64>,
179 pub stalled_cycles_backend: SourcedEvidenceV2<u64>,
180 pub stalled_cycles_memory: SourcedEvidenceV2<u64>,
181}
182
183#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
185pub struct HardwareCounterSetV2 {
186 #[serde(default = "legacy_component_version")]
187 pub version: u16,
188 pub cycles: SourcedEvidenceV2<u64>,
189 pub instructions: SourcedEvidenceV2<u64>,
190 pub cache_references: SourcedEvidenceV2<u64>,
191 pub cache_misses: SourcedEvidenceV2<u64>,
192 pub branch_instructions: SourcedEvidenceV2<u64>,
193 pub branch_misses: SourcedEvidenceV2<u64>,
194 pub stalled_cycles_frontend: SourcedEvidenceV2<u64>,
195 pub stalled_cycles_backend: SourcedEvidenceV2<u64>,
196 pub stalled_cycles_memory: SourcedEvidenceV2<u64>,
197 pub cpi_milli: Evidence<u64>,
199 pub cache_miss_ratio_milli: Evidence<u64>,
201 pub branch_miss_ratio_milli: Evidence<u64>,
203}
204
205impl HardwareCounterSetV2 {
206 pub fn between(start: &HardwareCounterSampleV2, end: &HardwareCounterSampleV2) -> Self {
208 let counters = Self {
209 version: HARDWARE_EVIDENCE_V2_VERSION,
210 cycles: sourced_delta(&end.cycles, &start.cycles),
211 instructions: sourced_delta(&end.instructions, &start.instructions),
212 cache_references: sourced_delta(&end.cache_references, &start.cache_references),
213 cache_misses: sourced_delta(&end.cache_misses, &start.cache_misses),
214 branch_instructions: sourced_delta(
215 &end.branch_instructions,
216 &start.branch_instructions,
217 ),
218 branch_misses: sourced_delta(&end.branch_misses, &start.branch_misses),
219 stalled_cycles_frontend: sourced_delta(
220 &end.stalled_cycles_frontend,
221 &start.stalled_cycles_frontend,
222 ),
223 stalled_cycles_backend: sourced_delta(
224 &end.stalled_cycles_backend,
225 &start.stalled_cycles_backend,
226 ),
227 stalled_cycles_memory: sourced_delta(
228 &end.stalled_cycles_memory,
229 &start.stalled_cycles_memory,
230 ),
231 cpi_milli: gap(EvidenceGap::Unavailable),
232 cache_miss_ratio_milli: gap(EvidenceGap::Unavailable),
233 branch_miss_ratio_milli: gap(EvidenceGap::Unavailable),
234 };
235 Self {
236 cpi_milli: milli_ratio_evidence(&counters.cycles.value, &counters.instructions.value),
237 cache_miss_ratio_milli: milli_ratio_evidence(
238 &counters.cache_misses.value,
239 &counters.cache_references.value,
240 ),
241 branch_miss_ratio_milli: milli_ratio_evidence(
242 &counters.branch_misses.value,
243 &counters.branch_instructions.value,
244 ),
245 ..counters
246 }
247 }
248
249 fn all_gapped(reason: EvidenceGap) -> Self {
250 let source = platform::COUNTER_SOURCE;
251 Self {
252 version: HARDWARE_EVIDENCE_V2_VERSION,
253 cycles: SourcedEvidenceV2::gapped(source, reason),
254 instructions: SourcedEvidenceV2::gapped(source, reason),
255 cache_references: SourcedEvidenceV2::gapped(source, reason),
256 cache_misses: SourcedEvidenceV2::gapped(source, reason),
257 branch_instructions: SourcedEvidenceV2::gapped(source, reason),
258 branch_misses: SourcedEvidenceV2::gapped(source, reason),
259 stalled_cycles_frontend: SourcedEvidenceV2::gapped(source, reason),
260 stalled_cycles_backend: SourcedEvidenceV2::gapped(source, reason),
261 stalled_cycles_memory: SourcedEvidenceV2::gapped(
262 HardwareFieldSourceV2::PerfEventOpen,
263 memory_stall_gap(),
264 ),
265 cpi_milli: gap(reason),
266 cache_miss_ratio_milli: gap(reason),
267 branch_miss_ratio_milli: gap(reason),
268 }
269 }
270}
271
272fn memory_stall_gap() -> EvidenceGap {
273 platform::MEMORY_STALL_GAP
274}
275
276#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
278pub struct SchedulerSampleV2 {
279 #[serde(default = "legacy_component_version")]
280 pub version: u16,
281 pub voluntary_context_switches: SourcedEvidenceV2<u64>,
282 pub involuntary_context_switches: SourcedEvidenceV2<u64>,
283 pub total_context_switches: SourcedEvidenceV2<u64>,
284 pub cpu_migrations: SourcedEvidenceV2<u64>,
285 pub runqueue_delay_ns: SourcedEvidenceV2<u64>,
286 pub timeslices: SourcedEvidenceV2<u64>,
287}
288
289#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
291pub struct SchedulerEvidenceV2 {
292 #[serde(default = "legacy_component_version")]
293 pub version: u16,
294 pub voluntary_context_switches: SourcedEvidenceV2<u64>,
295 pub involuntary_context_switches: SourcedEvidenceV2<u64>,
296 pub total_context_switches: SourcedEvidenceV2<u64>,
297 pub cpu_migrations: SourcedEvidenceV2<u64>,
298 pub scheduler_delay_ns: SourcedEvidenceV2<u64>,
299 pub timeslices: SourcedEvidenceV2<u64>,
300}
301
302impl SchedulerEvidenceV2 {
303 pub fn between(start: &SchedulerSampleV2, end: &SchedulerSampleV2) -> Self {
305 Self {
306 version: HARDWARE_EVIDENCE_V2_VERSION,
307 voluntary_context_switches: sourced_delta(
308 &end.voluntary_context_switches,
309 &start.voluntary_context_switches,
310 ),
311 involuntary_context_switches: sourced_delta(
312 &end.involuntary_context_switches,
313 &start.involuntary_context_switches,
314 ),
315 total_context_switches: sourced_delta(
316 &end.total_context_switches,
317 &start.total_context_switches,
318 ),
319 cpu_migrations: sourced_delta(&end.cpu_migrations, &start.cpu_migrations),
320 scheduler_delay_ns: sourced_delta(&end.runqueue_delay_ns, &start.runqueue_delay_ns),
321 timeslices: sourced_delta(&end.timeslices, &start.timeslices),
322 }
323 }
324}
325
326#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
328pub struct ThreadCpuV2 {
329 #[serde(default = "legacy_component_version")]
330 pub version: u16,
331 pub thread_id: u64,
333 pub cpu_time_ns: u64,
334}
335
336#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
338pub struct ThreadUtilizationSampleV2 {
339 #[serde(default = "legacy_component_version")]
340 pub version: u16,
341 pub elapsed_ns: u64,
342 pub threads: Vec<ThreadCpuV2>,
343 pub dropped_threads: u64,
344}
345
346#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
348pub struct ThreadUtilizationV2 {
349 #[serde(default = "legacy_component_version")]
350 pub version: u16,
351 pub thread_id: u64,
352 pub cpu_time_ns: u64,
353 pub utilization_milli: Evidence<u64>,
355}
356
357#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
359pub struct CpuFrequencySampleV2 {
360 #[serde(default = "legacy_component_version")]
361 pub version: u16,
362 pub elapsed_ns: u64,
363 pub min_khz: u64,
364 pub max_khz: u64,
365 pub mean_khz: u64,
366 pub cpu_count: u32,
367}
368
369#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
371pub struct UtilizationEvidenceV2 {
372 #[serde(default = "legacy_component_version")]
373 pub version: u16,
374 pub wall_ns: u64,
375 pub logical_cpus: u32,
376 pub total_thread_cpu_ns: u64,
378 pub effective_parallelism_milli: Evidence<u64>,
380 pub capacity_utilization_milli: Evidence<u64>,
382 pub threads: Vec<ThreadUtilizationV2>,
383 pub exited_threads: u64,
385 pub joined_threads: u64,
387 pub samples_retained: u64,
388 pub dropped_samples: u64,
389 pub frequency_samples: Vec<CpuFrequencySampleV2>,
390 pub frequency_availability: Evidence<HardwareFieldSourceV2>,
392 pub dropped_frequency_samples: u64,
393}
394
395#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
397pub struct TopologyEvidenceV2 {
398 #[serde(default = "legacy_component_version")]
399 pub version: u16,
400 pub logical_cpus: u32,
401 pub physical_cores: SourcedEvidenceV2<u32>,
402 pub packages: SourcedEvidenceV2<u32>,
403 pub numa_nodes: SourcedEvidenceV2<u32>,
404 pub affinity_cpus: SourcedEvidenceV2<u32>,
406 pub cpu_quota_milli: SourcedEvidenceV2<u64>,
408}
409
410#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
412pub struct StageHardwareV2 {
413 #[serde(default = "legacy_component_version")]
414 pub version: u16,
415 pub metric_id: crate::MetricId,
416 pub span_count: u64,
418 pub cycles: u64,
419 pub instructions: u64,
420 pub cpi_milli: Evidence<u64>,
422}
423
424#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
426pub struct ThreadHardwareV2 {
427 #[serde(default = "legacy_component_version")]
428 pub version: u16,
429 pub thread_id: u64,
430 pub span_count: u64,
431 pub cycles: u64,
432 pub instructions: u64,
433 pub cpi_milli: Evidence<u64>,
434}
435
436#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
438pub struct RunSpanHardwareV2 {
439 #[serde(default = "legacy_component_version")]
440 pub version: u16,
441 pub span_count: u64,
442 pub spans_with_counters: u64,
443 pub cycles: u64,
444 pub instructions: u64,
445 pub cpi_milli: Evidence<u64>,
446}
447
448#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
450pub struct SpanHardwareAggregationV2 {
451 #[serde(default = "legacy_component_version")]
452 pub version: u16,
453 pub run: RunSpanHardwareV2,
454 pub stages: Vec<StageHardwareV2>,
455 pub threads: Vec<ThreadHardwareV2>,
456}
457
458#[derive(Default)]
459struct HardwareSum {
460 spans: u64,
461 cycles: u64,
462 instructions: u64,
463}
464
465impl HardwareSum {
466 fn add(&mut self, hardware: &SpanHardwareV2) {
467 let mut counted = false;
468 if let Evidence::Recorded { value } = hardware.cycles() {
469 self.cycles = self.cycles.saturating_add(value);
470 counted = true;
471 }
472 if let Evidence::Recorded { value } = hardware.instructions() {
473 self.instructions = self.instructions.saturating_add(value);
474 counted = true;
475 }
476 if counted {
477 self.spans = self.spans.saturating_add(1);
478 }
479 }
480
481 fn cpi_milli(&self) -> Evidence<u64> {
482 milli_ratio(self.cycles, self.instructions)
483 .map_or_else(|| gap(EvidenceGap::Unavailable), Evidence::recorded)
484 }
485}
486
487pub fn aggregate_span_hardware(spans: &[SpanRecordV2]) -> SpanHardwareAggregationV2 {
492 let mut stages: std::collections::BTreeMap<crate::MetricId, HardwareSum> =
493 std::collections::BTreeMap::new();
494 let mut threads: std::collections::BTreeMap<u64, HardwareSum> =
495 std::collections::BTreeMap::new();
496 let mut run = HardwareSum::default();
497 for span in spans {
498 let Evidence::Recorded { value: hardware } = &span.hardware else {
499 continue;
500 };
501 stages.entry(span.metric_id).or_default().add(hardware);
502 threads.entry(span.thread_id).or_default().add(hardware);
503 run.add(hardware);
504 }
505 SpanHardwareAggregationV2 {
506 version: HARDWARE_EVIDENCE_V2_VERSION,
507 run: RunSpanHardwareV2 {
508 version: HARDWARE_EVIDENCE_V2_VERSION,
509 span_count: spans.len() as u64,
510 spans_with_counters: run.spans,
511 cycles: run.cycles,
512 instructions: run.instructions,
513 cpi_milli: run.cpi_milli(),
514 },
515 stages: stages
516 .into_iter()
517 .map(|(metric_id, sum)| StageHardwareV2 {
518 version: HARDWARE_EVIDENCE_V2_VERSION,
519 metric_id,
520 span_count: sum.spans,
521 cycles: sum.cycles,
522 instructions: sum.instructions,
523 cpi_milli: sum.cpi_milli(),
524 })
525 .collect(),
526 threads: threads
527 .into_iter()
528 .map(|(thread_id, sum)| ThreadHardwareV2 {
529 version: HARDWARE_EVIDENCE_V2_VERSION,
530 thread_id,
531 span_count: sum.spans,
532 cycles: sum.cycles,
533 instructions: sum.instructions,
534 cpi_milli: sum.cpi_milli(),
535 })
536 .collect(),
537 }
538}
539
540#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
542pub struct HardwareRunEvidenceV2 {
543 #[serde(default = "legacy_component_version")]
544 pub version: u16,
545 pub counters: HardwareCounterSetV2,
547 pub scheduler: SchedulerEvidenceV2,
548 pub utilization: UtilizationEvidenceV2,
549 pub topology: TopologyEvidenceV2,
550 pub span_aggregation: Evidence<SpanHardwareAggregationV2>,
552}
553
554impl HardwareRunEvidenceV2 {
555 pub fn with_span_aggregation(mut self, spans: &[SpanRecordV2]) -> Self {
557 self.span_aggregation = Evidence::recorded(aggregate_span_hardware(spans));
558 self
559 }
560}
561
562pub struct HardwareCounterCollector {
564 capability: CollectorCapability,
565 state: platform::CounterState,
566 started: std::time::Instant,
567}
568
569impl HardwareCounterCollector {
570 pub fn new() -> Self {
571 let platform = platform::platform_collectors();
572 Self {
573 capability: platform.counter_capability,
574 state: platform.counters,
575 started: std::time::Instant::now(),
576 }
577 }
578}
579
580impl Default for HardwareCounterCollector {
581 fn default() -> Self {
582 Self::new()
583 }
584}
585
586impl SnapshotCollector for HardwareCounterCollector {
587 type Snapshot = HardwareCounterSampleV2;
588
589 fn capability(&self) -> CollectorCapability {
590 self.capability.clone()
591 }
592
593 fn sample(&mut self) -> Self::Snapshot {
594 let elapsed_ns = u64::try_from(self.started.elapsed().as_nanos()).unwrap_or(u64::MAX);
595 platform::sample_counters(&mut self.state, elapsed_ns)
596 }
597}
598
599pub struct SchedulerCollector {
601 capability: CollectorCapability,
602 state: platform::SchedulerState,
603}
604
605impl SchedulerCollector {
606 pub fn new() -> Self {
607 let platform = platform::platform_collectors();
608 Self {
609 capability: platform.scheduler_capability,
610 state: platform.scheduler,
611 }
612 }
613}
614
615impl Default for SchedulerCollector {
616 fn default() -> Self {
617 Self::new()
618 }
619}
620
621impl SnapshotCollector for SchedulerCollector {
622 type Snapshot = SchedulerSampleV2;
623
624 fn capability(&self) -> CollectorCapability {
625 self.capability.clone()
626 }
627
628 fn sample(&mut self) -> Self::Snapshot {
629 platform::sample_scheduler(&mut self.state)
630 }
631}
632
633pub struct ThreadUtilizationCollector {
635 capability: CollectorCapability,
636 started: std::time::Instant,
637}
638
639impl ThreadUtilizationCollector {
640 pub fn new() -> Self {
641 let platform = platform::platform_collectors();
642 Self {
643 capability: platform.utilization_capability,
644 started: std::time::Instant::now(),
645 }
646 }
647}
648
649impl Default for ThreadUtilizationCollector {
650 fn default() -> Self {
651 Self::new()
652 }
653}
654
655impl SnapshotCollector for ThreadUtilizationCollector {
656 type Snapshot = ThreadUtilizationSampleV2;
657
658 fn capability(&self) -> CollectorCapability {
659 self.capability.clone()
660 }
661
662 fn sample(&mut self) -> Self::Snapshot {
663 let elapsed_ns = u64::try_from(self.started.elapsed().as_nanos()).unwrap_or(u64::MAX);
664 let (threads, dropped) = platform::sample_thread_utilization();
665 let dropped_threads =
666 dropped.saturating_add(threads.len().saturating_sub(MAX_SAMPLE_THREADS) as u64);
667 ThreadUtilizationSampleV2 {
668 version: HARDWARE_EVIDENCE_V2_VERSION,
669 elapsed_ns,
670 threads: threads.into_iter().take(MAX_SAMPLE_THREADS).collect(),
671 dropped_threads,
672 }
673 }
674}
675
676pub struct TopologyCollector {
678 capability: CollectorCapability,
679}
680
681impl TopologyCollector {
682 pub fn new() -> Self {
683 let platform = platform::platform_collectors();
684 Self {
685 capability: platform.topology_capability,
686 }
687 }
688}
689
690impl Default for TopologyCollector {
691 fn default() -> Self {
692 Self::new()
693 }
694}
695
696impl SnapshotCollector for TopologyCollector {
697 type Snapshot = TopologyEvidenceV2;
698
699 fn capability(&self) -> CollectorCapability {
700 self.capability.clone()
701 }
702
703 fn sample(&mut self) -> Self::Snapshot {
704 platform::capture_topology()
705 }
706}
707
708#[derive(Clone, Copy, Default)]
710pub(crate) struct SpanCounterReading {
711 pub cycles: Option<u64>,
712 pub instructions: Option<u64>,
713}
714
715#[inline]
720pub(crate) fn span_counter_reading() -> Option<SpanCounterReading> {
721 platform::span_counter_reading()
722}
723
724pub(crate) struct HardwareSession {
726 counter_collector: HardwareCounterCollector,
727 scheduler_collector: SchedulerCollector,
728 utilization_collector: ThreadUtilizationCollector,
729 topology_collector: TopologyCollector,
730 counters_start: Option<HardwareCounterSampleV2>,
731 scheduler_start: Option<SchedulerSampleV2>,
732 utilization_samples: Vec<ThreadUtilizationSampleV2>,
733 dropped_utilization_samples: u64,
734 frequency_samples: Vec<CpuFrequencySampleV2>,
735 dropped_frequency_samples: u64,
736 topology: Option<TopologyEvidenceV2>,
737 disabled_reason: Option<EvidenceGap>,
738}
739
740impl HardwareSession {
741 pub(crate) fn new() -> Self {
742 let mut counter_collector = HardwareCounterCollector::new();
743 let mut scheduler_collector = SchedulerCollector::new();
744 let utilization_collector = ThreadUtilizationCollector::new();
745 let mut topology_collector = TopologyCollector::new();
746 let disabled_reason = match counter_collector.capability.availability {
747 CollectorAvailability::Disabled => Some(EvidenceGap::CollectorDisabled),
748 _ => None,
749 };
750 let counters_start = (disabled_reason.is_none()).then(|| counter_collector.sample());
751 let scheduler_start = (disabled_reason.is_none()).then(|| scheduler_collector.sample());
752 let topology = (disabled_reason.is_none()).then(|| topology_collector.sample());
753 let mut session = Self {
754 counter_collector,
755 scheduler_collector,
756 utilization_collector,
757 topology_collector,
758 counters_start,
759 scheduler_start,
760 utilization_samples: Vec::new(),
761 dropped_utilization_samples: 0,
762 frequency_samples: Vec::new(),
763 dropped_frequency_samples: 0,
764 topology,
765 disabled_reason,
766 };
767 if session.disabled_reason.is_none() {
768 session.transition_sample();
769 }
770 session
771 }
772
773 pub(crate) fn transition_sample(&mut self) {
775 if self.disabled_reason.is_some() {
776 return;
777 }
778 if self.utilization_samples.len() == MAX_UTILIZATION_SAMPLES {
779 self.dropped_utilization_samples = self.dropped_utilization_samples.saturating_add(1);
780 } else {
781 self.utilization_samples
782 .push(self.utilization_collector.sample());
783 }
784 if self.frequency_samples.len() == MAX_UTILIZATION_SAMPLES {
785 self.dropped_frequency_samples = self.dropped_frequency_samples.saturating_add(1);
786 } else if let Some(sample) = platform::sample_frequency(self.utilization_elapsed_ns()) {
787 self.frequency_samples.push(sample);
788 }
789 }
790
791 fn utilization_elapsed_ns(&self) -> u64 {
792 self.utilization_samples
793 .last()
794 .map_or(0, |sample| sample.elapsed_ns)
795 }
796
797 pub(crate) fn capabilities(&self) -> Vec<CollectorCapability> {
798 vec![
799 self.counter_collector.capability(),
800 self.scheduler_collector.capability(),
801 self.utilization_collector.capability(),
802 self.topology_collector.capability(),
803 ]
804 }
805
806 pub(crate) fn finish_evidence(
808 mut self,
809 wall_ns: u64,
810 runtime: &crate::Runtime,
811 ) -> Evidence<HardwareRunEvidenceV2> {
812 if let Some(reason) = self.disabled_reason {
813 return Evidence::unavailable(reason);
814 }
815 self.transition_sample();
816 let counters = match &self.counters_start {
817 Some(start) => {
818 let end = self.counter_collector.sample();
819 HardwareCounterSetV2::between(start, &end)
820 }
821 None => HardwareCounterSetV2::all_gapped(EvidenceGap::Unavailable),
822 };
823 let scheduler = match &self.scheduler_start {
824 Some(start) => {
825 let end = self.scheduler_collector.sample();
826 SchedulerEvidenceV2::between(start, &end)
827 }
828 None => SchedulerEvidenceV2::between(
829 &platform::empty_scheduler_sample(),
830 &platform::empty_scheduler_sample(),
831 ),
832 };
833 let utilization = compute_utilization(
834 &self.utilization_samples,
835 self.dropped_utilization_samples,
836 wall_ns,
837 self.topology.as_ref().map_or_else(
838 || std::thread::available_parallelism().map_or(1, |n| n.get() as u32),
839 |t| t.logical_cpus,
840 ),
841 std::mem::take(&mut self.frequency_samples),
842 self.dropped_frequency_samples,
843 );
844 record_hardware_counters(runtime, &counters, &scheduler);
845 Evidence::recorded(HardwareRunEvidenceV2 {
846 version: HARDWARE_EVIDENCE_V2_VERSION,
847 counters,
848 scheduler,
849 utilization,
850 topology: self
851 .topology
852 .take()
853 .unwrap_or_else(platform::capture_topology),
854 span_aggregation: gap(EvidenceGap::Unavailable),
855 })
856 }
857}
858
859fn record_hardware_counters(
860 runtime: &crate::Runtime,
861 counters: &HardwareCounterSetV2,
862 scheduler: &SchedulerEvidenceV2,
863) {
864 let pairs: [(&SourcedEvidenceV2<u64>, crate::CounterId); 12] = [
865 (&counters.cycles, crate::CounterId::HardwareCycles),
866 (
867 &counters.instructions,
868 crate::CounterId::HardwareInstructions,
869 ),
870 (
871 &counters.cache_references,
872 crate::CounterId::HardwareCacheReferences,
873 ),
874 (
875 &counters.cache_misses,
876 crate::CounterId::HardwareCacheMisses,
877 ),
878 (
879 &counters.branch_instructions,
880 crate::CounterId::HardwareBranchInstructions,
881 ),
882 (
883 &counters.branch_misses,
884 crate::CounterId::HardwareBranchMisses,
885 ),
886 (
887 &counters.stalled_cycles_frontend,
888 crate::CounterId::HardwareStalledCyclesFrontend,
889 ),
890 (
891 &counters.stalled_cycles_backend,
892 crate::CounterId::HardwareStalledCyclesBackend,
893 ),
894 (
895 &scheduler.voluntary_context_switches,
896 crate::CounterId::SchedulerVoluntaryContextSwitches,
897 ),
898 (
899 &scheduler.involuntary_context_switches,
900 crate::CounterId::SchedulerInvoluntaryContextSwitches,
901 ),
902 (
903 &scheduler.cpu_migrations,
904 crate::CounterId::SchedulerCpuMigrations,
905 ),
906 (
907 &scheduler.scheduler_delay_ns,
908 crate::CounterId::SchedulerDelayNs,
909 ),
910 ];
911 for (field, counter) in pairs {
912 if let Evidence::Recorded { value } = field.value {
913 if value > 0 {
914 runtime.add_counter(counter, value);
915 }
916 }
917 }
918}
919
920fn compute_utilization(
921 samples: &[ThreadUtilizationSampleV2],
922 dropped_samples: u64,
923 wall_ns: u64,
924 logical_cpus: u32,
925 frequency_samples: Vec<CpuFrequencySampleV2>,
926 dropped_frequency_samples: u64,
927) -> UtilizationEvidenceV2 {
928 let frequency_availability = platform::frequency_availability();
929 let (Some(first), Some(last)) = (samples.first(), samples.last()) else {
930 return UtilizationEvidenceV2 {
931 version: HARDWARE_EVIDENCE_V2_VERSION,
932 wall_ns,
933 logical_cpus,
934 total_thread_cpu_ns: 0,
935 effective_parallelism_milli: gap(EvidenceGap::Unavailable),
936 capacity_utilization_milli: gap(EvidenceGap::Unavailable),
937 threads: Vec::new(),
938 exited_threads: 0,
939 joined_threads: 0,
940 samples_retained: samples.len() as u64,
941 dropped_samples,
942 frequency_samples,
943 frequency_availability,
944 dropped_frequency_samples,
945 };
946 };
947 let last_by_id: std::collections::BTreeMap<u64, u64> = last
948 .threads
949 .iter()
950 .map(|thread| (thread.thread_id, thread.cpu_time_ns))
951 .collect();
952 let first_ids: std::collections::BTreeSet<u64> = first
953 .threads
954 .iter()
955 .map(|thread| thread.thread_id)
956 .collect();
957 let mut threads = Vec::new();
958 let mut exited_threads = 0_u64;
959 let mut total = 0_u64;
960 for thread in &first.threads {
961 match last_by_id.get(&thread.thread_id) {
962 Some(end) => {
963 let cpu_time_ns = end.saturating_sub(thread.cpu_time_ns);
964 total = total.saturating_add(cpu_time_ns);
965 threads.push(ThreadUtilizationV2 {
966 version: HARDWARE_EVIDENCE_V2_VERSION,
967 thread_id: thread.thread_id,
968 cpu_time_ns,
969 utilization_milli: milli_ratio(cpu_time_ns, wall_ns)
970 .map_or_else(|| gap(EvidenceGap::Unavailable), Evidence::recorded),
971 });
972 }
973 None => exited_threads = exited_threads.saturating_add(1),
974 }
975 }
976 let joined_threads = last
977 .threads
978 .iter()
979 .filter(|thread| !first_ids.contains(&thread.thread_id))
980 .count() as u64;
981 let capacity_ns = u128::from(wall_ns).saturating_mul(u128::from(logical_cpus));
982 UtilizationEvidenceV2 {
983 version: HARDWARE_EVIDENCE_V2_VERSION,
984 wall_ns,
985 logical_cpus,
986 total_thread_cpu_ns: total,
987 effective_parallelism_milli: milli_ratio(total, wall_ns)
988 .map_or_else(|| gap(EvidenceGap::Unavailable), Evidence::recorded),
989 capacity_utilization_milli: if capacity_ns == 0 {
990 gap(EvidenceGap::Unavailable)
991 } else {
992 Evidence::recorded(
993 u64::try_from(u128::from(total) * 1_000 / capacity_ns).unwrap_or(u64::MAX),
994 )
995 },
996 threads,
997 exited_threads,
998 joined_threads,
999 samples_retained: samples.len() as u64,
1000 dropped_samples,
1001 frequency_samples,
1002 frequency_availability,
1003 dropped_frequency_samples,
1004 }
1005}
1006
1007#[cfg(any(
1010 not(feature = "hardware-counters"),
1011 all(
1012 feature = "hardware-counters",
1013 not(any(target_os = "linux", target_os = "macos", windows))
1014 )
1015))]
1016mod stubs {
1017 use super::*;
1018 use crate::collector::CollectorId;
1019
1020 pub(super) const COUNTER_SOURCE: HardwareFieldSourceV2 = HardwareFieldSourceV2::SystemCall;
1021 pub(super) const MEMORY_STALL_GAP: EvidenceGap = EvidenceGap::Unsupported;
1022
1023 pub(super) struct CounterState;
1024 pub(super) struct SchedulerState;
1025
1026 pub(super) struct PlatformCollectors {
1027 pub counter_capability: CollectorCapability,
1028 pub scheduler_capability: CollectorCapability,
1029 pub utilization_capability: CollectorCapability,
1030 pub topology_capability: CollectorCapability,
1031 pub counters: CounterState,
1032 pub scheduler: SchedulerState,
1033 }
1034
1035 fn stub_availability() -> (CollectorAvailability, &'static str) {
1036 #[cfg(not(feature = "hardware-counters"))]
1037 {
1038 (
1039 CollectorAvailability::Disabled,
1040 "enable the keyhog-profile hardware-counters feature",
1041 )
1042 }
1043 #[cfg(feature = "hardware-counters")]
1044 {
1045 (
1046 CollectorAvailability::Unsupported,
1047 "CPU hardware evidence is implemented for Linux, Windows, and macOS only",
1048 )
1049 }
1050 }
1051
1052 pub(super) fn platform_collectors() -> PlatformCollectors {
1053 let (availability, detail) = stub_availability();
1054 let capability = |collector: CollectorId| {
1055 CollectorCapability::unavailable(collector, availability, detail)
1056 };
1057 PlatformCollectors {
1058 counter_capability: capability(CollectorId::HardwareCounters),
1059 scheduler_capability: capability(CollectorId::SchedulerActivity),
1060 utilization_capability: capability(CollectorId::ThreadUtilization),
1061 topology_capability: capability(CollectorId::CpuTopology),
1062 counters: CounterState,
1063 scheduler: SchedulerState,
1064 }
1065 }
1066
1067 fn gap_sample(reason: EvidenceGap) -> SourcedEvidenceV2<u64> {
1068 SourcedEvidenceV2::gapped(COUNTER_SOURCE, reason)
1069 }
1070
1071 fn stub_reason() -> EvidenceGap {
1072 #[cfg(not(feature = "hardware-counters"))]
1073 {
1074 EvidenceGap::CollectorDisabled
1075 }
1076 #[cfg(feature = "hardware-counters")]
1077 {
1078 EvidenceGap::Unsupported
1079 }
1080 }
1081
1082 pub(super) fn sample_counters(
1083 _state: &mut CounterState,
1084 elapsed_ns: u64,
1085 ) -> HardwareCounterSampleV2 {
1086 let reason = stub_reason();
1087 HardwareCounterSampleV2 {
1088 version: HARDWARE_EVIDENCE_V2_VERSION,
1089 elapsed_ns,
1090 cycles: gap_sample(reason),
1091 instructions: gap_sample(reason),
1092 cache_references: gap_sample(reason),
1093 cache_misses: gap_sample(reason),
1094 branch_instructions: gap_sample(reason),
1095 branch_misses: gap_sample(reason),
1096 stalled_cycles_frontend: gap_sample(reason),
1097 stalled_cycles_backend: gap_sample(reason),
1098 stalled_cycles_memory: SourcedEvidenceV2::gapped(
1099 HardwareFieldSourceV2::PerfEventOpen,
1100 EvidenceGap::Unsupported,
1101 ),
1102 }
1103 }
1104
1105 pub(super) fn empty_scheduler_sample() -> SchedulerSampleV2 {
1106 let reason = stub_reason();
1107 SchedulerSampleV2 {
1108 version: HARDWARE_EVIDENCE_V2_VERSION,
1109 voluntary_context_switches: gap_sample(reason),
1110 involuntary_context_switches: gap_sample(reason),
1111 total_context_switches: gap_sample(reason),
1112 cpu_migrations: gap_sample(reason),
1113 runqueue_delay_ns: gap_sample(reason),
1114 timeslices: gap_sample(reason),
1115 }
1116 }
1117
1118 pub(super) fn sample_scheduler(_state: &mut SchedulerState) -> SchedulerSampleV2 {
1119 empty_scheduler_sample()
1120 }
1121
1122 pub(super) fn sample_thread_utilization() -> (Vec<ThreadCpuV2>, u64) {
1123 (Vec::new(), 0)
1124 }
1125
1126 pub(super) fn sample_frequency(_elapsed_ns: u64) -> Option<CpuFrequencySampleV2> {
1127 None
1128 }
1129
1130 pub(super) fn frequency_availability() -> Evidence<HardwareFieldSourceV2> {
1131 Evidence::unavailable(stub_reason())
1132 }
1133
1134 pub(super) fn capture_topology() -> TopologyEvidenceV2 {
1135 let reason = stub_reason();
1136 let logical_cpus =
1137 std::thread::available_parallelism().map_or(1, |count| count.get() as u32);
1138 TopologyEvidenceV2 {
1139 version: HARDWARE_EVIDENCE_V2_VERSION,
1140 logical_cpus,
1141 physical_cores: SourcedEvidenceV2::gapped(HardwareFieldSourceV2::SysfsCpu, reason),
1142 packages: SourcedEvidenceV2::gapped(HardwareFieldSourceV2::SysfsCpu, reason),
1143 numa_nodes: SourcedEvidenceV2::gapped(HardwareFieldSourceV2::SysfsCpu, reason),
1144 affinity_cpus: SourcedEvidenceV2::gapped(HardwareFieldSourceV2::SystemCall, reason),
1145 cpu_quota_milli: SourcedEvidenceV2::gapped(HardwareFieldSourceV2::SysfsCgroup, reason),
1146 }
1147 }
1148
1149 #[inline]
1150 pub(crate) fn span_counter_reading() -> Option<SpanCounterReading> {
1151 None
1152 }
1153}