Skip to main content

lenso_service/
performance_profile.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use utoipa::ToSchema;
7
8use crate::extraction_input_digest;
9
10pub const PERFORMANCE_PROFILE_PROTOCOL: &str = "lenso.performance-profile.v1";
11
12#[derive(
13    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, ToSchema,
14)]
15#[serde(rename_all = "snake_case")]
16pub enum PerformanceProfileScope {
17    ReducedDeterministic,
18    EnvironmentVerification,
19}
20
21#[derive(
22    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, ToSchema,
23)]
24#[serde(rename_all = "snake_case")]
25pub enum PerformanceMetric {
26    DirectCallLatency,
27    DirectCallThroughput,
28    ResolverClientOverhead,
29    PublishToConsumeLatency,
30    InboxOutboxLag,
31    WorkflowTransitionLatency,
32    WorkflowTimerDelay,
33    StoryFreshness,
34    ConsoleQueryLatency,
35    ConvergenceLatency,
36    CpuUtilization,
37    MemoryBytes,
38    DatabaseConnections,
39    BrokerBytes,
40}
41
42#[derive(
43    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, ToSchema,
44)]
45#[serde(rename_all = "snake_case")]
46pub enum PerformanceBudgetDirection {
47    AtMost,
48    AtLeast,
49}
50
51#[derive(
52    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, ToSchema,
53)]
54#[serde(rename_all = "snake_case")]
55pub enum PerformanceDecision {
56    Passed,
57    Blocked,
58}
59
60#[derive(
61    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, ToSchema,
62)]
63#[serde(rename_all = "snake_case")]
64pub enum PerformanceIssueCode {
65    TopologyInvalid,
66    MetadataIncomplete,
67    MetricMissing,
68    BudgetExceeded,
69    VarianceExceeded,
70    HiddenDataPlaneDependency,
71    EnvironmentEvidenceInsufficient,
72}
73
74impl PerformanceIssueCode {
75    #[must_use]
76    pub const fn as_str(self) -> &'static str {
77        match self {
78            Self::TopologyInvalid => "performance_topology_invalid",
79            Self::MetadataIncomplete => "performance_metadata_incomplete",
80            Self::MetricMissing => "performance_metric_missing",
81            Self::BudgetExceeded => "performance_budget_exceeded",
82            Self::VarianceExceeded => "performance_variance_exceeded",
83            Self::HiddenDataPlaneDependency => "performance_hidden_data_plane_dependency",
84            Self::EnvironmentEvidenceInsufficient => {
85                "performance_environment_evidence_insufficient"
86            }
87        }
88    }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
92#[serde(rename_all = "camelCase")]
93pub struct PerformanceIssue {
94    pub code: PerformanceIssueCode,
95    pub message: String,
96    pub remediation: String,
97    pub next_actions: Vec<String>,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
101#[serde(rename_all = "camelCase")]
102pub struct ReferenceService {
103    pub service_id: String,
104    pub contract_id: String,
105    pub store_id: String,
106    pub release_digest: String,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
110#[serde(rename_all = "camelCase")]
111pub struct ReferenceSystemTopology {
112    pub topology_digest: String,
113    pub services: Vec<ReferenceService>,
114    pub transport_adapter_version: String,
115    pub identity_adapter_version: String,
116    pub deployment_adapter_version: String,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
120#[serde(rename_all = "camelCase")]
121pub struct PerformanceBudget {
122    pub metric: PerformanceMetric,
123    pub unit: String,
124    pub direction: PerformanceBudgetDirection,
125    pub threshold: u64,
126}
127
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
129#[serde(rename_all = "camelCase")]
130pub struct PerformanceMeasurement {
131    pub metric: PerformanceMetric,
132    pub unit: String,
133    pub value: u64,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
137#[serde(rename_all = "camelCase")]
138pub struct PerformanceRun {
139    pub run_id: String,
140    pub release_set_digest: String,
141    pub dataset_digest: String,
142    pub concurrency: u32,
143    pub duration_ms: u64,
144    pub warmup_ms: u64,
145    pub machine: BTreeMap<String, String>,
146    pub infrastructure: BTreeMap<String, String>,
147    pub measurements: Vec<PerformanceMeasurement>,
148    pub system_plane_data_plane_requests: u64,
149    pub runtime_console_data_plane_requests: u64,
150    pub telemetry_data_plane_requests: u64,
151    pub policy_data_plane_requests: u64,
152    pub registry_data_plane_requests: u64,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
156#[serde(rename_all = "camelCase")]
157pub struct PerformanceProfileInput {
158    pub scope: PerformanceProfileScope,
159    pub support_manifest_digest: String,
160    pub topology: ReferenceSystemTopology,
161    pub budgets: Vec<PerformanceBudget>,
162    pub runs: Vec<PerformanceRun>,
163    pub variance_tolerance_basis_points: u32,
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
167#[serde(rename_all = "camelCase")]
168pub struct PerformanceProfile {
169    pub protocol: String,
170    pub profile_id: String,
171    pub profile_digest: String,
172    pub scope: PerformanceProfileScope,
173    pub support_manifest_digest: String,
174    pub topology: ReferenceSystemTopology,
175    pub budgets: Vec<PerformanceBudget>,
176    pub runs: Vec<PerformanceRun>,
177    pub variance_basis_points: BTreeMap<PerformanceMetric, u32>,
178    pub decision: PerformanceDecision,
179    pub issues: Vec<PerformanceIssue>,
180    pub next_actions: Vec<String>,
181}
182
183#[must_use]
184pub fn evaluate_performance_profile(mut input: PerformanceProfileInput) -> PerformanceProfile {
185    input
186        .topology
187        .services
188        .sort_by(|left, right| left.service_id.cmp(&right.service_id));
189    input.budgets.sort_by_key(|budget| budget.metric);
190    input
191        .runs
192        .sort_by(|left, right| left.run_id.cmp(&right.run_id));
193    for run in &mut input.runs {
194        run.measurements
195            .sort_by_key(|measurement| measurement.metric);
196    }
197
198    let mut issues = Vec::new();
199    let services = &input.topology.services;
200    let service_ids = services
201        .iter()
202        .map(|service| service.service_id.as_str())
203        .collect::<BTreeSet<_>>();
204    let contract_ids = services
205        .iter()
206        .map(|service| service.contract_id.as_str())
207        .collect::<BTreeSet<_>>();
208    let store_ids = services
209        .iter()
210        .map(|service| service.store_id.as_str())
211        .collect::<BTreeSet<_>>();
212    if services.len() != 3
213        || service_ids.len() != 3
214        || contract_ids.len() != 3
215        || store_ids.len() != 3
216        || services.iter().any(|service| {
217            service.service_id.trim().is_empty()
218                || service.contract_id.trim().is_empty()
219                || service.store_id.trim().is_empty()
220                || !valid_digest(&service.release_digest)
221        })
222        || !valid_digest(&input.topology.topology_digest)
223    {
224        issues.push(issue(
225            PerformanceIssueCode::TopologyInvalid,
226            "The reference System is not exactly three distinct logical Services, Contracts, and Stores.",
227            "Use three independently identified Service boundaries rather than replicas.",
228            "Correct the reference topology and repeat the profile.",
229        ));
230    }
231
232    let required_metrics = required_metrics();
233    let budget_metrics = input
234        .budgets
235        .iter()
236        .map(|budget| budget.metric)
237        .collect::<BTreeSet<_>>();
238    if !valid_digest(&input.support_manifest_digest)
239        || input.topology.transport_adapter_version.trim().is_empty()
240        || input.topology.identity_adapter_version.trim().is_empty()
241        || input.topology.deployment_adapter_version.trim().is_empty()
242        || input.budgets.len() != required_metrics.len()
243        || budget_metrics != required_metrics
244        || input.variance_tolerance_basis_points == 0
245    {
246        issues.push(issue(
247            PerformanceIssueCode::MetadataIncomplete,
248            "Performance profile metadata or evidence-backed budgets are incomplete.",
249            "Bind the profile to exact releases, topology, adapters, units, and tolerances.",
250            "Complete the pinned-environment profile metadata.",
251        ));
252    }
253
254    let required_run_count = match input.scope {
255        PerformanceProfileScope::ReducedDeterministic => 1,
256        PerformanceProfileScope::EnvironmentVerification => 3,
257    };
258    if input.runs.len() < required_run_count {
259        issues.push(issue(
260            PerformanceIssueCode::EnvironmentEvidenceInsufficient,
261            "The selected profile scope does not include enough repeated runs.",
262            "Use at least three runs for Environment Verification and one for reduced diagnosis.",
263            "Collect the missing pinned-environment runs.",
264        ));
265    }
266
267    let budgets = input
268        .budgets
269        .iter()
270        .map(|budget| (budget.metric, budget))
271        .collect::<BTreeMap<_, _>>();
272    for run in &input.runs {
273        let metrics = run
274            .measurements
275            .iter()
276            .map(|measurement| (measurement.metric, measurement))
277            .collect::<BTreeMap<_, _>>();
278        let metadata_valid = !run.run_id.trim().is_empty()
279            && valid_digest(&run.release_set_digest)
280            && valid_digest(&run.dataset_digest)
281            && run.concurrency > 0
282            && run.duration_ms > 0
283            && run.warmup_ms > 0
284            && !run.machine.is_empty()
285            && !run.infrastructure.is_empty();
286        if !metadata_valid {
287            issues.push(issue(
288                PerformanceIssueCode::MetadataIncomplete,
289                format!("Performance run `{}` has incomplete metadata.", run.run_id),
290                "Record concurrency, duration, warm-up, machine, infrastructure, dataset, and release facts.",
291                "Repeat the run with the complete versioned profile.",
292            ));
293        }
294        if metrics.len() != required_metrics.len()
295            || required_metrics
296                .iter()
297                .any(|metric| !metrics.contains_key(metric))
298        {
299            issues.push(issue(
300                PerformanceIssueCode::MetricMissing,
301                format!("Performance run `{}` does not cover every required path.", run.run_id),
302                "Measure request, Event, Workflow, Story, Console, convergence, and resource paths together.",
303                "Collect the missing measurements and rerun the profile.",
304            ));
305        }
306        for (metric, measurement) in metrics {
307            let Some(budget) = budgets.get(&metric) else {
308                issues.push(issue(
309                    PerformanceIssueCode::MetricMissing,
310                    format!("Performance metric `{:?}` has no reviewed budget.", metric),
311                    "Define one unique budget for every required metric.",
312                    "Correct the budget set and repeat the profile.",
313                ));
314                continue;
315            };
316            if measurement.unit != budget.unit
317                || match budget.direction {
318                    PerformanceBudgetDirection::AtMost => measurement.value > budget.threshold,
319                    PerformanceBudgetDirection::AtLeast => measurement.value < budget.threshold,
320                }
321            {
322                issues.push(issue(
323                    PerformanceIssueCode::BudgetExceeded,
324                    format!(
325                        "Performance run `{}` exceeded the {:?} budget.",
326                        run.run_id, metric
327                    ),
328                    "Treat the pinned threshold as an evidence-backed environment budget.",
329                    "Diagnose the regression or update the reviewed profile with new evidence.",
330                ));
331            }
332        }
333        if run.system_plane_data_plane_requests > 0
334            || run.runtime_console_data_plane_requests > 0
335            || run.telemetry_data_plane_requests > 0
336            || run.policy_data_plane_requests > 0
337            || run.registry_data_plane_requests > 0
338        {
339            issues.push(issue(
340                PerformanceIssueCode::HiddenDataPlaneDependency,
341                "Established Data Plane traffic depended on a coordination or observability surface.",
342                "Keep System Plane, Console, telemetry, policy, and registry services outside established traffic.",
343                "Correct the topology and repeat the profile with those surfaces withheld.",
344            ));
345        }
346    }
347
348    let variance_basis_points = calculate_variance(&input.runs);
349    if variance_basis_points
350        .values()
351        .any(|variance| *variance > input.variance_tolerance_basis_points)
352    {
353        issues.push(issue(
354            PerformanceIssueCode::VarianceExceeded,
355            "Repeated runs exceed the declared variance tolerance.",
356            "Separate environment drift from a product regression before accepting the profile.",
357            "Stabilize or re-pin the environment and repeat the measurements.",
358        ));
359    }
360
361    let decision = if issues.is_empty() {
362        PerformanceDecision::Passed
363    } else {
364        PerformanceDecision::Blocked
365    };
366    let next_actions = if issues.is_empty() {
367        vec!["Attach this profile to the M6 acceptance evidence set.".to_owned()]
368    } else {
369        issues
370            .iter()
371            .flat_map(|issue| issue.next_actions.iter().cloned())
372            .collect()
373    };
374    let mut profile = PerformanceProfile {
375        protocol: PERFORMANCE_PROFILE_PROTOCOL.to_owned(),
376        profile_id: String::new(),
377        profile_digest: String::new(),
378        scope: input.scope,
379        support_manifest_digest: input.support_manifest_digest,
380        topology: input.topology,
381        budgets: input.budgets,
382        runs: input.runs,
383        variance_basis_points,
384        decision,
385        issues,
386        next_actions,
387    };
388    profile.profile_digest = digest_without_identity(&profile);
389    profile.profile_id = format!("performance-profile:{}", &profile.profile_digest[7..23]);
390    profile
391}
392
393#[must_use]
394pub fn performance_profile_schema() -> Value {
395    let mut schema = serde_json::to_value(schemars::schema_for!(PerformanceProfile))
396        .expect("performance profile schema serializes");
397    schema["$id"] = Value::String(
398        "https://contracts.lenso.local/ga/lenso.performance-profile.v1.schema.json".to_owned(),
399    );
400    schema
401}
402
403fn required_metrics() -> BTreeSet<PerformanceMetric> {
404    [
405        PerformanceMetric::DirectCallLatency,
406        PerformanceMetric::DirectCallThroughput,
407        PerformanceMetric::ResolverClientOverhead,
408        PerformanceMetric::PublishToConsumeLatency,
409        PerformanceMetric::InboxOutboxLag,
410        PerformanceMetric::WorkflowTransitionLatency,
411        PerformanceMetric::WorkflowTimerDelay,
412        PerformanceMetric::StoryFreshness,
413        PerformanceMetric::ConsoleQueryLatency,
414        PerformanceMetric::ConvergenceLatency,
415        PerformanceMetric::CpuUtilization,
416        PerformanceMetric::MemoryBytes,
417        PerformanceMetric::DatabaseConnections,
418        PerformanceMetric::BrokerBytes,
419    ]
420    .into_iter()
421    .collect()
422}
423
424fn calculate_variance(runs: &[PerformanceRun]) -> BTreeMap<PerformanceMetric, u32> {
425    let mut values = BTreeMap::<PerformanceMetric, Vec<u64>>::new();
426    for run in runs {
427        for measurement in &run.measurements {
428            values
429                .entry(measurement.metric)
430                .or_default()
431                .push(measurement.value);
432        }
433    }
434    values
435        .into_iter()
436        .map(|(metric, values)| {
437            let min = values.iter().copied().min().unwrap_or(0);
438            let max = values.iter().copied().max().unwrap_or(0);
439            let variance = if min == 0 {
440                if max == 0 { 0 } else { u32::MAX }
441            } else {
442                u32::try_from(((max - min) as u128 * 10_000) / u128::from(min)).unwrap_or(u32::MAX)
443            };
444            (metric, variance)
445        })
446        .collect()
447}
448
449fn issue(
450    code: PerformanceIssueCode,
451    message: impl Into<String>,
452    remediation: impl Into<String>,
453    next_action: impl Into<String>,
454) -> PerformanceIssue {
455    PerformanceIssue {
456        code,
457        message: message.into(),
458        remediation: remediation.into(),
459        next_actions: vec![next_action.into()],
460    }
461}
462
463fn valid_digest(value: &str) -> bool {
464    value.strip_prefix("sha256:").is_some_and(|digest| {
465        digest.len() == 64
466            && digest
467                .bytes()
468                .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
469    })
470}
471
472fn digest_without_identity(profile: &PerformanceProfile) -> String {
473    let mut canonical = profile.clone();
474    canonical.profile_id.clear();
475    canonical.profile_digest.clear();
476    extraction_input_digest(
477        &serde_json::to_vec(&canonical).expect("performance profile serializes"),
478    )
479}
480
481#[must_use]
482pub fn performance_profile_integrity_is_valid(profile: &PerformanceProfile) -> bool {
483    valid_digest(&profile.profile_digest)
484        && profile.profile_digest == digest_without_identity(profile)
485        && profile.profile_id == format!("performance-profile:{}", &profile.profile_digest[7..23])
486}