Skip to content

Commit b731c30

Browse files
committed
feat(schema): add emission profile benchmark
1 parent 20e31e8 commit b731c30

24 files changed

Lines changed: 1321 additions & 69 deletions

‎README.md‎

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,56 @@ helm-schema ./path/to/chart \
8282
--compact
8383
```
8484

85-
Lean keeps base path, type, and provider constraints, but omits
86-
document-level conditional refinements. It can therefore accept some values
87-
that full validation rejects. Full validation remains the default.
85+
Lean retains every unconditional mandatory fact and every locally anchored
86+
ordinary conditional. It drops root-anchored ordinary conditionals, terminal
87+
clauses, and all kind partitions. It can therefore accept some values that full
88+
validation rejects. Full validation remains the default.
89+
90+
### Emission profiles and chart configuration
91+
92+
Profiles are presets over four widening-only emission controls:
93+
94+
| profile | root conditionals | local conditionals | terminals | kind partitions |
95+
|---|---:|---:|---:|---:|
96+
| `full` | on | on | on | on |
97+
| `lean` | off | on | off | off |
98+
99+
Mandatory facts—unconditional base/provider constraints, presence and not-null
100+
requirements, default preservation, host preparation, and scalar spelling
101+
semantics—are always retained. Switching an emission control off can only widen
102+
acceptance; it cannot make the schema reject a value accepted by `full`.
103+
104+
A root chart can pin its policy in `helm-schema.yaml`:
105+
106+
```yaml
107+
version: 1
108+
profile: lean
109+
emission:
110+
local-conditionals: off
111+
```
112+
113+
Only the root chart's file is discovered; dependency config files are ignored.
114+
Discovery works the same for chart directories and packaged `.tgz`/`.tar.gz`
115+
charts. Unknown fields, malformed YAML, unsupported versions, and contradictory
116+
knob combinations fail instead of being ignored.
117+
118+
Policy values resolve in this order, from highest to lowest precedence:
119+
120+
| priority | source |
121+
|---:|---|
122+
| 1 | CLI emission overrides (`--root-anchored-conditionals`, `--local-conditionals`, `--terminal-clauses`, `--kind-partitions`) |
123+
| 2 | `helm-schema.yaml` `emission` values |
124+
| 3 | the selected profile preset |
125+
| 4 | the built-in `full` default |
126+
127+
An explicit CLI `--profile` deliberately resets file-level emission overrides;
128+
CLI emission overrides still apply on top. Use `--config PATH` to select a file
129+
relative to the invocation directory, `--no-config` to disable configuration,
130+
or `--print-effective-config` to inspect every resolved value and its source
131+
without analyzing the chart.
132+
133+
See the [configuration reference](https://romnn.github.io/helm-schema/docs/reference/configuration/)
134+
for the complete version-1 contract.
88135

89136
### Helpful workflows
90137

‎crates/helm-schema-cli/src/cli/emission_args.rs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,19 @@ impl From<PolicyToggle> for bool {
1919
/// Optional W-class emission overrides applied after profile and config policy.
2020
#[derive(Args, Debug, Clone, Copy, Default)]
2121
pub struct EmissionArgs {
22-
/// Override root-anchored conditional emission.
22+
/// Override root-anchored ordinary conditional emission (W-class).
2323
#[arg(long, value_enum, value_name = "STATE")]
2424
pub root_anchored_conditionals: Option<PolicyToggle>,
2525

26-
/// Override locally anchored conditional emission.
26+
/// Override locally anchored ordinary conditional emission (W-class).
2727
#[arg(long, value_enum, value_name = "STATE")]
2828
pub local_conditionals: Option<PolicyToggle>,
2929

30-
/// Override unconditional and guarded terminal-clause emission.
30+
/// Override unconditional and guarded terminal-clause emission (W-class).
3131
#[arg(long, value_enum, value_name = "STATE")]
3232
pub terminal_clauses: Option<PolicyToggle>,
3333

34-
/// Override kind-partition refinement emission.
34+
/// Override kind partitions; at least one matching anchor lane must be on.
3535
#[arg(long, value_enum, value_name = "STATE")]
3636
pub kind_partitions: Option<PolicyToggle>,
3737
}

‎crates/helm-schema-cli/src/cli/mod.rs‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ pub use profile_args::SchemaProfile;
2626
#[derive(Parser, Debug, Clone)]
2727
#[command(
2828
name = "helm-schema",
29-
about = "Generate JSON schema for Helm values.yaml"
29+
about = "Generate JSON schema for Helm values.yaml",
30+
after_long_help = "EMISSION RETENTION:\n full keeps mandatory facts, root/local ordinary conditionals, terminal clauses,\n and kind partitions.\n lean keeps every mandatory fact and every locally anchored ordinary conditional;\n it drops root-anchored ordinary conditionals, terminal clauses, and kind\n partitions.\n\nMandatory facts cannot be disabled. The four emission override flags only change\nW-class refinements and therefore only widen acceptance when switched off."
3031
)]
3132
pub struct Cli {
3233
/// Chart directory or packaged chart archive to analyze.
@@ -76,9 +77,9 @@ pub struct Cli {
7677

7778
/// Select how much analyzed contract evidence is emitted.
7879
///
79-
/// `lean` omits document-level conditional validation. It only widens
80-
/// acceptance and substantially reduces Helm's schema compilation cost on
81-
/// large charts.
80+
/// `lean` retains mandatory and locally anchored ordinary facts. It drops
81+
/// root ordinary conditionals, terminals, and kind partitions, so it only
82+
/// widens acceptance while reducing Helm's schema compilation cost.
8283
#[arg(long, value_enum)]
8384
pub profile: Option<SchemaProfile>,
8485

‎crates/helm-schema-cli/tests/cli_flags.rs‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Plan coverage matrix for CLI flag handling: per-axis conflicts,
22
//! validation rules, and strict-mode invariants.
33
4-
use clap::Parser;
4+
use clap::{CommandFactory as _, Parser};
55
use color_eyre::eyre;
66
use helm_schema_cli::Cli;
77
use helm_schema_cli::cli::{CrdVersionLookup, DiagFormat, K8sVersionFallback, SchemaProfile};
@@ -56,6 +56,15 @@ fn cli_schema_profile_defaults_to_full_and_accepts_lean() -> eyre::Result<()> {
5656
Ok(())
5757
}
5858

59+
#[test]
60+
fn long_help_states_the_profile_retention_contract() {
61+
let help = Cli::command().render_long_help().to_string();
62+
assert!(help.contains("full keeps mandatory facts"));
63+
assert!(help.contains("lean keeps every mandatory fact"));
64+
assert!(help.contains("Mandatory facts cannot be disabled"));
65+
assert!(help.contains("--local-conditionals <STATE>"));
66+
}
67+
5968
#[test]
6069
fn cli_config_flags_and_emission_overrides_are_tri_state() -> eyre::Result<()> {
6170
let cli = parse(&[]).map_err(|error| eyre::eyre!(error))?;

‎crates/helm-schema-gen/Cargo.toml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ categories.workspace = true
1212
[lints]
1313
workspace = true
1414

15+
[features]
16+
bench-support = []
17+
1518
[dependencies]
1619
thiserror.workspace = true
1720
serde.workspace = true
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//! Feature-gated measurements for the repository's release benchmark.
2+
3+
use std::num::NonZeroUsize;
4+
use std::time::{Duration, Instant};
5+
6+
use serde_json::Value;
7+
8+
use crate::emission_plan::{CompletionPass, LoweredEmissionPlan};
9+
use crate::{EmissionPolicy, EmissionReport, ValuesSchemaInput};
10+
11+
/// One named policy projected from the shared benchmark plan.
12+
#[derive(Debug, Clone, Copy)]
13+
pub struct BenchmarkPolicy {
14+
/// Stable report label.
15+
pub name: &'static str,
16+
/// Checked policy projected by the benchmark.
17+
pub policy: EmissionPolicy,
18+
}
19+
20+
/// Repeated timings and final artifacts for one benchmark policy.
21+
#[derive(Debug)]
22+
pub struct PolicyProjectionBenchmark {
23+
/// Stable report label.
24+
pub name: &'static str,
25+
/// Time spent selecting and materializing the projected tree per run.
26+
pub projection_times: Vec<Duration>,
27+
/// Time spent in policy-free completion passes per run.
28+
pub completion_times: Vec<Duration>,
29+
/// Completed schema from the final run.
30+
pub schema: Value,
31+
/// Emission accounting from the final run.
32+
pub emission_report: EmissionReport,
33+
}
34+
35+
/// Measurements from repeatedly building one plan and projecting every policy.
36+
#[derive(Debug)]
37+
pub struct MultiPolicyBenchmark {
38+
/// Time spent constructing the policy-free plan per run.
39+
pub plan_construction_times: Vec<Duration>,
40+
/// Process-resident byte increase while retaining the first plan.
41+
pub retained_plan_bytes: Option<u64>,
42+
/// Unique canonical provider-candidate payload bytes retained by the plan.
43+
pub retained_candidate_bytes: usize,
44+
/// Per-policy projection and completion measurements.
45+
pub policies: Vec<PolicyProjectionBenchmark>,
46+
}
47+
48+
/// Builds one immutable plan per run and projects all policies from that plan.
49+
#[must_use]
50+
pub fn benchmark_policies(
51+
input: &ValuesSchemaInput<'_>,
52+
policies: &[BenchmarkPolicy],
53+
runs: NonZeroUsize,
54+
) -> MultiPolicyBenchmark {
55+
let mut plan_construction_times = Vec::with_capacity(runs.get());
56+
let mut retained_plan_bytes = None;
57+
let mut retained_candidate_bytes = 0;
58+
let mut outputs = policies
59+
.iter()
60+
.map(|policy| PolicyProjectionBenchmark {
61+
name: policy.name,
62+
projection_times: Vec::with_capacity(runs.get()),
63+
completion_times: Vec::with_capacity(runs.get()),
64+
schema: Value::Null,
65+
emission_report: EmissionReport::default(),
66+
})
67+
.collect::<Vec<_>>();
68+
69+
for run in 0..runs.get() {
70+
let resident_before = resident_memory_kib();
71+
let started = Instant::now();
72+
let plan = LoweredEmissionPlan::build(input);
73+
plan_construction_times.push(started.elapsed());
74+
if run == 0 {
75+
retained_plan_bytes = resident_memory_kib()
76+
.zip(resident_before)
77+
.map(|(after, before)| after.saturating_sub(before) * 1024);
78+
retained_candidate_bytes = plan.benchmark_retained_candidate_bytes();
79+
}
80+
81+
for (policy, output) in policies.iter().zip(&mut outputs) {
82+
let started = Instant::now();
83+
let projected = plan.project(policy.policy);
84+
output.projection_times.push(started.elapsed());
85+
86+
let started = Instant::now();
87+
let completed = plan.complete(projected, CompletionPass::Descriptions);
88+
output.completion_times.push(started.elapsed());
89+
output.schema = completed.schema;
90+
output.emission_report = completed.emission_report;
91+
}
92+
}
93+
94+
MultiPolicyBenchmark {
95+
plan_construction_times,
96+
retained_plan_bytes,
97+
retained_candidate_bytes,
98+
policies: outputs,
99+
}
100+
}
101+
102+
#[cfg(target_os = "linux")]
103+
fn resident_memory_kib() -> Option<u64> {
104+
let status = std::fs::read_to_string("/proc/self/status").ok()?;
105+
let line = status.lines().find(|line| line.starts_with("VmRSS:"))?;
106+
line.split_ascii_whitespace().nth(1)?.parse().ok()
107+
}
108+
109+
#[cfg(not(target_os = "linux"))]
110+
fn resident_memory_kib() -> Option<u64> {
111+
None
112+
}

‎crates/helm-schema-gen/src/emission_plan.rs‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,33 @@ impl LoweredEmissionPlan {
348348
drop(fill_span);
349349
finish_generated(schema, emission_report)
350350
}
351+
352+
#[cfg(feature = "bench-support")]
353+
pub(crate) fn benchmark_retained_candidate_bytes(&self) -> usize {
354+
fn record_candidate(
355+
candidate: Option<&crate::provider_schema::ProviderSchemaCandidate>,
356+
payloads: &mut BTreeSet<String>,
357+
) {
358+
let Some(candidate) = candidate else {
359+
return;
360+
};
361+
payloads.insert(candidate.key().to_string());
362+
if let Some(definition) = candidate.source_definition_schema() {
363+
payloads.insert(helm_schema_json_schema_walk::canonical_json_string(
364+
definition,
365+
));
366+
}
367+
}
368+
369+
let mut payloads = BTreeSet::new();
370+
for resolved in &self.resolved_paths {
371+
record_candidate(resolved.provider_schema_candidate.as_ref(), &mut payloads);
372+
}
373+
for conjunct in &self.conditional_schemas {
374+
record_candidate(conjunct.provider_candidate.as_ref(), &mut payloads);
375+
}
376+
payloads.iter().map(String::len).sum()
377+
}
351378
}
352379

353380
fn canonicalize_mandatory_constraints(

‎crates/helm-schema-gen/src/lib.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
//! JSON Schema lowering from normalized Helm contract signals.
22
33
mod base_schema;
4+
#[cfg(feature = "bench-support")]
5+
pub mod bench_support;
46
mod condition_encoding;
57
mod emission_plan;
68
mod emission_policy;

‎crates/helm-schema/Cargo.toml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ categories.workspace = true
1212
[lints]
1313
workspace = true
1414

15+
[features]
16+
bench-support = ["helm-schema-gen/bench-support"]
17+
1518
[dependencies]
1619
thiserror.workspace = true
1720
serde.workspace = true

‎crates/helm-schema/src/session.rs‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,28 @@ impl AnalysisSession {
411411
.resolved_emission_policy
412412
.get_or_try_init(|| Ok(self.opts.emission.resolve()?))?)
413413
}
414+
415+
#[cfg(all(feature = "bench-support", test))]
416+
pub(crate) fn benchmark_emission_policies(
417+
&self,
418+
policies: &[helm_schema_gen::bench_support::BenchmarkPolicy],
419+
runs: std::num::NonZeroUsize,
420+
) -> EngineResult<helm_schema_gen::bench_support::MultiPolicyBenchmark> {
421+
let prepared = self.prepared()?;
422+
let finalized_contract = self.finalized_contract()?;
423+
let mut provider_options = self.opts.provider.clone();
424+
provider_options.local_schema_universe = prepared.analysis.local_schemas.clone();
425+
let provider = provider_builder::build_provider(&provider_options, Some(&self.diagnostics));
426+
let input = ValuesSchemaInput::new(finalized_contract.schema_signals(), &provider)
427+
.with_values_yaml(prepared.values_yaml.as_deref())
428+
.with_dependency_values_yaml(prepared.dependency_values_yaml.as_deref())
429+
.with_dependency_refill_values_yaml(prepared.dependency_refill_values_yaml.as_deref())
430+
.with_shadowed_input_paths(&prepared.shadowed_input_paths)
431+
.with_values_descriptions(&prepared.values_descriptions);
432+
Ok(helm_schema_gen::bench_support::benchmark_policies(
433+
&input, policies, runs,
434+
))
435+
}
414436
}
415437

416438
pub(crate) fn emit_input_channel_diagnostics(

0 commit comments

Comments
 (0)