Skip to main content

keyhog_profile/
schema_v2.rs

1use crate::{
2    CacheState, CollectorCapability, DaemonState, ResourceSample, ResourceUsage, RunProfile,
3    RunState, StageMeasurement, StateMeasurement, StateTransition,
4};
5use serde::{Deserialize, Serialize};
6
7pub const PROFILE_SCHEMA_V2: &str = "keyhog-profile";
8pub const PROFILE_SCHEMA_V2_MAJOR: u16 = 2;
9pub const PROFILE_SCHEMA_V2_MINOR: u16 = 8;
10pub const PROFILE_ENVELOPE_V2_VERSION: u16 = 1;
11pub const CAUSAL_PROFILE_V2_VERSION: u16 = 8;
12pub const CAUSAL_IDENTITY_V2_VERSION: u16 = 1;
13pub const EVENT_SCHEMA_VERSION: u16 = 7;
14pub const METRIC_REGISTRY_VERSION: u16 = 6;
15pub const EXPORTER_VERSION: u16 = 1;
16pub const STAGE_CONCURRENCY_V2_VERSION: u16 = 1;
17pub const WORKER_OCCUPANCY_V2_VERSION: u16 = 1;
18pub const CACHE_EFFECTIVENESS_V2_VERSION: u16 = 1;
19pub const INDEXED_COUNTER_V2_VERSION: u16 = 1;
20pub const RETRY_RECORD_V2_VERSION: u16 = 1;
21
22/// Why a v2 evidence field has no measured value.
23#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
24#[serde(rename_all = "kebab-case")]
25pub enum EvidenceGap {
26    LegacyV1NotRecorded,
27    CollectorDisabled,
28    PermissionDenied,
29    Unsupported,
30    Unavailable,
31}
32
33/// A measured value or an explicit reason why no value exists.
34#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
35#[serde(tag = "status", rename_all = "kebab-case")]
36pub enum Evidence<T> {
37    Recorded { value: T },
38    Unavailable { reason: EvidenceGap },
39}
40
41impl<T> Evidence<T> {
42    pub fn recorded(value: T) -> Self {
43        Self::Recorded { value }
44    }
45
46    pub const fn unavailable(reason: EvidenceGap) -> Self {
47        Self::Unavailable { reason }
48    }
49}
50
51fn legacy_gap<T>() -> Evidence<T> {
52    Evidence::unavailable(EvidenceGap::LegacyV1NotRecorded)
53}
54
55/// Independent major and minor version for one schema family.
56#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
57pub struct SchemaVersionV2 {
58    pub version: u16,
59    pub major: u16,
60    pub minor: u16,
61}
62
63/// Producer identity for the code that emitted an artifact.
64#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
65pub struct ProducerIdentityV2 {
66    pub version: u16,
67    pub profile_crate_version: String,
68    pub exporter_version: u16,
69}
70
71/// Digest protecting the canonical profile artifact.
72#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
73pub struct ArtifactIntegrityV2 {
74    pub version: u16,
75    pub algorithm: String,
76    pub digest: String,
77}
78
79/// Self-describing envelope for a v2 causal profile.
80#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
81pub struct ProfileEnvelopeV2 {
82    pub version: u16,
83    pub schema: String,
84    pub schema_version: SchemaVersionV2,
85    pub event_schema_version: u16,
86    pub metric_registry_version: u16,
87    pub producer: ProducerIdentityV2,
88    pub integrity: Evidence<ArtifactIntegrityV2>,
89}
90
91/// Exact host and operating environment used by one run.
92#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
93pub struct HostIdentityV2 {
94    pub version: u16,
95    pub operating_system: Evidence<String>,
96    pub kernel_version: Evidence<String>,
97    pub architecture: Evidence<String>,
98    pub cpu_model: Evidence<String>,
99    pub logical_cpus: u32,
100    pub physical_cores: Evidence<u32>,
101    pub cpu_features_digest: Evidence<String>,
102    pub affinity_digest: Evidence<String>,
103    pub numa_digest: Evidence<String>,
104}
105
106/// Exact executable and toolchain identity used by one run.
107#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
108pub struct BuildIdentityV2 {
109    pub version: u16,
110    pub binary_version: String,
111    pub binary_digest: Evidence<String>,
112    pub source_revision: Evidence<String>,
113    pub build_profile: Evidence<String>,
114    pub target_triple: Evidence<String>,
115    pub feature_digest: Evidence<String>,
116    pub compiler_identity: Evidence<String>,
117    pub allocator_identity: Evidence<String>,
118    pub linked_backend_digest: Evidence<String>,
119}
120
121/// Detector corpus and compiled execution-plan identity.
122#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
123pub struct DetectorIdentityV2 {
124    pub version: u16,
125    pub corpus_digest: String,
126    pub compiled_plan_digest: Evidence<String>,
127    pub enabled_detector_digest: Evidence<String>,
128    pub backend_database_digest: Evidence<String>,
129    pub external_provenance_digest: Evidence<String>,
130}
131
132/// Canonical resolved configuration and policy identity.
133#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
134pub struct ConfigIdentityV2 {
135    pub version: u16,
136    pub resolved_config_digest: String,
137    pub policy_digest: Evidence<String>,
138    pub preset: Evidence<String>,
139    pub protection_state: Evidence<String>,
140}
141
142/// Safe source adapter and target identity.
143#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
144pub struct SourceIdentityV2 {
145    pub version: u16,
146    pub adapters: Vec<String>,
147    pub target_digest: Evidence<String>,
148    pub partition_digest: Evidence<String>,
149}
150
151/// Measured workload shape used to classify comparable runs.
152#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
153pub struct WorkloadIdentityV2 {
154    pub version: u16,
155    pub class: String,
156    pub raw_source_bytes: u64,
157    pub source_units: u64,
158    pub container_bytes: Evidence<u64>,
159    pub expanded_payload_bytes: Evidence<u64>,
160    pub derived_decoder_bytes: Evidence<u64>,
161    pub backend_dispatched_bytes: Evidence<u64>,
162    pub size_bucket: Evidence<String>,
163    pub fanout_bucket: Evidence<String>,
164}
165
166/// One actual route completed for a measured batch.
167#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
168pub struct BatchRouteV2 {
169    pub version: u16,
170    pub batch_sequence: u64,
171    pub workload_key_digest: String,
172    pub requested_backend: String,
173    pub selected_backend: String,
174    pub completed_backend: String,
175    pub recovered_from_backend: Evidence<String>,
176}
177
178/// Requested, selected, completed, and recovered backend identity.
179#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
180pub struct RouteIdentityV2 {
181    pub version: u16,
182    pub request_mode: String,
183    pub requested_backend: String,
184    pub selected_backend: Evidence<String>,
185    pub completed_backend: Evidence<String>,
186    pub autoroute_decision_digest: Evidence<String>,
187    pub batches: Vec<BatchRouteV2>,
188    /// Routes omitted after the retained-batch cap; defaults to 0 for older profiles.
189    #[serde(default)]
190    pub dropped_batches: u64,
191}
192
193impl RouteIdentityV2 {
194    /// Build aggregate route identity from the exact completed batch records.
195    pub fn from_recorded_batches(requested_backend: String, batches: Vec<BatchRouteV2>) -> Self {
196        Self::from_recorded_batches_with_drops(requested_backend, batches, 0)
197    }
198
199    /// Build aggregate route identity, including explicit drop accounting.
200    pub fn from_recorded_batches_with_drops(
201        requested_backend: String,
202        batches: Vec<BatchRouteV2>,
203        dropped_batches: u64,
204    ) -> Self {
205        let request_mode = if requested_backend == "auto" {
206            "autoroute"
207        } else {
208            "explicit"
209        };
210        Self {
211            version: 1,
212            request_mode: request_mode.to_owned(),
213            selected_backend: aggregate_backend(&batches, |batch| &batch.selected_backend),
214            completed_backend: aggregate_backend(&batches, |batch| &batch.completed_backend),
215            requested_backend,
216            autoroute_decision_digest: Evidence::unavailable(EvidenceGap::Unavailable),
217            batches,
218            dropped_batches,
219        }
220    }
221}
222
223fn aggregate_backend(
224    batches: &[BatchRouteV2],
225    backend: impl Fn(&BatchRouteV2) -> &str,
226) -> Evidence<String> {
227    let mut labels = batches.iter().map(backend);
228    let Some(first) = labels.next() else {
229        return Evidence::unavailable(EvidenceGap::Unavailable);
230    };
231    if labels.all(|label| label == first) {
232        Evidence::recorded(first.to_owned())
233    } else {
234        Evidence::recorded("mixed".to_owned())
235    }
236}
237
238/// Cache families whose preparation state changes run cost.
239#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
240#[serde(rename_all = "kebab-case")]
241pub enum CacheLayerKindV2 {
242    LegacyAggregate,
243    Detector,
244    Merkle,
245    Autoroute,
246    Verifier,
247    Daemon,
248    PageCache,
249}
250
251/// State and generation identity for one cache layer.
252#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
253pub struct CacheLayerV2 {
254    pub version: u16,
255    pub layer: CacheLayerKindV2,
256    pub state: CacheState,
257    pub generation: Evidence<String>,
258    pub digest: Evidence<String>,
259}
260
261/// Daemon mode and request linkage for one run.
262#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
263pub struct DaemonIdentityV2 {
264    pub version: u16,
265    pub state: DaemonState,
266    pub generation: Evidence<String>,
267    pub request_id: Evidence<String>,
268    pub parent_request_id: Evidence<String>,
269    pub ready_age_ns: Evidence<u64>,
270}
271
272/// Whether scanner coverage was complete, partial, or unknown.
273#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
274#[serde(rename_all = "kebab-case")]
275pub enum CoverageStateV2 {
276    Complete,
277    Partial,
278    Failed,
279    Cancelled,
280    Unknown,
281}
282
283/// Terminal outcome and result identity for one run.
284#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
285pub struct OutcomeIdentityV2 {
286    pub version: u16,
287    pub status: RunState,
288    pub coverage: CoverageStateV2,
289    pub error_count: Evidence<u64>,
290    pub exit_code: Evidence<i32>,
291    pub findings_digest: Evidence<String>,
292    pub report_digest: Evidence<String>,
293}
294
295impl OutcomeIdentityV2 {
296    /// Construct terminal outcome evidence recorded by the production caller.
297    pub fn recorded(
298        status: RunState,
299        coverage: CoverageStateV2,
300        error_count: u64,
301        exit_code: i32,
302        findings_digest: Evidence<String>,
303        report_digest: Evidence<String>,
304    ) -> Self {
305        Self {
306            version: 1,
307            status,
308            coverage,
309            error_count: Evidence::recorded(error_count),
310            exit_code: Evidence::recorded(exit_code),
311            findings_digest,
312            report_digest,
313        }
314    }
315}
316
317/// Comparison identity joining every timing-relevant dimension.
318#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
319pub struct CausalRunIdentityV2 {
320    pub version: u16,
321    pub run_id: String,
322    pub parent_run_id: Evidence<String>,
323    pub benchmark_pair_id: Evidence<String>,
324    pub repeat_group_id: Evidence<String>,
325    pub host: HostIdentityV2,
326    pub build: BuildIdentityV2,
327    pub detectors: DetectorIdentityV2,
328    pub config: ConfigIdentityV2,
329    pub source: SourceIdentityV2,
330    pub workload: WorkloadIdentityV2,
331    pub route: RouteIdentityV2,
332    pub caches: Vec<CacheLayerV2>,
333    pub daemon: DaemonIdentityV2,
334    pub outcome: OutcomeIdentityV2,
335    pub scanner_threads_requested: usize,
336    pub reader_threads_requested: Evidence<usize>,
337    pub reader_threads_resolved: Evidence<usize>,
338}
339
340/// Causal origin of the work measured by one span.
341#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
342#[serde(rename_all = "kebab-case")]
343#[repr(u8)]
344pub enum WorkOrigin {
345    /// Work performed directly on the caller's own input.
346    #[default]
347    Root = 0,
348    /// Work performed inside an accepted decode-through input.
349    Decoded = 1,
350    /// Work performed on input derived from earlier pipeline output.
351    Derived = 2,
352    /// Work repeated after an earlier attempt failed or was recovered.
353    Retried = 3,
354}
355
356impl WorkOrigin {
357    /// Every work origin in stable wire order.
358    pub const ALL: [Self; 4] = [Self::Root, Self::Decoded, Self::Derived, Self::Retried];
359
360    /// Whether this origin counts as attributed (non-root) pipeline work.
361    pub const fn is_attributed_work(self) -> bool {
362        !matches!(self, Self::Root)
363    }
364}
365
366/// One nested or linked causal interval.
367#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
368pub struct SpanRecordV2 {
369    pub version: u16,
370    pub span_id: u64,
371    pub parent_span_id: Evidence<u64>,
372    pub metric_id: crate::MetricId,
373    pub start_ns: u64,
374    pub inclusive_ns: u64,
375    pub exclusive_ns: u64,
376    pub thread_id: u64,
377    pub task_id: Evidence<u64>,
378    #[serde(default = "legacy_gap")]
379    pub worker_id: Evidence<u64>,
380    #[serde(default)]
381    pub work_origin: WorkOrigin,
382    /// Raw cycle and instruction readings captured at span begin and end.
383    #[serde(default = "legacy_gap")]
384    pub hardware: Evidence<crate::hardware::SpanHardwareV2>,
385}
386
387/// One exact logarithmic bucket of a caller-recorded value distribution.
388#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
389pub struct DistributionBucketV2 {
390    pub version: u16,
391    pub lower_bound: u64,
392    pub upper_bound: u64,
393    pub count: u64,
394}
395
396/// Caller-recorded logarithmic distribution for one typed metric.
397#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
398pub struct MetricDistributionV2 {
399    pub version: u16,
400    pub metric_id: crate::MetricId,
401    pub call_count: u64,
402    pub minimum: u64,
403    pub maximum: u64,
404    pub buckets: Vec<DistributionBucketV2>,
405}
406
407/// One matched producer enqueue and consumer dequeue through a bounded queue.
408#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
409pub struct QueueLinkV2 {
410    pub version: u16,
411    pub queue: crate::QueueId,
412    pub sequence: u64,
413    pub producer_thread_id: u64,
414    pub producer_elapsed_ns: u64,
415    pub consumer_thread_id: u64,
416    pub consumer_elapsed_ns: u64,
417}
418
419/// Current depth and high-water mark for one bounded queue slot.
420#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
421pub struct QueueDepthV2 {
422    pub version: u16,
423    pub queue: crate::QueueId,
424    pub current: u64,
425    pub high_water: u64,
426    pub enqueues: u64,
427    pub dequeues: u64,
428}
429
430/// Per-worker load observed from one counter shard.
431#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
432pub struct WorkerLoadV2 {
433    pub version: u16,
434    pub worker_id: u64,
435    pub calls: u64,
436    pub elapsed_ns: u64,
437}
438
439/// Work-stealing imbalance evidence merged from every worker shard.
440#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
441pub struct WorkerImbalanceV2 {
442    pub version: u16,
443    pub worker_count: u64,
444    pub total_calls: u64,
445    pub total_elapsed_ns: u64,
446    /// Share of total calls handled by the busiest worker, in parts per million.
447    pub max_share_ppm: u64,
448    /// Upper-median per-worker share of total calls, in parts per million.
449    pub median_share_ppm: u64,
450    /// Share of registered workers that recorded zero calls, in parts per million.
451    pub idle_share_ppm: u64,
452    pub workers: Vec<WorkerLoadV2>,
453}
454
455/// Wall-clock occupancy of one micro-function across every worker.
456///
457/// `window_ns` is the span from the first start to the last end of any call to
458/// this micro-function, so `elapsed_ns / window_ns` is the average number of
459/// workers inside it while it was running. A stage whose concurrency is near
460/// one ran serially even when the pool was large.
461#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
462pub struct StageConcurrencyV2 {
463    pub version: u16,
464    pub metric_id: crate::MetricId,
465    pub macro_stage_id: crate::MacroStageId,
466    pub calls: u64,
467    /// Summed inclusive time across every call on every worker.
468    pub elapsed_ns: u64,
469    /// First start to last end across every worker, relative to session start.
470    pub window_ns: u64,
471    pub first_start_ns: u64,
472    pub last_end_ns: u64,
473    /// Workers that recorded at least one call.
474    pub worker_count: u64,
475    /// Largest single-worker contribution to `elapsed_ns`.
476    pub max_worker_elapsed_ns: u64,
477    /// `elapsed_ns / window_ns` in thousandths; 1000 means strictly serial.
478    pub concurrency_milli: u64,
479    /// Time inside calls the caller explicitly declared serial.
480    pub declared_serial_ns: u64,
481    pub declared_serial_calls: u64,
482    /// Bytes the caller attributed to this micro-function.
483    pub bytes: u64,
484}
485
486/// Busy, blocked, and idle time for one worker across the whole session.
487#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
488pub struct WorkerOccupancyRowV2 {
489    pub version: u16,
490    pub worker_id: u64,
491    /// Time inside outermost non-blocked spans; nested spans are not counted twice.
492    pub busy_ns: u64,
493    /// Time inside outermost blocked-wait spans.
494    pub blocked_ns: u64,
495    /// Outermost spans entered by this worker.
496    pub calls: u64,
497}
498
499/// Pool-wide busy versus idle accounting merged from every worker shard.
500#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
501pub struct WorkerOccupancyV2 {
502    pub version: u16,
503    /// Workers that registered a counter shard, busy or not.
504    pub worker_count: u64,
505    /// Workers that recorded at least one outermost span.
506    pub active_worker_count: u64,
507    pub busy_ns: u64,
508    pub blocked_ns: u64,
509    pub calls: u64,
510    pub busiest_busy_ns: u64,
511    pub median_busy_ns: u64,
512    pub workers: Vec<WorkerOccupancyRowV2>,
513}
514
515/// Hit and miss counts for one reuse cache.
516#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
517pub struct CacheEffectivenessV2 {
518    pub version: u16,
519    pub cache: crate::CacheId,
520    pub hits: u64,
521    pub misses: u64,
522    /// `hits / (hits + misses)` in parts per million.
523    pub hit_rate_ppm: u64,
524}
525
526/// Retry attempts recorded for one cause.
527///
528/// `attempts` counts every attempt, not every operation that was eventually
529/// retried, so a path that retries a thousand times reads as a thousand.
530#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
531pub struct RetryRecordV2 {
532    pub version: u16,
533    pub cause: crate::RetryCause,
534    pub attempts: u64,
535}
536
537/// One indexed counter family, summed per slot across every worker.
538///
539/// The caller owns the slot labels. `slots` is always
540/// [`crate::INDEXED_COUNTER_SLOTS`] long so two runs diff positionally.
541#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
542pub struct IndexedCounterRecordV2 {
543    pub version: u16,
544    pub counter: crate::IndexedCounterId,
545    pub slots: Vec<u64>,
546    /// Records addressed to a slot outside the fixed range, never folded in.
547    pub dropped_out_of_range: u64,
548}
549
550/// Blocked wait time attributed separately from runnable execution for one stage.
551#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
552pub struct BlockedWaitRecordV2 {
553    pub version: u16,
554    pub metric_id: crate::MetricId,
555    pub macro_stage_id: crate::MacroStageId,
556    pub calls: u64,
557    pub blocked_ns: u64,
558}
559
560/// One exact logarithmic latency bucket.
561#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
562pub struct LatencyBucketV2 {
563    pub version: u16,
564    pub lower_bound_ns: u64,
565    pub upper_bound_ns: u64,
566    pub count: u64,
567}
568
569/// Allocation-free hot-path call latency distribution for one micro-function.
570#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
571pub struct LatencyDistributionV2 {
572    pub version: u16,
573    pub metric_id: crate::MetricId,
574    pub macro_stage_id: crate::MacroStageId,
575    pub call_count: u64,
576    pub minimum_ns: u64,
577    pub maximum_ns: u64,
578    /// Nearest-rank p50, represented by the retained logarithmic bucket's upper bound.
579    pub p50_ns: u64,
580    /// Nearest-rank p90, represented by the retained logarithmic bucket's upper bound.
581    pub p90_ns: u64,
582    /// Nearest-rank p95, represented by the retained logarithmic bucket's upper bound.
583    pub p95_ns: u64,
584    /// Nearest-rank p99, represented by the retained logarithmic bucket's upper bound.
585    pub p99_ns: u64,
586    pub buckets: Vec<LatencyBucketV2>,
587}
588
589/// One typed counter or gauge materialized from fixed runtime storage.
590#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
591pub struct TypedMetricRecordV2 {
592    pub version: u16,
593    pub metric_id: crate::MetricId,
594    pub kind: crate::MetricKind,
595    pub value: u64,
596}
597
598/// One bounded instantaneous event with a typed numeric payload.
599#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
600pub struct PointEventV2 {
601    pub version: u16,
602    pub sequence: u64,
603    pub event_id: crate::EventId,
604    pub elapsed_ns: u64,
605    pub thread_id: u64,
606    pub value: u64,
607    #[serde(default = "legacy_gap")]
608    pub task_id: Evidence<u64>,
609    #[serde(default = "legacy_gap")]
610    pub worker_id: Evidence<u64>,
611}
612
613/// One bounded typed numeric annotation on the run timeline.
614#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
615pub struct AnnotationV2 {
616    pub version: u16,
617    pub sequence: u64,
618    pub annotation_id: crate::AnnotationId,
619    pub elapsed_ns: u64,
620    pub thread_id: u64,
621    pub value: u64,
622    #[serde(default = "legacy_gap")]
623    pub task_id: Evidence<u64>,
624    #[serde(default = "legacy_gap")]
625    pub worker_id: Evidence<u64>,
626}
627
628/// Bounded event stream with explicit availability and loss accounting.
629#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
630pub struct EventStreamV2 {
631    pub version: u16,
632    pub availability: Evidence<bool>,
633    pub dropped_events: u64,
634    #[serde(default)]
635    pub dropped_span_events: u64,
636    #[serde(default)]
637    pub dropped_point_events: u64,
638    #[serde(default)]
639    pub dropped_annotations: u64,
640    #[serde(default)]
641    pub sampled_out_events: u64,
642    pub spans: Vec<SpanRecordV2>,
643    #[serde(default)]
644    pub point_events: Vec<PointEventV2>,
645    #[serde(default)]
646    pub annotations: Vec<AnnotationV2>,
647}
648
649/// Versioned causal profile envelope and measurements.
650#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
651pub struct CausalProfileV2 {
652    pub version: u16,
653    pub envelope: ProfileEnvelopeV2,
654    pub identity: CausalRunIdentityV2,
655    pub status: RunState,
656    pub wall_time_ns: u64,
657    pub stages: Vec<StageMeasurement>,
658    pub transitions: Vec<StateTransition>,
659    pub states: Vec<StateMeasurement>,
660    pub collectors: Vec<CollectorCapability>,
661    pub resource_samples: Vec<ResourceSample>,
662    pub resources: ResourceUsage,
663    #[serde(default)]
664    pub typed_metrics: Vec<TypedMetricRecordV2>,
665    #[serde(default)]
666    pub latency_distributions: Vec<LatencyDistributionV2>,
667    /// Wall-clock occupancy per micro-function; empty on profiles before 2.8.
668    #[serde(default)]
669    pub stage_concurrency: Vec<StageConcurrencyV2>,
670    /// Per-worker busy and blocked time; absent on profiles before 2.8.
671    #[serde(default)]
672    pub worker_occupancy: Option<WorkerOccupancyV2>,
673    /// Queue depth high-water evidence; empty on profiles before 2.8.
674    #[serde(default)]
675    pub queue_depths: Vec<QueueDepthV2>,
676    /// Per-stage blocked wait time; empty on profiles before 2.8.
677    #[serde(default)]
678    pub blocked_waits: Vec<BlockedWaitRecordV2>,
679    /// Reuse-cache hit rates; empty on profiles before 2.8.
680    #[serde(default)]
681    pub caches: Vec<CacheEffectivenessV2>,
682    /// Indexed counter families; empty on profiles before 2.8.
683    #[serde(default)]
684    pub indexed_counters: Vec<IndexedCounterRecordV2>,
685    /// Retry attempts by cause; empty on profiles before 2.8.
686    #[serde(default)]
687    pub retries: Vec<RetryRecordV2>,
688    /// Derived bottleneck analysis; absent on profiles before 2.8.
689    #[serde(default)]
690    pub insight: Option<crate::insight::RunInsightV2>,
691    pub events: EventStreamV2,
692    /// CPU hardware evidence collected across the run.
693    #[serde(default = "legacy_gap")]
694    pub hardware: Evidence<crate::hardware::HardwareRunEvidenceV2>,
695    /// Memory, IO, and system evidence collected across the run.
696    #[serde(default = "legacy_gap")]
697    pub system: Evidence<crate::system::SystemRunEvidenceV2>,
698}
699
700impl CausalProfileV2 {
701    /// Migrate a v1 aggregate profile and capture build evidence available to this executable.
702    pub fn from_v1(profile: RunProfile) -> Self {
703        let build = BuildIdentityV2::capture_legacy(&profile.identity.binary_version);
704        Self::from_v1_with_build(profile, build)
705    }
706
707    /// Migrate a v1 aggregate profile with exact final-binary build identity.
708    pub fn from_v1_with_build(profile: RunProfile, build: BuildIdentityV2) -> Self {
709        let RunProfile {
710            identity,
711            status,
712            wall_time_ns,
713            input_bytes,
714            input_units,
715            workload,
716            stages,
717            transitions,
718            states,
719            collectors,
720            resource_samples,
721            resources,
722            hardware,
723            system,
724            ..
725        } = profile;
726        let selected_backend = identity
727            .backend_selected
728            .clone()
729            .map_or_else(legacy_gap, Evidence::recorded);
730        let reader_threads_requested = identity
731            .reader_threads
732            .map_or_else(legacy_gap, Evidence::recorded);
733        let causal_identity = CausalRunIdentityV2 {
734            version: CAUSAL_IDENTITY_V2_VERSION,
735            run_id: identity.run_id,
736            parent_run_id: legacy_gap(),
737            benchmark_pair_id: legacy_gap(),
738            repeat_group_id: legacy_gap(),
739            host: HostIdentityV2::capture(),
740            build,
741            detectors: DetectorIdentityV2 {
742                version: 1,
743                corpus_digest: identity.detector_digest,
744                compiled_plan_digest: legacy_gap(),
745                enabled_detector_digest: legacy_gap(),
746                backend_database_digest: legacy_gap(),
747                external_provenance_digest: legacy_gap(),
748            },
749            config: ConfigIdentityV2 {
750                version: 1,
751                resolved_config_digest: identity.config_digest,
752                policy_digest: legacy_gap(),
753                preset: legacy_gap(),
754                protection_state: legacy_gap(),
755            },
756            source: SourceIdentityV2 {
757                version: 1,
758                adapters: vec![identity.source_kind],
759                target_digest: legacy_gap(),
760                partition_digest: legacy_gap(),
761            },
762            workload: WorkloadIdentityV2::capture(crate::WorkloadIdentityInput {
763                class: &identity.workload_class,
764                raw_source_bytes: input_bytes,
765                source_units: input_units,
766                container_bytes: workload.container_bytes,
767                expanded_payload_bytes: workload.expanded_payload_bytes,
768                derived_decoder_bytes: workload.derived_decoder_bytes,
769                backend_dispatched_bytes: workload.backend_dispatched_bytes,
770            }),
771            route: RouteIdentityV2 {
772                version: 1,
773                request_mode: "legacy-v1".to_owned(),
774                requested_backend: identity.backend_requested,
775                selected_backend,
776                completed_backend: legacy_gap(),
777                autoroute_decision_digest: legacy_gap(),
778                batches: Vec::new(),
779                dropped_batches: 0,
780            },
781            caches: vec![CacheLayerV2 {
782                version: 1,
783                layer: CacheLayerKindV2::LegacyAggregate,
784                state: identity.cache_state,
785                generation: legacy_gap(),
786                digest: legacy_gap(),
787            }],
788            daemon: DaemonIdentityV2 {
789                version: 1,
790                state: identity.daemon_state,
791                generation: legacy_gap(),
792                request_id: legacy_gap(),
793                parent_request_id: legacy_gap(),
794                ready_age_ns: legacy_gap(),
795            },
796            outcome: OutcomeIdentityV2 {
797                version: 1,
798                status,
799                coverage: CoverageStateV2::Unknown,
800                error_count: legacy_gap(),
801                exit_code: legacy_gap(),
802                findings_digest: legacy_gap(),
803                report_digest: legacy_gap(),
804            },
805            scanner_threads_requested: identity.scanner_threads,
806            reader_threads_requested,
807            reader_threads_resolved: legacy_gap(),
808        };
809        Self {
810            version: CAUSAL_PROFILE_V2_VERSION,
811            envelope: ProfileEnvelopeV2 {
812                version: PROFILE_ENVELOPE_V2_VERSION,
813                schema: PROFILE_SCHEMA_V2.to_owned(),
814                schema_version: SchemaVersionV2 {
815                    version: 1,
816                    major: PROFILE_SCHEMA_V2_MAJOR,
817                    minor: PROFILE_SCHEMA_V2_MINOR,
818                },
819                event_schema_version: EVENT_SCHEMA_VERSION,
820                metric_registry_version: METRIC_REGISTRY_VERSION,
821                producer: ProducerIdentityV2 {
822                    version: 1,
823                    profile_crate_version: env!("CARGO_PKG_VERSION").to_owned(),
824                    exporter_version: EXPORTER_VERSION,
825                },
826                integrity: legacy_gap(),
827            },
828            identity: causal_identity,
829            status,
830            wall_time_ns,
831            stages,
832            transitions,
833            states,
834            collectors,
835            resource_samples,
836            resources,
837            typed_metrics: Vec::new(),
838            latency_distributions: Vec::new(),
839            stage_concurrency: Vec::new(),
840            worker_occupancy: None,
841            queue_depths: Vec::new(),
842            blocked_waits: Vec::new(),
843            caches: Vec::new(),
844            indexed_counters: Vec::new(),
845            retries: Vec::new(),
846            insight: None,
847            events: EventStreamV2 {
848                version: 3,
849                availability: legacy_gap(),
850                dropped_events: 0,
851                dropped_span_events: 0,
852                dropped_point_events: 0,
853                dropped_annotations: 0,
854                sampled_out_events: 0,
855                spans: Vec::new(),
856                point_events: Vec::new(),
857                annotations: Vec::new(),
858            },
859            hardware,
860            system,
861        }
862    }
863}