1use crate::{EventSource, PerfSampler, SamplerConfig, SamplingMode, is_ctimer_active};
10use dial9_core::encoder::{Encodable, ThreadLocalEncoder};
11use dial9_core::source::{FlushContext, Source};
12use dial9_trace_format::types::{EventEncoder, FieldType};
13use dial9_trace_format::{InternedStackFrames, InternedString, TraceEvent, TraceField};
14use std::collections::HashMap;
15use std::io::{self, Write};
16use std::sync::Arc;
17
18const WORKER_ID_UNKNOWN: u64 = 255;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28#[non_exhaustive]
29pub enum CpuSampleSource {
30 CpuProfile = 0,
32 SchedEvent = 1,
34}
35
36impl TraceField for CpuSampleSource {
37 fn field_type() -> FieldType {
38 FieldType::U8
39 }
40 fn encode<W: Write>(&self, enc: &mut EventEncoder<'_, W>) -> io::Result<()> {
41 enc.write_u8(*self as u8)
42 }
43}
44
45#[derive(TraceEvent)]
48#[traceevent(wire_slot)]
49struct CpuSampleEvent {
50 #[traceevent(timestamp)]
51 timestamp_ns: u64,
52 worker_id: u64,
55 tid: u32,
56 source: CpuSampleSource,
57 thread_name: Option<InternedString>,
58 callchain: InternedStackFrames,
59 cpu: Option<u64>,
64}
65
66#[derive(Clone, Debug, Eq, PartialEq)]
71pub(crate) struct ThreadName(Arc<str>);
72
73impl ThreadName {
74 fn new(name: String) -> Self {
75 Self(name.into())
76 }
77
78 fn as_str(&self) -> &str {
79 self.0.as_ref()
80 }
81}
82
83pub(crate) struct RawCpuSample {
85 pub tid: u32,
86 pub timestamp_nanos: u64,
87 pub callchain: Vec<u64>,
88 pub source: CpuSampleSource,
89 pub cpu: Option<u32>,
90}
91
92struct CpuSampleData {
96 timestamp_nanos: u64,
97 tid: u32,
98 thread_name: Option<ThreadName>,
99 source: CpuSampleSource,
100 callchain: Vec<u64>,
101 cpu: Option<u32>,
102}
103
104impl Encodable for CpuSampleData {
105 fn encode(&self, enc: &mut ThreadLocalEncoder<'_>) {
106 let thread_name = self
107 .thread_name
108 .as_ref()
109 .map(|n| enc.intern_string(n.as_str()));
110 let callchain = enc.intern_stack_frames(&self.callchain);
111 enc.encode(&CpuSampleEvent {
112 timestamp_ns: self.timestamp_nanos,
113 worker_id: WORKER_ID_UNKNOWN,
114 tid: self.tid,
115 source: self.source,
116 thread_name,
117 callchain,
118 cpu: self.cpu.map(u64::from),
119 });
120 }
121}
122
123pub(crate) fn read_thread_name(tid: u32) -> Option<String> {
128 std::fs::read_to_string(format!("/proc/self/task/{tid}/comm"))
129 .ok()
130 .map(|s| s.trim().to_string())
131 .filter(|s| !s.is_empty())
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub(crate) enum CpuBackend {
150 Auto,
152 Perf,
154 Ctimer,
156}
157
158#[derive(Debug, Clone)]
177pub struct CpuProfilingConfig {
178 frequency_hz: u64,
179 backend: CpuBackend,
180 event_source: EventSource,
181 include_kernel: bool,
182}
183
184impl Default for CpuProfilingConfig {
185 fn default() -> Self {
186 Self {
187 frequency_hz: 99,
188 backend: CpuBackend::Auto,
189 event_source: EventSource::SwCpuClock,
190 include_kernel: false,
191 }
192 }
193}
194
195impl CpuProfilingConfig {
196 pub fn with_perf_backend() -> Self {
204 Self {
205 backend: CpuBackend::Perf,
206 ..Self::default()
207 }
208 }
209
210 pub fn with_ctimer_backend() -> Self {
216 Self {
217 backend: CpuBackend::Ctimer,
218 ..Self::default()
219 }
220 }
221
222 pub fn frequency_hz(mut self, hz: u64) -> Self {
226 self.frequency_hz = hz;
227 self
228 }
229
230 pub fn event_source(mut self, source: EventSource) -> Self {
234 self.event_source = source;
235 self
236 }
237
238 pub fn include_kernel(mut self, yes: bool) -> Self {
242 self.include_kernel = yes;
243 self
244 }
245}
246
247#[derive(Debug, Clone, Default)]
253pub struct SchedEventConfig {
254 sampling_interval: Option<u64>,
255 include_kernel: bool,
256}
257
258impl SchedEventConfig {
259 pub fn sampling_interval(mut self, n: u64) -> Self {
261 self.sampling_interval = Some(n);
262 self
263 }
264
265 pub fn include_kernel(mut self, yes: bool) -> Self {
267 self.include_kernel = yes;
268 self
269 }
270}
271
272pub struct CpuProfiler {
280 sampler: PerfSampler,
281 pid: u32,
282 tid_to_name: HashMap<u32, ThreadName>,
285 config: CpuProfilingConfig,
287 effective_backend: &'static str,
290 metadata_emitted: bool,
292}
293
294impl std::fmt::Debug for CpuProfiler {
295 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296 f.debug_struct("CpuProfiler")
297 .field("pid", &self.pid)
298 .field("backend", &self.effective_backend)
299 .finish_non_exhaustive()
300 }
301}
302
303impl CpuProfiler {
304 pub const SOURCE_NAME: &'static str = "cpu_profile";
307
308 pub fn start(config: CpuProfilingConfig) -> io::Result<Self> {
310 let (sampler, effective_backend) = match config.backend {
311 CpuBackend::Auto => {
312 let s = PerfSampler::start(
313 SamplerConfig::default()
314 .event_source(config.event_source)
315 .sampling(SamplingMode::FrequencyHz(config.frequency_hz))
316 .include_kernel(config.include_kernel),
317 )?;
318 let backend = if is_ctimer_active() { "ctimer" } else { "perf" };
320 (s, backend)
321 }
322 CpuBackend::Perf => {
323 let s = PerfSampler::start_perf_only(
324 SamplerConfig::default()
325 .event_source(config.event_source)
326 .sampling(SamplingMode::FrequencyHz(config.frequency_hz))
327 .include_kernel(config.include_kernel),
328 )?;
329 (s, "perf")
330 }
331 CpuBackend::Ctimer => {
332 let s = PerfSampler::start_ctimer_only(
333 SamplerConfig::default()
334 .sampling(SamplingMode::FrequencyHz(config.frequency_hz))
335 .include_kernel(false),
336 )?;
337 (s, "ctimer")
338 }
339 };
340 Ok(Self {
341 sampler,
342 pid: std::process::id(),
343 tid_to_name: HashMap::new(),
344 config,
345 effective_backend,
346 metadata_emitted: false,
347 })
348 }
349
350 pub(crate) fn drain(&mut self, mut f: impl FnMut(RawCpuSample, Option<&ThreadName>)) {
355 let pid = self.pid;
356 self.sampler.for_each_sample(|sample| {
357 if sample.pid != pid {
358 return;
359 }
360 if !self.tid_to_name.contains_key(&sample.tid)
361 && let Some(name) = read_thread_name(sample.tid)
362 {
363 self.tid_to_name.insert(sample.tid, ThreadName::new(name));
364 }
365 let thread_name = self.tid_to_name.get(&sample.tid);
366 f(
367 RawCpuSample {
368 tid: sample.tid,
369 timestamp_nanos: sample.time,
370 callchain: sample.callchain.clone(),
371 source: CpuSampleSource::CpuProfile,
372 cpu: sample.cpu,
373 },
374 thread_name,
375 );
376 });
377 }
378}
379
380impl Source for CpuProfiler {
381 fn flush(&mut self, ctx: &FlushContext<'_>) {
382 self.drain(|raw, thread_name| {
383 ctx.record_event(&CpuSampleData {
385 timestamp_nanos: raw.timestamp_nanos,
386 tid: raw.tid,
387 source: raw.source,
388 callchain: raw.callchain,
389 thread_name: thread_name.cloned(),
390 cpu: raw.cpu,
391 });
392 });
393 }
394
395 fn on_thread_start(&mut self) -> io::Result<()> {
398 crate::register_current_thread()
399 }
400
401 fn on_thread_stop(&mut self) {
402 crate::unregister_current_thread();
403 }
404
405 fn name(&self) -> &'static str {
406 Self::SOURCE_NAME
407 }
408
409 #[cfg(feature = "symbolize-processor")]
410 fn segment_processor(&mut self) -> Option<Box<dyn dial9_core::pipeline::SegmentProcessor>> {
411 Some(Box::new(crate::SymbolizeProcessor::new()))
412 }
413
414 fn segment_metadata(&mut self, out: &mut Vec<(String, String)>) {
415 if self.metadata_emitted {
416 return;
417 }
418 self.metadata_emitted = true;
419 out.push(("cpu.profile.enabled".to_string(), "true".to_string()));
420 out.push((
421 "cpu.profile.frequency_hz".to_string(),
422 self.config.frequency_hz.to_string(),
423 ));
424 out.push((
428 "cpu.profile.backend".to_string(),
429 self.effective_backend.to_string(),
430 ));
431 #[allow(unreachable_patterns)]
435 let event_source_name = if self.effective_backend == "ctimer" {
436 "sw_cpu_clock"
437 } else {
438 match self.config.event_source {
439 EventSource::SwCpuClock => "sw_cpu_clock",
440 EventSource::SwTaskClock => "sw_task_clock",
441 EventSource::HwCpuCycles => "hw_cpu_cycles",
442 EventSource::SwContextSwitches => "sw_context_switches",
443 EventSource::Tracepoint(id) => {
444 out.push((
445 "cpu.profile.event_source".to_string(),
446 format!("tracepoint:{id}"),
447 ));
448 return;
449 }
450 _ => "unknown",
451 }
452 };
453 out.push((
454 "cpu.profile.event_source".to_string(),
455 event_source_name.to_string(),
456 ));
457 }
458}
459
460pub struct SchedProfiler {
465 sampler: PerfSampler,
466 config: SchedEventConfig,
468 metadata_emitted: bool,
470}
471
472impl std::fmt::Debug for SchedProfiler {
473 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
474 f.debug_struct("SchedProfiler")
475 .field("config", &self.config)
476 .finish_non_exhaustive()
477 }
478}
479
480impl SchedProfiler {
481 pub fn new(config: SchedEventConfig) -> io::Result<Self> {
483 let sampler = PerfSampler::new_per_thread(
484 SamplerConfig::default()
485 .event_source(EventSource::SwContextSwitches)
486 .sampling(SamplingMode::Period(config.sampling_interval.unwrap_or(1)))
487 .include_kernel(config.include_kernel),
488 )?;
489 Ok(Self {
490 sampler,
491 config,
492 metadata_emitted: false,
493 })
494 }
495
496 pub(crate) fn track_current_thread(&mut self) -> io::Result<()> {
497 self.sampler.track_current_thread()
498 }
499
500 pub(crate) fn stop_tracking_current_thread(&mut self) {
501 self.sampler.stop_tracking_current_thread()
502 }
503
504 pub(crate) fn drain(&mut self, mut f: impl FnMut(RawCpuSample)) {
505 self.sampler.for_each_sample(|sample| {
506 f(RawCpuSample {
507 tid: sample.tid,
508 timestamp_nanos: sample.time,
509 callchain: sample.callchain.clone(),
510 source: CpuSampleSource::SchedEvent,
511 cpu: sample.cpu,
512 });
513 });
514 }
515}
516
517impl Source for SchedProfiler {
518 fn flush(&mut self, ctx: &FlushContext<'_>) {
519 self.drain(|raw| {
520 ctx.record_event(&CpuSampleData {
522 timestamp_nanos: raw.timestamp_nanos,
523 tid: raw.tid,
524 source: raw.source,
525 callchain: raw.callchain,
526 thread_name: None,
527 cpu: raw.cpu,
528 });
529 });
530 }
531
532 fn on_thread_start(&mut self) -> io::Result<()> {
533 self.track_current_thread()
534 }
535
536 fn on_thread_stop(&mut self) {
537 self.stop_tracking_current_thread();
538 }
539
540 fn name(&self) -> &'static str {
541 "sched"
542 }
543
544 fn segment_metadata(&mut self, out: &mut Vec<(String, String)>) {
545 if self.metadata_emitted {
546 return;
547 }
548 self.metadata_emitted = true;
549 out.push(("sched.profile.enabled".to_string(), "true".to_string()));
550 let effective_interval = self.config.sampling_interval.unwrap_or(1);
553 out.push((
554 "sched.profile.sample_interval".to_string(),
555 effective_interval.to_string(),
556 ));
557 }
558}
559
560#[cfg(test)]
561mod cpu_sample_round_trip_tests {
562 use super::{CpuSampleEvent, CpuSampleSource, WORKER_ID_UNKNOWN};
563 use dial9_trace_format::decoder::{DecodedFrame, Decoder};
564 use dial9_trace_format::encoder::Encoder;
565 use dial9_trace_format::types::FieldValue;
566
567 fn round_trip(cpu: Option<u64>) -> (u64, Vec<FieldValue>) {
570 let mut enc = Encoder::new();
571 let thread_name = enc.intern_string("tokio-runtime-worker").unwrap();
572 let callchain = enc
573 .intern_stack_frames(&[0xdead_beef, 0xcafe_babe])
574 .unwrap();
575 enc.write(&CpuSampleEvent {
576 timestamp_ns: 7_000_000,
577 worker_id: WORKER_ID_UNKNOWN,
578 tid: 9999,
579 source: CpuSampleSource::CpuProfile,
580 thread_name: Some(thread_name),
581 callchain,
582 cpu,
583 })
584 .unwrap();
585 let bytes = enc.finish();
586
587 Decoder::new(&bytes)
588 .unwrap()
589 .decode_all()
590 .into_iter()
591 .find_map(|frame| match frame {
592 DecodedFrame::Event {
593 timestamp_ns,
594 values,
595 ..
596 } => Some((timestamp_ns, values)),
597 _ => None,
598 })
599 .expect("event frame")
600 }
601
602 #[test]
603 fn cpu_sample_event_round_trips_with_cpu() {
604 let (timestamp_ns, values) = round_trip(Some(3));
605 assert_eq!(timestamp_ns, 7_000_000);
606 assert_eq!(values[1], FieldValue::Varint(9999)); assert_eq!(*values.last().unwrap(), FieldValue::Varint(3));
609 }
610
611 #[test]
612 fn cpu_sample_event_round_trips_without_cpu() {
613 let (_timestamp_ns, values) = round_trip(None);
614 assert_eq!(*values.last().unwrap(), FieldValue::None);
616 }
617}
618
619#[cfg(test)]
620mod metadata_tests {
621 use super::*;
622 use dial9_core::source::Source;
623
624 #[test]
628 fn cpu_profiler_metadata_reports_effective_backend_not_auto() {
629 let config = CpuProfilingConfig::default(); let Ok(mut profiler) = CpuProfiler::start(config) else {
633 eprintln!("skipping: CpuProfiler::start failed (likely no perf access)");
635 return;
636 };
637 let mut meta = Vec::new();
638 profiler.segment_metadata(&mut meta);
639
640 let backend = meta
641 .iter()
642 .find(|(k, _)| k == "cpu.profile.backend")
643 .expect("metadata must contain cpu.profile.backend");
644 assert!(
645 backend.1 == "perf" || backend.1 == "ctimer",
646 "effective backend must be 'perf' or 'ctimer', got '{}'",
647 backend.1
648 );
649 assert_ne!(
650 backend.1, "auto",
651 "metadata must never report 'auto' as the backend"
652 );
653
654 let freq = meta
656 .iter()
657 .find(|(k, _)| k == "cpu.profile.frequency_hz")
658 .expect("metadata must contain cpu.profile.frequency_hz");
659 assert_eq!(freq.1, "99"); let mut meta2 = Vec::new();
663 profiler.segment_metadata(&mut meta2);
664 assert!(meta2.is_empty(), "metadata should not re-emit");
665 }
666
667 #[test]
670 fn sched_profiler_metadata_reports_default_interval() {
671 let config = SchedEventConfig::default(); let Ok(mut profiler) = SchedProfiler::new(config) else {
674 eprintln!("skipping: SchedProfiler::new failed (likely no perf access)");
675 return;
676 };
677 let mut meta = Vec::new();
678 profiler.segment_metadata(&mut meta);
679
680 let interval = meta
681 .iter()
682 .find(|(k, _)| k == "sched.profile.sample_interval")
683 .expect("metadata must contain sched.profile.sample_interval");
684 assert_eq!(
685 interval.1, "1",
686 "effective default interval must be 1, got '{}'",
687 interval.1
688 );
689
690 let mut meta2 = Vec::new();
692 profiler.segment_metadata(&mut meta2);
693 assert!(meta2.is_empty(), "metadata should not re-emit");
694 }
695
696 #[test]
698 fn sched_profiler_metadata_reports_explicit_interval() {
699 let config = SchedEventConfig::default().sampling_interval(5);
700 let Ok(mut profiler) = SchedProfiler::new(config) else {
701 eprintln!("skipping: SchedProfiler::new failed (likely no perf access)");
702 return;
703 };
704 let mut meta = Vec::new();
705 profiler.segment_metadata(&mut meta);
706
707 let interval = meta
708 .iter()
709 .find(|(k, _)| k == "sched.profile.sample_interval")
710 .expect("metadata must contain sched.profile.sample_interval");
711 assert_eq!(
712 interval.1, "5",
713 "explicit interval must be reported, got '{}'",
714 interval.1
715 );
716 }
717
718 #[test]
722 fn ctimer_always_reports_cpu_clock_event_source() {
723 let config =
726 CpuProfilingConfig::with_ctimer_backend().event_source(EventSource::HwCpuCycles); let Ok(mut profiler) = CpuProfiler::start(config) else {
728 eprintln!("skipping: CpuProfiler::start(Ctimer) failed");
729 return;
730 };
731 let mut meta = Vec::new();
732 profiler.segment_metadata(&mut meta);
733
734 let backend = meta
735 .iter()
736 .find(|(k, _)| k == "cpu.profile.backend")
737 .expect("metadata must contain cpu.profile.backend");
738 assert_eq!(
739 backend.1, "ctimer",
740 "explicit ctimer backend must report 'ctimer'"
741 );
742
743 let event_source = meta
744 .iter()
745 .find(|(k, _)| k == "cpu.profile.event_source")
746 .expect("metadata must contain cpu.profile.event_source");
747 assert_eq!(
748 event_source.1, "sw_cpu_clock",
749 "ctimer backend must always report 'sw_cpu_clock' as event source, got '{}'",
750 event_source.1
751 );
752 }
753}