1use reverie_syscalls::Sysno;
31use serde::Deserialize;
32use serde::Serialize;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum SyscallOutcome {
37 Detcore,
39 Backend,
42 Refused,
45}
46
47#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default)]
55pub struct SyscallOutcomes {
56 pub calls: u64,
58 pub detcore: u64,
60 pub backend: u64,
62 pub refused: u64,
64}
65
66impl SyscallOutcomes {
67 pub const ZERO: Self = Self {
69 calls: 0,
70 detcore: 0,
71 backend: 0,
72 refused: 0,
73 };
74
75 pub fn merge(&mut self, other: &SyscallOutcomes) {
77 self.calls += other.calls;
78 self.detcore += other.detcore;
79 self.backend += other.backend;
80 self.refused += other.refused;
81 }
82
83 pub fn unfinished(&self) -> u64 {
85 self.calls
86 .saturating_sub(self.detcore + self.backend + self.refused)
87 }
88}
89
90#[derive(
92 Debug,
93 Serialize,
94 Deserialize,
95 Clone,
96 Copy,
97 PartialEq,
98 Eq,
99 PartialOrd,
100 Ord
101)]
102#[serde(rename_all = "snake_case")]
103pub enum SyscallObservationGap {
104 PassthruOpt,
107 InProcessRuntimeStart,
111}
112
113#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
115#[serde(tag = "kind", rename_all = "snake_case")]
116pub enum SyscallObservationScope {
117 Complete,
119 Partial { gaps: Vec<SyscallObservationGap> },
122}
123
124impl SyscallObservationScope {
125 pub fn from_gaps(gaps: impl IntoIterator<Item = SyscallObservationGap>) -> Self {
127 let mut gaps: Vec<_> = gaps.into_iter().collect();
128 gaps.sort();
129 gaps.dedup();
130 if gaps.is_empty() {
131 Self::Complete
132 } else {
133 Self::Partial { gaps }
134 }
135 }
136
137 pub fn is_complete(&self) -> bool {
139 matches!(self, Self::Complete)
140 }
141}
142
143#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
145pub struct SyscallHistogramEntry {
146 pub nr: i32,
148 pub name: String,
151 #[serde(flatten)]
153 pub counts: SyscallOutcomes,
154}
155
156#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
158pub struct SyscallHistogramSummary {
159 pub architecture: String,
161 pub observation_scope: SyscallObservationScope,
163 pub syscalls: Vec<SyscallHistogramEntry>,
166}
167
168impl SyscallHistogramSummary {
169 pub const ARCHITECTURE: &'static str = "x86_64";
171
172 pub fn total_calls(&self) -> u64 {
175 self.syscalls.iter().map(|entry| entry.counts.calls).sum()
176 }
177
178 pub fn get(&self, name: &str) -> Option<&SyscallHistogramEntry> {
180 self.syscalls.iter().find(|entry| entry.name == name)
181 }
182
183 pub fn compact(&self) -> CompactSyscallHistogram {
185 CompactSyscallHistogram {
186 architecture: self.architecture.clone(),
187 observation_scope: self.observation_scope.clone(),
188 counts: self
189 .syscalls
190 .iter()
191 .map(|entry| {
192 let SyscallOutcomes {
193 calls,
194 detcore,
195 backend,
196 refused,
197 } = entry.counts;
198 [
199 u64::from(entry.nr.unsigned_abs()),
200 calls,
201 detcore,
202 backend,
203 refused,
204 ]
205 })
206 .collect(),
207 }
208 }
209}
210
211#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
216#[serde(deny_unknown_fields)]
217pub struct CompactSyscallHistogram {
218 pub architecture: String,
220 pub observation_scope: SyscallObservationScope,
222 pub counts: Vec<[u64; 5]>,
225}
226
227impl CompactSyscallHistogram {
228 pub fn total_calls(&self) -> u64 {
230 self.counts.iter().map(|[_, calls, ..]| calls).sum()
231 }
232
233 pub fn malformation(&self) -> Option<String> {
238 if self.architecture != SyscallHistogramSummary::ARCHITECTURE {
239 return Some(format!(
240 "syscall histogram architecture is {:?}, expected {:?}",
241 self.architecture,
242 SyscallHistogramSummary::ARCHITECTURE
243 ));
244 }
245 let mut previous = None;
246 for [nr, calls, detcore, backend, refused] in &self.counts {
247 let in_table = usize::try_from(*nr)
248 .ok()
249 .and_then(|nr| nr.checked_sub(Sysno::first().id() as usize))
250 .is_some_and(|slot| slot < SYSCALL_TABLE_SIZE);
251 if !in_table {
252 return Some(format!("syscall {nr} is outside the x86_64 table"));
253 }
254 if previous.is_some_and(|previous| previous >= *nr) {
255 return Some(format!("syscall {nr} is out of ascending order"));
256 }
257 previous = Some(*nr);
258 if *calls == 0 {
259 return Some(format!("syscall {nr} has an entry with no calls"));
260 }
261 let outcomes = detcore
262 .checked_add(*backend)
263 .and_then(|sum| sum.checked_add(*refused));
264 if outcomes.is_none_or(|outcomes| outcomes > *calls) {
265 return Some(format!(
266 "syscall {nr} has more outcomes than its {calls} calls"
267 ));
268 }
269 }
270 None
271 }
272}
273
274pub const SYSCALL_TABLE_SIZE: usize = Sysno::table_size();
277
278#[derive(Clone, PartialEq, Eq)]
289pub struct SyscallHistogram {
290 counts: Box<[SyscallOutcomes; SYSCALL_TABLE_SIZE]>,
291}
292
293impl Default for SyscallHistogram {
294 fn default() -> Self {
295 Self {
296 counts: Box::new([SyscallOutcomes::ZERO; SYSCALL_TABLE_SIZE]),
297 }
298 }
299}
300
301fn slot(sysno: Sysno) -> usize {
303 (sysno.id() - Sysno::first().id()) as usize
304}
305
306impl SyscallHistogram {
307 pub fn enter(&mut self, sysno: Sysno) {
309 self.counts[slot(sysno)].calls += 1;
310 }
311
312 pub fn finish(&mut self, sysno: Sysno, outcome: SyscallOutcome) {
314 let counts = &mut self.counts[slot(sysno)];
315 match outcome {
316 SyscallOutcome::Detcore => counts.detcore += 1,
317 SyscallOutcome::Backend => counts.backend += 1,
318 SyscallOutcome::Refused => counts.refused += 1,
319 }
320 }
321
322 pub fn merge(&mut self, other: &SyscallHistogram) {
324 for (mine, theirs) in self.counts.iter_mut().zip(other.counts.iter()) {
325 mine.merge(theirs);
326 }
327 }
328
329 pub fn total_calls(&self) -> u64 {
331 self.counts.iter().map(|counts| counts.calls).sum()
332 }
333
334 pub fn is_empty(&self) -> bool {
336 self.nonzero().next().is_none()
337 }
338
339 pub fn get(&self, sysno: Sysno) -> Option<&SyscallOutcomes> {
341 Some(&self.counts[slot(sysno)]).filter(|counts| **counts != SyscallOutcomes::ZERO)
342 }
343
344 fn nonzero(&self) -> impl Iterator<Item = (i32, &SyscallOutcomes)> {
346 self.counts
347 .iter()
348 .enumerate()
349 .filter(|(_, counts)| **counts != SyscallOutcomes::ZERO)
350 .map(|(index, counts)| (Sysno::first().id() + index as i32, counts))
351 }
352
353 pub fn summary(&self, observation_scope: SyscallObservationScope) -> SyscallHistogramSummary {
357 SyscallHistogramSummary {
358 architecture: SyscallHistogramSummary::ARCHITECTURE.to_owned(),
359 observation_scope,
360 syscalls: self
361 .nonzero()
362 .map(|(nr, counts)| SyscallHistogramEntry {
363 nr,
364 name: syscall_name(nr),
365 counts: *counts,
366 })
367 .collect(),
368 }
369 }
370}
371
372impl std::fmt::Debug for SyscallHistogram {
373 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
374 f.debug_map().entries(self.nonzero()).finish()
375 }
376}
377
378impl Serialize for SyscallHistogram {
379 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
380 let pairs: Vec<_> = self.nonzero().collect();
383 pairs.serialize(serializer)
384 }
385}
386
387impl<'de> Deserialize<'de> for SyscallHistogram {
388 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
389 let pairs = Vec::<(i32, SyscallOutcomes)>::deserialize(deserializer)?;
390 let mut histogram = Self::default();
391 let mut previous = None;
392 for (nr, counts) in pairs {
393 let index = nr
394 .checked_sub(Sysno::first().id())
395 .and_then(|index| usize::try_from(index).ok())
396 .filter(|index| *index < SYSCALL_TABLE_SIZE)
397 .ok_or_else(|| {
398 serde::de::Error::custom(format!("syscall {nr} is outside the table"))
399 })?;
400 if previous.is_some_and(|previous| previous >= nr) {
401 return Err(serde::de::Error::custom(format!(
402 "syscall {nr} is out of ascending order"
403 )));
404 }
405 previous = Some(nr);
406 histogram.counts[index] = counts;
407 }
408 Ok(histogram)
409 }
410}
411
412#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
422pub struct SparseSyscallHistogram {
423 entries: Vec<(i32, SyscallOutcomes)>,
424}
425
426impl From<&SyscallHistogram> for SparseSyscallHistogram {
427 fn from(histogram: &SyscallHistogram) -> Self {
428 Self {
429 entries: histogram
430 .nonzero()
431 .map(|(nr, counts)| (nr, *counts))
432 .collect(),
433 }
434 }
435}
436
437impl SparseSyscallHistogram {
438 pub fn merge(&mut self, other: &SparseSyscallHistogram) {
440 let mut merged = Vec::with_capacity(self.entries.len() + other.entries.len());
441 let (mut mine, mut theirs) = (
442 self.entries.iter().peekable(),
443 other.entries.iter().peekable(),
444 );
445 loop {
446 let next = match (mine.peek(), theirs.peek()) {
447 (Some((left, _)), Some((right, _))) if left < right => *mine.next().unwrap(),
448 (Some((left, _)), Some((right, _))) if right < left => *theirs.next().unwrap(),
449 (Some(_), Some(_)) => {
450 let (nr, mut counts) = *mine.next().unwrap();
451 counts.merge(&theirs.next().unwrap().1);
452 (nr, counts)
453 }
454 (Some(_), None) => *mine.next().unwrap(),
455 (None, Some(_)) => *theirs.next().unwrap(),
456 (None, None) => break,
457 };
458 merged.push(next);
459 }
460 self.entries = merged;
461 }
462
463 pub fn add_to(&self, total: &mut SyscallHistogram) {
467 for (nr, counts) in &self.entries {
468 if let Some(slot) = nr
469 .checked_sub(Sysno::first().id())
470 .and_then(|slot| usize::try_from(slot).ok())
471 .filter(|slot| *slot < SYSCALL_TABLE_SIZE)
472 {
473 total.counts[slot].merge(counts);
474 }
475 }
476 }
477
478 pub fn total_calls(&self) -> u64 {
480 self.entries.iter().map(|(_, counts)| counts.calls).sum()
481 }
482}
483
484pub fn pinned_syscall_table() -> Vec<(i32, &'static str)> {
488 (0..SYSCALL_TABLE_SIZE)
489 .filter_map(|slot| Sysno::new(slot + Sysno::first().id() as usize))
490 .map(|sysno| (sysno.id(), sysno.name()))
491 .collect()
492}
493
494fn syscall_name(nr: i32) -> String {
495 usize::try_from(nr)
496 .ok()
497 .and_then(Sysno::new)
498 .map_or_else(|| format!("syscall_{nr}"), |sysno| sysno.name().to_owned())
499}
500
501#[cfg(test)]
502mod tests {
503 use super::*;
504
505 #[test]
506 fn entries_and_outcomes_are_counted_separately() {
507 let mut histogram = SyscallHistogram::default();
508 histogram.enter(Sysno::read);
509 histogram.finish(Sysno::read, SyscallOutcome::Backend);
510 histogram.enter(Sysno::read);
511 histogram.finish(Sysno::read, SyscallOutcome::Refused);
512 histogram.enter(Sysno::getpid);
513 histogram.finish(Sysno::getpid, SyscallOutcome::Detcore);
514 histogram.enter(Sysno::exit_group);
515
516 assert_eq!(histogram.total_calls(), 4);
517 assert_eq!(
518 histogram.get(Sysno::read),
519 Some(&SyscallOutcomes {
520 calls: 2,
521 detcore: 0,
522 backend: 1,
523 refused: 1,
524 })
525 );
526 let exit_group = histogram.get(Sysno::exit_group).unwrap();
527 assert_eq!(exit_group.calls, 1);
528 assert_eq!(exit_group.unfinished(), 1);
529 assert_eq!(histogram.get(Sysno::read).unwrap().unfinished(), 0);
530 assert_eq!(histogram.get(Sysno::write), None);
531 }
532
533 #[test]
534 fn merge_adds_every_count() {
535 let mut left = SyscallHistogram::default();
536 left.enter(Sysno::read);
537 left.finish(Sysno::read, SyscallOutcome::Backend);
538 let mut right = SyscallHistogram::default();
539 right.enter(Sysno::read);
540 right.finish(Sysno::read, SyscallOutcome::Detcore);
541 right.enter(Sysno::write);
542 left.merge(&right);
543
544 assert_eq!(left.total_calls(), 3);
545 let read = left.get(Sysno::read).unwrap();
546 assert_eq!((read.calls, read.detcore, read.backend), (2, 1, 1));
547 assert_eq!(left.get(Sysno::write).unwrap().calls, 1);
548
549 let mut empty = SyscallHistogram::default();
550 assert!(empty.is_empty());
551 empty.merge(&left);
552 assert_eq!(empty, left);
553 }
554
555 #[test]
556 fn the_summary_is_ordered_by_number_and_carries_names_and_scope() {
557 let mut histogram = SyscallHistogram::default();
558 histogram.enter(Sysno::write);
559 histogram.enter(Sysno::read);
560 histogram.finish(Sysno::read, SyscallOutcome::Backend);
561 let summary = histogram.summary(SyscallObservationScope::Complete);
562 let json = serde_json::to_string(&summary).unwrap();
563 assert_eq!(
564 json,
565 r#"{"architecture":"x86_64","observation_scope":{"kind":"complete"},"syscalls":[{"nr":0,"name":"read","calls":1,"detcore":0,"backend":1,"refused":0},{"nr":1,"name":"write","calls":1,"detcore":0,"backend":0,"refused":0}]}"#
566 );
567 assert_eq!(
568 serde_json::from_str::<SyscallHistogramSummary>(&json).unwrap(),
569 summary
570 );
571 assert_eq!(summary.total_calls(), 2);
572 assert_eq!(summary.get("write").unwrap().nr, 1);
573 assert_eq!(syscall_name(-1), "syscall_-1");
574 assert_eq!(syscall_name(100_000), "syscall_100000");
575 }
576
577 #[test]
578 fn a_scope_with_gaps_is_partial_and_sorted() {
579 assert_eq!(
580 SyscallObservationScope::from_gaps([]),
581 SyscallObservationScope::Complete
582 );
583 let partial = SyscallObservationScope::from_gaps([
584 SyscallObservationGap::InProcessRuntimeStart,
585 SyscallObservationGap::PassthruOpt,
586 SyscallObservationGap::InProcessRuntimeStart,
587 ]);
588 assert!(!partial.is_complete());
589 assert_eq!(
590 serde_json::to_string(&partial).unwrap(),
591 r#"{"kind":"partial","gaps":["passthru_opt","in_process_runtime_start"]}"#
592 );
593 }
594
595 #[test]
596 fn every_table_number_has_a_slot_and_the_wire_form_is_sparse() {
597 let mut histogram = SyscallHistogram::default();
598 for sysno in
600 (0..SYSCALL_TABLE_SIZE).filter_map(|nr| Sysno::new(nr + Sysno::first().id() as usize))
601 {
602 histogram.enter(sysno);
603 }
604 assert_eq!(histogram.total_calls(), Sysno::count() as u64);
605 assert_eq!(histogram.get(Sysno::last()).unwrap().calls, 1);
606
607 let mut sparse = SyscallHistogram::default();
608 sparse.enter(Sysno::write);
609 sparse.finish(Sysno::write, SyscallOutcome::Detcore);
610 assert_eq!(
611 serde_json::to_string(&sparse).unwrap(),
612 r#"[[1,{"calls":1,"detcore":1,"backend":0,"refused":0}]]"#
613 );
614 let out_of_table = format!(
615 r#"[[{SYSCALL_TABLE_SIZE},{{"calls":1,"detcore":0,"backend":0,"refused":0}}]]"#
616 );
617 assert!(serde_json::from_str::<SyscallHistogram>(&out_of_table).is_err());
618 let unordered = r#"[[1,{"calls":1,"detcore":0,"backend":0,"refused":0}],[0,{"calls":1,"detcore":0,"backend":0,"refused":0}]]"#;
619 assert!(serde_json::from_str::<SyscallHistogram>(unordered).is_err());
620 }
621
622 #[test]
623 fn the_pinned_table_names_every_countable_syscall_once() {
624 let table = pinned_syscall_table();
625 assert_eq!(table.len(), Sysno::count());
626 assert_eq!(table.first(), Some(&(0, "read")));
627 assert!(table.windows(2).all(|pair| pair[0].0 < pair[1].0));
628 let names: std::collections::BTreeSet<_> = table.iter().map(|(_, name)| name).collect();
629 assert_eq!(names.len(), table.len());
630 }
631
632 #[test]
633 fn the_compact_form_keeps_numbers_counts_and_scope() {
634 let mut histogram = SyscallHistogram::default();
635 histogram.enter(Sysno::openat);
636 histogram.finish(Sysno::openat, SyscallOutcome::Backend);
637 histogram.enter(Sysno::read);
638 histogram.enter(Sysno::read);
639 histogram.finish(Sysno::read, SyscallOutcome::Detcore);
640 let scope = SyscallObservationScope::from_gaps([SyscallObservationGap::PassthruOpt]);
641 let compact = histogram.summary(scope).compact();
642 assert_eq!(
643 serde_json::to_string(&compact).unwrap(),
644 r#"{"architecture":"x86_64","observation_scope":{"kind":"partial","gaps":["passthru_opt"]},"counts":[[0,2,1,0,0],[257,1,0,1,0]]}"#
645 );
646 assert_eq!(compact.total_calls(), 3);
647 assert_eq!(compact.malformation(), None);
648 }
649
650 #[test]
651 fn a_malformed_compact_histogram_names_its_fault() {
652 let good = CompactSyscallHistogram {
653 architecture: "x86_64".into(),
654 observation_scope: SyscallObservationScope::Complete,
655 counts: vec![[0, 2, 1, 1, 0], [1, 1, 0, 0, 0]],
656 };
657 assert_eq!(good.malformation(), None);
658 let faults = [
659 (
660 CompactSyscallHistogram {
661 architecture: "aarch64".into(),
662 ..good.clone()
663 },
664 "architecture",
665 ),
666 (
667 CompactSyscallHistogram {
668 counts: vec![[SYSCALL_TABLE_SIZE as u64, 1, 0, 0, 0]],
669 ..good.clone()
670 },
671 "outside",
672 ),
673 (
674 CompactSyscallHistogram {
675 counts: vec![[1, 1, 0, 0, 0], [1, 1, 0, 0, 0]],
676 ..good.clone()
677 },
678 "ascending",
679 ),
680 (
681 CompactSyscallHistogram {
682 counts: vec![[1, 0, 0, 0, 0]],
683 ..good.clone()
684 },
685 "no calls",
686 ),
687 (
688 CompactSyscallHistogram {
689 counts: vec![[1, 1, 1, 1, 0]],
690 ..good.clone()
691 },
692 "more outcomes",
693 ),
694 ];
695 for (histogram, fault) in faults {
696 let message = histogram.malformation().expect("malformed");
697 assert!(message.contains(fault), "{message}");
698 }
699 let unknown = r#"{"architecture":"x86_64","observation_scope":{"kind":"complete"},"counts":[],"names":[]}"#;
700 assert!(serde_json::from_str::<CompactSyscallHistogram>(unknown).is_err());
701 }
702
703 #[test]
704 fn the_finished_form_keeps_only_nonzero_slots_and_sums_back_exactly() {
705 let mut first = SyscallHistogram::default();
706 first.enter(Sysno::read);
707 first.finish(Sysno::read, SyscallOutcome::Backend);
708 first.enter(Sysno::exit);
709 let mut second = SyscallHistogram::default();
710 second.enter(Sysno::read);
711 second.finish(Sysno::read, SyscallOutcome::Detcore);
712 second.enter(Sysno::getpid);
713 second.enter(Sysno::exit);
714
715 let mut sparse = SparseSyscallHistogram::from(&first);
716 assert_eq!(sparse.entries.len(), 2);
717 sparse.merge(&SparseSyscallHistogram::from(&second));
718 assert_eq!(
719 sparse.entries.iter().map(|(nr, _)| *nr).collect::<Vec<_>>(),
720 [Sysno::read.id(), Sysno::getpid.id(), Sysno::exit.id()]
721 );
722 assert_eq!(sparse.total_calls(), 5);
723
724 let mut dense = first.clone();
725 dense.merge(&second);
726 let mut total = SyscallHistogram::default();
727 sparse.add_to(&mut total);
728 assert_eq!(total, dense);
729 let mut empty = SparseSyscallHistogram::default();
731 empty.merge(&sparse);
732 assert_eq!(empty, sparse);
733 empty.merge(&SparseSyscallHistogram::default());
734 assert_eq!(empty, sparse);
735 }
736
737 #[test]
738 fn the_per_thread_form_round_trips_through_bincode() {
739 let mut histogram = SyscallHistogram::default();
740 histogram.enter(Sysno::openat);
741 histogram.finish(Sysno::openat, SyscallOutcome::Refused);
742 let config = bincode::config::legacy();
743 let bytes = bincode::serde::encode_to_vec(&histogram, config).unwrap();
744 let (decoded, _): (SyscallHistogram, usize) =
745 bincode::serde::decode_from_slice(&bytes, config).unwrap();
746 assert_eq!(decoded, histogram);
747 }
748}