Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions bench/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ def _at(base: int, scale: float) -> int:
Bench('source-structure', 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))),
Bench('service-navigation', lambda root, scale: corpus.write_hover(root, family_count=_at(400, scale))),
)

_BY_NAME = {bench.name: bench for bench in BENCHES}
Expand Down Expand Up @@ -165,6 +166,7 @@ def run_one(bench: str, config: str, entry: Path, repeat: int) -> Measurement:
'source-structure',
'service-session',
'service-source-first',
'service-navigation',
}:
return _measure_view(bench, entry, repeat)
if config == 'service':
Expand Down Expand Up @@ -211,6 +213,7 @@ def _measure_view(bench: str, entry: Path, repeat: int) -> Measurement:
'source-structure': _measure_source_structure,
'service-session': _measure_service_session,
'service-source-first': lambda entry, repeat: _measure_service_session(entry, repeat, source_first=True),
'service-navigation': _measure_service_navigation,
}.get(bench)
if service_workload is not None:
return service_workload(entry, repeat)
Expand Down Expand Up @@ -414,6 +417,15 @@ def _measure_service_session(entry: Path, repeat: int, *, source_first: bool = F
return measure(name, 'unwrap', lambda: exercise(prepared, source_first=source_first), repeat=repeat)


def _measure_service_navigation(entry: Path, repeat: int) -> Measurement:
from bench.service_navigation import exercise, prepare # noqa: PLC0415 - feature workload only
from fastraml.gctuning import tuned_gc # noqa: PLC0415 - feature workload only

prepared = prepare(entry)
with tuned_gc():
return measure('service-navigation', 'unwrap', lambda: exercise(prepared), 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
Expand Down Expand Up @@ -585,6 +597,7 @@ def compare(results: Sequence[Measurement], tolerance: float) -> int:
'source-structure': 'unwrap',
'service-session': 'unwrap',
'service-source-first': 'unwrap',
'service-navigation': 'unwrap',
}


Expand Down
85 changes: 85 additions & 0 deletions bench/service_navigation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Sparse semantic requests across three edits, not an observed client trace."""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING

from bench.service_session import SessionInput
from bench.service_session import prepare as prepare_session
from fastraml.service import outline, queries
from fastraml.service.workspace import Workspace

if TYPE_CHECKING:
from pathlib import Path


@dataclass(frozen=True, slots=True, eq=False)
class NavigationInput:
session: SessionInput
types: tuple[tuple[int, int], ...]
data: tuple[tuple[int, int], ...]


def prepare(entry: Path) -> NavigationInput:
session = prepare_session(entry)
types = []
data = []
for line, raw in enumerate(session.focus_text.splitlines(), 1):
if raw.startswith((' Word', ' Name')):
types.append((line, 3))
elif raw.strip() == 'note: Facet note.':
data.append((line, len(raw) - len(raw.lstrip()) + 1))
pair_size = 2
if len(types) < pair_size or not data:
raise RuntimeError('navigation corpus lost its sparse type/data sites')
# Three family pairs and three data sites, independent of input width.
families = len(types) // pair_size
chosen = (0, families // 2, families - 1)
return NavigationInput(
session,
tuple(types[pair_size * family + member] for family in chosen for member in range(pair_size)),
tuple(data[family] for family in chosen),
)


def exercise(prepared: NavigationInput) -> tuple[Workspace, dict[str, int]]:
session = prepared.session
workspace = Workspace([session.folder])
counts: dict[str, int] = dict.fromkeys(
('snapshots', 'types', 'supers', 'subs', 'definitions', 'outlines', 'symbols'), 0
)
for version, text in enumerate(session.versions, 1):
workspace.change(session.root, text, version)
workspace.collect()
snapshot = workspace.snapshot(session.root)
if snapshot.error is not None or snapshot.raml is None:
raise RuntimeError('navigation corpus lost its valid snapshot')
counts['snapshots'] += 1
queries.diagnostics(snapshot, lint=False)
snapshot.occurrences # noqa: B018 - part of diagnostic/navigation readiness
for line, column in prepared.types:
item = queries.type_at(snapshot, session.root, line, column)
if item is None:
raise RuntimeError('navigation corpus lost a type preparation')
counts['types'] += 1
parents = queries.supertypes(snapshot, item)
children = queries.subtypes(snapshot, item)
if (item.name.startswith('Name') and not parents) or (item.name.startswith('Word') and not children):
raise RuntimeError('navigation corpus lost an inheritance edge')
counts['supers'] += 1
counts['subs'] += 1
for line, column in prepared.data:
if not queries.definition(snapshot, session.root, line, column):
raise RuntimeError('navigation corpus lost a typed-data definition')
counts['definitions'] += 1
for _ in range(2):
if not outline.document_symbols(snapshot, session.root):
raise RuntimeError('navigation corpus lost its outline')
counts['outlines'] += 1
for wanted in ('Word', 'MetadataLeaf'):
if not queries.workspace_symbols([snapshot], wanted):
raise RuntimeError('navigation corpus lost its workspace symbols')
counts['symbols'] += 1
del snapshot
return workspace, counts
11 changes: 8 additions & 3 deletions docs/12-performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ must receive a parser diagnostic rather than `RecursionError`.

## 4. Benchmark suite

`bench/` generates deterministic corpora and measures thirty-three workloads:
`bench/` generates deterministic corpora and measures thirty-four workloads:

| Bench | Primary coverage |
|---|---|
Expand Down Expand Up @@ -122,11 +122,12 @@ must receive a parser diagnostic rather than `RecursionError`.
| `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) |
| `source-structure` | three folding requests and twelve selection requests at scattered keys, served from the workspace's composed source tree, composed once per current text ([21](21-language-service.md) § 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 |
| `service-navigation` | three root-buffer versions, each with diagnostics/occurrences, six fixed sparse hierarchy preparations and parent/child queries, three typed-data definitions before hover, two outlines and two workspace-symbol searches |

The six general workloads are `small`, `large`, `endpoints`, `extensions`,
`validate` and `jsonschema`. The other twenty-seven are feature workloads:
`validate` and `jsonschema`. The other twenty-eight 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
tests under `tests/bench/` count calls or check bound results, and fail
if a corpus stops reaching that code at every size it covers.

A feature added to the language has no baseline on `master`, where the corpus
Expand Down Expand Up @@ -163,6 +164,10 @@ 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.
For `service-navigation`, `unwrap` runs the sparse three-version navigation
sequence. Requests are fixed as file width grows; only the latest workspace/model
and caches remain live, not request answers. Generation and probe selection are
outside measurement; there is no lint, source-only request or protocol conversion.

```bash
python -m bench run
Expand Down
11 changes: 8 additions & 3 deletions docs/15-implementation-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,14 @@ all CI checks passed and the baseline is merged. The first isolated recovery
experiment implements model-backed declaration inlays against that accepted
control. Its correctness, scaling and A/B results are recorded in the
[inlay recovery report](reports/2026-10-10/service-model-inlays.md), including its
accepted snapshot-first mixed allocation-peak trade-off. Remaining recovery
candidates are shared semantic/hierarchy indices and outline caching, cursor-local
source lookup, and accurate authored section ranges, each measured independently.
accepted snapshot-first mixed allocation-peak trade-off.
The shared-index/outline bundle is the next isolated recovery against merged
`3912118`. Correctness, reach and scaling pass; sparse navigation is 17.0% faster,
but mixed retained allocation rises 7.5%, chiefly the cached outline. An explicit
acceptance of that new retention trade-off is recorded; the scope, measurements and
ownership evidence are in the [shared-index report](reports/2026-10-10/service-shared-indices.md).
Remaining candidates are cursor-local source lookup and accurate authored section
ranges, each measured independently.
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).
Expand Down
26 changes: 23 additions & 3 deletions docs/21-language-service.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ and several views, and only `cli/` imports it (`docs/02` § 2;
| `service/outline.py` | the outline, over the authorship view (`docs/16` § 10) |
| `service/hover.py`, `service/hoverdocs.py` | author-facing hover, source-key indices and explanatory prose (§ 4.2) |
| `service/datahover.py` | typed `DataNode` key and value spans, shared across value-bearing sites (§ 4.2) |
| `service/index.py` | lazy declaration, ID and reverse-hierarchy lookups shared by snapshot queries (§ 4) |
| `service/lenses.py` | code-lens sites and on-demand effective RAML type rendering (§ 4.3) |
| `service/inlays.py` | inline type/facet labels over the shared authoring indices (§ 4.4) |
| `service/lsp.py` | the LSP adapter, over pygls (§ 5) |
Expand Down Expand Up @@ -162,6 +163,17 @@ stages it completed. A declaration is an occurrence after decoding; a type
name used in an expression is one only once P7 bound it. No query raises on a
snapshot that stopped at any stage (`test_service_queries.py`).

**Shared semantic indices.** A snapshot owns lazy declaration enumeration and
file grouping, shared by outline, workspace symbols and hover subjects. ID lookup
populates separately on first hierarchy preparation/rebinding; the reverse map
populates separately on first subtype request. It records direct `inherits` and
`alias` edges, deduplicates repeated parents, and preserves child declaration order.
Multiple-inheritance scalar wrappers follow their bound alias to the named
declaration, so hierarchy locations select the declaration rather than the parent
list token. A declared alias itself remains a named hierarchy item.
Diagnostics and occurrences do not populate these caches. Each root snapshot owns
its semantic context; an edit replaces the caches even for unchanged dependencies.

**Outline.** Every entry is read from the model, and grouped as the file
groups it, the way a code outline reads: `title`, `version` and `baseUri` with
their values, then one section per declaration table (`uses`, `types`,
Expand All @@ -181,6 +193,12 @@ model; `JSON schema` for a `JsonShape`; or where nothing was written,
is its kind: object, array, union, enum or a scalar's. A resource's, method's
or response's detail is its `displayName`, a response's else its description.

The first outline request for a URI caches its complete result on the snapshot,
including an empty outline. Later requests borrow the same list and symbols;
callers must treat them as read-only. Root/dependency edits create a new snapshot
and outline cache. A held older snapshot continues to answer from its older model.
Caching does not add authored section positions or populate source grammar.

The model keeps no position for a section's key (`types:`, a method's
`headers:`), so a section spans its entries and selects the first.

Expand Down Expand Up @@ -316,9 +334,11 @@ parser diagnostics.
Hover indices and formatted subjects are lazy per snapshot. Source keys are indexed once per queried
file from retained nodes, or a composition of its retained text when source
trees were not retained. These nodes and indices die with the snapshot.
The typed-data token index is also lazy and built once per snapshot; formatted
data targets are cached. The same typed-data targets supply go-to-definition
for nested field keys and scalar values.
The typed-data token index is owned by the snapshot, lazy and built once; formatted
data targets are cached by hover. The same typed-data targets supply go-to-definition
for nested field keys and scalar values. A definition request may populate that
index without creating hover subjects, formatting hover or composing source. A
later hover/inlay request uses the same token index, not a second traversal.

### 4.3 Effective-type code lenses

Expand Down
2 changes: 1 addition & 1 deletion docs/archive/briefs/phase-9.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ argued for in Phase 2 and may have been overtaken.
3. **`uniqueItems` switches strategy at 20 items** (`docs/10` § 5.2). Measured
against go-raml, not guessed.
4. **The union-facet gap stays open.** It is After-v1 item 2 in `docs/15`, and
the fix has to land in go-raml too — the user's call, already made.
the fix has to land in go-raml too.

---

Expand Down
Loading
Loading