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}