use crate::allocation::{
allocation_capability, allocation_snapshot, AllocationSessionToken, AllocationSnapshotV2,
};
use crate::collector::{CollectorCapability, SnapshotCollector};
use crate::hardware::{milli_ratio, HardwareFieldSourceV2, SourcedEvidenceV2};
use crate::schema::ResourceSnapshot;
use crate::schema_v2::{Evidence, EvidenceGap};
use serde::{Deserialize, Serialize};
#[cfg(all(feature = "process-metrics", target_os = "linux"))]
mod linux;
#[cfg(any(not(feature = "process-metrics"), not(target_os = "linux")))]
mod stubs;
#[cfg(all(feature = "process-metrics", target_os = "linux"))]
use linux as platform;
#[cfg(any(not(feature = "process-metrics"), not(target_os = "linux")))]
use stubs as platform;
pub const SYSTEM_EVIDENCE_V2_VERSION: u16 = 1;
const fn legacy_component_version() -> u16 {
1
}
fn gap<T>(reason: EvidenceGap) -> Evidence<T> {
Evidence::unavailable(reason)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[repr(u8)]
pub enum IoCacheStateV2 {
Cold = 1,
Warm = 2,
Direct = 3,
}
impl IoCacheStateV2 {
pub const fn as_value(self) -> u64 {
self as u64
}
pub fn from_value(value: u64) -> Option<Self> {
match value {
1 => Some(Self::Cold),
2 => Some(Self::Warm),
3 => Some(Self::Direct),
_ => None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct AllocationTotalsV2 {
#[serde(default = "legacy_component_version")]
pub version: u16,
pub allocations: u64,
pub deallocations: u64,
pub allocated_bytes: u64,
pub deallocated_bytes: u64,
pub live_bytes: u64,
pub peak_live_bytes: u64,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct StageAllocationV2 {
#[serde(default = "legacy_component_version")]
pub version: u16,
#[serde(default)]
pub metric_id: Option<crate::MetricId>,
pub allocations: u64,
pub allocated_bytes: u64,
pub live_bytes: u64,
pub peak_live_bytes: u64,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct AllocationEvidenceV2 {
#[serde(default = "legacy_component_version")]
pub version: u16,
pub totals: Evidence<AllocationTotalsV2>,
#[serde(default)]
pub stages: Vec<StageAllocationV2>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct FaultEvidenceV2 {
#[serde(default = "legacy_component_version")]
pub version: u16,
pub minor_faults: SourcedEvidenceV2<u64>,
pub major_faults: SourcedEvidenceV2<u64>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct IoEvidenceV2 {
#[serde(default = "legacy_component_version")]
pub version: u16,
pub read_bytes: SourcedEvidenceV2<u64>,
pub write_bytes: SourcedEvidenceV2<u64>,
pub read_syscalls: SourcedEvidenceV2<u64>,
pub write_syscalls: SourcedEvidenceV2<u64>,
pub cancelled_write_bytes: SourcedEvidenceV2<u64>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct MemoryEvidenceV2 {
#[serde(default = "legacy_component_version")]
pub version: u16,
pub resident_bytes: SourcedEvidenceV2<u64>,
pub virtual_bytes: SourcedEvidenceV2<u64>,
pub resident_high_water_bytes: SourcedEvidenceV2<u64>,
pub swap_bytes: SourcedEvidenceV2<u64>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct PressureEvidenceV2 {
#[serde(default = "legacy_component_version")]
pub version: u16,
pub cpu_some_avg10_milli: SourcedEvidenceV2<u64>,
pub cpu_full_avg10_milli: SourcedEvidenceV2<u64>,
pub memory_some_avg10_milli: SourcedEvidenceV2<u64>,
pub io_some_avg10_milli: SourcedEvidenceV2<u64>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ThermalEvidenceV2 {
#[serde(default = "legacy_component_version")]
pub version: u16,
pub max_zone_millicelsius: SourcedEvidenceV2<u64>,
pub throttle_events: SourcedEvidenceV2<u64>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct NetworkProcessCountersV2 {
#[serde(default = "legacy_component_version")]
pub version: u16,
pub read_bytes: u64,
pub written_bytes: u64,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct NetworkEvidenceV2 {
#[serde(default = "legacy_component_version")]
pub version: u16,
pub process_counters: SourcedEvidenceV2<NetworkProcessCountersV2>,
pub retry_annotations: u64,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct DecodeRetentionEvidenceV2 {
#[serde(default = "legacy_component_version")]
pub version: u16,
pub expansion_ratio_milli: Evidence<u64>,
pub retained_bytes: Evidence<u64>,
pub retained_peak_bytes: Evidence<u64>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct SystemRunEvidenceV2 {
#[serde(default = "legacy_component_version")]
pub version: u16,
pub allocation: AllocationEvidenceV2,
pub faults: FaultEvidenceV2,
pub io: IoEvidenceV2,
pub memory: MemoryEvidenceV2,
pub pressure: PressureEvidenceV2,
pub thermal: ThermalEvidenceV2,
pub network: NetworkEvidenceV2,
pub decode: DecodeRetentionEvidenceV2,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct SystemIoSampleV2 {
#[serde(default = "legacy_component_version")]
pub version: u16,
pub minor_faults: SourcedEvidenceV2<u64>,
pub major_faults: SourcedEvidenceV2<u64>,
pub read_bytes: SourcedEvidenceV2<u64>,
pub write_bytes: SourcedEvidenceV2<u64>,
pub read_syscalls: SourcedEvidenceV2<u64>,
pub write_syscalls: SourcedEvidenceV2<u64>,
pub cancelled_write_bytes: SourcedEvidenceV2<u64>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct PressureThermalSampleV2 {
#[serde(default = "legacy_component_version")]
pub version: u16,
pub cpu_some_avg10_milli: SourcedEvidenceV2<u64>,
pub cpu_full_avg10_milli: SourcedEvidenceV2<u64>,
pub memory_some_avg10_milli: SourcedEvidenceV2<u64>,
pub io_some_avg10_milli: SourcedEvidenceV2<u64>,
pub max_zone_millicelsius: SourcedEvidenceV2<u64>,
pub throttle_events: SourcedEvidenceV2<u64>,
}
pub struct SystemIoCollector {
capability: CollectorCapability,
}
impl SystemIoCollector {
pub fn new() -> Self {
Self {
capability: platform::system_io_capability(),
}
}
}
impl Default for SystemIoCollector {
fn default() -> Self {
Self::new()
}
}
impl SnapshotCollector for SystemIoCollector {
type Snapshot = SystemIoSampleV2;
fn capability(&self) -> CollectorCapability {
self.capability.clone()
}
fn sample(&mut self) -> Self::Snapshot {
platform::sample_system_io()
}
}
pub struct PressureThermalCollector {
capability: CollectorCapability,
}
impl PressureThermalCollector {
pub fn new() -> Self {
Self {
capability: platform::pressure_thermal_capability(),
}
}
}
impl Default for PressureThermalCollector {
fn default() -> Self {
Self::new()
}
}
impl SnapshotCollector for PressureThermalCollector {
type Snapshot = PressureThermalSampleV2;
fn capability(&self) -> CollectorCapability {
self.capability.clone()
}
fn sample(&mut self) -> Self::Snapshot {
platform::sample_pressure_thermal()
}
}
fn sourced_delta_u64(
end: &SourcedEvidenceV2<u64>,
start: &SourcedEvidenceV2<u64>,
) -> SourcedEvidenceV2<u64> {
crate::hardware::sourced_delta(end, start)
}
pub(crate) struct SystemSession {
io_collector: SystemIoCollector,
pressure_collector: PressureThermalCollector,
io_start: Option<SystemIoSampleV2>,
allocation_start: Option<AllocationSnapshotV2>,
allocation_session: AllocationSessionToken,
}
impl SystemSession {
pub(crate) fn new() -> Self {
let mut io_collector = SystemIoCollector::new();
let io_available = matches!(
io_collector.capability.availability,
crate::collector::CollectorAvailability::Available
);
let io_start = io_available.then(|| io_collector.sample());
let allocation_start = crate::allocation::allocation_tracking_installed()
.then(crate::allocation::allocation_snapshot);
let allocation_session = if allocation_start.is_some() {
crate::allocation::enter_allocation_session()
} else {
AllocationSessionToken::inactive()
};
Self {
io_collector,
pressure_collector: PressureThermalCollector::new(),
io_start,
allocation_start,
allocation_session,
}
}
pub(crate) fn capabilities(&self) -> Vec<CollectorCapability> {
vec![
allocation_capability(),
self.io_collector.capability(),
self.pressure_collector.capability(),
]
}
pub(crate) fn finish_evidence(
mut self,
runtime: &crate::Runtime,
finish_resources: &ResourceSnapshot,
input_bytes: u64,
derived_decoder_bytes: u64,
) -> Evidence<SystemRunEvidenceV2> {
let io_end = self.io_collector.sample();
let (faults, io) = match &self.io_start {
Some(start) => (
FaultEvidenceV2 {
version: SYSTEM_EVIDENCE_V2_VERSION,
minor_faults: sourced_delta_u64(&io_end.minor_faults, &start.minor_faults),
major_faults: sourced_delta_u64(&io_end.major_faults, &start.major_faults),
},
IoEvidenceV2 {
version: SYSTEM_EVIDENCE_V2_VERSION,
read_bytes: sourced_delta_u64(&io_end.read_bytes, &start.read_bytes),
write_bytes: sourced_delta_u64(&io_end.write_bytes, &start.write_bytes),
read_syscalls: sourced_delta_u64(&io_end.read_syscalls, &start.read_syscalls),
write_syscalls: sourced_delta_u64(
&io_end.write_syscalls,
&start.write_syscalls,
),
cancelled_write_bytes: sourced_delta_u64(
&io_end.cancelled_write_bytes,
&start.cancelled_write_bytes,
),
},
),
None => {
let reason = match io_end.minor_faults.value {
Evidence::Unavailable { reason } => reason,
Evidence::Recorded { .. } => EvidenceGap::Unavailable,
};
let io_reason = match io_end.read_bytes.value {
Evidence::Unavailable { reason } => reason,
Evidence::Recorded { .. } => EvidenceGap::Unavailable,
};
(
FaultEvidenceV2 {
version: SYSTEM_EVIDENCE_V2_VERSION,
minor_faults: SourcedEvidenceV2::gapped(
HardwareFieldSourceV2::ProcSelfStat,
reason,
),
major_faults: SourcedEvidenceV2::gapped(
HardwareFieldSourceV2::ProcSelfStat,
reason,
),
},
IoEvidenceV2 {
version: SYSTEM_EVIDENCE_V2_VERSION,
read_bytes: SourcedEvidenceV2::gapped(
HardwareFieldSourceV2::ProcSelfIo,
io_reason,
),
write_bytes: SourcedEvidenceV2::gapped(
HardwareFieldSourceV2::ProcSelfIo,
io_reason,
),
read_syscalls: SourcedEvidenceV2::gapped(
HardwareFieldSourceV2::ProcSelfIo,
io_reason,
),
write_syscalls: SourcedEvidenceV2::gapped(
HardwareFieldSourceV2::ProcSelfIo,
io_reason,
),
cancelled_write_bytes: SourcedEvidenceV2::gapped(
HardwareFieldSourceV2::ProcSelfIo,
io_reason,
),
},
)
}
};
let pressure = self.pressure_collector.sample();
let allocation = self.allocation_evidence();
let memory = memory_evidence(finish_resources);
let retry_annotations = runtime.retry_annotation_count();
let network = NetworkEvidenceV2 {
version: SYSTEM_EVIDENCE_V2_VERSION,
process_counters: platform::network_process_counters(),
retry_annotations,
};
let decode = DecodeRetentionEvidenceV2 {
version: SYSTEM_EVIDENCE_V2_VERSION,
expansion_ratio_milli: milli_ratio(derived_decoder_bytes, input_bytes)
.map_or_else(|| gap(EvidenceGap::Unavailable), Evidence::recorded),
retained_bytes: runtime
.session_gauge(crate::GaugeId::RetainedBufferBytes)
.map_or_else(|| gap(EvidenceGap::Unavailable), Evidence::recorded),
retained_peak_bytes: runtime
.session_gauge(crate::GaugeId::RetainedBufferPeakBytes)
.map_or_else(|| gap(EvidenceGap::Unavailable), Evidence::recorded),
};
record_system_metrics(
runtime,
&faults,
&io,
&memory,
&allocation,
retry_annotations,
);
Evidence::recorded(SystemRunEvidenceV2 {
version: SYSTEM_EVIDENCE_V2_VERSION,
allocation,
faults,
io,
memory,
pressure: PressureEvidenceV2 {
version: SYSTEM_EVIDENCE_V2_VERSION,
cpu_some_avg10_milli: pressure.cpu_some_avg10_milli.clone(),
cpu_full_avg10_milli: pressure.cpu_full_avg10_milli.clone(),
memory_some_avg10_milli: pressure.memory_some_avg10_milli.clone(),
io_some_avg10_milli: pressure.io_some_avg10_milli.clone(),
},
thermal: ThermalEvidenceV2 {
version: SYSTEM_EVIDENCE_V2_VERSION,
max_zone_millicelsius: pressure.max_zone_millicelsius.clone(),
throttle_events: pressure.throttle_events.clone(),
},
network,
decode,
})
}
fn allocation_evidence(&self) -> AllocationEvidenceV2 {
let Some(start) = &self.allocation_start else {
let reason = if cfg!(feature = "allocation-tracking") {
EvidenceGap::Unavailable
} else {
EvidenceGap::CollectorDisabled
};
return AllocationEvidenceV2 {
version: SYSTEM_EVIDENCE_V2_VERSION,
totals: gap(reason),
stages: Vec::new(),
};
};
if !self.allocation_session.evidence_is_reliable() {
return AllocationEvidenceV2 {
version: SYSTEM_EVIDENCE_V2_VERSION,
totals: gap(EvidenceGap::Unavailable),
stages: Vec::new(),
};
}
let end = allocation_snapshot();
let totals = AllocationTotalsV2 {
version: SYSTEM_EVIDENCE_V2_VERSION,
allocations: end.allocations.saturating_sub(start.allocations),
deallocations: end.deallocations.saturating_sub(start.deallocations),
allocated_bytes: end.allocated_bytes.saturating_sub(start.allocated_bytes),
deallocated_bytes: end
.deallocated_bytes
.saturating_sub(start.deallocated_bytes),
live_bytes: end.live_bytes,
peak_live_bytes: end.peak_live_bytes,
};
let stages = crate::Stage::ALL
.into_iter()
.map(|stage| {
let start_slot = start.slot(stage);
let end_slot = end.slot(stage);
StageAllocationV2 {
version: SYSTEM_EVIDENCE_V2_VERSION,
metric_id: Some(stage.metric_id()),
allocations: end_slot.allocations.saturating_sub(start_slot.allocations),
allocated_bytes: end_slot
.allocated_bytes
.saturating_sub(start_slot.allocated_bytes),
live_bytes: end_slot.live_bytes,
peak_live_bytes: end_slot.peak_live_bytes,
}
})
.chain(std::iter::once(StageAllocationV2 {
version: SYSTEM_EVIDENCE_V2_VERSION,
metric_id: None,
allocations: end
.root()
.allocations
.saturating_sub(start.root().allocations),
allocated_bytes: end
.root()
.allocated_bytes
.saturating_sub(start.root().allocated_bytes),
live_bytes: end.root().live_bytes,
peak_live_bytes: end.root().peak_live_bytes,
}))
.collect();
AllocationEvidenceV2 {
version: SYSTEM_EVIDENCE_V2_VERSION,
totals: Evidence::recorded(totals),
stages,
}
}
}
fn memory_evidence(finish: &ResourceSnapshot) -> MemoryEvidenceV2 {
let sourced = |value: Option<u64>| match value {
Some(value) => SourcedEvidenceV2::recorded(value, HardwareFieldSourceV2::ProcSelfStatus),
None => SourcedEvidenceV2::gapped(
HardwareFieldSourceV2::ProcSelfStatus,
EvidenceGap::Unavailable,
),
};
MemoryEvidenceV2 {
version: SYSTEM_EVIDENCE_V2_VERSION,
resident_bytes: sourced(finish.resident_bytes),
virtual_bytes: sourced(finish.virtual_bytes),
resident_high_water_bytes: sourced(finish.resident_high_water_bytes),
swap_bytes: sourced(finish.swap_bytes),
}
}
fn record_system_metrics(
runtime: &crate::Runtime,
faults: &FaultEvidenceV2,
io: &IoEvidenceV2,
memory: &MemoryEvidenceV2,
allocation: &AllocationEvidenceV2,
retry_annotations: u64,
) {
let counters: [(&SourcedEvidenceV2<u64>, crate::CounterId); 7] = [
(&faults.minor_faults, crate::CounterId::MinorFaults),
(&faults.major_faults, crate::CounterId::MajorFaults),
(&io.read_bytes, crate::CounterId::IoReadBytes),
(&io.write_bytes, crate::CounterId::IoWriteBytes),
(&io.read_syscalls, crate::CounterId::IoReadSyscalls),
(&io.write_syscalls, crate::CounterId::IoWriteSyscalls),
(
&io.cancelled_write_bytes,
crate::CounterId::IoCancelledWriteBytes,
),
];
for (field, counter) in counters {
if let Evidence::Recorded { value } = field.value {
if value > 0 {
runtime.add_counter(counter, value);
}
}
}
if retry_annotations > 0 {
runtime.add_counter(crate::CounterId::NetworkRetries, retry_annotations);
}
if let Evidence::Recorded { value: hwm } = memory.resident_high_water_bytes.value {
runtime.set_gauge(crate::GaugeId::ResidentHighWaterBytes, hwm);
}
if let Evidence::Recorded { value: totals } = &allocation.totals {
if totals.allocations > 0 {
runtime.add_counter(crate::CounterId::AllocationCount, totals.allocations);
}
if totals.deallocations > 0 {
runtime.add_counter(crate::CounterId::DeallocationCount, totals.deallocations);
}
if totals.allocated_bytes > 0 {
runtime.add_counter(crate::CounterId::AllocationBytes, totals.allocated_bytes);
}
if totals.deallocated_bytes > 0 {
runtime.add_counter(
crate::CounterId::DeallocationBytes,
totals.deallocated_bytes,
);
}
runtime.set_gauge(crate::GaugeId::AllocationLiveBytes, totals.live_bytes);
runtime.set_gauge(
crate::GaugeId::AllocationPeakLiveBytes,
totals.peak_live_bytes,
);
}
}