Skip to main content

winx_code_agent/report/
lifecycle.rs

1//! Inclusive phase totals: one duration sample per measured call, not per phase
2//! invocation. Phases may overlap, nest and repeat; their sum is not overhead.
3
4use std::collections::BTreeMap;
5
6use serde::Serialize;
7use serde_json::{Map, Value};
8
9use super::{latency, LatencySummary};
10
11const PHASE_FIELDS: [(&str, &str, &str); 10] = [
12    ("workspace_check", "phase_workspace_check_ms", "phase_workspace_check_calls"),
13    ("session_admission", "phase_session_admission_ms", "phase_session_admission_calls"),
14    ("catalog", "phase_catalog_ms", "phase_catalog_calls"),
15    ("runtime_inspect", "phase_runtime_inspect_ms", "phase_runtime_inspect_calls"),
16    ("runtime_capture", "phase_runtime_capture_ms", "phase_runtime_capture_calls"),
17    ("prompt_rpc", "phase_prompt_rpc_ms", "phase_prompt_rpc_calls"),
18    ("herdr_adapter", "phase_herdr_adapter_ms", "phase_herdr_adapter_calls"),
19    ("wait_idle", "phase_wait_idle_ms", "phase_wait_idle_calls"),
20    ("temp_audit", "phase_temp_audit_ms", "phase_temp_audit_calls"),
21    ("response_finalize", "phase_response_finalize_ms", "phase_response_finalize_calls"),
22];
23
24#[derive(Debug, Default, Serialize)]
25#[serde(rename_all = "camelCase")]
26pub struct WorkflowPhaseUsage {
27    pub calls: u64,
28    pub latency: LatencySummary,
29}
30
31#[derive(Default)]
32struct PhaseAccumulator {
33    calls: u64,
34    durations: Vec<u64>,
35}
36
37#[derive(Default)]
38pub(super) struct LifecycleAccumulator {
39    phases: BTreeMap<&'static str, PhaseAccumulator>,
40    pub abandoned: u64,
41    pub suppressed: u64,
42}
43
44impl LifecycleAccumulator {
45    pub fn record(&mut self, fields: &Map<String, Value>) {
46        for (name, duration_key, calls_key) in PHASE_FIELDS {
47            let Some(calls) =
48                fields.get(calls_key).and_then(Value::as_u64).filter(|calls| *calls > 0)
49            else {
50                continue;
51            };
52            let phase = self.phases.entry(name).or_default();
53            phase.calls = phase.calls.saturating_add(calls);
54            if let Some(duration) = fields.get(duration_key).and_then(Value::as_u64) {
55                phase.durations.push(duration);
56            }
57        }
58        self.abandoned = self.abandoned.saturating_add(
59            fields.get("phase_abandoned_count").and_then(Value::as_u64).unwrap_or(0),
60        );
61        self.suppressed = self.suppressed.saturating_add(
62            fields.get("phase_trace_suppressed_count").and_then(Value::as_u64).unwrap_or(0),
63        );
64    }
65
66    pub fn finish(self) -> BTreeMap<String, WorkflowPhaseUsage> {
67        self.phases
68            .into_iter()
69            .map(|(name, phase)| {
70                (
71                    name.to_string(),
72                    WorkflowPhaseUsage { calls: phase.calls, latency: latency(phase.durations) },
73                )
74            })
75            .collect()
76    }
77}