Skip to content

fix(arborist): skip named-edge refresh work for empty leaves - #10131

Open
jeffwidman wants to merge 1 commit into
npm:latestfrom
jeffwidman:perf/skip-empty-leaf-edge-refresh
Open

jeffwidman wants to merge 1 commit into
npm:latestfrom
jeffwidman:perf/skip-empty-leaf-edge-refresh

Conversation

@jeffwidman

@jeffwidman jeffwidman commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

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, and fsChildren are 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:

Flow Median CPU, before -> after Median wall, before -> after
Lockfile writer 6.389 -> 5.815 s (9.0% lower) 5.325 -> 4.675 s (12.2% lower)
Dry-run checker 5.712 -> 5.063 s (11.4% lower) 4.724 -> 4.022 s (14.9% lower)

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:

  • Writer CPU 5.375 -> 5.231 s (2.7% lower); wall 4.223 -> 4.022 s (4.8% lower).
  • Checker CPU 4.679 -> 4.440 s (5.1% lower); wall 3.672 -> 3.421 s (6.8% lower).

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.

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>
@jeffwidman
jeffwidman marked this pull request as ready for review October 15, 2026 08:50
@jeffwidman
jeffwidman requested a review from a team as a code owner October 15, 2026 08:50
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