Skip to content

perf: incremental indexing and preprocessor caching - #344

Draft
hongjr03 wants to merge 211 commits into
masterfrom
perf
Draft

perf: incremental indexing and preprocessor caching#344
hongjr03 wants to merge 211 commits into
masterfrom
perf

Conversation

@hongjr03

Copy link
Copy Markdown
Member

No description provided.

@github-actions

Copy link
Copy Markdown

Docs preview: https://vide.pascal-lab.net/preview/pr-344/

Adds host_with_project (loads a real SystemVerilog directory into
AnalysisHost) and index_benchmarks_real_project, an ignored bench that
times cold load, cold parse, module index, semantic index, and the
semantic-index rebuild after touching one file.

Run with:
  VIDE_BENCH_PROJECT=third_party/slang/tests/unittests/data \
    cargo test -p ide --release --lib -- --ignored --nocapture index_benchmarks_real_project
ModuleIndex::for_source_root and other file iterators call item_tree on
every file in a source root, including .map library-map files whose slang
syntax root is LibraryMap rather than a compilation unit. The old
assertion panicked on those files.

Return an empty item tree for non-compilation-unit roots: library-map
declarations are lowered via lower_library_map, not the item tree, so
they contribute no items.
Adds index_benchmarks_module_index_profile, an ignored test that times
parse, macro file discovery, AST id map, owner table, and item-tree
residual per query, isolating the module-index bottleneck.

Run with:
  VIDE_BENCH_PROJECT=third_party/slang/tests/unittests/data \
    cargo test -p ide --release --lib -- --ignored --nocapture index_benchmarks_module_index_profile
included_source_end_order scanned events after each include and walked the
include-parent chain per event, making record_source_order_scopes
O(sources * events * depth). A self-including file hits slang's 1024
include-depth limit, producing 1025 sources/events and ~10^9 walks
(~4.5s on a 3.6KB project).

Trace events are a depth-first traversal of the include forest, so an
included source's scope ends exactly when the stream returns to a
shallower source. Replace the scan with a monotonic stack over events
keyed by precomputed include depth: O(events). Empty includes default to
include_order + 1 as before.

Module index on slang's multi-file test data: 4.4s -> 48ms.
PathIdentityIndex tracked OS file identity ((dev,ino) on Unix,
(volume,index) on Windows) to deduplicate hard links. Hard-linked source
files are effectively nonexistent; symlinks are already covered by the
canonical-path alias. This halves the per-insert syscalls (canonicalize +
stat -> canonicalize only) and drops the winapi-util dependency.
path_file_ids rebuilt the full path-spelling index on every call and was
invoked per file from source_preproc_file_ids, giving O(n^2)
canonicalization. Make it a salsa tracked query keyed by a workspace
singleton so it is computed once per revision.
lower_name only handled IdentifierName, IdentifierSelectName, and
ScopedName, missing KeywordName (used for constructor 'new' keyword).
This caused lower_subroutine_prototype to return None for class
constructors, leaving body.subroutine unset and panicking later in
db.subroutine().

Add KeywordName handling via as_keyword_name().keyword().
Two trace-building paths threw std::logic_error on data slang reports
for real-world code, which escaped the FFI boundary and terminated the
process:

- Source macro argument/body tokens with missing token-origin metadata
  now map to an unavailable origin instead of throwing.
- Overlapping macro usages at the same source range (a macro expanding
  to another macro at the same location) now emit the event without a
  call identity instead of throwing.

Both are graceful degradations; downstream consumers already treat
unavailable/missing origins as non-resolvable.
Adds file_semantic_index and file_module_edges timing to the module-index
profile, with a per-file breakdown sorted by semantic-index cost to
surface the worst files.
The per-file and per-root module/semantic/symbol index queries were plain
functions, so every call rebuilt from scratch. Name resolution during index
construction therefore rebuilt the whole-root module index once per
module-related token (named port connections, parameters, instantiations),
and a single-file change rebuilt the entire root.

Track them as salsa queries so the module index is computed once per root
and reused across tokens, and a one-file edit invalidates only that file's
index plus the root merge.

