1use std::fs::File;
2use std::io;
3use std::ops::Range;
4use std::path::Path;
5use std::sync::Arc;
6
7use memmap2::Mmap;
8use nix::fcntl::{fallocate, FallocateFlags};
9use rustc_hash::{FxHashMap, FxHashSet};
10
11use crate::native_module::ExactImageStore;
12
13use super::{
14 decode_spool_record, invalid_data, next_source_id, DecodedSpoolRecord, MmapSpoolCursor,
15 SampleRecord, SampleStack, SpoolDefinitions, ThreadRecord,
16};
17
18const MAX_BATCH_SAMPLES: usize = 16 * 1024;
19
20struct SpoolDiscarder {
21 file: File,
22 position: usize,
23}
24
25pub struct Tail {
32 file: File,
33 discarder: Option<SpoolDiscarder>,
34 mmap: Arc<Mmap>,
35 position: usize,
36 definitions: SpoolDefinitions,
37 threads: Vec<ThreadRecord>,
38 last_timestamp_ns: u64,
39 first_sample_timestamp_ns: Option<u64>,
40 samples: Vec<SampleRecord>,
41 initial_samples_pending: bool,
42 observed_processes: Vec<crate::Pid>,
43 observed_process_set: FxHashSet<crate::Pid>,
44 retired_processes: Vec<crate::Pid>,
45 retired_modules: Vec<u32>,
46 active_modules_by_process: FxHashMap<crate::Pid, Vec<usize>>,
47 kernel_mappings_changed: bool,
48 more_available: bool,
49 exact_images: Option<ExactImageStore>,
50}
51
52impl std::fmt::Debug for Tail {
53 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 formatter
55 .debug_struct("Tail")
56 .field("position", &self.position)
57 .field("modules", &self.definitions.modules.len())
58 .field("frames", &self.definitions.frames.len())
59 .field("pending_samples", &self.samples.len())
60 .finish_non_exhaustive()
61 }
62}
63
64pub struct TailBatch<'a> {
69 tail: &'a Tail,
70 modules: Range<usize>,
71 frames: Range<usize>,
72}
73
74impl std::fmt::Debug for TailBatch<'_> {
75 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 formatter
77 .debug_struct("TailBatch")
78 .field("samples", &self.tail.samples.len())
79 .field("modules", &self.modules.len())
80 .field("frames", &self.frames.len())
81 .field("kernel_mappings_changed", &self.kernel_mappings_changed())
82 .finish()
83 }
84}
85
86impl Tail {
87 pub fn open(path: impl AsRef<Path>) -> crate::Result<Self> {
96 Self::open_inner(path).map_err(crate::Error::spool)
97 }
98
99 fn open_inner(path: impl AsRef<Path>) -> io::Result<Self> {
100 Self::from_file_inner(File::open(path)?, None, None)
101 }
102
103 pub(crate) fn from_recorder(
104 file: File,
105 discarder: File,
106 exact_images: ExactImageStore,
107 ) -> crate::Result<Self> {
108 Self::from_file_inner(file, Some(discarder), Some(exact_images))
109 .map_err(crate::Error::spool)
110 }
111
112 fn from_file_inner(
113 file: File,
114 discarder: Option<File>,
115 exact_images: Option<ExactImageStore>,
116 ) -> io::Result<Self> {
117 let mmap = Arc::new(unsafe { Mmap::map(&file)? });
122 let mut cursor = MmapSpoolCursor::new(Arc::clone(&mmap));
123 cursor.check_magic()?;
124 let start_timestamp_us = cursor.read_varint::<u64>()?;
125 let sample_interval_us = cursor.read_varint::<u64>()?;
126 let position = cursor.position;
127 let mut tail = Self {
128 file,
129 discarder: discarder.map(|file| SpoolDiscarder { file, position: 0 }),
130 mmap,
131 position,
132 definitions: SpoolDefinitions {
133 source_id: next_source_id(),
134 start_timestamp_us,
135 sample_interval_us,
136 modules: Vec::new(),
137 frames: Vec::new(),
138 frame_contexts: super::SpoolFrameModuleContexts::default(),
139 stack_nodes: Vec::new(),
140 python_runtime_records: Vec::new(),
141 truncated_tail: false,
142 },
143 threads: Vec::new(),
144 last_timestamp_ns: 0,
145 first_sample_timestamp_ns: None,
146 samples: Vec::new(),
147 initial_samples_pending: true,
148 observed_processes: Vec::new(),
149 observed_process_set: FxHashSet::default(),
150 retired_processes: Vec::new(),
151 retired_modules: Vec::new(),
152 active_modules_by_process: FxHashMap::default(),
153 kernel_mappings_changed: false,
154 more_available: false,
155 exact_images,
156 };
157 tail.more_available = tail.parse_available()?;
158
159 tail.kernel_mappings_changed = false;
164 Ok(tail)
165 }
166
167 #[must_use]
172 pub fn symbolizer(&self) -> crate::SymbolizerBuilder<'_> {
173 crate::SymbolizerBuilder::for_tail(self)
174 }
175
176 pub fn poll(&mut self) -> crate::Result<TailBatch<'_>> {
192 self.poll_inner().map_err(crate::Error::spool)
193 }
194
195 pub fn discard_consumed(&mut self) -> crate::Result<()> {
207 self.discard_consumed_inner().map_err(crate::Error::spool)
208 }
209
210 fn discard_consumed_inner(&mut self) -> io::Result<()> {
211 let discarder = self.discarder.as_mut().ok_or_else(|| {
212 io::Error::new(
213 io::ErrorKind::PermissionDenied,
214 "tail was not created by a recorder",
215 )
216 })?;
217 let page_size = usize::try_from(crate::elf::system_page_size())
218 .map_err(|_| invalid_data("system page size does not fit usize"))?;
219 let discard_end = self.position - self.position % page_size;
220 if discard_end <= discarder.position {
221 return Ok(());
222 }
223 let offset = libc::off_t::try_from(discarder.position)
224 .map_err(|_| invalid_data("spool discard offset is too large"))?;
225 let length = libc::off_t::try_from(discard_end - discarder.position)
226 .map_err(|_| invalid_data("spool discard length is too large"))?;
227 fallocate(
228 &discarder.file,
229 FallocateFlags::FALLOC_FL_PUNCH_HOLE | FallocateFlags::FALLOC_FL_KEEP_SIZE,
230 offset,
231 length,
232 )?;
233 discarder.position = discard_end;
234 Ok(())
235 }
236
237 fn poll_inner(&mut self) -> io::Result<TailBatch<'_>> {
238 let initial = std::mem::take(&mut self.initial_samples_pending);
239 let module_start = self.definitions.modules.len();
240 let frame_start = self.definitions.frames.len();
241
242 if initial {
243 let mapped_len = self.mmap.len();
244 self.remap_if_grown()?;
245 self.more_available |= self.mmap.len() > mapped_len;
246 } else {
247 self.samples.clear();
248 self.observed_processes.clear();
249 self.observed_process_set.clear();
250 self.retired_processes.clear();
251 self.retired_modules.clear();
252 self.kernel_mappings_changed = false;
253 self.remap_if_grown()?;
254 self.more_available = self.parse_available()?;
255 }
256
257 Ok(TailBatch {
258 tail: self,
259 modules: module_start..self.definitions.modules.len(),
260 frames: frame_start..self.definitions.frames.len(),
261 })
262 }
263
264 #[must_use]
266 pub fn start_timestamp_us(&self) -> Option<u64> {
267 (self.definitions.start_timestamp_us != 0).then_some(self.definitions.start_timestamp_us)
268 }
269
270 #[must_use]
272 pub fn sample_interval_us(&self) -> Option<u64> {
273 (self.definitions.sample_interval_us != 0).then_some(self.definitions.sample_interval_us)
274 }
275
276 pub(crate) fn modules(&self) -> &[super::ModuleRecord] {
277 &self.definitions.modules
278 }
279
280 pub(crate) fn frames(&self) -> &[super::FrameRecord] {
281 &self.definitions.frames
282 }
283
284 fn timestamp_us(&self, sample: &SampleRecord) -> Option<u64> {
285 let anchor = self.start_timestamp_us()?;
286 let first = self
287 .first_sample_timestamp_ns
288 .unwrap_or(sample.timestamp_ns);
289 Some(anchor.saturating_add(sample.timestamp_ns.saturating_sub(first) / 1_000))
290 }
291
292 pub(crate) fn source_id(&self) -> u64 {
293 self.definitions.source_id
294 }
295
296 pub(crate) fn exact_images(&self) -> Option<ExactImageStore> {
297 self.exact_images.clone()
298 }
299
300 pub(crate) fn frame_module_contexts(&self) -> super::SpoolFrameModuleContexts {
301 self.definitions.frame_contexts.clone()
302 }
303
304 fn observe_process(&mut self, process: crate::Pid) {
305 if self.observed_processes.last() != Some(&process)
306 && self.observed_process_set.insert(process)
307 {
308 self.observed_processes.push(process);
309 }
310 }
311
312 fn remap_if_grown(&mut self) -> io::Result<()> {
313 let file_len = usize::try_from(self.file.metadata()?.len())
314 .map_err(|_| invalid_data("spool file is too large to map"))?;
315 if file_len < self.mmap.len() {
316 return Err(invalid_data("spool file shrank while being tailed"));
317 }
318 if file_len > self.mmap.len() {
319 self.mmap = Arc::new(unsafe { Mmap::map(&self.file)? });
322 }
323 Ok(())
324 }
325
326 fn parse_available(&mut self) -> io::Result<bool> {
327 let mut cursor = MmapSpoolCursor::at_position(Arc::clone(&self.mmap), self.position);
328 loop {
329 let record = match decode_spool_record(
330 &mut cursor,
331 &self.definitions.modules,
332 self.definitions.frames.len(),
333 &self.definitions.stack_nodes,
334 &self.threads,
335 &mut self.last_timestamp_ns,
336 ) {
337 Ok(Some(record)) => record,
338 Ok(None) => break,
339 Err(error) if error.kind() == io::ErrorKind::UnexpectedEof && cursor.at_eof() => {
340 break;
341 }
342 Err(error) => return Err(error),
343 };
344 let ends_batch = matches!(
345 &record,
346 DecodedSpoolRecord::DeactivateProcess(_) | DecodedSpoolRecord::DeactivateModule(_)
347 );
348 match record {
349 DecodedSpoolRecord::Module(module) => {
350 let process = module.pid();
351 let module_index = self.definitions.modules.len();
352 self.kernel_mappings_changed |= module.is_kernel();
353 self.definitions.modules.push(module);
354 self.definitions.frame_contexts.push_module();
355 if let Some(process) = process {
356 self.active_modules_by_process
357 .entry(process)
358 .or_default()
359 .push(module_index);
360 self.observe_process(process);
361 }
362 }
363 DecodedSpoolRecord::Frame(frame) => {
364 let module_limit = self.definitions.modules.len();
365 self.definitions.frames.push(frame);
366 self.definitions.frame_contexts.push_frame(module_limit);
367 }
368 DecodedSpoolRecord::Stack(stack) => self.definitions.stack_nodes.push(stack),
369 DecodedSpoolRecord::Thread(thread) => self.threads.push(thread),
370 DecodedSpoolRecord::Sample(sample) => {
371 self.first_sample_timestamp_ns
372 .get_or_insert(sample.timestamp_ns);
373 self.observe_process(sample.process_id);
374 self.samples.push(sample);
375 }
376 DecodedSpoolRecord::PythonRuntime(record) => {
377 self.observe_process(record.process_id);
378 }
379 DecodedSpoolRecord::DeactivateProcess(process_id) => {
380 for module_index in self
381 .active_modules_by_process
382 .remove(&process_id)
383 .unwrap_or_default()
384 {
385 self.retired_modules
386 .push(self.definitions.modules[module_index].id);
387 self.definitions
388 .frame_contexts
389 .deactivate_module(module_index, self.definitions.frames.len());
390 }
391 self.observe_process(process_id);
392 self.retired_processes.push(process_id);
393 }
394 DecodedSpoolRecord::DeactivateModule(module_id) => {
395 let module = &self.definitions.modules[module_id];
396 let process = module.pid();
397 self.retired_modules.push(module.id);
398 self.kernel_mappings_changed |= module.is_kernel();
399 self.definitions
400 .frame_contexts
401 .deactivate_module(module_id, self.definitions.frames.len());
402 if let Some(process) = process {
403 if let Some(modules) = self.active_modules_by_process.get_mut(&process) {
404 modules.retain(|&active| active != module_id);
405 if modules.is_empty() {
406 self.active_modules_by_process.remove(&process);
407 }
408 }
409 self.observe_process(process);
410 }
411 }
412 }
413 self.position = cursor.position;
414 if self.samples.len() == MAX_BATCH_SAMPLES || ends_batch {
415 return Ok(self.position < self.mmap.len());
416 }
417 }
418 Ok(false)
419 }
420}
421
422impl<'a> TailBatch<'a> {
423 #[cfg(test)]
424 pub(crate) fn samples(&self) -> &[SampleRecord] {
425 &self.tail.samples
426 }
427
428 pub fn stacks(&self) -> impl ExactSizeIterator<Item = SampleStack<'a>> + '_ {
430 self.tail
431 .samples
432 .iter()
433 .copied()
434 .map(|sample| self.tail.definitions.sample_stack(sample))
435 }
436
437 #[must_use]
442 pub fn processes(&self) -> &[crate::Pid] {
443 &self.tail.observed_processes
444 }
445
446 #[must_use]
451 pub fn has_more(&self) -> bool {
452 self.tail.more_available
453 }
454
455 pub(crate) fn modules(&self) -> &[super::ModuleRecord] {
456 &self.tail.definitions.modules[self.modules.clone()]
457 }
458
459 pub(crate) fn frames(&self) -> &[super::FrameRecord] {
460 &self.tail.definitions.frames[self.frames.clone()]
461 }
462
463 #[must_use]
465 pub fn timestamp_us(&self, sample: &SampleRecord) -> Option<u64> {
466 self.tail.timestamp_us(sample)
467 }
468
469 pub(crate) fn source_id(&self) -> u64 {
470 self.tail.definitions.source_id
471 }
472
473 pub(crate) fn all_modules(&self) -> &[super::ModuleRecord] {
474 &self.tail.definitions.modules
475 }
476
477 pub(crate) fn all_frames(&self) -> &[super::FrameRecord] {
478 &self.tail.definitions.frames
479 }
480
481 pub(crate) fn frame_module_contexts(&self) -> super::SpoolFrameModuleContexts {
482 self.tail.definitions.frame_contexts.clone()
483 }
484
485 pub(crate) fn retired_processes(&self) -> &[crate::Pid] {
486 &self.tail.retired_processes
487 }
488
489 pub(crate) fn retired_modules(&self) -> &[u32] {
490 &self.tail.retired_modules
491 }
492
493 pub(crate) fn frame_contexts_changed(&self) -> bool {
494 !self.modules.is_empty() || !self.frames.is_empty() || !self.tail.retired_modules.is_empty()
495 }
496
497 pub(crate) fn kernel_mappings_changed(&self) -> bool {
498 self.tail.kernel_mappings_changed
499 }
500}
501
502#[cfg(test)]
503mod tests {
504 use std::io::Write;
505
506 use super::*;
507 use crate::spool::{
508 FrameMode, FrameRecord, ModulePath, ModuleRecord, PerfSpoolWriter, Snapshot,
509 };
510 use crate::test_support::TempDir;
511
512 fn module() -> ModuleRecord {
513 ModuleRecord::new(
514 0,
515 crate::Pid::new(7).unwrap(),
516 0x1000..0x2000,
517 0,
518 ModulePath::from("/tmp/test.so"),
519 )
520 .unwrap()
521 .file_identity(0, 0, 1, 0)
522 }
523
524 fn frame() -> FrameRecord {
525 FrameRecord {
526 module_id: Some(0),
527 file_relative_ip: 0x10,
528 abs_ip: 0x1010,
529 mode: FrameMode::User,
530 }
531 }
532
533 fn assert_no_invalidation(invalidation: crate::symbolize::Invalidation<'_>) {
534 assert!(!invalidation.all());
535 assert!(invalidation.processes().next().is_none());
536 }
537
538 #[test]
539 fn polls_flushed_prefixes_and_matches_finished_reader() {
540 let dir = TempDir::new("tail-prefixes");
541 let path = dir.path().join("recording.spool");
542 let mut writer = PerfSpoolWriter::create(&path, 123, 10).unwrap();
543 writer.flush().unwrap();
544 let mut tail = Tail::open(&path).unwrap();
545 assert!(format!("{tail:?}").contains("pending_samples: 0"));
546 assert_eq!(tail.start_timestamp_us(), Some(123));
547 assert_eq!(tail.sample_interval_us(), Some(10));
548 let mut symbolizer = tail
549 .symbolizer()
550 .disable_perf_maps()
551 .stack_cache(crate::StackCache::External)
552 .build()
553 .unwrap();
554 let initial = tail.poll().unwrap();
555 assert!(initial.samples().is_empty());
556 assert!(!initial.has_more());
557 assert!(initial.processes().is_empty());
558 let other_tail = Tail::open(&path).unwrap();
559 let mut other_symbolizer = other_tail.symbolizer().build().unwrap();
560 let error = other_symbolizer
561 .update(&initial)
562 .err()
563 .expect("a symbolizer must reject batches from another tail");
564 assert_eq!(error.kind(), crate::ErrorKind::InvalidInput);
565 assert_no_invalidation(symbolizer.update(&initial).unwrap());
566
567 writer.write_module(&module()).unwrap();
568 writer.write_sample_frames(1_000, 7, 8, [frame()]).unwrap();
569 writer.write_python_runtime(1_500, 9, true).unwrap();
570 writer.flush().unwrap();
571 let first = tail.poll().unwrap();
572 assert!(format!("{first:?}").contains("samples: 1"));
573 assert_eq!(first.samples().len(), 1);
574 assert_eq!(
575 first.processes(),
576 &[crate::Pid::new(7).unwrap(), crate::Pid::new(9).unwrap()]
577 );
578 assert_eq!(first.timestamp_us(&first.samples()[0]), Some(123));
579 assert!(!symbolizer.update(&first).unwrap().all());
580 let native_backend = symbolizer.has_native_backend();
581 let mut stack = symbolizer.resolve(first.stacks().next().unwrap()).unwrap();
582 assert_eq!(stack.is_cacheable(), !native_backend);
583 assert!(stack.next_with_id().is_some());
584 assert!(stack.next().is_none());
585
586 writer.write_sample_frames(3_000, 7, 8, [frame()]).unwrap();
587 writer.write_module_deactivation(7).unwrap();
588 writer.flush().unwrap();
589 let second = tail.poll().unwrap();
590 assert_eq!(second.timestamp_us(&second.samples()[0]), Some(125));
591 symbolizer.update(&second).unwrap();
592 assert_eq!(
593 symbolizer
594 .resolve(second.stacks().next().unwrap())
595 .unwrap()
596 .len(),
597 1
598 );
599
600 let after_retirement = tail.poll().unwrap();
601 let invalidation = symbolizer.update(&after_retirement).unwrap();
602 let pid = crate::Pid::new(7).unwrap();
603 assert_eq!(invalidation.processes().collect::<Vec<_>>(), vec![pid]);
604 assert!(invalidation.affects_process(pid));
605 drop(writer);
606
607 let finished = Snapshot::open(&path).unwrap();
608 assert_eq!(tail.modules(), finished.modules());
609 assert_eq!(tail.frames(), finished.frames());
610 assert_eq!(finished.samples().len(), 2);
611 assert_eq!(finished.python_runtime_records().len(), 1);
612
613 let mut append = std::fs::OpenOptions::new()
614 .append(true)
615 .open(&path)
616 .unwrap();
617 append.write_all(&[u8::MAX]).unwrap();
618 append.flush().unwrap();
619 let error = tail.poll().unwrap_err();
620 assert_eq!(error.kind(), crate::ErrorKind::CorruptSpool);
621 }
622
623 #[test]
624 fn retries_a_sample_cut_off_mid_record_without_double_delta() {
625 let dir = TempDir::new("tail-partial-sample");
626 let path = dir.path().join("recording.spool");
627 let mut writer = PerfSpoolWriter::from_writer(Vec::new(), 0, 10).unwrap();
628 writer
629 .write_sample_frames(
630 1_000,
631 7,
632 8,
633 [FrameRecord {
634 module_id: None,
635 file_relative_ip: 0,
636 abs_ip: 0x1010,
637 mode: FrameMode::User,
638 }],
639 )
640 .unwrap();
641 let bytes = writer.into_inner();
642 let split = bytes.len() - 1;
643 std::fs::write(&path, &bytes[..split]).unwrap();
644
645 let mut tail = Tail::open(&path).unwrap();
646 assert!(tail.poll().unwrap().samples().is_empty());
647 let mut append = std::fs::OpenOptions::new()
648 .append(true)
649 .open(&path)
650 .unwrap();
651 append.write_all(&bytes[split..]).unwrap();
652 append.flush().unwrap();
653 {
654 let batch = tail.poll().unwrap();
655 let mut stacks = batch.stacks();
656 let stack = stacks.next().expect("expected one sample");
657 assert!(stacks.next().is_none());
658 assert_eq!(stack.sample().timestamp_ns, 1_000);
659 }
660
661 append.set_len(8).unwrap();
662 let error = tail.poll().unwrap_err();
663 assert_eq!(error.kind(), crate::ErrorKind::CorruptSpool);
664 }
665
666 #[test]
667 fn discards_consumed_prefix_and_continues_tailing() {
668 let dir = TempDir::new("tail-discard");
669 let path = dir.path().join("recording.spool");
670 let mut writer = PerfSpoolWriter::create(&path, 0, 1).unwrap();
671 let frame = FrameRecord {
672 module_id: None,
673 file_relative_ip: 0,
674 abs_ip: 0x1000,
675 mode: FrameMode::User,
676 };
677 for index in 0..20_000_u64 {
678 writer
679 .write_sample_frames(index + 1, 7, 8, [frame])
680 .unwrap();
681 }
682 writer.flush().unwrap();
683 let file = writer.open_reader().unwrap();
684 let discarder = writer.open_discarder().unwrap();
685 file.sync_all().unwrap();
686 let mut tail = Tail::from_file_inner(file, Some(discarder), None).unwrap();
687 {
688 let first = tail.poll().unwrap();
689 assert_eq!(first.samples().len(), MAX_BATCH_SAMPLES);
690 }
691
692 if let Err(error) = tail.discard_consumed() {
693 assert_eq!(error.kind(), crate::ErrorKind::Unsupported);
694 return;
695 }
696 tail.discard_consumed().unwrap();
697
698 {
699 let second = tail.poll().unwrap();
700 assert_eq!(second.samples().len(), 20_000 - MAX_BATCH_SAMPLES);
701 }
702
703 writer.write_sample_frames(20_001, 7, 8, [frame]).unwrap();
704 writer.flush().unwrap();
705 {
706 let appended = tail.poll().unwrap();
707 assert_eq!(appended.samples().len(), 1);
708 assert_eq!(appended.samples()[0].timestamp_ns, 20_001);
709 }
710
711 assert!(super::super::Replay::open(&path).is_err());
712 }
713}