use keyhog_profile::{
CollectorAvailability, CollectorId, Evidence, EvidenceGap, RunIdentity, RunState, Session,
};
fn session(name: &str) -> Session {
Session::start(RunIdentity::new(
"0.5.49",
"detectors",
"config",
name,
"test",
"cpu-simd",
))
.expect("start profile")
}
fn collector(
profile: &keyhog_profile::RunProfile,
id: CollectorId,
) -> &keyhog_profile::CollectorCapability {
profile
.collectors
.iter()
.find(|capability| capability.collector == id)
.expect("collector capability present")
}
#[cfg(not(feature = "hardware-counters"))]
#[test]
fn disabled_feature_reports_disabled_collectors_and_gap() {
let profile = session("hw-disabled").finish(RunState::Completed);
for id in [
CollectorId::HardwareCounters,
CollectorId::SchedulerActivity,
CollectorId::ThreadUtilization,
CollectorId::CpuTopology,
] {
let capability = collector(&profile, id);
assert_eq!(capability.availability, CollectorAvailability::Disabled);
assert_eq!(
capability.detail.as_deref(),
Some("enable the keyhog-profile hardware-counters feature")
);
}
assert_eq!(
profile.hardware,
Evidence::unavailable(EvidenceGap::CollectorDisabled)
);
}
#[cfg(all(feature = "hardware-counters", target_os = "linux"))]
mod linux {
use keyhog_profile::{
CounterId, HardwareCounterCollector, HardwareFieldSourceV2, MetricId, SchedulerCollector,
SnapshotCollector,
};
use super::*;
#[test]
fn perf_counters_collect_or_report_permission_gap() {
let mut collector = HardwareCounterCollector::new();
let capability = collector.capability();
assert_eq!(capability.collector, CollectorId::HardwareCounters);
let begin = collector.sample();
let mut sink = 0_u64;
for index in 0..1_000_000_u64 {
sink = sink.wrapping_add(index).wrapping_mul(2_654_435_761);
}
std::hint::black_box(sink);
let end = collector.sample();
match capability.availability {
CollectorAvailability::Available => {
let begin_cycles = match &begin.cycles.value {
Evidence::Recorded { value } => *value,
other => panic!("cycles must be recorded when available: {other:?}"),
};
let end_cycles = match &end.cycles.value {
Evidence::Recorded { value } => *value,
other => panic!("cycles must be recorded when available: {other:?}"),
};
assert!(end_cycles > begin_cycles);
let begin_instructions = match &begin.instructions.value {
Evidence::Recorded { value } => *value,
other => panic!("instructions must be recorded when available: {other:?}"),
};
let end_instructions = match &end.instructions.value {
Evidence::Recorded { value } => *value,
other => panic!("instructions must be recorded when available: {other:?}"),
};
assert!(end_instructions > begin_instructions);
assert_eq!(begin.cycles.source, HardwareFieldSourceV2::PerfEventOpen);
assert_eq!(
begin.instructions.source,
HardwareFieldSourceV2::PerfEventOpen
);
}
CollectorAvailability::PermissionDenied => {
let detail = capability.detail.expect("denial must explain itself");
assert!(
detail.contains("perf_event_paranoid"),
"denial must name the paranoid level: {detail}"
);
assert_eq!(
begin.cycles.value,
Evidence::unavailable(EvidenceGap::PermissionDenied)
);
assert_eq!(
begin.instructions.value,
Evidence::unavailable(EvidenceGap::PermissionDenied)
);
assert_eq!(
begin.cache_references.value,
Evidence::unavailable(EvidenceGap::PermissionDenied)
);
assert_eq!(
begin.branch_misses.value,
Evidence::unavailable(EvidenceGap::PermissionDenied)
);
assert_eq!(
begin.stalled_cycles_frontend.value,
Evidence::unavailable(EvidenceGap::PermissionDenied)
);
assert_eq!(
begin.stalled_cycles_backend.value,
Evidence::unavailable(EvidenceGap::PermissionDenied)
);
let set = keyhog_profile::HardwareCounterSetV2::between(&begin, &end);
assert_eq!(
set.cpi_milli,
Evidence::unavailable(EvidenceGap::PermissionDenied)
);
assert_eq!(
set.cache_miss_ratio_milli,
Evidence::unavailable(EvidenceGap::PermissionDenied)
);
assert_eq!(
set.branch_miss_ratio_milli,
Evidence::unavailable(EvidenceGap::PermissionDenied)
);
}
other => panic!("unexpected hardware counter availability: {other:?}"),
}
assert_eq!(
begin.stalled_cycles_memory.value,
Evidence::unavailable(EvidenceGap::Unsupported)
);
assert_eq!(
begin.stalled_cycles_memory.source,
HardwareFieldSourceV2::PerfEventOpen
);
}
#[test]
fn scheduler_records_switches_and_delay_with_sources() {
let mut collector = SchedulerCollector::new();
assert_eq!(
collector.capability().availability,
CollectorAvailability::Available
);
let begin = collector.sample();
std::thread::sleep(std::time::Duration::from_millis(2));
let end = collector.sample();
let delta = keyhog_profile::SchedulerEvidenceV2::between(&begin, &end);
let voluntary = match &delta.voluntary_context_switches.value {
Evidence::Recorded { value } => *value,
other => panic!("voluntary switches must be recorded on Linux: {other:?}"),
};
assert!(voluntary >= 1, "a 2ms sleep must switch voluntarily");
assert_eq!(
delta.voluntary_context_switches.source,
HardwareFieldSourceV2::ProcSelfSched
);
assert_eq!(
delta.involuntary_context_switches.source,
HardwareFieldSourceV2::ProcSelfSched
);
assert!(
matches!(
delta.involuntary_context_switches.value,
Evidence::Recorded { .. }
),
"involuntary switches must be recorded on Linux"
);
assert_eq!(
delta.scheduler_delay_ns.source,
HardwareFieldSourceV2::ProcSelfSchedstat
);
assert!(
matches!(delta.scheduler_delay_ns.value, Evidence::Recorded { .. }),
"schedstat runqueue delay must be recorded on this kernel"
);
assert_eq!(
delta.timeslices.source,
HardwareFieldSourceV2::ProcSelfSchedstat
);
assert_eq!(
delta.cpu_migrations.source,
HardwareFieldSourceV2::PerfEventOpen
);
}
#[test]
fn scheduler_total_matches_independent_procfs_read() {
fn independent_switches() -> (u64, u64) {
let sched = std::fs::read_to_string("/proc/thread-self/sched").expect("thread sched");
let mut voluntary = None;
let mut involuntary = None;
for line in sched.lines() {
if let Some(value) = line.strip_prefix("nr_voluntary_switches") {
voluntary = value.split(':').nth(1).and_then(|v| v.trim().parse().ok());
} else if let Some(value) = line.strip_prefix("nr_involuntary_switches") {
involuntary = value.split(':').nth(1).and_then(|v| v.trim().parse().ok());
}
}
(
voluntary.expect("voluntary field"),
involuntary.expect("involuntary field"),
)
}
let mut collector = SchedulerCollector::new();
let begin = collector.sample();
std::thread::sleep(std::time::Duration::from_millis(2));
let end = collector.sample();
let (end_voluntary, end_involuntary) = independent_switches();
let recorded_voluntary = match &end.voluntary_context_switches.value {
Evidence::Recorded { value } => *value,
other => panic!("voluntary switches must be recorded: {other:?}"),
};
let recorded_involuntary = match &end.involuntary_context_switches.value {
Evidence::Recorded { value } => *value,
other => panic!("involuntary switches must be recorded: {other:?}"),
};
assert!(end_voluntary >= recorded_voluntary);
assert!(end_voluntary - recorded_voluntary <= 4);
assert!(end_involuntary >= recorded_involuntary);
assert!(end_involuntary - recorded_involuntary <= 4);
let delta = keyhog_profile::SchedulerEvidenceV2::between(&begin, &end);
let voluntary = match &delta.voluntary_context_switches.value {
Evidence::Recorded { value } => *value,
other => panic!("voluntary delta must be recorded: {other:?}"),
};
let involuntary = match &delta.involuntary_context_switches.value {
Evidence::Recorded { value } => *value,
other => panic!("involuntary delta must be recorded: {other:?}"),
};
assert_eq!(
delta.total_context_switches.value,
Evidence::recorded(voluntary + involuntary)
);
}
#[test]
fn session_records_scheduler_totals_as_typed_counters() {
let session = session("hw-typed-scheduler");
let runtime = session.runtime();
std::thread::sleep(std::time::Duration::from_millis(2));
drop(keyhog_profile::span(Stage::SourceRead));
let profile = session.finish(RunState::Completed);
let evidence = match &profile.hardware {
Evidence::Recorded { value } => value,
other => panic!("hardware evidence must be recorded with the feature on: {other:?}"),
};
let voluntary = match &evidence.scheduler.voluntary_context_switches.value {
Evidence::Recorded { value } => *value,
other => panic!("session voluntary switches must be recorded: {other:?}"),
};
assert!(voluntary >= 1);
let typed = runtime.take_session_typed_metrics();
let typed_voluntary = typed
.iter()
.find(|record| record.metric_id == MetricId::SchedulerVoluntaryContextSwitches)
.expect("typed voluntary context switches recorded");
assert_eq!(typed_voluntary.value, voluntary);
let typed_counter = CounterId::SchedulerVoluntaryContextSwitches;
assert_eq!(
typed_counter.metric_id(),
MetricId::SchedulerVoluntaryContextSwitches
);
let capability = collector(&profile, CollectorId::HardwareCounters);
if capability.availability == CollectorAvailability::PermissionDenied {
assert_eq!(
evidence.counters.cycles.value,
Evidence::unavailable(EvidenceGap::PermissionDenied)
);
assert!(!typed
.iter()
.any(|record| record.metric_id == MetricId::HardwareCycles));
}
}
}