common_cells (214 files, 24k lines), B6 real-project benchmark:
- semantic index cold:   304s -> 3.4s
- semantic index rebuild: 310s -> 5.3s
set_parse_lru_capacity only sized parsed_profile and parse_src_for_compilation,
leaving parsed_compilation_unit, source_preproc_model, macro expansion, and
trace index pinned at lru=128. On projects above 128 files those per-file memos
are evicted during the build, so a revision bump makes salsa revalidation
recompute them instead of consulting their memo headers.

Wire all four into the capacity setter and raise DEFAULT_PARSE_LRU_CAP to 1024.
The incremental semantic-index rebuild's revalidation pass drops accordingly
(unchanged-file revalidation ~170ms -> ~70ms per file in common_cells).
source_root_semantic_index merged reference groups and module call edges in
one query, so a find-references or rename request revalidated both. Split it
into source_root_reference_index and source_root_module_edge_index so each
request only pays for the half it needs.

common_cells references rebuild after one-file edit: ~5.3s -> ~3.9s.
The merged reference index was a salsa query aggregating every file index, so
a revision bump made salsa deep-verify all files (~2.5s on common_cells). Move
the materialized index out of salsa into a RootDb-side cache that re-indexes
only the files changed by apply_change.

A changed file whose ItemTree is unchanged cannot have changed its
cross-file-visible definitions, so the other files' indexes stay valid and are
reused from the cache; a structural change conservatively falls back to a full
rebuild. definition_ranges_for is also memoized per DefId so the incremental
re-merge no longer re-projects every definition's origins.

common_cells references rebuild after one-file edit: ~3.9s -> ~1.8s. The
remaining cost is the monolithic parsed_profile re-parse of unchanged files.
parsed_compilation_unit pulled every root's tree from the monolithic
parsed_profile, so editing one file re-parsed the whole profile. Parse roots
standalone instead, injecting the running compilation-unit macro set of
predecessor roots as predefines so cross-file `$unit` macro visibility is
preserved without a conservative fallback.

Salsa propagates precisely: a root whose own `$unit` macros change invalidates
only the downstream roots, not the whole profile.
parsed_compilation_unit returned syntax tree and preprocessor trace together,
so a syntax-only edit (a comment) revalidated the trace and, through the
$unit macro chain, every downstream root. Split it into parse_tree and
preproc_trace so the trace is a separate memo that backdates on comment edits.

Verified with preproc-expand (73) and ide (203) test suites; the two
profile-reuse tests were updated to assert the new standalone-parse contract.
The incremental path re-merged all cached file indexes through
from_file_indexes, which re-projected definition origins for every definition
on each rebuild. Patch the cached index in place: existing definitions keep
their cached name and definition ranges, and only a dirty file's references
are swapped.

Profile: file_semantic_index re-read is the remaining rebuild cost; the merge
itself is now ~4ms.
The nameres core read three O(project) globals per file: unit_scope,
design_map and unit_index (plus the per-root module index in ide). Thread a
precomputed ResolutionContext through resolve_name/resolve_path/
resolve_in_resolved_scopes and the ide slow path so the file index's salsa
dependency graph no longer includes the whole-project globals.

