Skip to content

Memoize getName/getNameParts on namespaced AST nodes#1685

Closed
chrisdp wants to merge 1 commit into
masterfrom
perf/name-memoization
Closed

Memoize getName/getNameParts on namespaced AST nodes#1685
chrisdp wants to merge 1 commit into
masterfrom
perf/name-memoization

Conversation

@chrisdp
Copy link
Copy Markdown
Contributor

@chrisdp chrisdp commented Apr 29, 2026

Summary

Memoize the result of getName / getNameParts on NamespacedVariableNameExpression, NamespaceStatement, FunctionStatement, ClassStatement, and InterfaceStatement. Validators call these methods per potential namespace reference per scope, and each call walks the AST (findAncestor) and rebuilds the qualified name string from scratch. The result is a pure function of the parsed AST, so caching it on the node is safe and eliminates a large amount of repeated work.

Measured impact

Profiled on a brighterscript build of ~1300 source files (using node --cpu-prof --heap-prof):

Metric Before After Δ
Total CPU sampled time 13.58 s 12.03 s -1.55 s (-11.4%)
Validate phase wall-clock 8.74 s 7.60 s -1.14 s (-13%)
GC self-time 1.37 s 1.08 s -290 ms (-21%)
Total heap allocations 876 MB 869 MB flat (within variance)

CPU profile attribution before this change: NamespacedVariableNameExpression.getName 455 ms self, getNameParts 247 ms self, Statement.getName and the (get name) getter chain together another ~800 ms self. After this change those frames drop out of the top-25 entirely.

Heap stays essentially flat: each relevant AST node grows by two string | undefined slots (~16 bytes), but that's offset by the eliminated intermediate string and array allocations from repeated calls.

Implementation notes

getNameParts returns a defensive slice. Two existing callers mutate the returned array — BrsFile.ts:999 uses .shift(), symbolUtils.ts:141 uses .pop(). The slice on each call is trivial relative to the AST walk it replaces, and preserves the original mutable-array contract for any external callers.

Statement-level getName only writes to the cache when this.parent is set. Without that guard, NamespaceStatement's constructor (which calls this.name to build a SymbolTable label before the AST walk that establishes parent links) would lock in the parentless local name forever. The guard defers caching until after parent linking, so a nested namespace correctly caches its full qualified name on first post-walk read.

Cache fields use the _cached* prefix. AstNode.spec's clone-equality test was updated to skip those slots: the cache populates lazily and need not match between a node and its clone (each populates independently on first read).

🤖 Generated with Claude Code

Validators call `NamespacedVariableNameExpression.getName`,
`NamespaceStatement.getName`, and the corresponding methods on
`FunctionStatement`, `ClassStatement`, and `InterfaceStatement` per
potential namespace reference per scope. Each call walks the AST
(`findAncestor` for the parent namespace) and recursively rebuilds the
fully-qualified name string. Results are pure functions of the parsed
AST and never change, so caching them on the node is safe.

Profiled on a large brighterscript build (~1300 source files):
- Total CPU sampled time: 13.58 s -> 12.03 s (-1.55 s, -11.4%)
- Validate phase wall-clock: 8.74 s -> 7.60 s (-1.14 s, -13%)
- GC self-time: 1.37 s -> 1.08 s (-290 ms, -21%)
- Heap allocations: 876 MB -> 869 MB (essentially flat)

`getNameParts` returns a defensive slice of the cached array because
two existing callers (`BrsFile.ts:999` and `symbolUtils.ts:141`) mutate
the result via `.shift()` and `.pop()`. The slice cost is trivial
relative to the AST walk it replaces.

For Statement-level `getName`, the cache is only written when
`this.parent` is set. Without that guard, `NamespaceStatement`'s
constructor (which calls `this.name` to build the SymbolTable label
before AST walk runs) would lock in the parentless local name forever.

`AstNode.spec` clone-equality test updated to skip `_cached*` slots:
the cache populates lazily and need not match between a node and its
clone.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@chrisdp chrisdp added the create-package create a temporary npm package on every commit label Apr 29, 2026
@rokucommunity-bot
Copy link
Copy Markdown
Contributor

Hey there! I just built a new temporary npm package based on 52394bb. You can download it here or install it by running the following command:

npm install https://github.com/rokucommunity/brighterscript/releases/download/v0.0.0-packages/brighterscript-0.71.1-perf-name-memoization.20260429001621.tgz

@TwitchBronBron
Copy link
Copy Markdown
Member

I don't really want to do this at this time. Too much difficulty knowing when to clear these cached values anytime AST edits are made. We need to think about this more and come up with a better more broad caching system.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

create-package create a temporary npm package on every commit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants