Architecture
The crate's modules, how a check flows through them, and who owns what.
peakmark is one crate with a library and a thin binary. src/main.rs calls peakmark::cli::main
and nothing else; everything the command does is reachable through the library’s public API, which
is documented with cargo doc. The crate forbids unsafe code.
Modules
src/
├── main.rs the binary: passes the arguments to cli::main
├── lib.rs crate documentation and the module list
├── cli/ arguments (args.rs), commands (commands.rs), exit codes (mod.rs)
├── config.rs peakmark.toml: modules, budgets, scenarios, validation
├── baseline.rs peakmark.baseline.json (format 2: per-scenario values and trace, contract, inventory snapshot) and the history log
├── measure.rs one module in, one Measured out
├── wasm.rs validation, sections and declared memory, read with wasmparser
├── inventory.rs the static inventory: interface, memories, derived features, sections, composition, function bodies, data segments, metadata
├── snapshot.rs the inventory as the baseline keeps it, and the diff of two: Snapshot, Drift, section and function drift
├── policy.rs [module.policy]: required exports, allowed imports and features, Violation
├── reach.rs the conservative reference graph of a module: Reach, Explanation (path from a root, callers)
├── explain.rs the ranked, evidence-backed explanations of a module's findings: Explanation, Explained
├── compress.rs gzip and brotli length
├── runner.rs runs a scenario in Node (runner.mjs) under a deadline, reads the memory capacity and coverage
├── check.rs budget and baseline against the measured values: Issue, Change, Level, verdict
├── report/ text report (text.rs) and JSON (json.rs)
├── metric.rs Metric and Values: the five gated values
├── digest.rs SHA-256 of artifacts and scenario scripts
├── units.rs "258 kB" in, "257.1 kB" out
├── date.rs today's date without a date library
└── error.rs Error and UsageError
tests/
├── cli.rs the binary against a generated module and Node scenarios
└── library.rs the public API without Node
Data flow
peakmark check:
cli::argsparses the arguments into aCli.config::loadreadspeakmark.toml;baseline::loadreads the baseline next to it.- For each configured module,
measure::measurereads the artifact, askswasm::inspectfor its sections, declared memory andInventory,reachfor the explanation of its largest functions,compressfor the gzip and brotli length, andrunnerfor the memory after each scenario. The result is aMeasured. check::evaluatejudges every value: over the budget is an error, past the baseline by more than the tolerance is a warning. Budgets judge the module-wide values; the baseline judges size per module and memory per scenario. Where the baseline and the measurement no longer line up (a scenario removed or new, a metric no longer measured, a measurement that cannot be compared) it records aChangeinstead of a number. The inventory is compared as well:snapshot::diffof the acceptedSnapshotand the measured one becomesChange::Interface, and the module’sPolicyis evaluated intoViolations, which fail the check like a budget. A change of size is attributed the same way:snapshot::section_driftandsnapshot::function_driftbecomeChange::SectionsandChange::Functions, notes that never decide the verdict.explain::explainreads the result of step 4 (issues, changes, measured inventory, reach and trace) and keeps at most three explanations on theModuleResult. It runs after every finding is decided and adds none.report::reportorreport::jsonturns the results into the output;check::verdictdecides the exit code.
peakmark accept runs steps 1 to 3 for the chosen modules, refuses a module that is over its
budget or breaks its policy, then baseline::save and baseline::append_history store the entry (Entry::from_measured:
module values, per-scenario values, contract, inventory snapshot) and the reason.
Responsibilities
- Only
runnerstarts Node, and onlyrunner.mjsknows how a module is instantiated and which of its memories are watched. Nothing else depends on the scenario protocol, a singlePEAKMARK_RESULTline on stdout. - Memory means capacity of the configured module’s observed memories.
runner.mjsattributes an instance to the configured module by the bytes it was compiled from, holds memories weakly, counts a shared memory once, and reports what it could not watch.runner.rsadds what only the artifact knows (declared memories, viawasm) and turns that intoCoverage;checkshows it as a note and never as a number. - The inventory is a model, not text.
inventoryreads a validated module into typed data (Inventory) thatreportformats and--jsonserialises. Later parts (drift, size attribution, explanations) use the model, not the output. It states what the bytes contain;Coveragestates what it could not decide, and a gap is never “not used”. The baseline keeps asnapshot::Snapshotof it, not the model: compact, sorted, with an identity per declaration (module, name, kind), and extensible by optional fields; section sizes and the largest function bodies are such fields.Inventory::functionsholds every body with its raw size, name and exports for later parts (the snapshot keeps only the largest); functions are matched across builds by name or export, never by index. Differences are warnings; only apolicy::Policythat the project configured fails the check, and it is strict about what it cannot classify. reachexplains, it does not judge. It reads the module itself (noInventoryneeded) and answers by function index: a shortest path from an export or the start function, and the direct callers. The table is a pseudo node, so an indirect call is marked and never resolved, a function without a path isno_static_root(orunknown), and nothing in it is a size, a saving or a verdict.reportadds the names fromInventory::functions. Nothing of it is stored in the baseline.explainassembles, it does not measure or judge. It reads the typed results thatcheckalready holds (Issue,Change,Measured) and runs no analysis of its own, so its cost is that of sorting a handful of candidates. Its tiers (input, interface, size and memory), the status of each explanation and the cap of three are its own; a verdict, a budget or a baseline never depends on it, and no finding is dropped because an explanation took its place. An explanation without evidence is not made.- Only
checkdecides what is an error and what is a warning. A policyViolationis an error and no baseline waives it; an inventory difference is a warning.reportformats those decisions and does not make its own. - Only
errorowns message text. Modules returnErrorvariants that carry the path or name involved;Displayrenders the sentence the command prints. Exit code 2 always means anError. - Only
baselineknows the file format. It reads versions 1 and 2, writes 2, refuses others withError::BaselineVersion, and replaces the file atomically. An entry from version 1 has nocontractand noscenarios;checktreats that as “compare the module values”, never as zero. - Formats stay put. The shapes of
peakmark.toml, the baseline file, the history log and--jsonare public; changing them is a user-facing change.
Dependencies
| Crate | Used for |
|---|---|
wasmparser |
validation, sections, memory and inventory of a module |
flate2, brotli |
gzip and brotli length |
serde, serde_json, toml |
configuration (including the policy), baseline, runner protocol, --json |
runemark |
report layout, colors and the error block |
Dev-only: wat builds the modules the tests use.