Skip to content

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

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

jeffwidman wants to merge 3 commits 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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The test can pass with a stale empty-leaf cache because it never verifies that the leaf’s edge resolves to a new foo inserted under root after detachment.

1 open issue
What changed in this PR

Optimizes Arborist’s named-edge refresh by skipping unnecessary work for empty leaves while preserving traversal through nodes with children.

Changes:

  • Return early only when outgoing dependencies, package children, and filesystem children are all empty.
  • Add regression tests for skipped lookups, later dependency additions, and descendant updates.
File Description
workspaces/​arborist/​test/​node.js Adds empty-leaf and descendant-refresh regression tests.
workspaces/​arborist/​lib/​node.js Adds the empty-leaf early return to named-edge refresh.

Review effort: Max

Comment thread workspaces/arborist/test/node.js Resolved
nkolev92
nkolev92 previously approved these changes Nov 11, 2026

@nkolev92 nkolev92 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.
Copilot's suggested for the test make sense if you'd like to address it.

@nkolev92 nkolev92 self-assigned this Nov 11, 2026
jeffwidman and others added 2 commits November 10, 2026 18:52
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues are reported for the empty-leaf refresh optimization.

No open issues 🎉

1 resolved since last review

Review effort: Max

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.

3 participants