From 549af67fabb468b71e8d892c7afa1df408fe66d7 Mon Sep 17 00:00:00 2001 From: Yury Date: Sat, 10 Oct 2026 17:58:28 +0300 Subject: [PATCH] test: add portable mixed service workload gates Document scenario-first profiling and the accepted baseline/recovery sequence. Exercise original text-based source requests as well as newer workspace caches, with three-version snapshot-first and source-first workloads and reach checks. Carry the dated parked-branch evidence without its production replacement. On the unchanged master implementation, unwrap linearity (three repeats) is 0.993 time / 0.991 peak / 0.997 retained for service-session and 0.986 / 0.991 / 0.997 for service-source-first. The Windows gate passes: 5810 passed, 67 skipped, 1 xfailed. Linux verification remains required in CI. --- AGENTS.md | 33 + bench/__main__.py | 18 + bench/service_session.py | 112 + docs/12-performance.md | 199 +- docs/15-implementation-plan.md | 9 + .../service-authoring-cost-review.md | 447 ++++ .../service-baseline-integration.md | 43 + .../2026-10-10/service-cost-metrics.json | 2294 +++++++++++++++++ docs/reports/2026-10-10/service_cost_probe.py | 542 ++++ .../2026-10-10/service-recovery-plan.md | 133 + tests/bench/test_corpus.py | 2 + tests/bench/test_service_session.py | 99 + 12 files changed, 3926 insertions(+), 5 deletions(-) create mode 100644 bench/service_session.py create mode 100644 docs/reports/2026-10-10/service-authoring-cost-review.md create mode 100644 docs/reports/2026-10-10/service-baseline-integration.md create mode 100644 docs/reports/2026-10-10/service-cost-metrics.json create mode 100644 docs/reports/2026-10-10/service_cost_probe.py create mode 100644 docs/research/2026-10-10/service-recovery-plan.md create mode 100644 tests/bench/test_service_session.py diff --git a/AGENTS.md b/AGENTS.md index 4ed22e8..42c8398 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,6 +27,39 @@ first. Measure with `python -m bench ab BASE --bench NAME`, and put the time del (only if it exceeds the reported noise) and the allocation delta in the commit message. For a new feature with no base number, use `bench linearity --bench NAME`. +## Performance investigations + +Before profiling or choosing an optimization, read `docs/12` § 4.1–4.2 and +§ 5.1–5.2. Start from the user operation and its cost, not a profiler's ranking. + +- Describe the scenario: input dimensions, ordered requests/edits, measured + boundaries, retained results, and which work occurs automatically or on demand. + Separate verified client behavior from a proposed representative scenario. + A corpus name and `unwrap`/`service` label are not a workload description. +- Map each relevant cache's owner, key, first population, sharing, invalidation + and release. Distinguish cold process, cold snapshot, first file query, warm + query, and the next edit; include failed composition and unchanged dependencies + where relevant. Check these boundaries with counters or reach tests. +- Establish unprofiled end-to-end and phase costs first. Select a profiler to + answer a named question. Use sampling for elapsed-time attribution where + supported, targeted counters for work multiplicity, and allocation tools for + ownership. Use scoped `cProfile` only when Python call paths/counts are the + question; its instrumented timings are not benchmark results. +- Before making a frequently called function cheaper, explain its callers and + expected calls per file, distinct node/edge, materialization, request or cache + miss. Check growth against those dimensions. Fix unintended repeated work or + an incorrect cache boundary first; optimize the call itself only when the + remaining multiplicity is justified and its cost matters end to end. +- Measure parse, first-use and warm-query costs separately, then the real request + mix over edits. Account for eager work moved into parsing, automatic outline/ + inlay/folding requests, includes and multiple roots. Neither an idle snapshot + nor thousands of hovers alone establishes the editor's latency or memory cost. +- Keep correctness, scaling and absolute-cost acceptance separate. Compare the + same observable work on identical corpora; distinguish unavailable historical + APIs from absent historical behavior. Report time/noise, peak and retained + allocation, cache state and trade-offs. Stop optimizing when the stated goal + is met; a green linearity run alone does not accept a slower implementation. + ## Layers and import boundaries - `fastraml/parser/`, `fastraml/types/`: the passes P0–P10. A rule the RAML language diff --git a/bench/__main__.py b/bench/__main__.py index 7fa56dd..ac245df 100644 --- a/bench/__main__.py +++ b/bench/__main__.py @@ -124,6 +124,8 @@ def _at(base: int, scale: float) -> int: Bench('hover', lambda root, scale: corpus.write_hover(root, family_count=_at(400, scale))), Bench('effective-types', lambda root, scale: corpus.write_hover(root, family_count=_at(300, scale))), Bench('inlays', lambda root, scale: corpus.write_hover(root, family_count=_at(400, scale))), + Bench('service-session', lambda root, scale: corpus.write_hover(root, family_count=_at(400, scale))), + Bench('service-source-first', lambda root, scale: corpus.write_hover(root, family_count=_at(400, scale))), ) _BY_NAME = {bench.name: bench for bench in BENCHES} @@ -159,6 +161,8 @@ def run_one(bench: str, config: str, entry: Path, repeat: int) -> Measurement: 'hover', 'effective-types', 'inlays', + 'service-session', + 'service-source-first', }: return _measure_view(bench, entry, repeat) if config == 'service': @@ -202,6 +206,8 @@ def _measure_view(bench: str, entry: Path, repeat: int) -> Measurement: 'hover': _measure_hover, 'effective-types': _measure_effective_types, 'inlays': _measure_inlays, + 'service-session': _measure_service_session, + 'service-source-first': lambda entry, repeat: _measure_service_session(entry, repeat, source_first=True), }.get(bench) if service_workload is not None: return service_workload(entry, repeat) @@ -363,6 +369,16 @@ def hints() -> object: return measure('inlays', 'unwrap', hints, repeat=repeat) +def _measure_service_session(entry: Path, repeat: int, *, source_first: bool = False) -> Measurement: + from bench.service_session import exercise, prepare # noqa: PLC0415 - feature workload only + from fastraml.gctuning import tuned_gc # noqa: PLC0415 - feature workload only + + prepared = prepare(entry) + name = 'service-source-first' if source_first else 'service-session' + with tuned_gc(): + return measure(name, 'unwrap', lambda: exercise(prepared, source_first=source_first), repeat=repeat) + + def _measure_edit(bench: str, entry: Path, repeat: int) -> Measurement: """One edit to the root's buffer, and what the editor then asks for first.""" from itertools import count # noqa: PLC0415 - as above @@ -531,6 +547,8 @@ def compare(results: Sequence[Measurement], tolerance: float) -> int: 'hover': 'unwrap', 'effective-types': 'unwrap', 'inlays': 'unwrap', + 'service-session': 'unwrap', + 'service-source-first': 'unwrap', } diff --git a/bench/service_session.py b/bench/service_session.py new file mode 100644 index 0000000..8999972 --- /dev/null +++ b/bench/service_session.py @@ -0,0 +1,112 @@ +"""Representative editor requests across three versions, not a client trace.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from fastraml.positions import Position +from fastraml.service import inlays, lenses, outline, queries +from fastraml.service.workspace import Workspace +from fastraml.uris import path_to_file_uri + +if TYPE_CHECKING: + from pathlib import Path + + +@dataclass(frozen=True, slots=True, eq=False) +class SessionInput: + root: str + folder: str + focus: str + focus_text: str + versions: tuple[str, ...] + probes: tuple[tuple[int, int], ...] + + +def prepare(entry: Path, *, focus: Path | None = None) -> SessionInput: + """Prepare texts and sparse probes outside measurement.""" + target = entry if focus is None else focus + text = entry.read_text(encoding='utf-8') + focus_text = target.read_text(encoding='utf-8') + candidates = [ + (line, len(raw) - len(raw.lstrip()) + 1) + for line, raw in enumerate(focus_text.splitlines(), 1) + if raw.lstrip().startswith(('minLength:', 'maxLength:', 'type:', 'description:')) + ] + if not candidates: + raise RuntimeError('session corpus lost its sparse hover sites') + probes = tuple(candidates[index] for index in (0, len(candidates) // 2, len(candidates) - 1)) + return SessionInput( + path_to_file_uri(entry), + path_to_file_uri(entry.parent), + path_to_file_uri(target), + focus_text, + (text, text + '\n# edit 2\n', text + '\n# edit 3\n'), + probes, + ) + + +def folding(workspace: Workspace, uri: str) -> list[tuple[int, int]]: + """Use the branch's public structural-query interface.""" + if not hasattr(workspace, 'source'): + return queries.folding_ranges(workspace.text(uri) or '', uri) + source = workspace.source(uri) + if hasattr(source, 'folding_ranges'): + return source.folding_ranges() + return getattr(queries, 'folding_ranges_of')(source) # noqa: B009 - historical branch API + + +def selection(workspace: Workspace, uri: str, line: int, column: int) -> list[Position]: + """Use the branch's public structural-query interface.""" + if not hasattr(workspace, 'source'): + return queries.selection_ranges(workspace.text(uri) or '', uri, line, column) + source = workspace.source(uri) + if hasattr(source, 'selection_ranges'): + return source.selection_ranges(line, column) + return getattr(queries, 'selection_ranges_of')(source, line, column) # noqa: B009 - historical branch API + + +def exercise(prepared: SessionInput, *, source_first: bool = False) -> tuple[Workspace, dict[str, int]]: + """Keep the current workspace/caches, discarding each request's answer.""" + workspace = Workspace([prepared.folder]) + if prepared.focus != prepared.root: + workspace.open(prepared.focus, prepared.focus_text, 1) + counts: dict[str, int] = dict.fromkeys( + ('snapshots', 'outlines', 'lenses', 'hints', 'folds', 'hovers', 'selections'), 0 + ) + viewport = Position(1, 1, 120, 1) + for version, text in enumerate(prepared.versions, 1): + workspace.change(prepared.root, text, version) + workspace.collect() + if source_first: + counts['folds'] += len(folding(workspace, prepared.focus)) + snapshot = workspace.snapshot(prepared.root) + if snapshot.error is not None or snapshot.raml is None: + raise RuntimeError('session corpus lost its valid semantic snapshot') + counts['snapshots'] += 1 + queries.diagnostics(snapshot, lint=False) + snapshot.occurrences # noqa: B018 - navigation index is part of the request mix + counts['outlines'] += len(outline.document_symbols(snapshot, prepared.focus)) + queries.links(snapshot, prepared.focus) + counts['lenses'] += len(lenses.code_lenses(snapshot, prepared.focus)) + counts['hints'] += len(inlays.inlay_hints(snapshot, prepared.focus, viewport)) + counts['folds'] += len(folding(workspace, prepared.focus)) + for line, column in prepared.probes: + if queries.hover(snapshot, prepared.focus, line, column) is None: + raise RuntimeError('session corpus lost a sparse hover answer') + counts['hovers'] += 1 + # Warm requests in the same version; selection is user-triggered. + counts['outlines'] += len(outline.document_symbols(snapshot, prepared.focus)) + counts['hints'] += len(inlays.inlay_hints(snapshot, prepared.focus, viewport)) + counts['folds'] += len(folding(workspace, prepared.focus)) + for index in range(8): + line, column = prepared.probes[index % len(prepared.probes)] + if not selection(workspace, prepared.focus, line, column): + raise RuntimeError('session corpus lost a selection path') + counts['selections'] += 1 + # Release the previous model before collect/build on the next edit. + del snapshot + if not all(counts.values()): + raise RuntimeError('session corpus no longer reaches every request kind') + return workspace, counts diff --git a/docs/12-performance.md b/docs/12-performance.md index ea01d61..20de920 100644 --- a/docs/12-performance.md +++ b/docs/12-performance.md @@ -86,7 +86,7 @@ must receive a parser diagnostic rather than `RecursionError`. ## 4. Benchmark suite -`bench/` generates deterministic corpora and measures thirty workloads: +`bench/` generates deterministic corpora and measures thirty-two workloads: | Bench | Primary coverage | |---|---| @@ -120,9 +120,10 @@ must receive a parser diagnostic rather than `RecursionError`. | `hover` | a cold language-service snapshot, fourteen authoring hovers per type family and custom-facet definition queries: inherited summaries, facet explanations, built-ins, custom-facet declarations and supplied keys, nested custom-facet and annotation keys and values, property presence, methods and response statuses ([21](21-language-service.md) § 4.2) | | `effective-types` | a cold service snapshot, code-lens enumeration and full-depth RAML rendering for every named type and annotation type, including nested arrays, unions, scalar item constraints and recursive references ([21](21-language-service.md) § 4.3) | | `inlays` | a cold snapshot, inferred declaration types, expected types at supplied custom-facet/annotation roots and nested data keys, and compact inherited constraints anchored to type references ([21](21-language-service.md) § 4.4) | +| `service-session`, `service-source-first` | representative mixed editor requests over three root-buffer versions: parser diagnostics, occurrences, outline, links, lens enumeration, 120-line viewport inlays, folding, three sparse hovers and warm queries; source-first additionally requests folding before each snapshot | The six general workloads are `small`, `large`, `endpoints`, `extensions`, -`validate` and `jsonschema`. The other twenty-four are feature workloads: +`validate` and `jsonschema`. The other twenty-six are feature workloads: each exists because no general workload runs the code it covers. Their reach tests (`tests/bench/test_corpus.py`) count calls or check bound results, and fail if a corpus stops reaching that code at every size it covers. @@ -155,6 +156,9 @@ The small corpus-validity tests run in the ordinary test suite. For `hover`, `unwrap` builds a service snapshot with unwrap and validation, then queries every generated hover site. It measures the cold indices and their reuse; probe selection and corpus generation are outside the measured region. +For `service-session` and `service-source-first`, `unwrap` runs the three-version +mixed request sequences of § 4.2; their other configurations keep their ordinary +parse/view meanings. Input texts and sparse probes are prepared outside measurement. ```bash python -m bench run @@ -181,6 +185,96 @@ untraced runs; allocation tracing runs separately; RSS is a process high-water mark. `bench/baselines.json` is fingerprinted by Python version, platform, and YAML backend. A comparison across fingerprints is intentionally not a result. +### 4.1 Describe the usage scenario before measuring + +For a performance investigation, record the scenario before selecting a profiler +or proposing an optimization. Keep the description with the workload, or in the +dated report for an investigation using an existing workload. It must identify: + +- **User operation and goal.** What the user waits for, or what memory the host + keeps. State the acceptance criterion before interpreting results. Separate + measured client behavior from a representative scenario whose request order or + frequency is an assumption; do not invent an editor-wide average. +- **Input dimensions.** Bytes, files/roots, distinct shapes/nodes/edges, template + applications, data width/depth and output size, as relevant. State which grow + with scale and which stay fixed. A 300 KiB entry and 150 small libraries stress + different ownership and invalidation paths even at equal total bytes. +- **Ordered actions.** For example: open, source-only folding, snapshot build, + parser diagnostics, lint, outline, viewport inlays, one cursor hover, repeated + selection, change, and rebuild. Name automatic versus explicit requests and + whether a request arrives before the debounce expires. Use only the actions + the scenario needs; the example is not a required request sequence. +- **Measured boundary and result lifetime.** State what setup is outside timing, + whether collection, discovery, position conversion and protocol serialization + are inside it, and which results remain live when memory is read. Distinguish + server processing time from user-visible latency, which can also include + debounce, queueing and transport. Include lint only when the scenario reaches + that tier rather than cancelling it with another edit. +- **Cache lifecycle.** For every relevant cache, name its owner, key, population + trigger, shared consumers, invalidation and release. Identify whether a changed + dependency discards the entire root snapshot, whether unchanged files survive + that boundary, and whether several roots retain separate copies. Check actual + input/version and composition-policy compatibility (backend and limits), not + just the URI: current buffer text and snapshot text can differ. Check whether + returned views or old answers keep an evicted owner alive. Describe successful, + empty and failed results separately where they change whether another request + repeats the work. + +“Cold” must name a boundary: process/import, workspace discovery, semantic +snapshot, file source/index, or presentation. A warm workspace can still hold a +cold snapshot after an edit; a warm snapshot can still receive the first query +for a library. Separate setup, snapshot rebuild, first-use construction and warm +queries, then measure the scenario's complete sequence. Warm is not synonymous +with free: folding can still traverse a cached tree, and returning hints still +costs work proportional to their output. + +Trace the measured driver and its service calls against this description. +Reach tests must protect meaningful boundaries, not just the presence of a +function call: for example, one population shared by multiple consumers, a miss +after an edit, or no grammar construction for a model-only query. Use targeted +counters to check a disputed boundary before attributing a delta to it. + +### 4.2 What the service workloads establish + +The suite has both total-cost workloads and focused mechanism workloads. Read +the configuration as well as the corpus name: + +| Workload/configuration | Measured scenario | Limit of the evidence | +|---|---|---| +| `large/service` (and other `service` rows) | One persistent workspace receives successive root-buffer comment edits; each measurement changes the text, collects discarded snapshots, rebuilds the root, reads parser diagnostics without lint, and builds occurrences. | Includes snapshot-side source capture on implementations that request it, but no hover, inlays, outline, folding, lint or protocol publication. Unchanged libraries are still part of each full reparse. | +| `hover/unwrap` | A new workspace and cold validated/unwrapped snapshot, followed by fourteen hovers per generated family and custom-facet definitions; answers and the snapshot remain live. | Combines parsing, first-use indices and bulk query reuse. It is coverage/load evidence, not the latency of one hover in an already parsed editor buffer or an observed hover frequency. | +| `inlays/unwrap` | A new workspace and cold validated/unwrapped snapshot, followed by a whole-file hint request; the snapshot and returned hints remain live. | Does not isolate first viewport latency, scrolling, range-cache reuse or a persistent edit/request mix. | +| `effective-types/unwrap` | A cold snapshot, lens enumeration, then full-depth rendering of every named type and annotation type. | Enumeration and explicit rendering are different user operations; an editor listing lenses does not necessarily render any type. | + +The parked-branch cost review in `reports/2026-10-10/service-authoring-cost-review.md` +also describes diagnostic workloads specific to that implementation. They are +not part of this tree's benchmark suite or evidence of features shipped here. + +These limits are not reasons to discard the workloads. They tell you which +question each can answer. A change to source ownership also needs a persistent +mixed-request scenario if the claim concerns editor use: exercise source before +and after snapshot creation, automatic requests and sparse cursor requests across +edits. Include dependency edits, literal includes and shared-root cases when they +reach the changed code. Add the missing workload and reach test rather than +inferring that cost from unrelated rows. `service-session` and +`service-source-first` supply this mix over the single-file hover corpus, with +and without source preceding semantics. Each version requests an outline, links, +lenses, viewport hints and folding, then three sparse hovers, a second outline, +hint and folding request and eight selections. Generation and edit-text preparation +are outside timing; workspace construction, collection and all requests are inside. +The returned workspace retains the latest model and caches, but request answers +and previous snapshots are released. These are representative service-core +sequences, not observed client traces; they exclude lint, protocol conversion, +transport and debounce. `tests/bench/test_service_session.py` protects their +rebuild and reuse boundaries. They do not establish multi-root or dependency-edit +costs. + +Do not explain an implementation using a report's shorthand alone. Verify where +it composes, captures, interprets and presents source now. Reading captured +records directly is different from constructing a full node view; recomposing +text is different from building grammar and token indices over the result. +Attribute each to its actual population trigger and owner. + ## 5. Gates and local policy CI does not compare absolute benchmark times or committed baselines. Its `bench` @@ -208,9 +302,104 @@ a change to a hot path, or to any code a claim is made about: Include the workload, both deltas, and the noise in the commit message. `bench compare` against the committed baseline is only a coarse check for large regressions: the baseline and the comparison run at different times, and -the machine drifts in between. Profile before optimizing: use `cProfile` for -call counts, `tracemalloc` for allocation attribution, and a wall-clock -profiler for elapsed-time evidence. +the machine drifts in between. Follow § 5.1 before optimizing and § 5.2 when +interpreting the comparison. + +### 5.1 Investigate multiplicity before constant factors + +1. **Establish the cost without instrumentation.** Use the scenario of § 4.1 + and an existing comparable workload, or add the missing workload and reach + test. Identify the expensive phase with coarse elapsed-time boundaries. Keep + parsing, first-use construction, warm queries and collection distinguishable; + measuring only warmed queries can hide work moved into each reparse. Do not + optimize a phase that has not been shown to matter to the stated goal. +2. **Form a falsifiable explanation.** Name the suspected work and its expected + multiplicity. For example, “this file's source should populate once for these + identical texts, but folding after hover composes it again.” Check the callers, + input ownership and invalidation routes. A high call count or cumulative-time + row is a question, not a diagnosis. +3. **Choose the measurement that answers that question.** Prefer a supported + sampling profiler for elapsed-time attribution across the relevant scenario; + check its ability to attribute native work such as libyaml and distinguish + waiting from CPU work. Use scoped counters for compositions, node visits, + index populations and cache hits/misses. Use `tracemalloc` snapshots for + Python allocation attribution, and inspect live owners to explain retention; + native allocation and RSS need separate evidence. Use scoped `cProfile` when + Python call paths or counts are the unresolved question. It adds overhead per + instrumented event and can exaggerate the cost of tiny, frequent accessors or + wrappers. Do not choose an optimization from its timing ranking alone, or + report its timings as wall-time improvements. If a suitable profiler is + unavailable, use coarse timers, counters and source inspection and record the + attribution limit. +4. **Check calls against the work that justifies them.** Normalize counts by the + relevant files, distinct nodes, edges, materialized declarations, requests, + output records or cache misses. Distinguish primitive calls from recursive + totals and self time from cumulative time; nested cumulative rows overlap and + cannot be added as independent costs. Trace the caller responsible for excess + work before changing its callee. Check at more than one size when growth is in + question; vary query count independently of input width, and root count + independently of shared-file size, when both can multiply the work. +5. **Repair unjustified work first.** Look for a full-file scan per cursor, a + full-model index rebuilt per request, visits per path through a shared graph, + duplicate source owners, or repeated failed cache population. Some repetition + is required: template materializations have separate semantic contexts, roots + can bind a library differently, and output itself can grow. State that bound + instead of removing necessary work. A million accessor calls can be legitimate + over a million nodes, or evidence of repeated traversal over a thousand; + making the accessor cheaper does not resolve the second case. +6. **Optimize the remaining constant factor only if it matters.** Once the + multiplicity and cache boundaries are justified, use a representative + microbenchmark for a leaf cost if needed. Bound the possible end-to-end gain by + that phase's share of unprofiled time. Validate the change with uninstrumented + A/B, allocations, reach/scaling checks and the correctness gate. A faster leaf + is not an accepted optimization when its construction, retention or work in + another phase makes the real scenario worse. + +Instrumentation is diagnostic and runs separately from acceptance timing. Do not +run concurrent benchmark comparisons on the same machine or compare one side +with tracing/profiling to the other without it. Keep warmup and cache population +symmetrical, except where their difference is the change being measured; in that +case include both costs in the same user operation. + +### 5.2 Compare behavior, phases and retained ownership + +Compare identical inputs and the same observable work, not matching flag names +or internal representation. The A/B runner copies this tree's `bench/` to the +base checkout, so an API-specific driver can fail even when the base performs the +same user operation through another API. Inspect that distinction: absent +historical behavior has no base number; an unavailable interface may need a +behavior-equivalent driver. Never turn a failed or skipped worker into a speedup, +or omit costly work merely to make the older worker run. + +For source/cache changes, report snapshot rebuild, first-use and warm-query costs +alongside the total request sequence. Parse-time capture is paid on rebuild even +when no query uses it. Lazy work is paid by the first consumer after its own cache +boundary, which may differ from the snapshot boundary. Repeated queries can +amortize that work within a version; an edit can make it recur. Eliminating one +composition does not establish a win if capture, value selection, wrapper reads +or index construction cost more. Likewise, a cold bulk-query regression does not +show which portion is parsing or first use without separate evidence. + +Report memory at the points the scenario actually retains: after diagnostics, +after automatic editor queries, after optional cursor queries, and across +repeated edits where relevant. Idle text-only snapshots can save memory that is +spent again on lazily built source trees and indices. State which model, source +owners and answer lists remain live; a result retaining thousands of formatted +hovers is different from an editor discarding each answer. Keep traced peak, +post-collection retained Python allocation and process RSS distinct. A +single-build allocation result is not evidence of steady-state RSS or successful +release of old versions. + +Keep three decisions separate: correctness, complexity and absolute-cost +acceptance. Linearity can pass while every edit becomes slower by a large +constant factor. A memory gain is a trade-off, not automatic permission for a +latency regression. State both costs and the goal they serve; do not change the +goal after seeing the winning row. Accept an architectural replacement against +the user scenarios and original relevant baseline, not only against an earlier, +already-regressed prototype. Record revisions, corpus/driver, configuration, +cache states, commands, time/noise, allocations and remaining measurement gaps +in the dated report. Once the goal passes and no new evidence exposes a problem, +stop rather than chasing the next profiler row. ## 6. Garbage collection diff --git a/docs/15-implementation-plan.md b/docs/15-implementation-plan.md index aeb3398..032fe63 100644 --- a/docs/15-implementation-plan.md +++ b/docs/15-implementation-plan.md @@ -19,6 +19,15 @@ that each extend the same master remains deferred (docs/19 § 7). XML Schema external types are unsupported ([01](01-scope-and-coverage.md) § 3). +**Service baseline acceptance and recovery.** The simpler source-cache branch +is undergoing comparison with master using the portable mixed-request workloads. +The record-backed representation replacement remains parked; its recurring +rebuild and first-use costs are recorded in the +[service cost review](reports/2026-10-10/service-authoring-cost-review.md). +The accepted [baseline and recovery plan](research/2026-10-10/service-recovery-plan.md) +lands portable measurements first, checks the simpler branch against master, and +then evaluates isolated ports against the accepted baseline. + `fastraml join` ([20](20-join.md)) accepts API documents only; Overlays, Extensions and Libraries as inputs, and renaming to resolve a conflict, are not covered (docs/20 § 11). diff --git a/docs/reports/2026-10-10/service-authoring-cost-review.md b/docs/reports/2026-10-10/service-authoring-cost-review.md new file mode 100644 index 0000000..f0fed0b --- /dev/null +++ b/docs/reports/2026-10-10/service-authoring-cost-review.md @@ -0,0 +1,447 @@ +# Service-authoring cost review + +Date: 2026-10-10. This is measured decision evidence, not an accepted redesign. + +## 1. Question and conclusion + +The decision is whether to repair `refactor/service-authoring` or selectively port +its useful changes to `refactor/service-source-simple`. The user's priority is +the recurring edit-to-parser-diagnostics/occurrences cost: a substantial penalty +on every rebuild is unacceptable, even if later queries get faster. + +The current branch fails that priority. Across three different corpora its +measured rebuild sequence is 34–44% slower than the simplified branch. Removing +only the workspace's capture request removes almost all of this difference. +Occurrence construction is not the main cause. The added work is largely +record-backed composition, source publication and collection of the larger +discarded snapshot. + +This establishes a recoverable boundary, not an accepted one-line fix. With +capture disabled, the existing fallback makes the first large-file outline +265 ms instead of the simplified branch's 30 ms. Keeping the branch requires +fixing both the mandatory rebuild cost and the source-query construction path. +Selective porting avoids taking that entire representation replacement as a +prerequisite for the useful model/query improvements. + +## 2. Revisions, methods and scope + +- Parked implementation: `bc4b49d`, `refactor/service-authoring`. +- Simplified implementation: `fb0fb73`, `refactor/service-source-simple`. +- **No-capture**: `bc4b49d` with a diagnostic wrapper around the workspace's + `parse_lenient` call that replaces only `projection=ProjectionRequest()` with + `projection=None`. Retention, unwrap, validation, loader and query code stay the + same. This is an ablation, not a production change or full correctness proof. +- Python 3.12.13, Windows AMD64, libyaml. Timed comparisons run in fresh + subprocesses, sequentially, over the same generated input per comparison. +- Primary phase observations: three alternating rounds, three repeats per worker. + Use the fastest repeat per round and the minimum round. Noise is the larger + side's spread of round minima. Query rows report minima across these runs; + their exact small differences are not separately accepted speedup claims. +- Coarse stage/composition/publication timers run separately from the primary + uninstrumented timings. They are nested measurements, not additive profiler + rankings. Node/accessor counters run in another untimed pass. No `cProfile` + timings are used. +- Python allocation tracing runs separately from timings. Memory checkpoints + collect with the current workspace/snapshot alive and discard query answers. + Values use decimal MB, not MiB. Current Windows working set is measured without + tracing in separate ten-version runs; it is not the A/B harness's peak RSS. + +The driver is [service_cost_probe.py](service_cost_probe.py). Compact numerical +results for eleven runs, including phase minima, same-fastest-run phase breakdowns, +query costs, allocations, counts and RSS observations, are in +[service-cost-metrics.json](service-cost-metrics.json). + +These are bounded service-core measurements. They exclude debounce, transport, +protocol position conversion/serialization and real client scheduling. The mixed +request order is representative, not an observed VS Code trace. The normal rebuild +scenario includes occurrences because that is the workload under review; parser +diagnostic publication alone does not force that index. Snapshot-only phase costs +also show the penalty before occurrence construction. + +## 3. Inputs and operations + +| Corpus | Files | Total bytes | Entry / queried file bytes | Model dimensions | +|---|---:|---:|---:|---| +| `large` | 152 | 576,354 | 3,930 / 3,850 | 7,000 generated library types plus common types; 19,904 registered shapes; 150 libraries and common/root documents | +| `hover` | 1 | 342,406 | 342,406 / 342,406 | 400 families; 7,200 shapes, 400 resources, typed facet/annotation values and unapplied traits | +| `endpoints` | 1 | 286,801 | 286,801 / 286,801 | 500 resources; 12,003 shapes; template and endpoint materialization | + +These corpora come from the parked tree's generators. Unlike the simplified +branch's newer general corpora, these general inputs do not inject deprecated +spellings. Each comparison uses byte-identical input on all sides; the comparison +is not against separately generated branch-specific inputs. + +**Normal rebuild.** A persistent workspace is warmed with an initial snapshot and +occurrences, then root-buffer comment edits are prepared outside timing. Each +timed operation changes the buffer, collects the discarded model, rebuilds the +root, reads parser diagnostics without lint, and builds occurrences. It requests +no source/query presentation. All unchanged libraries are still reparsed. + +**Query phases.** A new workspace builds a snapshot and occurrences outside query +timing. It then requests an outline, lens enumeration, hints for lines 1–120, +folding, and a field hover; the same query kinds are repeated to observe warm +behavior. On `large`, only the first library is queried and opened as an unchanged +buffer. Query order is significant: the outline can populate source needed by a +later hover. Lens enumeration does not render effective types. + +**Mixed sequence.** The new `service-session` workload runs three root versions +in one workspace. Each version requests parser diagnostics, occurrences, an +outline, links, lens enumeration, viewport hints and folding; then three sparse +hovers, a repeated outline/hint/folding request and eight selections. Old snapshot +locals and request answers are released before the next version. The result holds +the latest workspace/model/caches. `service-source-first` adds folding before each +snapshot. Their reach tests protect rebuilding, composition sharing and warm +query reuse, including both the timed and allocation passes. + +## 4. The normal rebuild penalty + +### 4.1 Uninstrumented total cost + +| Corpus | Parked ms | Simplified ms | Parked penalty | Comparison noise | No-capture ms | No-capture vs simplified | +|---|---:|---:|---:|---:|---:|---| +| `large` | 634.6 | 472.3 | +34.4%, +162.3 ms | 13.2% | 467.7 | within noise | +| `hover` | 277.9 | 193.6 | +43.6%, +84.3 ms | 10.6% | 196.6 | within noise | +| `endpoints` | 458.1 | 336.0 | +36.3%, +122.0 ms | 0.9% | 342.7 | +2.0%, beyond 0.9% noise | + +The endpoint ablation leaves a small repeatable difference. The experiment does +not attribute that residual to an individual earlier change. It does establish +that the large mandatory capture penalty is not needed to run the model/query +code retained on this branch. + +### 4.2 Where the time goes + +Coarse measurements below use the phase values from each side's fastest complete +instrumented run, not a sum of independently selected phase minima. + +For `hover`: + +| Boundary | Parked ms | Simplified ms | No-capture ms | +|---|---:|---:|---:| +| Change | 0.5 | 0.4 | 0.4 | +| Collection | 21.1 | 12.2 | 12.3 | +| Snapshot rebuild | 240.6 | 164.2 | 163.9 | +| Parser diagnostic extraction | <0.01 | <0.01 | <0.01 | +| Occurrence construction | 18.3 | 17.5 | 17.8 | +| Complete rebuild sequence | 280.6 | 194.3 | 194.4 | + +Inside that snapshot rebuild: + +| Nested boundary | Parked ms | Simplified ms | Interpretation | +|---|---:|---:|---| +| YAML composition itself | 35.8 | 35.3 | Little difference in this native/load portion | +| Composition including conversion | 88.9 | 60.6 | About 28 ms extra on the record-backed construction route | +| P0–P3 decoded stage, including composition | 132.3 | 101.1 | Contains the previous row; do not add both | +| Source publication after the passes | 44.0 | absent | Additional grammar/value-selection and publication work | +| Unwrap | 9.9 | 9.6 | Not the principal difference | +| Validation | 14.7 | 14.6 | Not the principal difference | + +The extra composition, publication and collection account for most of the observed +84–86 ms difference on this corpus. This is a boundary-level attribution; it does +not establish a profitable individual accessor optimization. + +The multi-file `large` coarse run confirms the same pattern: snapshot 531.5 versus +390.2 ms, source publication 74.6 ms versus absent, collection 41.1 versus 24.2 ms, +occurrences 53.4 versus 55.4 ms. Composition is 183.8 versus 118.0 ms and is nested +inside decoding. The native/load component there also changes with the allocation +schedule; do not interpret it as a change to libyaml's algorithm or add it to the +inclusive composition time. + +### 4.3 What repeats, and where + +The code boundary is `service/workspace.py:Workspace._parse`, which always supplies +a `ProjectionRequest` for service snapshots. `registry.py:Raml.compose_source` +selects the record-backed converter. `sourcecapture.py:ProjectionBuilder.__call__` +constructs canonical records and parser views. `parser/entry.py:_parse` invokes +`parser/projections.py:finish_projections` after the passes, including semantic +failure exits. + +| Per snapshot | `large` | `hover` | `endpoints` | +|---|---:|---:|---:| +| YAML compositions, all three implementations | 152 | 1 | 1 | +| Converted source occurrences | 69,730 | 35,209 | 36,057 | +| Parked retained original-source records | 69,730 | 35,209 | 36,057 | +| Parked `_RecordNode.kind` reads during rebuild | 145,999 | 58,810 | 172,589 | +| Parked `SourceRecord.line` reads during rebuild | 18,759 | 14,804 | 44,031 | + +Three root edits repeat the same counts on every version. On `large`, 151 unchanged +file inputs each compose three times across three versions; the root's three +different comment versions compose once each. There is no duplicate physical-file +composition within a single measured parse. + +For 200 versus 400 hover families, converted nodes are 17,609 versus 35,209; +kind reads 29,410 versus 58,810; line reads 7,404 versus 14,804. Rebuild time is +141.3 versus 277.9 ms on the parked branch. First outline time is 65.1 versus +131.0 ms. These counts/costs are consistent with linear work at fixed depth. +They do not show a quadratic or per-path explosion in these scenarios. Large +call counts alone are not the defect: the source capability is populated at the +wrong mandatory boundary and repeated over all dependencies after every edit. + +## 5. Memory: before queries and after them + +### 5.1 Post-collection retained Python allocation + +| Corpus/checkpoint | Parked MB | Simplified MB | No-capture MB | +|---|---:|---:|---:| +| `large`, snapshot only | 28.71 | 20.18 | 20.46 | +| `large`, diagnostics + occurrences | 36.85 | 28.33 | 28.60 | +| `large`, automatic queries on one library | 46.38 | 36.29 | 38.27 | +| `hover`, snapshot only | 14.61 | 10.40 | 10.16 | +| `hover`, diagnostics + occurrences | 17.23 | 13.02 | 12.78 | +| `hover`, automatic queries | 28.87 | 33.94 | 31.65 | +| `endpoints`, snapshot only | 22.19 | 17.59 | 17.43 | +| `endpoints`, diagnostics + occurrences | 23.61 | 19.01 | 18.85 | +| `endpoints`, automatic queries | 35.95 | 38.93 | 38.95 | + +Three sparse field hovers after those automatic requests add less than 2 KiB at +these checkpoints. They do not construct a newly cold source index in that order. + +The memory verdict depends on input shape and queried files. Capturing all 152 +files is more expensive when only one small library is queried. On a monolithic +file, the simplified branch eventually retains two full source trees plus its +grammar/token stores, and the compact-record branch becomes smaller. + +Peak allocation through automatic queries is 47.53 / 37.36 / 39.42 MB on `large`, +30.06 / 45.60 / 32.83 MB on `hover`, and 37.36 / 50.79 / 40.36 MB on `endpoints` +(parked / simplified / no-capture). These are separate allocation runs, not +instrumented elapsed-time comparisons. + +### 5.2 Allocation attribution + +A separate eight-frame `tracemalloc` pass over `large` with diagnostics/occurrences +alive attributes 8.144 MB to `views/occurrences.py` on every implementation. +That index is not the retained-memory difference either. + +The parked pass attributes 6.618 MB to `sourceprojection.py` and 6.116 MB to +`sourcecapture.py`. This 12.734 MB is not all net extra memory: it replaces some +allocations formerly attributed to plain `yamlnode.py` and detached expressions +in `parser/entry.py`. Total retained allocation is 36.873 MB versus 28.626 MB for +the no-capture ablation, a net capture-associated difference of 8.247 MB. + +Attribution means the innermost fastRAML allocation frame, not a proof that one +module exclusively owns every reachable byte. The source record counts and +capture ablation provide the corresponding structural/lifetime evidence. + +### 5.3 Untraced current working set and old-version release + +Separate ten-version Windows runs request diagnostics/occurrences on versions +1–5, then add outline, viewport hints, folding and sparse hovers on versions 6–10. +Request answers are discarded. Collection occurs before rebuilding and before +each observation; these runs check release/working set, not request latency. + +| Corpus/state | Parked MB | Simplified MB | No-capture MB | +|---|---:|---:|---:| +| `large`, versions 2–5 | 84.1–84.8 | 74.4–77.2 | 74.9–77.4 | +| `large`, versions 7–10 after queries | 90.7–93.6 | 79.6–83.2 | 83.4–84.2 | +| `hover`, versions 2–5 | 68.9–70.4 | 60.1–63.2 | 60.1–61.9 | +| `hover`, versions 7–10 after queries | 78.9–80.2 | 87.7–89.7 | 82.3–84.0 | + +All observed versions hold one cached service snapshot and two live `Raml` +objects. The hover run's warmed process baseline holds one `Raml` before a +workspace snapshot exists. Counts do not accumulate across the ten versions. +This bounded check gives no evidence of an accumulating old-model leak. It does +not prove indefinite steady state or explain every allocator/RSS fluctuation. + +## 6. First-use queries and cache boundaries + +### 6.1 Query costs after a current snapshot exists + +The table uses the monolithic `hover` input; these costs exclude snapshot parsing +and occurrence construction. + +| Query | Parked ms | Simplified ms | No-capture ms | +|---|---:|---:|---:| +| First outline | 131.0 | 29.8 | 265.2 | +| Lens enumeration | 1.27 | 1.33 | 1.54 | +| First 120-line inlay request | 47.9 | 154.3 | 47.3 | +| First folding request | 14.9 | 80.8 | 15.4 | +| Field hover after automatic requests | 0.044 | 0.014 | 0.041 | +| Warm outline | <0.001 | 30.3 | <0.001 | +| Warm inlay request | 0.089 | 0.093 | 0.088 | +| Warm folding | 13.6 | 13.4 | 13.4 | + +Warm source is not free folding: it still traverses the structure and returns +ranges. Warm inlays still construct/return requested hints. The microsecond hover +differences are not the priority next to tens of milliseconds of mandatory work. + +On `large`, a first inlay query for one small library still takes approximately +54–57 ms on all sides. The subject and typed-data indices span the semantic model; +a 120-line viewport does not limit their first population to those lines. + +The first outline is a separate parked-branch cost. Coarse outline timers show +about 27 ms creating the cursor/start index and about 72 ms in recursive section +placement on the captured input. Those measurements overlap: placement can cause +cursor-index population. The no-capture fallback spends another approximately +124 ms obtaining/composing/publishing the source projection, and section placement +is approximately 84 ms. Moving capture out of parsing simply moves some of this +cost to the first outline. + +The outlines do not have identical presentation capability: the parked branch +places real section keys and empty authored sections, while the simplified branch +uses model-only grouping/fallback spans. Thus the entire outline difference is +not an isolated representation regression under identical output requirements. +The accurate-source feature still needs its own acceptable construction path. + +### 6.2 Owners, population and invalidation + +| Structure | Owner/population | Reuse and invalidation observed | +|---|---|---| +| Parked captured source | Parse-owned `Raml.source_projections`; populated during every original composition and finalized after the passes | Shared by source queries within that snapshot; rebuilt for every dependency when the snapshot is dropped | +| Parked standalone folding/selection source | `Workspace._sources`; first source-only request for current input | A compatible semantic capture replaces it; source-first still composes before parsing, then parsing composes again | +| Parked semantic/authoring indices | Snapshot; semantic consumers and first hover/inlay/typed-data requests | Shared within the snapshot; root edits discard them even when a queried dependency is unchanged | +| Parked outline list | Snapshot + authored URI; first outline | The second outline in the same snapshot reuses the returned list | +| Simplified hover tree, grammar and built-ins | Snapshot's `Hover`, per queried URI; first hover/inlay | Composes and indexes the queried file; discarded with the snapshot | +| Simplified folding/selection tree | Workspace URI + text-object identity; first source-only request | Shares between folding/selection; persists for an unchanged open library across root edits; separate from the hover tree | +| No-capture source fallback | Snapshot's `SourceIndex`; first source-dependent query | Composes only the queried file; published/cached per snapshot; separate from an earlier generic standalone source | + +The single-file composition counters verify these boundaries: + +- Parked semantic-first: one composition per version, shared by later queries. +- Simplified semantic-first: three per version: parse, hover/inlay source and + folding/selection source. +- No-capture semantic-first: two per version: parse and query fallback. +- Parked source-first: two per version, pinned by the reach test: standalone and + original semantic capture. Replacement releases the old workspace owner, but + does not reuse it as parser input. +- On the unchanged queried library across three root edits, simplified source + requests add four compositions total: one workspace source population and one + hover population per snapshot. No-capture adds three query-fallback compositions. + The shared parse itself still composes all 152 files on every version. + +On one unchanged broken buffer, each implementation's semantic parse attempts +composition once. Three subsequent `Workspace.source` requests compose zero +additional times on parked, three on simplified, and once on no-capture. The +simplified workspace's success-only source cache does not memoize failure. + +## 7. Complete mixed sequences and unaffected paths + +### 7.1 Mixed request results + +The lower-noise diagnostic A/B over the tested scenario yields: + +| Three-version sequence | Parked ms | Simplified ms | No-capture ms | Larger parked/simple time noise | +|---|---:|---:|---:|---:| +| Semantic snapshot first | 1,483.5 | 1,558.6 | 1,652.0 | 2.0% | +| Folding before each snapshot | 1,836.7 | 1,603.2 | 2,010.6 | 1.8% | + +Parked is 4.8% faster on the first order and 14.6% slower on the second, both +outside their reported noise. No-capture is 6.0% slower than simplified on the +first order and 25.4% slower on the second. It is not an accepted mixed-use fix. + +Retained allocation for semantic-first is 28.881 / 33.948 / 31.655 MB; peaks +30.066 / 45.602 / 32.840 MB. Source-first keeps 28.881 / 33.944 / 38.743 MB; peaks +30.185 / 39.948 / 39.929 MB. In the ablation, a generic standalone source can +remain beside the snapshot's expression-capable fallback. + +An earlier standard `bench ab fb0fb73` run measured semantic-first 1,526.7 versus +1,596.6 ms inside 18.2% noise, and source-first 1,898.3 versus 1,713.5 ms (+10.8%, +noise 3.8%). The subsequent lower-noise run confirms the direction but does not +erase the earlier uncertainty. Both comparisons execute the same service scenario. + +Both new workloads pass their actual `unwrap` linearity gate: normalized time +0.988 and 0.982, peak 0.998 and 0.999, retained 0.997 and 0.997, respectively. +This checks scaling, not acceptance of the rebuild penalty. + +### 7.2 Ordinary parsing and full-retention consumers + +Standard A/B against `fb0fb73`, three rounds/best of three, using the same corpus: + +| Workload | Simplified ms | Parked ms | Time verdict/noise | Peak delta | Kept delta | +|---|---:|---:|---|---:|---:| +| `large/parse` | 316.3 | 319.1 | within 2.7% noise | +1.4% | +1.5% | +| `large/unwrap+validate` | 395.5 | 400.6 | within 2.1% noise | +1.4% | +1.5% | +| `large/unwrap+lint` | 619.1 | 739.2 | +19.4%, noise 2.5% | +2.7% | within noise | +| `endpoints/parse` | 265.9 | 270.8 | within 3.1% noise | +0.4% | +0.8% | +| `endpoints/unwrap+validate` | 297.2 | 304.0 | +2.3%, noise 2.1% | +0.4% | +0.7% | +| `endpoints/unwrap+lint` | 633.7 | 739.7 | within 23.2% noise | +1.1% | within noise | + +The ordinary no-source path does not pay the large capture penalty. Full retention +does use the new record/read-facade machinery even without a projection request. +The bench's `unwrap+lint` configuration explicitly retains full source; it is not +evidence that default service lint needs that retention. Its result retains lint +findings rather than the complete model, so its small kept total does not measure +full-source storage. + +Earlier direct bulk comparisons from this investigation measured parked versus +simplified: cold hover 363.1 / 332.6 ms (noise 1.7%), whole-file inlays 316.4 / +350.8 ms (noise 3.4%), effective rendering 261.6 / 205.8 ms (noise 4.8%), and +navigation 305.9 / 417.6 ms (noise 2.2%). These compare entire branch bundles; +they do not attribute navigation's gain solely to one shared-index change. + +## 8. Decision criteria + +**Reject the current mandatory capture path as-is.** Its cost recurs on every +service rebuild, is observable even without source queries, and makes the +multi-file case larger both before and after querying one library. A favorable +bulk/mixed row does not satisfy the user's separate diagnostic-latency requirement. + +**Keeping and fixing the branch is technically plausible**, because an existing +option boundary restores rebuild performance close to simplified without throwing +away the semantic/query code. Acceptance would still require: + +1. Recover the normal rebuild path against the simplified and original relevant + controls; investigate the endpoint residual rather than declaring all costs + eliminated. Preserve ordinary no-source parsing and full-value consumers. +2. Give source its appropriate file/input lifetime and eliminate duplicate + generic/semantic source ownership where compatible. Merely moving all capture + to the first query is insufficient. +3. Fix or redesign first-outline/source-site construction while preserving the + accurate authored spans that the feature adds. The observed scaling permits + constant-factor work, but only after the ownership/population boundary is right. +4. Measure both request orders, library/dependency changes and multiple roots. + Maintain separate readiness budgets for diagnostics, outline and hints instead + of accepting only a sum that trades one delay for another. + +**Selective porting is the lower-scope route** if repairing that representation +and query boundary is not a product goal now. Candidate bundles include: + +- `type_written` and model-backed declaration inlays, avoiding a source grammar + index merely to determine whether a type was explicit; +- shared semantic/reverse-hierarchy indices, cached outlines, and typed-data + definitions independent of hover presentation; +- cursor-local source-only primitive lookup and text-offset checks, with their + receiving-context and opaque-data correctness cases; +- accurate authored section ranges as a separate feature with its own workload, + rather than making the record-backed parser a prerequisite. + +Some representation-independent work is already on simplified: typed-value +navigation, syntax-alias facts, visible names and postparse include handling. +Do not port it twice. Port candidates need isolated A/B evidence; the whole-branch +navigation/inlay improvements do not guarantee each candidate has the same gain. + +The measurements support making mandatory capture optional/redesigning its +boundary or selecting smaller bundles. They do not support trying to rescue the +current acceptance result by optimizing an arbitrary high-call accessor. + +## 9. Reproduction and verification + +Run from the parked worktree with its environment. Set `TEMP` to an existing +scratch directory; corpora and workers are disposable. For each corpus, use: + +```powershell +uv run python docs/reports/2026-10-10/service_cost_probe.py --corpus large --rounds 3 --repeat 3 --tree 'parked=C:\Sources\pyRAML' --tree 'simple=C:\Sources\pyRAML\.claude\worktrees\service-source-simple' --tree 'no-capture=C:\Sources\pyRAML' --output large.json +``` + +Replace `large` with `hover` or `endpoints`. Add `--detail` for the separate coarse +timers, `--scale 0.5` for the half-size observation, `--ownership --rounds 1` for +allocation attribution, or `--rss-edits 10 --rounds 1` for the untraced retention +probe. `--mixed` selects the tested three-version sequence; `--source-first` changes +its request order. Summarize raw artifacts with `--summarize FILE... --output FILE`. + +Standard benchmark commands: + +```powershell +uv run python -m bench ab fb0fb73 --bench service-session --bench service-source-first --config unwrap --rounds 3 --repeat 3 +uv run python -m bench linearity --bench service-session --bench service-source-first --repeat 3 +uv run python -m bench ab fb0fb73 --bench large --bench endpoints --config parse --config unwrap+validate --config unwrap+lint --rounds 3 --repeat 3 +``` + +The Windows gate passed after adding the scenarios and reach tests: Ruff, +formatting, strict mypy and pytest, **6,202 passed, 48 skipped, 1 xfailed**. +Production parser/service behavior was not changed by this investigation. + +Remaining evidence limits: no observed client trace, multi-root or dependency-edit +timing, join/postparse full-value workload, or Linux timing/RSS measurements. The +capture ablation's query assertions cover these corpora, not the full public API, +TCK and error-recovery contract under a permanent option change. Noise limits +individual phase/query comparisons; no phase minima are summed to manufacture +an end-to-end result. diff --git a/docs/reports/2026-10-10/service-baseline-integration.md b/docs/reports/2026-10-10/service-baseline-integration.md new file mode 100644 index 0000000..37e0d7a --- /dev/null +++ b/docs/reports/2026-10-10/service-baseline-integration.md @@ -0,0 +1,43 @@ +# Service baseline integration record + +Date: 2026-10-10. Execution of the +[accepted recovery plan](../../research/2026-10-10/service-recovery-plan.md). + +## 1. Measurement-only control + +Base: `master` / `origin/master` at `2b6502e`. Branch: +`test/service-workload-baseline`. The measurement work is preserved on the parked +branch in `7c954ba`; this port includes its two mixed scenarios, reach tests, +profiling guidance, plan and dated evidence, without parked production changes. + +The portable driver uses each revision's service interface. On original master, +folding and selection call the text-based query functions, as its LSP adapter +does. On newer revisions they use the workspace's source cache. Repeated outlines +are exercised whether or not a revision caches them; cache identity is checked +only where the implementation provides it. The suite has 32 workloads here, +not the parked implementation's 40. + +Windows gate: Ruff, formatting, strict mypy and pytest pass, with **5,810 passed, +67 skipped, 1 xfailed**. The TCK is initialized at the original pinned submodule +revision. Local Docker's Linux engine is unavailable; GitHub CI must supply the +Linux/security verification before integration. + +Linearity, three repeats, actual `unwrap` mixed scenarios: + +| Workload | Full / half ms | Normalized time | Peak | Retained | +|---|---:|---:|---:|---:| +| `service-session` | 2,952.0 / 1,486.6 | 0.993 | 0.991 | 0.997 | +| `service-source-first` | 3,147.7 / 1,596.9 | 0.986 | 0.991 | 0.997 | + +This verifies the historical uncached behavior is reached and scales. It does +not establish acceptance of the simplified production candidate. + +## 2. Simplified candidate + +Pending comparison against the measurement-only master control and inspection +of first-use/source ownership costs. Starting candidate: `fb0fb73`. + +## 3. Selective recovery + +Pending acceptance of the production baseline. The first planned experiment is +model-backed declaration inlays and `type_written`, without record capture. diff --git a/docs/reports/2026-10-10/service-cost-metrics.json b/docs/reports/2026-10-10/service-cost-metrics.json new file mode 100644 index 0000000..62e9bad --- /dev/null +++ b/docs/reports/2026-10-10/service-cost-metrics.json @@ -0,0 +1,2294 @@ +{ + "runs": [ + { + "corpus": "large", + "scale": 1, + "inputs": { + "files": 152, + "bytes": 576354, + "entry_bytes": 3930, + "focus_bytes": 3850 + }, + "detail_timers": false, + "mixed": false, + "source_first": false, + "trees": { + "parked": { + "best_total_ms": 634.6212001517415, + "round_spread_percent": 13.216608583855471, + "same_best_run_phases_ms": { + "change": 1.3299002312123775, + "collect": 47.808799892663956, + "snapshot": 529.8775001429021, + "diagnostics": 0.0010998919606208801, + "occurrences": 55.603899993002415, + "total": 634.6212001517415 + }, + "phase_minima_ms": { + "change": 1.3063997030258179, + "collect": 43.995299842208624, + "snapshot": 529.8775001429021, + "diagnostics": 0.0010998919606208801, + "occurrences": 53.35110006853938, + "total": 634.6212001517415 + }, + "query_minima_ms": { + "outline.first": 5.758000072091818, + "lenses.first": 4.106900189071894, + "inlays.first": 54.97639998793602, + "folding.first": 0.21269964054226875, + "hover.first_after_auto": 0.04220008850097656, + "outline.warm": 0.0005997717380523682, + "inlays.warm": 0.03679981455206871, + "folding.warm": 0.1647002063691616, + "hover.warm": 0.018800143152475357 + }, + "memory_MB": { + "snapshot": { + "kept": 28.708184, + "peak": 30.066179 + }, + "diagnostics_occurrences": { + "kept": 36.852064, + "peak": 43.966658 + }, + "automatic_queries": { + "kept": 46.382107, + "peak": 47.53487 + }, + "sparse_hovers": { + "kept": 46.382443, + "peak": 47.53487 + } + }, + "dimensions": { + "read_files": 152, + "shapes": 19904, + "endpoints": 0, + "captured_files": 152, + "retained_source_records": 69730 + }, + "counts": { + "v1.snapshot.yaml_composes": 152, + "v2.snapshot.yaml_composes": 152, + "v3.snapshot.yaml_composes": 152 + }, + "identical_input_composition_histogram": { + "1": 3, + "3": 151 + } + }, + "simple": { + "best_total_ms": 472.30949997901917, + "round_spread_percent": 2.035423800944436, + "same_best_run_phases_ms": { + "change": 1.2742001563310623, + "collect": 26.03259962052107, + "snapshot": 393.386899959296, + "diagnostics": 0.000800006091594696, + "occurrences": 51.61500023677945, + "total": 472.30949997901917 + }, + "phase_minima_ms": { + "change": 1.2314002960920334, + "collect": 22.88420032709837, + "snapshot": 393.386899959296, + "diagnostics": 0.000800006091594696, + "occurrences": 51.61500023677945, + "total": 472.30949997901917 + }, + "query_minima_ms": { + "outline.first": 0.9539001621305943, + "lenses.first": 4.111399874091148, + "inlays.first": 57.2048001922667, + "folding.first": 1.0965997353196144, + "hover.first_after_auto": 0.01090019941329956, + "outline.warm": 0.9314003400504589, + "inlays.warm": 0.038699712604284286, + "folding.warm": 0.14829961583018303, + "hover.warm": 0.006600283086299896 + }, + "memory_MB": { + "snapshot": { + "kept": 20.182052, + "peak": 21.111702 + }, + "diagnostics_occurrences": { + "kept": 28.325932, + "peak": 35.440526 + }, + "automatic_queries": { + "kept": 36.28834, + "peak": 37.364272 + }, + "sparse_hovers": { + "kept": 36.288772, + "peak": 37.364272 + } + }, + "dimensions": { + "read_files": 152, + "shapes": 19904, + "endpoints": 0, + "captured_files": 0, + "retained_source_records": 0 + }, + "counts": { + "v1.snapshot.yaml_composes": 152, + "v1.automatic.yaml_composes": 2, + "v2.snapshot.yaml_composes": 152, + "v2.automatic.yaml_composes": 1, + "v3.snapshot.yaml_composes": 152, + "v3.automatic.yaml_composes": 1 + }, + "identical_input_composition_histogram": { + "1": 3, + "7": 1, + "3": 150 + } + }, + "no-capture": { + "best_total_ms": 467.7263000048697, + "round_spread_percent": 10.924059672533314, + "same_best_run_phases_ms": { + "change": 1.2632999569177628, + "collect": 24.41939990967512, + "snapshot": 390.540299937129, + "diagnostics": 0.001200009137392044, + "occurrences": 51.5021001920104, + "total": 467.7263000048697 + }, + "phase_minima_ms": { + "change": 1.2632999569177628, + "collect": 24.41939990967512, + "snapshot": 390.540299937129, + "diagnostics": 0.00090012326836586, + "occurrences": 51.5021001920104, + "total": 467.7263000048697 + }, + "query_minima_ms": { + "outline.first": 7.072399836033583, + "lenses.first": 4.118700046092272, + "inlays.first": 54.91819977760315, + "folding.first": 0.22660009562969208, + "hover.first_after_auto": 0.033999793231487274, + "outline.warm": 0.000500120222568512, + "inlays.warm": 0.0362996943295002, + "folding.warm": 0.16279984265565872, + "hover.warm": 0.019399914890527725 + }, + "memory_MB": { + "snapshot": { + "kept": 20.46131, + "peak": 21.399014 + }, + "diagnostics_occurrences": { + "kept": 28.60519, + "peak": 35.719784 + }, + "automatic_queries": { + "kept": 38.268042, + "peak": 39.421716 + }, + "sparse_hovers": { + "kept": 38.268474, + "peak": 39.421716 + } + }, + "dimensions": { + "read_files": 152, + "shapes": 19904, + "endpoints": 0, + "captured_files": 0, + "retained_source_records": 0 + }, + "counts": { + "v1.snapshot.yaml_composes": 152, + "v1.automatic.yaml_composes": 1, + "v2.snapshot.yaml_composes": 152, + "v2.automatic.yaml_composes": 1, + "v3.snapshot.yaml_composes": 152, + "v3.automatic.yaml_composes": 1 + }, + "identical_input_composition_histogram": { + "1": 3, + "6": 1, + "3": 150 + } + } + } + }, + { + "corpus": "large", + "scale": 1, + "inputs": { + "files": 152, + "bytes": 576354, + "entry_bytes": 3930, + "focus_bytes": 3850 + }, + "detail_timers": true, + "mixed": false, + "source_first": false, + "trees": { + "parked": { + "best_total_ms": 627.3258002474904, + "round_spread_percent": 3.4982778418972016, + "same_best_run_phases_ms": { + "yaml.native": 93.38799910619855, + "compose.total": 183.8019979186356, + "stage.decoded": 314.75860020145774, + "stage.endpoints": 0.004100147634744644, + "stage.security": 0.0038999132812023163, + "stage.resolved": 56.506600230932236, + "_detach_type_expressions": 12.489200104027987, + "stage.annotations": 0.006200280040502548, + "stage.unwrapped": 26.5347003005445, + "stage.validated": 46.37870006263256, + "finish_projections": 74.59470024332404, + "finish_sources": 0.0011003576219081879, + "change": 1.283199992030859, + "collect": 41.11859994009137, + "snapshot": 531.4774001017213, + "diagnostics": 0.001200009137392044, + "occurrences": 53.4454002045095, + "total": 627.3258002474904 + }, + "phase_minima_ms": { + "yaml.native": 92.09150122478604, + "compose.total": 182.719302829355, + "stage.decoded": 312.23920034244657, + "stage.endpoints": 0.003200024366378784, + "stage.security": 0.0038999132812023163, + "stage.resolved": 55.92169985175133, + "_detach_type_expressions": 11.878999881446362, + "stage.annotations": 0.004699919372797012, + "stage.unwrapped": 25.937399826943874, + "stage.validated": 45.30239989981055, + "finish_projections": 74.59470024332404, + "finish_sources": 0.0010998919606208801, + "change": 1.2257001362740993, + "collect": 41.11859994009137, + "snapshot": 531.2709999270737, + "diagnostics": 0.00090012326836586, + "occurrences": 52.289499901235104, + "total": 627.3258002474904 + }, + "query_minima_ms": { + "outline.first": 5.434900056570768, + "lenses.first": 4.1000996716320515, + "inlays.first": 54.346200078725815, + "folding.first": 0.19980035722255707, + "hover.first_after_auto": 0.034300144761800766, + "outline.warm": 0.000400003045797348, + "inlays.warm": 0.036300159990787506, + "folding.warm": 0.16169995069503784, + "hover.warm": 0.01859990879893303 + }, + "memory_MB": { + "snapshot": { + "kept": 28.708351, + "peak": 30.066346 + }, + "diagnostics_occurrences": { + "kept": 36.852231, + "peak": 43.966825 + }, + "automatic_queries": { + "kept": 46.382168, + "peak": 47.534931 + }, + "sparse_hovers": { + "kept": 46.382504, + "peak": 47.534931 + } + }, + "dimensions": { + "read_files": 152, + "shapes": 19904, + "endpoints": 0, + "captured_files": 152, + "retained_source_records": 69730 + }, + "counts": { + "v1.snapshot.yaml_composes": 152, + "v1.snapshot.converted_nodes": 69730, + "v1.snapshot._RecordNode.kind": 145999, + "v1.snapshot.SourceRecord.line": 18759, + "v1.automatic.SourceRecord.line": 25, + "v1.warm.SourceRecord.line": 1, + "v2.snapshot.yaml_composes": 152, + "v2.snapshot.converted_nodes": 69730, + "v2.snapshot._RecordNode.kind": 145999, + "v2.snapshot.SourceRecord.line": 18759, + "v2.automatic.SourceRecord.line": 25, + "v2.warm.SourceRecord.line": 1, + "v3.snapshot.yaml_composes": 152, + "v3.snapshot.converted_nodes": 69730, + "v3.snapshot._RecordNode.kind": 145999, + "v3.snapshot.SourceRecord.line": 18759, + "v3.automatic.SourceRecord.line": 25, + "v3.warm.SourceRecord.line": 1 + }, + "identical_input_composition_histogram": { + "1": 3, + "3": 151 + } + }, + "simple": { + "best_total_ms": 471.27189999446273, + "round_spread_percent": 2.9177212548346043, + "same_best_run_phases_ms": { + "yaml.native": 75.76669892296195, + "compose.total": 118.00889950245619, + "stage.decoded": 252.10190005600452, + "stage.endpoints": 0.00470038503408432, + "stage.security": 0.004299916326999664, + "stage.resolved": 57.5292999856174, + "_detach_type_expressions": 9.139199741184711, + "stage.annotations": 0.00509992241859436, + "stage.unwrapped": 25.946199893951416, + "stage.validated": 45.316200237721205, + "change": 1.4454000629484653, + "collect": 24.227200075984, + "snapshot": 390.20719984546304, + "diagnostics": 0.001300126314163208, + "occurrences": 55.39079988375306, + "total": 471.27189999446273 + }, + "phase_minima_ms": { + "yaml.native": 75.76669892296195, + "compose.total": 118.00889950245619, + "stage.decoded": 252.10190005600452, + "stage.endpoints": 0.0038999132812023163, + "stage.security": 0.00400003045797348, + "stage.resolved": 57.5292999856174, + "_detach_type_expressions": 9.063999634236097, + "stage.annotations": 0.00509992241859436, + "stage.unwrapped": 25.946199893951416, + "stage.validated": 45.316200237721205, + "change": 1.2210998684167862, + "collect": 23.391699884086847, + "snapshot": 390.20719984546304, + "diagnostics": 0.001000240445137024, + "occurrences": 52.20529995858669, + "total": 471.27189999446273 + }, + "query_minima_ms": { + "outline.first": 0.9540002793073654, + "lenses.first": 4.1924999095499516, + "inlays.first": 57.170200161635876, + "folding.first": 1.064700074493885, + "hover.first_after_auto": 0.010699965059757233, + "outline.warm": 0.9236996993422508, + "inlays.warm": 0.0379001721739769, + "folding.warm": 0.14590006321668625, + "hover.warm": 0.006299931555986404 + }, + "memory_MB": { + "snapshot": { + "kept": 20.181653, + "peak": 21.111312 + }, + "diagnostics_occurrences": { + "kept": 28.325533, + "peak": 35.440127 + }, + "automatic_queries": { + "kept": 36.288059, + "peak": 37.363991 + }, + "sparse_hovers": { + "kept": 36.288444, + "peak": 37.363991 + } + }, + "dimensions": { + "read_files": 152, + "shapes": 19904, + "endpoints": 0, + "captured_files": 0, + "retained_source_records": 0 + }, + "counts": { + "v1.snapshot.yaml_composes": 152, + "v1.snapshot.converted_nodes": 69730, + "v1.automatic.yaml_composes": 2, + "v1.automatic.converted_nodes": 934, + "v2.snapshot.yaml_composes": 152, + "v2.snapshot.converted_nodes": 69730, + "v2.automatic.yaml_composes": 1, + "v2.automatic.converted_nodes": 467, + "v3.snapshot.yaml_composes": 152, + "v3.snapshot.converted_nodes": 69730, + "v3.automatic.yaml_composes": 1, + "v3.automatic.converted_nodes": 467 + }, + "identical_input_composition_histogram": { + "1": 3, + "7": 1, + "3": 150 + } + }, + "no-capture": { + "best_total_ms": 476.8725000321865, + "round_spread_percent": 0.4324006665224811, + "same_best_run_phases_ms": { + "yaml.native": 75.62150247395039, + "compose.total": 119.75390091538429, + "stage.decoded": 260.3138000704348, + "stage.endpoints": 0.0037997961044311523, + "stage.security": 0.004400033503770828, + "stage.resolved": 56.675899773836136, + "_detach_type_expressions": 11.248299852013588, + "stage.annotations": 0.004400033503770828, + "stage.unwrapped": 25.782099924981594, + "stage.validated": 45.157100073993206, + "finish_sources": 0.0012996606528759003, + "change": 1.212799921631813, + "collect": 25.09720018133521, + "snapshot": 399.3730000220239, + "diagnostics": 0.0010998919606208801, + "occurrences": 51.18840001523495, + "total": 476.8725000321865 + }, + "phase_minima_ms": { + "yaml.native": 75.30749961733818, + "compose.total": 119.44590229541063, + "stage.decoded": 255.17160026356578, + "stage.endpoints": 0.0034999102354049683, + "stage.security": 0.003600027412176132, + "stage.resolved": 56.011800188571215, + "_detach_type_expressions": 10.89440006762743, + "stage.annotations": 0.004400033503770828, + "stage.unwrapped": 25.782099924981594, + "stage.validated": 44.31790020316839, + "finish_sources": 0.0012996606528759003, + "change": 1.2074001133441925, + "collect": 24.006799794733524, + "snapshot": 397.26899983361363, + "diagnostics": 0.00090012326836586, + "occurrences": 51.18840001523495, + "total": 476.8725000321865 + }, + "query_minima_ms": { + "outline.first": 6.886400282382965, + "lenses.first": 3.87750007212162, + "inlays.first": 54.940300062298775, + "folding.first": 0.22780010476708412, + "hover.first_after_auto": 0.03439979627728462, + "outline.warm": 0.0004996545612812042, + "inlays.warm": 0.036199577152729034, + "folding.warm": 0.16359984874725342, + "hover.warm": 0.018500257283449173 + }, + "memory_MB": { + "snapshot": { + "kept": 20.460405, + "peak": 21.398109 + }, + "diagnostics_occurrences": { + "kept": 28.604285, + "peak": 35.718879 + }, + "automatic_queries": { + "kept": 38.26709, + "peak": 39.420717 + }, + "sparse_hovers": { + "kept": 38.267522, + "peak": 39.420717 + } + }, + "dimensions": { + "read_files": 152, + "shapes": 19904, + "endpoints": 0, + "captured_files": 0, + "retained_source_records": 0 + }, + "counts": { + "v1.snapshot.yaml_composes": 152, + "v1.snapshot.converted_nodes": 69730, + "v1.automatic.yaml_composes": 1, + "v1.automatic.converted_nodes": 467, + "v1.automatic.SourceRecord.line": 151, + "v1.warm.SourceRecord.line": 1, + "v2.snapshot.yaml_composes": 152, + "v2.snapshot.converted_nodes": 69730, + "v2.automatic.yaml_composes": 1, + "v2.automatic.converted_nodes": 467, + "v2.automatic.SourceRecord.line": 151, + "v3.snapshot.yaml_composes": 152, + "v3.snapshot.converted_nodes": 69730, + "v3.automatic.yaml_composes": 1, + "v3.automatic.converted_nodes": 467, + "v3.automatic.SourceRecord.line": 151 + }, + "identical_input_composition_histogram": { + "1": 3, + "6": 1, + "3": 150 + } + } + } + }, + { + "corpus": "hover", + "scale": 1, + "inputs": { + "files": 1, + "bytes": 342406, + "entry_bytes": 342406, + "focus_bytes": 342406 + }, + "detail_timers": false, + "mixed": false, + "source_first": false, + "trees": { + "parked": { + "best_total_ms": 277.91749965399504, + "round_spread_percent": 7.30666497486494, + "same_best_run_phases_ms": { + "change": 0.42819976806640625, + "collect": 21.253500133752823, + "snapshot": 238.00309980288148, + "diagnostics": 0.00090012326836586, + "occurrences": 18.231799826025963, + "total": 277.91749965399504 + }, + "phase_minima_ms": { + "change": 0.42010005563497543, + "collect": 21.217599976807833, + "snapshot": 238.00309980288148, + "diagnostics": 0.00090012326836586, + "occurrences": 18.149499781429768, + "total": 277.91749965399504 + }, + "query_minima_ms": { + "outline.first": 131.03230018168688, + "lenses.first": 1.271899789571762, + "inlays.first": 47.89610020816326, + "folding.first": 14.87339986488223, + "hover.first_after_auto": 0.04369998350739479, + "outline.warm": 0.0004996545612812042, + "inlays.warm": 0.08850032463669777, + "folding.warm": 13.625599909573793, + "hover.warm": 0.029399991035461426 + }, + "memory_MB": { + "snapshot": { + "kept": 14.612644, + "peak": 22.788319 + }, + "diagnostics_occurrences": { + "kept": 17.234702, + "peak": 22.788319 + }, + "automatic_queries": { + "kept": 28.873667, + "peak": 30.057235 + }, + "sparse_hovers": { + "kept": 28.87391, + "peak": 30.057235 + } + }, + "dimensions": { + "read_files": 1, + "shapes": 7200, + "endpoints": 400, + "captured_files": 1, + "retained_source_records": 35209 + }, + "counts": { + "v1.snapshot.yaml_composes": 1, + "v1.snapshot.converted_nodes": 35209, + "v1.snapshot._RecordNode.kind": 58810, + "v1.snapshot.SourceRecord.line": 14804, + "v1.automatic.SourceRecord.line": 2803, + "v1.warm.SourceRecord.line": 1, + "v2.snapshot.yaml_composes": 1, + "v2.snapshot.converted_nodes": 35209, + "v2.snapshot._RecordNode.kind": 58810, + "v2.snapshot.SourceRecord.line": 14804, + "v2.automatic.SourceRecord.line": 2803, + "v2.warm.SourceRecord.line": 1, + "v3.snapshot.yaml_composes": 1, + "v3.snapshot.converted_nodes": 35209, + "v3.snapshot._RecordNode.kind": 58810, + "v3.snapshot.SourceRecord.line": 14804, + "v3.automatic.SourceRecord.line": 2803, + "v3.warm.SourceRecord.line": 1 + }, + "identical_input_composition_histogram": { + "1": 3 + } + }, + "simple": { + "best_total_ms": 193.58960026875138, + "round_spread_percent": 10.601085774074592, + "same_best_run_phases_ms": { + "change": 0.39560021832585335, + "collect": 12.459699995815754, + "snapshot": 163.28029986470938, + "diagnostics": 0.00090012326836586, + "occurrences": 17.453100066632032, + "total": 193.58960026875138 + }, + "phase_minima_ms": { + "change": 0.39560021832585335, + "collect": 12.054399587213993, + "snapshot": 163.28029986470938, + "diagnostics": 0.000800006091594696, + "occurrences": 17.252400051802397, + "total": 193.58960026875138 + }, + "query_minima_ms": { + "outline.first": 29.77130003273487, + "lenses.first": 1.3278997503221035, + "inlays.first": 154.34050001204014, + "folding.first": 80.79739985987544, + "hover.first_after_auto": 0.014400109648704529, + "outline.warm": 30.306499917060137, + "inlays.warm": 0.0934000127017498, + "folding.warm": 13.365899678319693, + "hover.warm": 0.011000316590070724 + }, + "memory_MB": { + "snapshot": { + "kept": 10.396426, + "peak": 18.069846 + }, + "diagnostics_occurrences": { + "kept": 13.018531, + "peak": 18.069846 + }, + "automatic_queries": { + "kept": 33.944268, + "peak": 45.598476 + }, + "sparse_hovers": { + "kept": 33.9447, + "peak": 45.598476 + } + }, + "dimensions": { + "read_files": 1, + "shapes": 7200, + "endpoints": 400, + "captured_files": 0, + "retained_source_records": 0 + }, + "counts": { + "v1.snapshot.yaml_composes": 1, + "v1.snapshot.converted_nodes": 35209, + "v1.automatic.yaml_composes": 2, + "v1.automatic.converted_nodes": 70418, + "v2.snapshot.yaml_composes": 1, + "v2.snapshot.converted_nodes": 35209, + "v2.automatic.yaml_composes": 2, + "v2.automatic.converted_nodes": 70418, + "v3.snapshot.yaml_composes": 1, + "v3.snapshot.converted_nodes": 35209, + "v3.automatic.yaml_composes": 2, + "v3.automatic.converted_nodes": 70418 + }, + "identical_input_composition_histogram": { + "3": 3 + } + }, + "no-capture": { + "best_total_ms": 196.6083999723196, + "round_spread_percent": 1.8237775935275868, + "same_best_run_phases_ms": { + "change": 0.40869973599910736, + "collect": 12.647700030356646, + "snapshot": 165.75360018759966, + "diagnostics": 0.0009997747838497162, + "occurrences": 17.79740024358034, + "total": 196.6083999723196 + }, + "phase_minima_ms": { + "change": 0.40269969031214714, + "collect": 12.647700030356646, + "snapshot": 165.75360018759966, + "diagnostics": 0.0005997717380523682, + "occurrences": 17.732299864292145, + "total": 196.6083999723196 + }, + "query_minima_ms": { + "outline.first": 265.23760007694364, + "lenses.first": 1.5432997606694698, + "inlays.first": 47.342800069600344, + "folding.first": 15.35779982805252, + "hover.first_after_auto": 0.040899962186813354, + "outline.warm": 0.000400003045797348, + "inlays.warm": 0.08809985592961311, + "folding.warm": 13.377899769693613, + "hover.warm": 0.031000003218650818 + }, + "memory_MB": { + "snapshot": { + "kept": 10.159452, + "peak": 18.070441 + }, + "diagnostics_occurrences": { + "kept": 12.78151, + "peak": 18.070441 + }, + "automatic_queries": { + "kept": 31.648124, + "peak": 32.831692 + }, + "sparse_hovers": { + "kept": 31.648451, + "peak": 32.831692 + } + }, + "dimensions": { + "read_files": 1, + "shapes": 7200, + "endpoints": 400, + "captured_files": 0, + "retained_source_records": 0 + }, + "counts": { + "v1.snapshot.yaml_composes": 1, + "v1.snapshot.converted_nodes": 35209, + "v1.automatic.yaml_composes": 1, + "v1.automatic.converted_nodes": 35209, + "v1.automatic.SourceRecord.line": 16007, + "v1.warm.SourceRecord.line": 1, + "v2.snapshot.yaml_composes": 1, + "v2.snapshot.converted_nodes": 35209, + "v2.automatic.yaml_composes": 1, + "v2.automatic.converted_nodes": 35209, + "v2.automatic.SourceRecord.line": 16007, + "v2.warm.SourceRecord.line": 1, + "v3.snapshot.yaml_composes": 1, + "v3.snapshot.converted_nodes": 35209, + "v3.automatic.yaml_composes": 1, + "v3.automatic.converted_nodes": 35209, + "v3.automatic.SourceRecord.line": 16007, + "v3.warm.SourceRecord.line": 1 + }, + "identical_input_composition_histogram": { + "2": 3 + } + } + } + }, + { + "corpus": "hover", + "scale": 1, + "inputs": { + "files": 1, + "bytes": 342406, + "entry_bytes": 342406, + "focus_bytes": 342406 + }, + "detail_timers": true, + "mixed": false, + "source_first": false, + "trees": { + "parked": { + "best_total_ms": 280.57230031117797, + "round_spread_percent": 2.1271877828780594, + "same_best_run_phases_ms": { + "yaml.native": 35.822799894958735, + "compose.total": 88.89540005475283, + "stage.decoded": 132.30480020865798, + "stage.endpoints": 10.876100044697523, + "stage.security": 0.062400009483098984, + "stage.resolved": 24.897399824112654, + "_detach_type_expressions": 3.323799930512905, + "stage.annotations": 0.2616001293063164, + "stage.unwrapped": 9.878899902105331, + "stage.validated": 14.690199866890907, + "finish_projections": 44.02310028672218, + "finish_sources": 0.000800006091594696, + "change": 0.4696999676525593, + "collect": 21.143500227481127, + "snapshot": 240.61049986630678, + "diagnostics": 0.000800006091594696, + "occurrences": 18.347800243645906, + "total": 280.57230031117797 + }, + "phase_minima_ms": { + "yaml.native": 35.575099755078554, + "compose.total": 88.89540005475283, + "stage.decoded": 132.1292999200523, + "stage.endpoints": 10.765699669718742, + "stage.security": 0.062000006437301636, + "stage.resolved": 24.897399824112654, + "_detach_type_expressions": 3.323799930512905, + "stage.annotations": 0.2566003240644932, + "stage.unwrapped": 9.878899902105331, + "stage.validated": 14.423100277781487, + "finish_projections": 43.525599874556065, + "finish_sources": 0.000800006091594696, + "change": 0.4217000678181648, + "collect": 20.632700063288212, + "snapshot": 240.61049986630678, + "diagnostics": 0.000800006091594696, + "occurrences": 18.347800243645906, + "total": 280.57230031117797 + }, + "query_minima_ms": { + "outline.first": 132.0977001450956, + "outline.source_projection": 0.00400003045797348, + "outline.cursor_index": 27.05699997022748, + "outline.place_sections": 72.32529995962977, + "lenses.first": 1.28660025075078, + "inlays.first": 47.421999741345644, + "folding.first": 14.879399910569191, + "hover.first_after_auto": 0.04239985719323158, + "outline.warm": 0.000500120222568512, + "inlays.warm": 0.08689984679222107, + "folding.warm": 13.79489991813898, + "hover.warm": 0.03060000017285347 + }, + "memory_MB": { + "snapshot": { + "kept": 14.613607, + "peak": 22.788201 + }, + "diagnostics_occurrences": { + "kept": 17.235759, + "peak": 22.788201 + }, + "automatic_queries": { + "kept": 28.874618, + "peak": 30.058186 + }, + "sparse_hovers": { + "kept": 28.874814, + "peak": 30.058186 + } + }, + "dimensions": { + "read_files": 1, + "shapes": 7200, + "endpoints": 400, + "captured_files": 1, + "retained_source_records": 35209 + }, + "counts": { + "v1.snapshot.yaml_composes": 1, + "v1.snapshot.converted_nodes": 35209, + "v1.snapshot._RecordNode.kind": 58810, + "v1.snapshot.SourceRecord.line": 14804, + "v1.automatic.SourceRecord.line": 2803, + "v1.warm.SourceRecord.line": 1, + "v2.snapshot.yaml_composes": 1, + "v2.snapshot.converted_nodes": 35209, + "v2.snapshot._RecordNode.kind": 58810, + "v2.snapshot.SourceRecord.line": 14804, + "v2.automatic.SourceRecord.line": 2803, + "v2.warm.SourceRecord.line": 1, + "v3.snapshot.yaml_composes": 1, + "v3.snapshot.converted_nodes": 35209, + "v3.snapshot._RecordNode.kind": 58810, + "v3.snapshot.SourceRecord.line": 14804, + "v3.automatic.SourceRecord.line": 2803, + "v3.warm.SourceRecord.line": 1, + "broken.snapshot_composes": 1, + "broken.three_source_request_composes": 0 + }, + "identical_input_composition_histogram": { + "1": 3 + } + }, + "simple": { + "best_total_ms": 194.27810003980994, + "round_spread_percent": 1.2685422544880032, + "same_best_run_phases_ms": { + "yaml.native": 35.296100191771984, + "compose.total": 60.60710037127137, + "stage.decoded": 101.09549993649125, + "stage.endpoints": 9.703100193291903, + "stage.security": 0.05979975685477257, + "stage.resolved": 25.90239979326725, + "_detach_type_expressions": 2.68759997561574, + "stage.annotations": 0.2536000683903694, + "stage.unwrapped": 9.63230011984706, + "stage.validated": 14.64810036122799, + "change": 0.4459996707737446, + "collect": 12.186700012534857, + "snapshot": 164.15770025923848, + "diagnostics": 0.0004996545612812042, + "occurrences": 17.487200442701578, + "total": 194.27810003980994 + }, + "phase_minima_ms": { + "yaml.native": 35.13310011476278, + "compose.total": 60.60710037127137, + "stage.decoded": 101.09549993649125, + "stage.endpoints": 9.582499973475933, + "stage.security": 0.05979975685477257, + "stage.resolved": 25.460700038820505, + "_detach_type_expressions": 2.6749996468424797, + "stage.annotations": 0.2520997077226639, + "stage.unwrapped": 9.62470006197691, + "stage.validated": 14.57079965621233, + "change": 0.40349969640374184, + "collect": 11.854900047183037, + "snapshot": 164.15770025923848, + "diagnostics": 0.0004996545612812042, + "occurrences": 17.482499592006207, + "total": 194.27810003980994 + }, + "query_minima_ms": { + "outline.first": 30.28120007365942, + "lenses.first": 1.4224001206457615, + "inlays.first": 155.90299991890788, + "folding.first": 81.63930010050535, + "hover.first_after_auto": 0.014699995517730713, + "outline.warm": 30.620899982750416, + "inlays.warm": 0.0929003581404686, + "folding.warm": 13.526899740099907, + "hover.warm": 0.011199619621038437 + }, + "memory_MB": { + "snapshot": { + "kept": 10.383889, + "peak": 18.069905 + }, + "diagnostics_occurrences": { + "kept": 13.006041, + "peak": 18.069905 + }, + "automatic_queries": { + "kept": 33.932189, + "peak": 45.586397 + }, + "sparse_hovers": { + "kept": 33.932621, + "peak": 45.586397 + } + }, + "dimensions": { + "read_files": 1, + "shapes": 7200, + "endpoints": 400, + "captured_files": 0, + "retained_source_records": 0 + }, + "counts": { + "v1.snapshot.yaml_composes": 1, + "v1.snapshot.converted_nodes": 35209, + "v1.automatic.yaml_composes": 2, + "v1.automatic.converted_nodes": 70418, + "v2.snapshot.yaml_composes": 1, + "v2.snapshot.converted_nodes": 35209, + "v2.automatic.yaml_composes": 2, + "v2.automatic.converted_nodes": 70418, + "v3.snapshot.yaml_composes": 1, + "v3.snapshot.converted_nodes": 35209, + "v3.automatic.yaml_composes": 2, + "v3.automatic.converted_nodes": 70418, + "broken.snapshot_composes": 1, + "broken.three_source_request_composes": 3 + }, + "identical_input_composition_histogram": { + "3": 3 + } + }, + "no-capture": { + "best_total_ms": 194.37629962339997, + "round_spread_percent": 1.7514997636971952, + "same_best_run_phases_ms": { + "yaml.native": 35.13609990477562, + "compose.total": 61.41010019928217, + "stage.decoded": 101.72259993851185, + "stage.endpoints": 9.99179994687438, + "stage.security": 0.06230035796761513, + "stage.resolved": 24.330600164830685, + "_detach_type_expressions": 3.2163001596927643, + "stage.annotations": 0.24770013988018036, + "stage.unwrapped": 9.607200045138597, + "stage.validated": 14.469200279563665, + "finish_sources": 0.0009997747838497162, + "change": 0.4042000509798527, + "collect": 12.310999911278486, + "snapshot": 163.88759994879365, + "diagnostics": 0.000800006091594696, + "occurrences": 17.77269970625639, + "total": 194.37629962339997 + }, + "phase_minima_ms": { + "yaml.native": 35.13609990477562, + "compose.total": 61.41010019928217, + "stage.decoded": 101.72259993851185, + "stage.endpoints": 9.99179994687438, + "stage.security": 0.06199954077601433, + "stage.resolved": 24.330600164830685, + "_detach_type_expressions": 3.1546996906399727, + "stage.annotations": 0.24770013988018036, + "stage.unwrapped": 9.537499863654375, + "stage.validated": 14.450099784880877, + "finish_sources": 0.0008996576070785522, + "change": 0.39530033245682716, + "collect": 12.310999911278486, + "snapshot": 163.88759994879365, + "diagnostics": 0.000500120222568512, + "occurrences": 17.77269970625639, + "total": 194.37629962339997 + }, + "query_minima_ms": { + "outline.first": 266.91440027207136, + "outline.source_projection": 123.89730010181665, + "outline.cursor_index": 26.394799817353487, + "outline.place_sections": 84.12860007956624, + "lenses.first": 1.587399747222662, + "inlays.first": 47.25470021367073, + "folding.first": 15.314800199121237, + "hover.first_after_auto": 0.042700208723545074, + "outline.warm": 0.000500120222568512, + "inlays.warm": 0.08870009332895279, + "folding.warm": 13.517600018531084, + "hover.warm": 0.030499882996082306 + }, + "memory_MB": { + "snapshot": { + "kept": 10.159675, + "peak": 18.070382 + }, + "diagnostics_occurrences": { + "kept": 12.781686, + "peak": 18.070382 + }, + "automatic_queries": { + "kept": 31.648018, + "peak": 32.831586 + }, + "sparse_hovers": { + "kept": 31.648451, + "peak": 32.831586 + } + }, + "dimensions": { + "read_files": 1, + "shapes": 7200, + "endpoints": 400, + "captured_files": 0, + "retained_source_records": 0 + }, + "counts": { + "v1.snapshot.yaml_composes": 1, + "v1.snapshot.converted_nodes": 35209, + "v1.automatic.yaml_composes": 1, + "v1.automatic.converted_nodes": 35209, + "v1.automatic.SourceRecord.line": 16007, + "v1.warm.SourceRecord.line": 1, + "v2.snapshot.yaml_composes": 1, + "v2.snapshot.converted_nodes": 35209, + "v2.automatic.yaml_composes": 1, + "v2.automatic.converted_nodes": 35209, + "v2.automatic.SourceRecord.line": 16007, + "v2.warm.SourceRecord.line": 1, + "v3.snapshot.yaml_composes": 1, + "v3.snapshot.converted_nodes": 35209, + "v3.automatic.yaml_composes": 1, + "v3.automatic.converted_nodes": 35209, + "v3.automatic.SourceRecord.line": 16007, + "v3.warm.SourceRecord.line": 1, + "broken.snapshot_composes": 1, + "broken.three_source_request_composes": 1 + }, + "identical_input_composition_histogram": { + "2": 3 + } + } + } + }, + { + "corpus": "hover", + "scale": 0.5, + "inputs": { + "files": 1, + "bytes": 170406, + "entry_bytes": 170406, + "focus_bytes": 170406 + }, + "detail_timers": false, + "mixed": false, + "source_first": false, + "trees": { + "parked": { + "best_total_ms": 141.30990020930767, + "round_spread_percent": 0.26862918328469654, + "same_best_run_phases_ms": { + "change": 0.19379984587430954, + "collect": 11.892000213265419, + "snapshot": 119.53329993411899, + "diagnostics": 0.001000240445137024, + "occurrences": 9.689799975603819, + "total": 141.30990020930767 + }, + "phase_minima_ms": { + "change": 0.18950039520859718, + "collect": 11.142899747937918, + "snapshot": 119.28800027817488, + "diagnostics": 0.0006002373993396759, + "occurrences": 9.689799975603819, + "total": 141.30990020930767 + }, + "query_minima_ms": { + "outline.first": 65.10180002078414, + "lenses.first": 0.6070001982152462, + "inlays.first": 23.773000109940767, + "folding.first": 7.464099675416946, + "hover.first_after_auto": 0.03460003063082695, + "outline.warm": 0.000500120222568512, + "inlays.warm": 0.08569983765482903, + "folding.warm": 6.747399922460318, + "hover.warm": 0.026699621230363846 + }, + "memory_MB": { + "snapshot": { + "kept": 7.334108, + "peak": 11.440569 + }, + "diagnostics_occurrences": { + "kept": 8.644016, + "peak": 11.440569 + }, + "automatic_queries": { + "kept": 14.475282, + "peak": 14.988762 + }, + "sparse_hovers": { + "kept": 14.475572, + "peak": 14.988762 + } + }, + "dimensions": { + "read_files": 1, + "shapes": 3600, + "endpoints": 200, + "captured_files": 1, + "retained_source_records": 17609 + }, + "counts": { + "v1.snapshot.yaml_composes": 1, + "v1.snapshot.converted_nodes": 17609, + "v1.snapshot._RecordNode.kind": 29410, + "v1.snapshot.SourceRecord.line": 7404, + "v1.automatic.SourceRecord.line": 1403, + "v1.warm.SourceRecord.line": 1, + "v2.snapshot.yaml_composes": 1, + "v2.snapshot.converted_nodes": 17609, + "v2.snapshot._RecordNode.kind": 29410, + "v2.snapshot.SourceRecord.line": 7404, + "v2.automatic.SourceRecord.line": 1403, + "v2.warm.SourceRecord.line": 1, + "v3.snapshot.yaml_composes": 1, + "v3.snapshot.converted_nodes": 17609, + "v3.snapshot._RecordNode.kind": 29410, + "v3.snapshot.SourceRecord.line": 7404, + "v3.automatic.SourceRecord.line": 1403, + "v3.warm.SourceRecord.line": 1, + "broken.snapshot_composes": 1, + "broken.three_source_request_composes": 0 + }, + "identical_input_composition_histogram": { + "1": 3 + } + }, + "simple": { + "best_total_ms": 99.32140028104186, + "round_spread_percent": 1.756217347351563, + "same_best_run_phases_ms": { + "change": 0.22860011085867882, + "collect": 7.8742001205682755, + "snapshot": 82.2006999514997, + "diagnostics": 0.0006998889148235321, + "occurrences": 9.017200209200382, + "total": 99.32140028104186 + }, + "phase_minima_ms": { + "change": 0.1889001578092575, + "collect": 7.300700061023235, + "snapshot": 82.2006999514997, + "diagnostics": 0.0006998889148235321, + "occurrences": 9.017200209200382, + "total": 99.32140028104186 + }, + "query_minima_ms": { + "outline.first": 14.80219978839159, + "lenses.first": 0.6479998119175434, + "inlays.first": 76.14250015467405, + "folding.first": 39.78150011971593, + "hover.first_after_auto": 0.012699980288743973, + "outline.warm": 15.056999865919352, + "inlays.warm": 0.08979998528957367, + "folding.warm": 6.587800104171038, + "hover.warm": 0.008800067007541656 + }, + "memory_MB": { + "snapshot": { + "kept": 5.2153, + "peak": 9.080927 + }, + "diagnostics_occurrences": { + "kept": 6.525349, + "peak": 9.080927 + }, + "automatic_queries": { + "kept": 17.003386, + "peak": 22.92593 + }, + "sparse_hovers": { + "kept": 17.003818, + "peak": 22.92593 + } + }, + "dimensions": { + "read_files": 1, + "shapes": 3600, + "endpoints": 200, + "captured_files": 0, + "retained_source_records": 0 + }, + "counts": { + "v1.snapshot.yaml_composes": 1, + "v1.snapshot.converted_nodes": 17609, + "v1.automatic.yaml_composes": 2, + "v1.automatic.converted_nodes": 35218, + "v2.snapshot.yaml_composes": 1, + "v2.snapshot.converted_nodes": 17609, + "v2.automatic.yaml_composes": 2, + "v2.automatic.converted_nodes": 35218, + "v3.snapshot.yaml_composes": 1, + "v3.snapshot.converted_nodes": 17609, + "v3.automatic.yaml_composes": 2, + "v3.automatic.converted_nodes": 35218, + "broken.snapshot_composes": 1, + "broken.three_source_request_composes": 3 + }, + "identical_input_composition_histogram": { + "3": 3 + } + }, + "no-capture": { + "best_total_ms": 99.79270026087761, + "round_spread_percent": 0.16263686823665413, + "same_best_run_phases_ms": { + "change": 0.18509989604353905, + "collect": 7.316400296986103, + "snapshot": 83.23979983106256, + "diagnostics": 0.00090012326836586, + "occurrences": 9.050500113517046, + "total": 99.79270026087761 + }, + "phase_minima_ms": { + "change": 0.18509989604353905, + "collect": 7.233799900859594, + "snapshot": 83.23979983106256, + "diagnostics": 0.000500120222568512, + "occurrences": 9.037099778652191, + "total": 99.79270026087761 + }, + "query_minima_ms": { + "outline.first": 131.9022998213768, + "lenses.first": 0.6125001236796379, + "inlays.first": 23.741299752146006, + "folding.first": 7.802099920809269, + "hover.first_after_auto": 0.03350013867020607, + "outline.warm": 0.000500120222568512, + "inlays.warm": 0.08570030331611633, + "folding.warm": 6.709499750286341, + "hover.warm": 0.025399960577487946 + }, + "memory_MB": { + "snapshot": { + "kept": 5.10588, + "peak": 9.080991 + }, + "diagnostics_occurrences": { + "kept": 6.415835, + "peak": 9.080991 + }, + "automatic_queries": { + "kept": 15.857351, + "peak": 16.370831 + }, + "sparse_hovers": { + "kept": 15.857643, + "peak": 16.370831 + } + }, + "dimensions": { + "read_files": 1, + "shapes": 3600, + "endpoints": 200, + "captured_files": 0, + "retained_source_records": 0 + }, + "counts": { + "v1.snapshot.yaml_composes": 1, + "v1.snapshot.converted_nodes": 17609, + "v1.automatic.yaml_composes": 1, + "v1.automatic.converted_nodes": 17609, + "v1.automatic.SourceRecord.line": 8007, + "v1.warm.SourceRecord.line": 1, + "v2.snapshot.yaml_composes": 1, + "v2.snapshot.converted_nodes": 17609, + "v2.automatic.yaml_composes": 1, + "v2.automatic.converted_nodes": 17609, + "v2.automatic.SourceRecord.line": 8007, + "v2.warm.SourceRecord.line": 1, + "v3.snapshot.yaml_composes": 1, + "v3.snapshot.converted_nodes": 17609, + "v3.automatic.yaml_composes": 1, + "v3.automatic.converted_nodes": 17609, + "v3.automatic.SourceRecord.line": 8007, + "v3.warm.SourceRecord.line": 1, + "broken.snapshot_composes": 1, + "broken.three_source_request_composes": 1 + }, + "identical_input_composition_histogram": { + "2": 3 + } + } + } + }, + { + "corpus": "endpoints", + "scale": 1, + "inputs": { + "files": 1, + "bytes": 286801, + "entry_bytes": 286801, + "focus_bytes": 286801 + }, + "detail_timers": false, + "mixed": false, + "source_first": false, + "trees": { + "parked": { + "best_total_ms": 458.08029966428876, + "round_spread_percent": 0.8022611492155107, + "same_best_run_phases_ms": { + "change": 0.24799956008791924, + "collect": 30.983800068497658, + "snapshot": 407.37170027568936, + "diagnostics": 0.0010998919606208801, + "occurrences": 19.475699868053198, + "total": 458.08029966428876 + }, + "phase_minima_ms": { + "change": 0.24030031636357307, + "collect": 29.590199701488018, + "snapshot": 407.37170027568936, + "diagnostics": 0.0009997747838497162, + "occurrences": 18.61339993774891, + "total": 458.08029966428876 + }, + "query_minima_ms": { + "outline.first": 174.71429985016584, + "lenses.first": 0.005200039595365524, + "inlays.first": 56.05820007622242, + "folding.first": 17.607899848371744, + "hover.first_after_auto": 0.03969995304942131, + "outline.warm": 0.000500120222568512, + "inlays.warm": 3.651699982583523, + "folding.warm": 17.115100286900997, + "hover.warm": 0.027999747544527054 + }, + "memory_MB": { + "snapshot": { + "kept": 22.188802, + "peak": 31.716558 + }, + "diagnostics_occurrences": { + "kept": 23.607674, + "peak": 31.716558 + }, + "automatic_queries": { + "kept": 35.948945, + "peak": 37.355313 + }, + "sparse_hovers": { + "kept": 35.949782, + "peak": 37.355313 + } + }, + "dimensions": { + "read_files": 1, + "shapes": 12003, + "endpoints": 500, + "captured_files": 1, + "retained_source_records": 36057 + }, + "counts": { + "v1.snapshot.yaml_composes": 1, + "v1.snapshot.converted_nodes": 36057, + "v1.snapshot._RecordNode.kind": 172589, + "v1.snapshot.SourceRecord.line": 44031, + "v1.automatic.SourceRecord.line": 2006, + "v1.warm.SourceRecord.line": 1, + "v2.snapshot.yaml_composes": 1, + "v2.snapshot.converted_nodes": 36057, + "v2.snapshot._RecordNode.kind": 172589, + "v2.snapshot.SourceRecord.line": 44031, + "v2.automatic.SourceRecord.line": 2006, + "v2.warm.SourceRecord.line": 1, + "v3.snapshot.yaml_composes": 1, + "v3.snapshot.converted_nodes": 36057, + "v3.snapshot._RecordNode.kind": 172589, + "v3.snapshot.SourceRecord.line": 44031, + "v3.automatic.SourceRecord.line": 2006, + "v3.warm.SourceRecord.line": 1 + }, + "identical_input_composition_histogram": { + "1": 3 + } + }, + "simple": { + "best_total_ms": 336.0377000644803, + "round_spread_percent": 0.9158198718575816, + "same_best_run_phases_ms": { + "change": 0.27820002287626266, + "collect": 20.79940028488636, + "snapshot": 296.79219983518124, + "diagnostics": 0.0006998889148235321, + "occurrences": 18.167200032621622, + "total": 336.0377000644803 + }, + "phase_minima_ms": { + "change": 0.22960035130381584, + "collect": 19.949499983340502, + "snapshot": 296.79219983518124, + "diagnostics": 0.0006998889148235321, + "occurrences": 18.06390006095171, + "total": 336.0377000644803 + }, + "query_minima_ms": { + "outline.first": 52.72539984434843, + "lenses.first": 0.007600057870149612, + "inlays.first": 151.92680014297366, + "folding.first": 87.68170000985265, + "hover.first_after_auto": 0.014699995517730713, + "outline.warm": 53.32720000296831, + "inlays.warm": 3.6534001119434834, + "folding.warm": 20.515600219368935, + "hover.warm": 0.011200085282325745 + }, + "memory_MB": { + "snapshot": { + "kept": 17.592078, + "peak": 26.749222 + }, + "diagnostics_occurrences": { + "kept": 19.01095, + "peak": 26.749222 + }, + "automatic_queries": { + "kept": 38.931493, + "peak": 50.786768 + }, + "sparse_hovers": { + "kept": 38.931925, + "peak": 50.786768 + } + }, + "dimensions": { + "read_files": 1, + "shapes": 12003, + "endpoints": 500, + "captured_files": 0, + "retained_source_records": 0 + }, + "counts": { + "v1.snapshot.yaml_composes": 1, + "v1.snapshot.converted_nodes": 36057, + "v1.automatic.yaml_composes": 2, + "v1.automatic.converted_nodes": 72114, + "v2.snapshot.yaml_composes": 1, + "v2.snapshot.converted_nodes": 36057, + "v2.automatic.yaml_composes": 2, + "v2.automatic.converted_nodes": 72114, + "v3.snapshot.yaml_composes": 1, + "v3.snapshot.converted_nodes": 36057, + "v3.automatic.yaml_composes": 2, + "v3.automatic.converted_nodes": 72114 + }, + "identical_input_composition_histogram": { + "3": 3 + } + }, + "no-capture": { + "best_total_ms": 342.66440011560917, + "round_spread_percent": 0.6856563091804269, + "same_best_run_phases_ms": { + "change": 0.2373000606894493, + "collect": 20.324199926108122, + "snapshot": 303.53760020807385, + "diagnostics": 0.0013997778296470642, + "occurrences": 18.563900142908096, + "total": 342.66440011560917 + }, + "phase_minima_ms": { + "change": 0.23090001195669174, + "collect": 20.324199926108122, + "snapshot": 303.53760020807385, + "diagnostics": 0.000800006091594696, + "occurrences": 18.412900157272816, + "total": 342.66440011560917 + }, + "query_minima_ms": { + "outline.first": 311.4338000304997, + "lenses.first": 0.00490015372633934, + "inlays.first": 56.053000036627054, + "folding.first": 21.59000001847744, + "hover.first_after_auto": 0.03929995000362396, + "outline.warm": 0.0004996545612812042, + "inlays.warm": 3.6606001667678356, + "folding.warm": 18.293200060725212, + "hover.warm": 0.03190012648701668 + }, + "memory_MB": { + "snapshot": { + "kept": 17.433548, + "peak": 26.846059 + }, + "diagnostics_occurrences": { + "kept": 18.85242, + "peak": 26.846059 + }, + "automatic_queries": { + "kept": 38.949796, + "peak": 40.356276 + }, + "sparse_hovers": { + "kept": 38.950836, + "peak": 40.356276 + } + }, + "dimensions": { + "read_files": 1, + "shapes": 12003, + "endpoints": 500, + "captured_files": 0, + "retained_source_records": 0 + }, + "counts": { + "v1.snapshot.yaml_composes": 1, + "v1.snapshot.converted_nodes": 36057, + "v1.automatic.yaml_composes": 1, + "v1.automatic.converted_nodes": 36057, + "v1.automatic.SourceRecord.line": 23031, + "v1.warm.SourceRecord.line": 1, + "v2.snapshot.yaml_composes": 1, + "v2.snapshot.converted_nodes": 36057, + "v2.automatic.yaml_composes": 1, + "v2.automatic.converted_nodes": 36057, + "v2.automatic.SourceRecord.line": 23031, + "v2.warm.SourceRecord.line": 1, + "v3.snapshot.yaml_composes": 1, + "v3.snapshot.converted_nodes": 36057, + "v3.automatic.yaml_composes": 1, + "v3.automatic.converted_nodes": 36057, + "v3.automatic.SourceRecord.line": 23031, + "v3.warm.SourceRecord.line": 1 + }, + "identical_input_composition_histogram": { + "2": 3 + } + } + } + }, + { + "corpus": "hover", + "scale": 1, + "inputs": { + "files": 1, + "bytes": 342406, + "entry_bytes": 342406, + "focus_bytes": 342406 + }, + "detail_timers": false, + "mixed": true, + "source_first": false, + "trees": { + "parked": { + "mixed_best_ms": 1483.5411002859473, + "round_spread_percent": 0.46058043084251654, + "peak_MB": 30.066339, + "kept_MB": 28.880955 + }, + "simple": { + "mixed_best_ms": 1558.5537999868393, + "round_spread_percent": 2.024626940053942, + "peak_MB": 45.601847, + "kept_MB": 33.947808 + }, + "no-capture": { + "mixed_best_ms": 1652.0231999456882, + "round_spread_percent": 1.8709361997274687, + "peak_MB": 32.840348, + "kept_MB": 31.654756 + } + } + }, + { + "corpus": "hover", + "scale": 1, + "inputs": { + "files": 1, + "bytes": 342406, + "entry_bytes": 342406, + "focus_bytes": 342406 + }, + "detail_timers": false, + "mixed": true, + "source_first": true, + "trees": { + "parked": { + "mixed_best_ms": 1836.6552996449172, + "round_spread_percent": 1.7587078167967674, + "peak_MB": 30.184504, + "kept_MB": 28.88072 + }, + "simple": { + "mixed_best_ms": 1603.1725998036563, + "round_spread_percent": 0.6073145200725216, + "peak_MB": 39.947598, + "kept_MB": 33.943601 + }, + "no-capture": { + "mixed_best_ms": 2010.6466002762318, + "round_spread_percent": 3.588203890058228, + "peak_MB": 39.928876, + "kept_MB": 38.743284 + } + } + }, + { + "corpus": "large", + "scale": 1, + "inputs": { + "files": 152, + "bytes": 576354, + "entry_bytes": 3930, + "focus_bytes": 3850 + }, + "detail_timers": false, + "mixed": false, + "source_first": false, + "trees": { + "parked": [ + { + "baseline_rss_MB": 37.576704, + "retention": [ + { + "version": 1, + "automatic_queries": false, + "rss_MB": 82.771968, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 2, + "automatic_queries": false, + "rss_MB": 84.078592, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 3, + "automatic_queries": false, + "rss_MB": 84.353024, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 4, + "automatic_queries": false, + "rss_MB": 84.832256, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 5, + "automatic_queries": false, + "rss_MB": 84.803584, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 6, + "automatic_queries": true, + "rss_MB": 92.176384, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 7, + "automatic_queries": true, + "rss_MB": 90.738688, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 8, + "automatic_queries": true, + "rss_MB": 90.865664, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 9, + "automatic_queries": true, + "rss_MB": 91.230208, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 10, + "automatic_queries": true, + "rss_MB": 93.589504, + "live_Raml": 2, + "snapshots": 1 + } + ] + } + ], + "simple": [ + { + "baseline_rss_MB": 36.978688, + "retention": [ + { + "version": 1, + "automatic_queries": false, + "rss_MB": 74.694656, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 2, + "automatic_queries": false, + "rss_MB": 74.395648, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 3, + "automatic_queries": false, + "rss_MB": 76.423168, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 4, + "automatic_queries": false, + "rss_MB": 74.428416, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 5, + "automatic_queries": false, + "rss_MB": 77.225984, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 6, + "automatic_queries": true, + "rss_MB": 82.341888, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 7, + "automatic_queries": true, + "rss_MB": 82.681856, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 8, + "automatic_queries": true, + "rss_MB": 79.62624, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 9, + "automatic_queries": true, + "rss_MB": 82.624512, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 10, + "automatic_queries": true, + "rss_MB": 83.161088, + "live_Raml": 2, + "snapshots": 1 + } + ] + } + ], + "no-capture": [ + { + "baseline_rss_MB": 37.494784, + "retention": [ + { + "version": 1, + "automatic_queries": false, + "rss_MB": 75.42784, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 2, + "automatic_queries": false, + "rss_MB": 77.4144, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 3, + "automatic_queries": false, + "rss_MB": 75.534336, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 4, + "automatic_queries": false, + "rss_MB": 76.214272, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 5, + "automatic_queries": false, + "rss_MB": 74.907648, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 6, + "automatic_queries": true, + "rss_MB": 84.123648, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 7, + "automatic_queries": true, + "rss_MB": 83.423232, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 8, + "automatic_queries": true, + "rss_MB": 83.8656, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 9, + "automatic_queries": true, + "rss_MB": 84.185088, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 10, + "automatic_queries": true, + "rss_MB": 83.8656, + "live_Raml": 2, + "snapshots": 1 + } + ] + } + ] + } + }, + { + "corpus": "hover", + "scale": 1, + "inputs": { + "files": 1, + "bytes": 342406, + "entry_bytes": 342406, + "focus_bytes": 342406 + }, + "detail_timers": false, + "mixed": false, + "source_first": false, + "trees": { + "parked": [ + { + "baseline_rss_MB": 40.36608, + "baseline_live_Raml": 1, + "retention": [ + { + "version": 1, + "automatic_queries": false, + "rss_MB": 69.799936, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 2, + "automatic_queries": false, + "rss_MB": 69.574656, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 3, + "automatic_queries": false, + "rss_MB": 68.898816, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 4, + "automatic_queries": false, + "rss_MB": 70.385664, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 5, + "automatic_queries": false, + "rss_MB": 69.419008, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 6, + "automatic_queries": true, + "rss_MB": 77.68064, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 7, + "automatic_queries": true, + "rss_MB": 79.6672, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 8, + "automatic_queries": true, + "rss_MB": 80.105472, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 9, + "automatic_queries": true, + "rss_MB": 80.162816, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 10, + "automatic_queries": true, + "rss_MB": 78.938112, + "live_Raml": 2, + "snapshots": 1 + } + ] + } + ], + "simple": [ + { + "baseline_rss_MB": 39.968768, + "baseline_live_Raml": 1, + "retention": [ + { + "version": 1, + "automatic_queries": false, + "rss_MB": 60.862464, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 2, + "automatic_queries": false, + "rss_MB": 62.005248, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 3, + "automatic_queries": false, + "rss_MB": 63.20128, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 4, + "automatic_queries": false, + "rss_MB": 62.169088, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 5, + "automatic_queries": false, + "rss_MB": 60.08832, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 6, + "automatic_queries": true, + "rss_MB": 90.791936, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 7, + "automatic_queries": true, + "rss_MB": 87.67488, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 8, + "automatic_queries": true, + "rss_MB": 88.690688, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 9, + "automatic_queries": true, + "rss_MB": 89.68192, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 10, + "automatic_queries": true, + "rss_MB": 88.686592, + "live_Raml": 2, + "snapshots": 1 + } + ] + } + ], + "no-capture": [ + { + "baseline_rss_MB": 40.435712, + "baseline_live_Raml": 1, + "retention": [ + { + "version": 1, + "automatic_queries": false, + "rss_MB": 61.054976, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 2, + "automatic_queries": false, + "rss_MB": 61.902848, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 3, + "automatic_queries": false, + "rss_MB": 61.120512, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 4, + "automatic_queries": false, + "rss_MB": 61.927424, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 5, + "automatic_queries": false, + "rss_MB": 60.125184, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 6, + "automatic_queries": true, + "rss_MB": 82.853888, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 7, + "automatic_queries": true, + "rss_MB": 82.731008, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 8, + "automatic_queries": true, + "rss_MB": 82.522112, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 9, + "automatic_queries": true, + "rss_MB": 82.341888, + "live_Raml": 2, + "snapshots": 1 + }, + { + "version": 10, + "automatic_queries": true, + "rss_MB": 84.021248, + "live_Raml": 2, + "snapshots": 1 + } + ] + } + ] + } + }, + { + "corpus": "large", + "scale": 1, + "inputs": { + "files": 152, + "bytes": 576354, + "entry_bytes": 3930, + "focus_bytes": 3850 + }, + "detail_timers": false, + "mixed": false, + "source_first": false, + "trees": { + "parked": [ + { + "ownership_MB": { + "views/occurrences.py": 8.14352, + "types/shape.py": 7.412952, + "sourceprojection.py": 6.618072, + "sourcecapture.py": 6.1164, + "types/resolve.py": 2.0268, + "yamlnode.py": 1.787413, + "datanode.py": 1.54664, + "registry.py": 1.171312, + "parser/facets.py": 0.41832, + "types/base.py": 0.3864, + "types/inherit.py": 0.2496, + "types/unwrap.py": 0.211256, + "types/examples.py": 0.1848, + "parser/fragments.py": 0.117896, + "facet_names.py": 0.1025, + "uris.py": 0.074187, + "types/values.py": 0.0736, + "types/complex_.py": 0.0672, + "parser/references.py": 0.04935, + "parser/projections.py": 0.044608, + "views/lint/rules/__init__.py": 0.027128, + "views/lint/engine.py": 0.01412, + "views/lint/plugins.py": 0.010593, + "service/workspace.py": 0.009618, + "types/expressions/parser.py": 0.003776, + "types/expressions/lexer.py": 0.002439, + "other": 0.001448, + "loaders.py": 0.000554, + "parser/entry.py": 0.000432, + "views/lint/config.py": 0.000264, + "types/custom_facets.py": 4.8e-05, + "parser/uritemplates.py": 4.7e-05, + "parser/endpoints.py": 4.6e-05 + }, + "traced_kept_MB": 36.873339 + } + ], + "simple": [ + { + "ownership_MB": { + "views/occurrences.py": 8.14352, + "types/shape.py": 7.28252, + "yamlnode.py": 5.370904, + "types/resolve.py": 1.8708, + "datanode.py": 1.54664, + "registry.py": 1.167936, + "parser/entry.py": 0.955808, + "parser/facets.py": 0.41832, + "types/base.py": 0.3864, + "types/inherit.py": 0.2496, + "types/unwrap.py": 0.211256, + "types/examples.py": 0.1848, + "parser/fragments.py": 0.117896, + "facet_names.py": 0.1025, + "uris.py": 0.074187, + "types/values.py": 0.0736, + "types/complex_.py": 0.0672, + "parser/references.py": 0.04935, + "views/lint/rules/__init__.py": 0.027128, + "views/lint/engine.py": 0.01412, + "views/lint/plugins.py": 0.010467, + "service/workspace.py": 0.009434, + "types/expressions/parser.py": 0.003776, + "types/expressions/lexer.py": 0.001687, + "other": 0.001448, + "loaders.py": 0.000554, + "views/lint/config.py": 0.000264, + "types/custom_facets.py": 4.8e-05, + "parser/endpoints.py": 4.6e-05 + }, + "traced_kept_MB": 28.342209 + } + ], + "no-capture": [ + { + "ownership_MB": { + "views/occurrences.py": 8.14352, + "types/shape.py": 7.412952, + "yamlnode.py": 5.366933, + "types/resolve.py": 2.0268, + "datanode.py": 1.54664, + "registry.py": 1.168048, + "parser/entry.py": 0.955824, + "parser/facets.py": 0.41832, + "types/base.py": 0.3864, + "types/inherit.py": 0.2496, + "types/unwrap.py": 0.211256, + "types/examples.py": 0.1848, + "parser/fragments.py": 0.117896, + "facet_names.py": 0.1025, + "uris.py": 0.074187, + "types/values.py": 0.0736, + "types/complex_.py": 0.0672, + "parser/references.py": 0.04935, + "views/lint/rules/__init__.py": 0.027128, + "views/lint/engine.py": 0.01412, + "views/lint/plugins.py": 0.010534, + "service/workspace.py": 0.009586, + "types/expressions/parser.py": 0.003776, + "types/expressions/lexer.py": 0.002486, + "other": 0.001448, + "loaders.py": 0.000554, + "views/lint/config.py": 0.000264, + "types/custom_facets.py": 4.8e-05, + "parser/uritemplates.py": 4.7e-05, + "parser/endpoints.py": 4.6e-05 + }, + "traced_kept_MB": 28.625863 + } + ] + } + } + ] +} diff --git a/docs/reports/2026-10-10/service_cost_probe.py b/docs/reports/2026-10-10/service_cost_probe.py new file mode 100644 index 0000000..bed7889 --- /dev/null +++ b/docs/reports/2026-10-10/service_cost_probe.py @@ -0,0 +1,542 @@ +"""Reproducible phase/cache diagnostics; no production implementation changes. + +Run with the repository's Python. Each tree runs in a fresh subprocess, while +all trees share the corpus and this driver's scenario. The no-capture variant +only removes ProjectionRequest from the parked workspace's parse options. +""" + +from __future__ import annotations + +import argparse +import gc +import hashlib +import json +import platform +import runpy +import subprocess +import sys +import tempfile +import time +import tracemalloc +from collections import Counter, defaultdict +from contextlib import ExitStack, contextmanager, nullcontext +from dataclasses import replace +from pathlib import Path +from unittest.mock import patch + +REPOSITORY = Path(__file__).resolve().parents[3] +SCENARIO = REPOSITORY / 'bench/service_session.py' + + +@contextmanager +def variant(module, no_capture): + real = module.parse_lenient + + def parse(path, options): + return real(path, replace(options, projection=None) if no_capture else options) + + with patch.object(module, 'parse_lenient', parse): + yield + + +@contextmanager +def phase_timers(registry, entry, yamlnode, elapsed): + """Coarse nested timers, never per-node profiling.""" + real_stage = registry.Raml.stage + + @contextmanager + def stage(raml, which, **kwargs): + start = time.perf_counter() + try: + with real_stage(raml, which, **kwargs): + yield + finally: + elapsed['stage.' + which.value] += time.perf_counter() - start + + def timer(name, fn): + def measured(*args, **kwargs): + start = time.perf_counter() + try: + return fn(*args, **kwargs) + finally: + elapsed[name] += time.perf_counter() - start + return measured + + real_compose = yamlnode.compose + with ExitStack() as stack: + stack.enter_context(patch.object(registry.Raml, 'stage', stage)) + # Imported function aliases must all point at the same timer. + for module in tuple(sys.modules.values()): + if module is not None and getattr(module, '__name__', '').startswith('fastraml.'): + if getattr(module, 'compose', None) is real_compose: + stack.enter_context(patch.object(module, 'compose', timer('compose.total', real_compose))) + stack.enter_context(patch.object(yamlnode.yaml, 'compose', timer('yaml.native', yamlnode.yaml.compose))) + for name in ('_detach_type_expressions', 'finish_projections'): + if hasattr(entry, name): + stack.enter_context(patch.object(entry, name, timer(name, getattr(entry, name)))) + if hasattr(registry.Raml, 'finish_sources'): + stack.enter_context(patch.object(registry.Raml, 'finish_sources', timer('finish_sources', registry.Raml.finish_sources))) + yield + + +@contextmanager +def outline_timers(outline, elapsed): + """Time coarse outline suboperations; recursive placement is counted once.""" + def timer(name, fn): + depth = [0] + + def measured(*arguments, **kwargs): + outer = depth[0] == 0 + depth[0] += 1 + start = time.perf_counter() if outer else 0 + try: + return fn(*arguments, **kwargs) + finally: + depth[0] -= 1 + if outer: + elapsed[name] += time.perf_counter() - start + return measured + + with ExitStack() as stack: + if hasattr(outline, '_place_sections'): + stack.enter_context(patch.object(outline, '_place_sections', timer('outline.place_sections', outline._place_sections))) + import fastraml.service.source as source + import fastraml.sourceprojection as projection + stack.enter_context(patch.object(source.SourceIndex, '_projection', timer('outline.source_projection', source.SourceIndex._projection))) + stack.enter_context(patch.object(projection.SourceProjection, '_index_starts', timer('outline.cursor_index', projection.SourceProjection._index_starts))) + yield + + +def working_set(): + """Current Windows working set, not tracemalloc or a process high-water mark.""" + if sys.platform != 'win32': + return None + import ctypes + from ctypes import wintypes + + class Counters(ctypes.Structure): + _fields_ = [('cb', wintypes.DWORD), ('PageFaultCount', wintypes.DWORD)] + [ + (name, ctypes.c_size_t) for name in ( + 'PeakWorkingSetSize', 'WorkingSetSize', 'QuotaPeakPagedPoolUsage', + 'QuotaPagedPoolUsage', 'QuotaPeakNonPagedPoolUsage', 'QuotaNonPagedPoolUsage', + 'PagefileUsage', 'PeakPagefileUsage', + ) + ] + + kernel = ctypes.WinDLL('kernel32', use_last_error=True) + kernel.GetCurrentProcess.restype = wintypes.HANDLE + kernel.K32GetProcessMemoryInfo.argtypes = (wintypes.HANDLE, ctypes.POINTER(Counters), wintypes.DWORD) + kernel.K32GetProcessMemoryInfo.restype = wintypes.BOOL + counters = Counters() + counters.cb = ctypes.sizeof(Counters) + if not kernel.K32GetProcessMemoryInfo(kernel.GetCurrentProcess(), ctypes.byref(counters), ctypes.sizeof(counters)): + raise ctypes.WinError(ctypes.get_last_error()) + return counters.WorkingSetSize + + +def worker(args): + sys.path.insert(0, str(Path.cwd())) + import fastraml + import fastraml.registry as registry + import fastraml.parser.entry as entry_module + import fastraml.service.workspace as module + import fastraml.yamlnode as yamlnode + from fastraml.gctuning import tuned_gc + from fastraml.service import inlays, lenses, outline, queries + from fastraml.positions import Position + + if not Path(fastraml.__file__).resolve().is_relative_to(Path.cwd().resolve()): + raise RuntimeError('worker imported the wrong tree: ' + fastraml.__file__) + scenario = runpy.run_path(str(SCENARIO)) + prepared = scenario['prepare'](Path(args.entry), focus=Path(args.focus)) + phases = [] + root = prepared.root + with tuned_gc(), variant(module, args.no_capture): + if args.mixed: + harness = runpy.run_path(str(REPOSITORY / 'bench/harness.py')) + measured = harness['measure']('service-source-first' if args.source_first else 'service-session', + 'unwrap', lambda: scenario['exercise'](prepared, source_first=args.source_first), + repeat=args.repeat) + print(json.dumps({'mixed': measured.as_dict()})) + return + if args.ownership: + module.Workspace([prepared.folder]).linter + gc.collect() + tracemalloc.start(8) + workspace = module.Workspace([prepared.folder]) + workspace.open(root, prepared.versions[0], 1) + snapshot = workspace.snapshot(root) + assert snapshot.error is None + snapshot.occurrences + gc.collect() + allocation = tracemalloc.take_snapshot() + attributed = Counter() + for statistic in allocation.statistics('traceback'): + origin = 'other' + for frame in reversed(statistic.traceback): + normalized = frame.filename.replace('\\', '/') + if '/fastraml/' in normalized: + origin = normalized.split('/fastraml/', 1)[1] + break + attributed[origin] += statistic.size + print(json.dumps({'ownership_MB': {name: size / 1e6 for name, size in attributed.most_common()}, + 'traced_kept_MB': sum(attributed.values()) / 1e6})) + tracemalloc.stop() + return + if args.rss_edits: + # Warm configuration before the import-only reference measurement. + module.Workspace([prepared.folder]).linter + gc.collect() + baseline = working_set() + baseline_models = sum(isinstance(obj, registry.Raml) for obj in gc.get_objects()) + workspace = module.Workspace([prepared.folder]) + if prepared.focus != root: + workspace.open(prepared.focus, prepared.focus_text, 1) + retention = [] + for version in range(1, args.rss_edits + 1): + text = prepared.versions[0] + f'\n# retained edit {version}\n' + workspace.change(root, text, version) + workspace.collect() + snapshot = workspace.snapshot(root) + assert snapshot.error is None + queries.diagnostics(snapshot, lint=False) + snapshot.occurrences + if version > args.rss_edits // 2: + outline.document_symbols(snapshot, prepared.focus) + inlays.inlay_hints(snapshot, prepared.focus, Position(1, 1, 120, 1)) + scenario['folding'](workspace, prepared.focus) + for line, column in prepared.probes: + assert queries.hover(snapshot, prepared.focus, line, column) + del snapshot + gc.collect() + models = sum(isinstance(obj, registry.Raml) for obj in gc.get_objects()) + rss = working_set() + retention.append({'version': version, 'automatic_queries': version > args.rss_edits // 2, + 'rss_MB': None if rss is None else rss / 1e6, 'live_Raml': models, + 'snapshots': len(workspace._snapshots)}) + print(json.dumps({'baseline_rss_MB': baseline / 1e6 if baseline else None, + 'baseline_live_Raml': baseline_models, 'retention': retention})) + return + # The host/lint configuration and first snapshot are warm for edit timing. + workspace = module.Workspace([prepared.folder]) + workspace.open(root, prepared.versions[0], 1) + if prepared.focus != root: + workspace.open(prepared.focus, prepared.focus_text, 1) + initial = workspace.snapshot(root) + assert initial.error is None + initial.occurrences + del initial + for version in range(2, args.repeat + 2): + text = prepared.versions[0] + f'\n# measured edit {version}\n' + elapsed = defaultdict(float) + timer = phase_timers(registry, entry_module, yamlnode, elapsed) if args.detail else nullcontext() + with timer: + start = time.perf_counter() + workspace.change(root, text, version) + changed = time.perf_counter() + workspace.collect() + collected = time.perf_counter() + snapshot = workspace.snapshot(root) + parsed = time.perf_counter() + assert snapshot.error is None + diagnostics = queries.diagnostics(snapshot, lint=False) + diagnosed = time.perf_counter() + occurrences = snapshot.occurrences + indexed = time.perf_counter() + elapsed.update({ + 'change': changed - start, + 'collect': collected - changed, + 'snapshot': parsed - collected, + 'diagnostics': diagnosed - parsed, + 'occurrences': indexed - diagnosed, + 'total': indexed - start, + }) + phases.append(dict(elapsed)) + del snapshot, occurrences, diagnostics + del workspace + gc.collect() + + # Separate allocation run: retain one current workspace, not answers. + tracemalloc.start() + workspace = module.Workspace([prepared.folder]) + workspace.open(root, prepared.versions[0], 1) + if prepared.focus != root: + workspace.open(prepared.focus, prepared.focus_text, 1) + snapshot = workspace.snapshot(root) + assert snapshot.error is None + memory = {} + + def keep(name): + gc.collect() + current, peak = tracemalloc.get_traced_memory() + memory[name] = {'kept': current, 'peak': peak} + + keep('snapshot') + queries.diagnostics(snapshot, lint=False) + snapshot.occurrences + keep('diagnostics_occurrences') + outline.document_symbols(snapshot, prepared.focus) + queries.links(snapshot, prepared.focus) + lenses.code_lenses(snapshot, prepared.focus) + inlays.inlay_hints(snapshot, prepared.focus, Position(1, 1, 120, 1)) + scenario['folding'](workspace, prepared.focus) + keep('automatic_queries') + for line, column in prepared.probes: + assert queries.hover(snapshot, prepared.focus, line, column) is not None + keep('sparse_hovers') + model = snapshot.raml + assert model is not None + dimensions = {'read_files': len(snapshot.read), 'shapes': len(model.shapes), 'endpoints': len(model.endpoints)} + captures = getattr(model, 'source_projections', {}) + records = set() + pending = [document.record(document.root) for document in captures.values() if document is not None] + while pending: + record = pending.pop() + if record not in records: + records.add(record) + pending.extend(record.content) + dimensions.update({'captured_files': len(captures), 'retained_source_records': len(records)}) + tracemalloc.stop() + del snapshot, workspace, model, records, pending, captures + gc.collect() + + # Untraced first-use/warm timings, new snapshot per repeat. + queries_timed = [] + for _ in range(args.repeat): + workspace = module.Workspace([prepared.folder]) + workspace.open(root, prepared.versions[0], 1) + if prepared.focus != root: + workspace.open(prepared.focus, prepared.focus_text, 1) + snapshot = workspace.snapshot(root) + snapshot.occurrences + row = {} + + def request(name, operation): + start = time.perf_counter() + result = operation() + row[name] = time.perf_counter() - start + return result + + details = defaultdict(float) + with outline_timers(outline, details) if args.detail else nullcontext(): + request('outline.first', lambda: outline.document_symbols(snapshot, prepared.focus)) + row.update(details) + request('lenses.first', lambda: lenses.code_lenses(snapshot, prepared.focus)) + request('inlays.first', lambda: inlays.inlay_hints(snapshot, prepared.focus, Position(1, 1, 120, 1))) + request('folding.first', lambda: scenario['folding'](workspace, prepared.focus)) + line, column = prepared.probes[0] + assert request('hover.first_after_auto', lambda: queries.hover(snapshot, prepared.focus, line, column)) + request('outline.warm', lambda: outline.document_symbols(snapshot, prepared.focus)) + request('inlays.warm', lambda: inlays.inlay_hints(snapshot, prepared.focus, Position(1, 1, 120, 1))) + request('folding.warm', lambda: scenario['folding'](workspace, prepared.focus)) + request('hover.warm', lambda: queries.hover(snapshot, prepared.focus, line, column)) + queries_timed.append(row) + del snapshot, workspace + gc.collect() + + # Work counters: separate run; no instrumented timings are reported. + counts = Counter() + event = ['setup'] + real_compose = yamlnode.yaml.compose + real_convert = yamlnode._Converter.convert + + def counted_compose(text, **kwargs): + counts[event[0] + '.yaml_composes'] += 1 + counts['input.' + hashlib.sha256(text.encode()).hexdigest()[:12]] += 1 + return real_compose(text, **kwargs) + + def counted_convert(converter, *arguments, **kwargs): + counts[event[0] + '.converted_nodes'] += 1 + return real_convert(converter, *arguments, **kwargs) + + workspace = module.Workspace([prepared.folder]) + if prepared.focus != root: + workspace.open(prepared.focus, prepared.focus_text, 1) + with ExitStack() as stack: + stack.enter_context(patch.object(yamlnode.yaml, 'compose', counted_compose)) + stack.enter_context(patch.object(yamlnode._Converter, 'convert', counted_convert)) + if hasattr(registry.Raml, 'compose_source'): + import fastraml.sourcecapture as capture + import fastraml.sourceprojection as projection + for owner, attribute in ((capture._RecordNode, 'kind'), (projection.SourceRecord, 'line')): + original = getattr(owner, attribute) + label = owner.__name__ + '.' + attribute + + def getter(node, original=original, label=label): + counts[event[0] + '.' + label] += 1 + return original.fget(node) + + stack.enter_context(patch.object(owner, attribute, property(getter))) + for version, text in enumerate(prepared.versions, 1): + workspace.change(root, text, version) + workspace.collect() + event[0] = f'v{version}.snapshot' + snapshot = workspace.snapshot(root) + event[0] = f'v{version}.occurrences' + snapshot.occurrences + event[0] = f'v{version}.automatic' + outline.document_symbols(snapshot, prepared.focus) + inlays.inlay_hints(snapshot, prepared.focus, Position(1, 1, 120, 1)) + scenario['folding'](workspace, prepared.focus) + event[0] = f'v{version}.hovers' + for line, column in prepared.probes: + assert queries.hover(snapshot, prepared.focus, line, column) + event[0] = f'v{version}.warm' + scenario['folding'](workspace, prepared.focus) + for line, column in prepared.probes: + scenario['selection'](workspace, prepared.focus, line, column) + del snapshot + # Repeated source-only requests on one unchanged broken buffer. + broken = '#%RAML 1.0\ntitle: [\n' + workspace.change(root, broken, 4) + workspace.collect() + broken_counts = Counter() + + def broken_compose(*arguments, **kwargs): + broken_counts['yaml_composes'] += 1 + return real_compose(*arguments, **kwargs) + + with patch.object(yamlnode.yaml, 'compose', broken_compose): + failed = workspace.snapshot(root) + assert failed.error is not None + after_parse = broken_counts['yaml_composes'] + for _ in range(3): + if hasattr(workspace, 'source'): + workspace.source(root) + else: + scenario['folding'](workspace, root) + counts['broken.snapshot_composes'] = after_parse + counts['broken.three_source_request_composes'] = broken_counts['yaml_composes'] - after_parse + print(json.dumps({'phases': phases, 'queries': queries_timed, 'memory': memory, 'dimensions': dimensions, + 'counts': dict(counts), 'python': sys.version, 'backend': yamlnode.backend_name(), + 'import': fastraml.__file__})) + + +def summarize(document): + result = {'corpus': document['corpus'], 'scale': document['scale'], 'inputs': document['inputs'], + 'detail_timers': document['detail_timers'], 'mixed': document.get('mixed', False), + 'source_first': document.get('source_first', False), 'trees': {}} + for label, rounds in document['results'].items(): + if 'mixed' in rounds[0]: + values = [round_['mixed']['seconds'] for round_ in rounds] + result['trees'][label] = { + 'mixed_best_ms': min(values) * 1000, + 'round_spread_percent': (max(values) / min(values) - 1) * 100, + 'peak_MB': min(round_['mixed']['allocated_bytes'] for round_ in rounds) / 1e6, + 'kept_MB': min(round_['mixed']['retained_bytes'] for round_ in rounds) / 1e6, + } + continue + if 'retention' in rounds[0] or 'ownership_MB' in rounds[0]: + result['trees'][label] = rounds + continue + phase_rows = [row for round_ in rounds for row in round_['phases']] + best_rounds = [min(round_['phases'], key=lambda row: row['total']) for round_ in rounds] + totals = [row['total'] for row in best_rounds] + best = min(best_rounds, key=lambda row: row['total']) + query_rows = [row for round_ in rounds for row in round_['queries']] + counts = rounds[0]['counts'] + result['trees'][label] = { + 'best_total_ms': min(totals) * 1000, + 'round_spread_percent': (max(totals) / min(totals) - 1) * 100, + 'same_best_run_phases_ms': {key: value * 1000 for key, value in best.items()}, + 'phase_minima_ms': {key: min(row.get(key, 0) for row in phase_rows) * 1000 for key in best}, + 'query_minima_ms': {key: min(row[key] for row in query_rows) * 1000 for key in query_rows[0]}, + 'memory_MB': {key: {name: min(round_['memory'][key][name] for round_ in rounds) / 1e6 + for name in ('kept', 'peak')} for key in rounds[0]['memory']}, + 'dimensions': rounds[0]['dimensions'], + 'counts': {key: value for key, value in counts.items() if not key.startswith('input.')}, + 'identical_input_composition_histogram': dict(Counter(value for key, value in counts.items() if key.startswith('input.'))), + } + return result + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--worker', action='store_true') + parser.add_argument('--entry') + parser.add_argument('--focus') + parser.add_argument('--no-capture', action='store_true') + parser.add_argument('--detail', action='store_true') + parser.add_argument('--repeat', type=int, default=3) + parser.add_argument('--rounds', type=int, default=3) + parser.add_argument('--corpus', choices=('large', 'hover', 'endpoints'), default='large') + parser.add_argument('--scale', type=float, default=1) + parser.add_argument('--rss-edits', type=int, default=0) + parser.add_argument('--ownership', action='store_true') + parser.add_argument('--mixed', action='store_true') + parser.add_argument('--source-first', action='store_true') + parser.add_argument('--tree', action='append', default=[], help='LABEL=PATH; label no-capture enables ablation') + parser.add_argument('--output', type=Path) + parser.add_argument('--summarize', type=Path, nargs='+') + args = parser.parse_args() + if args.summarize: + summaries = [summarize(json.loads(path.read_text(encoding='utf-8'))) for path in args.summarize] + summarized = summaries[0] if len(summaries) == 1 else {'runs': summaries} + text = json.dumps(summarized, indent=2) + '\n' + if args.output: + args.output.write_text(text, encoding='utf-8') + print(f'wrote {len(summaries)} summarized runs to {args.output}') + else: + print(text) + return + if args.worker: + worker(args) + return + sys.path.insert(0, str(REPOSITORY)) + from bench import corpus + + results = defaultdict(list) + trees = [value.split('=', 1) for value in args.tree] + with tempfile.TemporaryDirectory(prefix='service-costs-') as where: + root = Path(where) + if args.corpus == 'large': + entry = corpus.write_large(root, type_count=max(1, round(7000 * args.scale)), library_count=max(1, round(150 * args.scale))) + focus = root / 'lib/g0/l0.raml' + elif args.corpus == 'hover': + entry = corpus.write_hover(root, family_count=max(1, round(400 * args.scale))) + focus = entry + else: + entry = corpus.write_endpoints(root, resource_count=max(1, round(500 * args.scale))) + focus = entry + files = list(root.rglob('*')) + inputs = {'files': sum(path.is_file() for path in files), + 'bytes': sum(path.stat().st_size for path in files if path.is_file()), + 'entry_bytes': entry.stat().st_size, 'focus_bytes': focus.stat().st_size} + for round_index in range(args.rounds): + for label, tree in trees: + command = [sys.executable, str(Path(__file__).resolve()), '--worker', '--entry', str(entry), + '--focus', str(focus), '--repeat', str(args.repeat)] + if label == 'no-capture': + command.append('--no-capture') + if args.detail: + command.append('--detail') + if args.rss_edits: + command.extend(['--rss-edits', str(args.rss_edits)]) + if args.ownership: + command.append('--ownership') + if args.mixed: + command.append('--mixed') + if args.source_first: + command.append('--source-first') + completed = subprocess.run(command, cwd=tree, capture_output=True, text=True, check=True) + result = json.loads(completed.stdout.strip().splitlines()[-1]) + results[label].append(result) + if args.mixed: + print(f'{args.corpus} {label}: {result["mixed"]["seconds"] * 1000:.1f} ms', flush=True) + elif args.ownership: + print(f'{args.corpus} {label}: {result["traced_kept_MB"]:.3f} MB kept', flush=True) + elif args.rss_edits: + print(f'{args.corpus} {label}: {result["retention"][-1]}', flush=True) + else: + minimum = min(row['total'] for row in result['phases']) + print(f'{args.corpus} round {round_index + 1} {label}: {minimum * 1000:.1f} ms', flush=True) + document = {'corpus': args.corpus, 'scale': args.scale, 'inputs': inputs, 'detail_timers': args.detail, + 'mixed': args.mixed, 'source_first': args.source_first, + 'platform': platform.platform(), 'trees': trees, 'results': results} + if args.output: + args.output.write_text(json.dumps(document, indent=2) + '\n', encoding='utf-8') + print(json.dumps(summarize(document), indent=2)) + + +if __name__ == '__main__': + main() diff --git a/docs/research/2026-10-10/service-recovery-plan.md b/docs/research/2026-10-10/service-recovery-plan.md new file mode 100644 index 0000000..96d24a3 --- /dev/null +++ b/docs/research/2026-10-10/service-recovery-plan.md @@ -0,0 +1,133 @@ +# Service baseline and selective recovery plan + +Status: accepted approach; execution started on 2026-10-10. + +## 1. Objective and authority + +The user approved this sequence after the +[cost review](../../reports/2026-10-10/service-authoring-cost-review.md): land the +measurement infrastructure, validate and merge the simpler service branch if it +meets the acceptance goals, then recover parked changes in independently measured +bundles. The first deliverable is this plan so later work keeps the same goals. + +The primary goal is normal edit-to-parser-diagnostics/occurrences readiness. +Authoring features must not impose a substantial recurring capture penalty on +every semantic rebuild. Source/query latency and retained memory are separate +acceptance decisions, not a weighted total that hides that penalty. + +Numbered documents remain the current contracts. This plan records execution and +decisions; measured results belong in dated reports. The parked branch and its +original SHA remain available as evidence. + +## 2. Starting state + +- `master` and `origin/master`: `2b6502e` at the initial inspection. +- Simplified candidate: `refactor/service-source-simple`, `fb0fb73`, seven + commits on the original master. Its handoff is untracked scratch and must not + enter a commit. +- Parked representation replacement: `refactor/service-authoring`, `bc4b49d`. +- At the initial inspection, measurement/instruction work was uncommitted on the parked worktree. + It contains two mixed workloads, reach tests, profiling instructions, the cost + report and its numerical artifacts/driver. Production behavior is unchanged. + +Recheck remote state before each integration. Preserve unrelated worktrees and +the parked implementation. Do not merge the parked representation replacement +into master as part of landing measurements. + +## 3. Stage A: portable measurements first + +1. Preserve the measurement work in its own logical commit and prepare an + integration branch based on master, without the parked production changes. +2. Make historical drivers behavior-equivalent where public APIs differ. Master + has text-based folding/selection; newer branches have workspace source caches. + Exercise the real branch behavior rather than making an unavailable interface + look like absent functionality. Reach tests must tolerate an uncached historical + implementation while still proving the ordered requests and rebuilds occur. +3. Keep the report and no-capture ablation labeled as diagnostic evidence. No + production parser option changes are part of this stage. +4. Reconcile the benchmark count and descriptions with the target tree; do not + import parked-only contracts or obsolete source workloads into master. +5. Run the gate and mixed-workload linearity. Review the staged diff and all PR + commits. Merge only this independent measurement/instruction change. + +## 4. Stage B: accept the simpler production baseline + +Integrate the measurement commit with the simplified branch and compare its +production changes against the measurement-only master control on identical +corpora. Record revision IDs and commands in a new dated acceptance report. + +Required observations: + +- Normal root edits: parse, collection, parser diagnostics and occurrences. +- Ordinary no-source parse/unwrap/validation, and relevant lint consumers. +- Both mixed orders: snapshot-first and source-before-snapshot, over multiple + versions with request answers released before the next version. +- First outline, viewport hints and folding; warm requests; memory after + diagnostics and after automatic queries, not just idle snapshots. +- Cache sharing, invalidation and failed composition, including an unchanged + queried dependency and compatibility of snapshot text with current text. + +Acceptance: + +1. The routine rebuild path has no repeatable time regression beyond measured + noise against the original relevant baseline. Diagnostic-only retained memory + must preserve the candidate's improvement. +2. The new mixed workloads must demonstrate the benefit of the source cache and + expose any newly retained duplicate owner. A favorable aggregate time does not + silently accept a new first-use or memory regression. +3. Resolve bounded correctness/ownership blockers separately, with tests and + measured effects. Each fix gets its own logical commit and owning-document + update. Do not fold unrelated parked features into baseline acceptance. +4. If a material trade-off remains after those fixes, document it and stop at the + decision rather than treating the old handoff's acceptance as user approval. +5. Pass the full gate and the relevant platform checks, including Linux-only + loader/security cases through CI or the documented Docker run. Do not change + fixtures or TCK expectations to obtain a green run. + +After acceptance, merge the simpler branch through the repository's normal GitHub +flow. Record the accepted master SHA; it becomes the recovery comparison control. + +## 5. Stage C: recover small bundles, not a blind bisect + +The parked representation work is substantially bundled in one WIP commit. +Ordinary `git bisect` is not the primary method. Use controlled options and +isolated ports that answer a named question, preserving the original branch. + +Start an experiment branch from the accepted baseline. Candidate order: + +1. `type_written` and model-backed declaration inlays: avoid whole-file source + grammar construction merely to identify explicit types. +2. Shared semantic/reverse-hierarchy indices, typed-data definitions independent + of hover presentation, and outline caching. +3. Cursor-local primitive lookup and source text-offset checks, retaining include + contexts and opaque-data correctness. +4. Accurate authored section ranges as a separately measured feature. + +Check each candidate against changes already on simplified; do not duplicate +navigation, compatibility facts, visible names or postparse include handling. +Document expected work/calls and cache boundaries before profiling. Compare each +bundle against the accepted baseline, with reach, correctness and scaling checks. + +Retaining record backing itself is a separate architectural decision. It must +earn its cost against the real scenarios, including full-value consumers, and +cannot become a prerequisite for unrelated useful features. + +## 6. Stopping and execution record + +Stop an experiment when its stated goal passes, or when new evidence requires a +new architectural/product decision. Reject a bundle that restores substantial +mandatory rebuild overhead, even if bulk hovers or one mixed order improves. +Do not optimize a high-call accessor before explaining its multiplicity. + +Current execution: + +- Plan written before integration or production changes. +- Stage A: measurement work preserved as `7c954ba` on the parked branch; the + master-based `test/service-workload-baseline` port passes the Windows gate + (5,810 passed, 67 skipped, 1 xfailed) and both mixed-workload linearity checks. + GitHub/Linux verification and measurement-only integration are next. +- Stage B: pending master-controlled acceptance measurements. +- Stage C: pending an accepted baseline. + +Update these entries as each stage completes. Keep measurements in the dated +report, current pending work in docs/15, and execution history here. diff --git a/tests/bench/test_corpus.py b/tests/bench/test_corpus.py index ff74167..15bd7fc 100644 --- a/tests/bench/test_corpus.py +++ b/tests/bench/test_corpus.py @@ -52,6 +52,8 @@ 'hover': lambda root: corpus.write_hover(root, family_count=3), 'effective-types': lambda root: corpus.write_hover(root, family_count=3), 'inlays': lambda root: corpus.write_hover(root, family_count=3), + 'service-session': lambda root: corpus.write_hover(root, family_count=3), + 'service-source-first': lambda root: corpus.write_hover(root, family_count=3), } diff --git a/tests/bench/test_service_session.py b/tests/bench/test_service_session.py new file mode 100644 index 0000000..3b3fb67 --- /dev/null +++ b/tests/bench/test_service_session.py @@ -0,0 +1,99 @@ +"""Reach and ownership boundaries of the representative mixed editor workload.""" + +import pytest + +from bench import corpus +from bench.service_session import exercise, prepare +from fastraml.service.workspace import Workspace + + +@pytest.mark.parametrize('count', [2, 4]) +@pytest.mark.parametrize('source_first', [False, True]) +def test_session_rebuilds_three_versions_and_checks_available_source_reuse(tmp_path, monkeypatch, count, source_first): + from fastraml import yamlnode + from fastraml.service import inlays, outline + + entry = corpus.write_hover(tmp_path, family_count=count) + prepared = prepare(entry) + parsed, compositions, outlines, hints, sources = [], [], [], [], [] + original_parse = Workspace._parse + original_compose = yamlnode.yaml.compose + original_outline = outline.document_symbols + original_hints = inlays.inlay_hints + original_source = getattr(Workspace, 'source', None) + + def parse(workspace, root): + parsed.append(workspace.buffers[root].version) + return original_parse(workspace, root) + + def compose(text, **kwargs): + if text in prepared.versions: + compositions.append(text) + return original_compose(text, **kwargs) + + def symbols(snapshot, uri): + result = original_outline(snapshot, uri) + outlines.append(result) + return result + + def inlay(snapshot, uri, span): + result = original_hints(snapshot, uri, span) + hints.append([(hint.position, hint.label) for hint in result]) + return result + + def source(workspace, uri): + assert original_source is not None + result = original_source(workspace, uri) + sources.append((workspace.buffers[uri].version, result)) + return result + + monkeypatch.setattr(Workspace, '_parse', parse) + monkeypatch.setattr(yamlnode.yaml, 'compose', compose) + monkeypatch.setattr(outline, 'document_symbols', symbols) + monkeypatch.setattr(inlays, 'inlay_hints', inlay) + if original_source is not None: + monkeypatch.setattr(Workspace, 'source', source) + workspace, counts = exercise(prepared, source_first=source_first) + assert parsed == [1, 2, 3] + snapshot = workspace.snapshot(prepared.root) + if getattr(snapshot.raml, 'projection', None) is not None: + assert len(compositions) == (6 if source_first else 3) + else: + assert len(compositions) >= 3, 'historical source readers may compose for each request' + if hasattr(snapshot, 'outlines'): + assert all(outlines[index] is outlines[index + 1] for index in (0, 2, 4)) + selections = [[(symbol.name, symbol.span, symbol.selection) for symbol in symbols] for symbols in outlines] + assert all(selections[index] == selections[index + 1] for index in (0, 2, 4)) + for version in (1, 2, 3): + current = [root for at, root in sources if at == version] + warm = current[1:] if source_first else current + assert all(root is warm[0] for root in warm), 'available source caches share the warm requests' + assert all(hints[index] == hints[index + 1] for index in (0, 2, 4)) + assert counts['hovers'] == 9 + assert counts['selections'] == 24 + assert len(workspace._snapshots) == 1 + + +def test_session_can_query_an_unchanged_library_after_root_edits(tmp_path): + entry = corpus.write_large(tmp_path, type_count=24, library_count=4) + workspace, counts = exercise(prepare(entry, focus=tmp_path / 'lib/g0/l0.raml')) + assert counts['snapshots'] == 3 + assert len(workspace._snapshots) == 1 + + +@pytest.mark.parametrize('name', ['service-session', 'service-source-first']) +def test_benchmark_and_linearity_drive_the_session_in_both_measurement_runs(tmp_path, monkeypatch, name): + from bench.__main__ import LINEARITY_CONFIGS, run_one + + entry = corpus.write_hover(tmp_path, family_count=2) + versions = [] + original = Workspace._parse + + def parse(workspace, uri): + versions.append(workspace.buffers[uri].version) + return original(workspace, uri) + + monkeypatch.setattr(Workspace, '_parse', parse) + assert LINEARITY_CONFIGS[name] == 'unwrap' + run_one(name, 'unwrap', entry, repeat=1) + assert versions == [1, 2, 3, 1, 2, 3]