Repository navigation
fix(arborist): skip named-edge refresh work for empty leaves - #10131
Open
jeffwidman wants to merge 1 commit into
Open
jeffwidman wants to merge 1 commit into
jeffwidman wants to merge 1 commit into
Conversation
Return before looking up named edges when a node has no outgoing dependencies or children. Keep descendant traversal and later graph mutations intact, with regression coverage for both package and filesystem children. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Skip the no-op body of named-edge refresh for nodes with no outgoing dependencies and no children.
Why
While constructing a virtual dependency tree, adding or moving a package refreshes named edges through existing descendants. An empty leaf cannot have an edge to refresh or a descendant to visit, yet it still performs a case-insensitive lookup and enters both child loops.
Return immediately only when
edgesOut,children, andfsChildrenare all empty. This does not cache emptiness: later dependency or child additions remain visible. Nodes without outgoing dependencies but with package or filesystem children must still be traversed.The traversal still calls the method for empty leaves; this skips its unnecessary body rather than changing the algorithm's asymptotic complexity. There is no new cache, index, deferred resolution, or intended public API/output change.
Regression tests verify that an empty leaf performs no edge lookup, that adding dependencies later restores normal refresh behavior, and that dependency-free parents still refresh both package and filesystem descendants when targets are added or removed.
Performance
In an offline large-workspace native npm benchmark:
The baseline already contains runtime adaptations of #10127, #10128, npm/hosted-git-info#347, and #10130; the candidate adds only this early return. This branch is independently based on
latest, not stacked on those PRs.Method: Node 24.22.0, npm 11.17.0 / Arborist 9.8.0, networking disabled, 4 CPUs / 12 GB, one excluded warmup per arm and three alternating measured pairs. Writer uses
--package-lock-only --force --ignore-scripts; checker uses--package-lock-only --dry-run=true --ignore-scripts; both additionally disable audit/funding and use--offline --timing.All compared manifests, lockfiles (including the writer's hidden lockfile), configuration files, and normalized logs were identical. Separate instrumentation avoided 3,997,148 empty-leaf map lookups, reducing total map gets from 9,439,354 to 5,442,206. A public synthetic tree with 128 dependency-free packages reduced map gets from 8,512 to 256, with the same children and inventory.
Savings overlap the canonical-ASCII map normalization fast path in #10132. A separate five-pair comparison after that fast path still measured:
Do not add the standalone percentages together. Tiny seven-pair controls were roughly neutral (standalone CPU -1.5%, wall +0.1%; after the ASCII fast path CPU -0.9%, wall -0.4%). These are workload-specific native-command measurements, not a universal regression-free claim or complete Dependabot job savings.
Validation
On this standalone branch, based on npm 12.2.0 / Arborist 10.0.3, using existing development dependencies:
node bin/npm-cli.js test --workspace=@npmcli/arborist --ignore-scripts -- --jobs=4— passed all 59 test files; 100% statements, branches, functions, and lines.eslint workspaces/arborist/lib/node.js workspaces/arborist/test/node.js— passed.git diff --check— passed.No snapshots were changed. During the preceding investigation, an additional 24-command offline workspace/lockfile-version matrix passed with this optimization and the ASCII fast path together; the full suite above was rerun with this optimization alone.
The full npm CLI suite and Windows/macOS runs were not performed.
References
Related independent native-work reductions: #10127, #10128, npm/hosted-git-info#347, #10130.
The related normalization fast path is proposed independently in #10132. Neither PR depends on the other.