The index build computes the context once per revision and reuses it across
files; non-index callers (goto-def, hover, completion, hints) compute it once
per request. No fallback: every caller supplies the context explicitly.
Lookup treated only HierarchyInstantiationSyntax.type as a definition.
Checker and primitive instances use their own syntax, and named port
labels live on the connection, not the port symbol name in the child.
Hierarchy comes from query_instances on a profile compile. Keystroke
lookup still uses the file-closure compile and does not wait on that
profile. UDP and named-port goto now bind through Compilation, so their
navigation kinds are no longer HIR DefKind labels.
Open-file parse of a .svh include currently returns nothing because
headers are not standalone parse units. P3.1 requires those diagnostics
to come from the covering file-closure Compilation, mapped to the
original FileId.
Include headers are not standalone parse units. Open-file parse now
compiles the covering file closure and maps include-buffer diagnostics
back to the original FileId, using the same Compiler seam as keystroke
lookup. Profile semantic diagnostics stay on the background path.
Document diagnostics currently fall back to a full profile Compilation
when the slang ledger is empty. P3.1 requires profile semantic
diagnostics to come from the background compile, not the request path.
Document diagnostics now take parse from the covering file-closure
Compilation and semantic diagnostics from the background ledger.
The request path no longer compiles a full profile. Cached slang
facts stay semantic-only so Vide and parse are merged at publish
time, and stale ledger rows are not treated as current.
P3.1 requires mapping failures to stay visible through the single
materializer. Unlocated slang diagnostics must not be dropped.
The materializer no longer drops slang diagnostics that have no
source range. They stay visible on the mapped file as an empty
file-level range instead of disappearing.
A diagnostic whose slang buffer is not in the compilation map is
currently dropped. P3.1 requires that mapping failure to stay visible
on the covering file.
Diagnostics whose slang buffer is not in this compilation stay visible
on the covering file as file-level results. Their original buffer
coordinates are not applied to a different FileId.
didClose after a profile compile must not duplicate Vide diagnostics.
The compile and semantic-compiler paths must not mention process
Command, which is the WASM diagnostic-path constraint.
P4.1 requires AnalysisHost::new not to start an elaboration worker,
and request-path hover/goto must not wait on revision prewarm.
apply_change no longer spawns vide-revision-prewarm. Hover and goto
compile on the calling thread, so request correctness does not wait
on a background worker and Drop has no join protocol.
P4.2 requires the native BrowserServer/vide_lsp_message seam to answer
package+user hover and goto on the calling thread, without a compiler
worker or a blocking profile compile.
AnalysisHost::new is the only production constructor. The revision
worker is gone, so the alias is dead. Record the WASM emscripten
build script as the extra P4 link gate.
P5.1 requires UnitCatalog to be name → files. Duplicate CU names must
surface as locator search hits, not a Unique/files[0] catalog binding.
UnitCatalog production lookups are name → files. Cursor classification
and find-references/call-edges reconstruct this-file FileFacts hits from
those files instead of treating catalog Unique UnitIds as identity.
P5.2 requires production hierarchy to come from Compilation. Catalog
OwnerId projection of an instantiation type is the old HIR authority.
Cross-file hierarchy is Compilation. DefinitionClass keeps this-file
lexical nameres and paid-parse HirFileId::Macro generated owners; it
does not project catalog source files to a Unique OwnerId.
Production already locates by file. The Unique-style candidates list is
dead. Lock AnalysisSnapshot/ProductStore against a third epoch clock:
the store stays parse-deps, Compiler does not cache Compilation values.
nameres Type does not see compilation-unit modules. Keep this-file
cu_owners_named_in_file so same-file rename/highlight survive. Cross-file
best-effort rename used to reject via HIR catalog binding
(ProjectScopeRequired); it now reports NoDefFound because that token is
Compilation-owned.
Macro-generated modules must stay HirFileId::Macro so rename/highlight
reject or reproject the call site. Source CU owners in this file remain
the this-file lexical path after that miss.
Named ports are compilation names. Lock that pathres locate_hierarchy_targets
must not Unique-bind a port declared in another file; goto still comes from
Compilation.
pathres locate_hierarchy_targets was catalog → OwnerId binding for named
ports, params, inlay, and completion. Cross-file hierarchy is Compilation;
this-file CU owners and paid-parse generated owners stay for local nav.
Old HIR Unique/Ambiguous snapshots of other-file modules are Unresolved.
Package import resolution belongs on DesignMap's export owners, not a
pathres catalog name → OwnerId locator.
Delete pathres locate_packages. Import resolution matches names against
the export-map owners already built for this revision, so pathres is
this-file lexical again. Ambiguous packages stay Ambiguous.
pathres import resolution already looks up DesignMap owners. Reexport
closure must not go back through catalog name → OwnerId.
Package export closure already has the source package OwnerIds. Reexport
edges match those names instead of pathres catalog locate_cu_owners.
Instance `.` members and NamedType are compilation names. Lock that
pathres instance descent and Type nameres do not Unique-bind them;
goto still comes from Compilation.
Instance, interface, checker, and covergroup `.` members are Compilation.
pathres still descends this-file blocks, generate, and clocking. `::` was
already unresolved. This-file typedef Type nameres stays because lookup
at the use is not a compilation symbol.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant