Skip to content

fix(arborist): skip legacy metadata work for v3+ lockfiles - #10130

Draft
jeffwidman wants to merge 1 commit into
npm:latestfrom
jeffwidman:perf/skip-v3-legacy-metadata
Draft

jeffwidman wants to merge 1 commit into
npm:latestfrom
jeffwidman:perf/skip-v3-legacy-metadata

Conversation

@jeffwidman

@jeffwidman jeffwidman commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Blocked on #10276

Avoid doing legacy metadata work that package-only lockfiles do not use, while preserving older lockfile formats.

Review and landing order

The standalone live-tree metadata lookup bug fix in npm/cli#10276 should be reviewed and decided before this optimization. That PR isolates the independent API behavior question of whether node.meta.get() should fall back to metadata from the attached live tree when persisted lockfile metadata is absent.

This PR currently remains based on latest because GitHub cannot keep it as an npm/cli pull request while targeting the fork-only branch used by #10276. As a result, the current diff temporarily includes the prerequisite bug-fix changes from #10276. After #10276 lands, this branch will be rebased and resquashed so #10130 contains only the version 3+ traversal and serialization optimization described below.

Optimization implementation

For lockfile version 3, the legacy builder still performs the required root and header work, then returns before recursively traversing descendants. Version 4 already bypasses the legacy builder because it does not need that representation.

When a tree is attached, the shared version 3+ serialization path removes stale legacy dependencies metadata from retained in-memory state, and returned version 3+ output omits it. Before a tree is attached, legacy input remains available internally for lookup and node hydration, while commit() and toJSON() still omit it from version 3+ returned output. This also lets the same metadata instance switch back to version 1 or version 2 output without losing its legacy source.

Lockfile versions 1 and 2 remain readable, and their output retains its existing legacy metadata generation.

Temporary prerequisite changes in the current diff

The following behavior belongs to #10276 and is present here only until that prerequisite lands and this PR is rebased.

meta.get() checks pending updates and modern packages metadata first. If that lookup misses, it derives metadata from the current tree inventory using the inventory root's coordinate system, without caching or serializing the derived value, then falls back to legacy lockfile metadata. This preserves relative and absolute lookups for linked tops as well as the documented live-tree behavior for omitted nodes, such as a versionless inert optional dependency.

During add(), hydration intentionally bypasses the incomplete node already present in the live inventory, so version 1 and version 2 legacy resolved and integrity values can still decorate newly loaded nodes.

Performance evidence

The performance measurements below are specifically for the version 3 flow that skips recursive legacy descendant generation. Version 4 already bypassed that builder, so this PR does not claim an additional version 4 speedup.

In an offline, warmed large-workspace --package-lock-only writer benchmark, adding the version 3 traversal change to a baseline already containing adaptations of #10127, #10128, and npm/hosted-git-info#347 produced:

Median Baseline With this change Reduction
CPU time 6.898 s 6.130 s 11.1%
Wall time 5.677 s 5.124 s 9.7%

These are three measured alternating pairs after one warmup per arm. All 4,686 manifests/lockfiles plus the npm configuration file were byte-identical across all eight runs, and normalized command output matched. Separate instrumentation showed 4,358 -> 0 generated legacy entries. A public synthetic 128-dependency graph also reduced descendant bundle-membership reads from 256 -> 0 for version 3, while versions 1 and 2 retained their traversal.

The benchmark runtime was Node 24.22.0, npm 11.17.0, and Arborist 9.8.0. This PR currently remains based on latest for the review constraint described above. These measurements describe the incremental native writer benefit after the earlier optimizations, not this PR alone versus unmodified npm, nor end-to-end Dependabot job savings. A dry-run checker does not serialize a lockfile and is not expected to benefit.

A tiny control over 21 pairs had medians of +2.9% CPU and +0.4% wall time; paired bootstrap intervals crossed zero. That result is inconclusive, not proof of regression-free performance.

Validation

Latest targeted validation of the current combined diff:

  • Targeted Tap runs for test/node.js and test/shrinkwrap.js — passed; the Shrinkwrap suite passed all 53 subtests.
  • The node.js snapshot diff is limited to the three linked-top metadata expectations, which now contain the intended live child metadata from the temporary fix(arborist): use live metadata for shrinkwrap lookups #10276 prerequisite changes.
  • ./node_modules/.bin/eslint workspaces/arborist/lib/shrinkwrap.js workspaces/arborist/test/shrinkwrap.js — passed.
  • git diff --check — passed.
  • Reusing the Shrinkwrap-relative location for a linked top's root inventory made both relative and absolute linked-top lookup regressions fail.
  • Allowing add() hydration to use the live-tree fallback made its legacy resolved and integrity restoration regression fail.
  • Unconditionally deleting legacy metadata from a no-tree version 3 commit made both commit() and toJSON() retention regressions fail.
  • Removing attached-tree version 3+ cleanup made the version 3 and version 4 stale-metadata regressions fail.
  • Removing the live-tree inventory fallback made the version 3 and version 4 inert-node lookup regressions fail.

Earlier validation of the original version 3 traversal optimization ran node bin/npm-cli.js test --workspace=@npmcli/arborist --ignore-scripts -- --jobs=4 and passed all 59 test files with 100% statements, branches, functions, and lines. The full npm CLI suite was not run.

The optimization tests cover skipped version 3 descendant work, retained root metadata, version 3 and version 4 stale legacy cleanup, no-tree legacy retention, legacy input, equivalent version 1 and version 2 output, git metadata, aliases, mixed dependency flags, and cyclic links. The temporary #10276 prerequisite tests cover linked-top relative and absolute lookup coordinates, non-caching live-tree lookup for omitted inert nodes, and legacy hydration during add().

References

Comment thread workspaces/arborist/lib/shrinkwrap.js Resolved

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.

🔵 Needs a closer look

Maintainer judgment is needed on whether removing synthesized node.meta.data.dependencies and changing meta.get() fallback after v3 commits affects supported API behavior and requires a compatibility or release plan.

No open issues 🎉

What changed in this PR

This PR reduces Arborist’s v3 lockfile generation work by skipping legacy dependency traversal while preserving root metadata and v1/v2 generation. Serialized v3 output is intended to remain unchanged; the draft explicitly leaves compatibility of the changed in-memory metadata open for discussion.

Changes:

  • Skip legacy descendants for v3 commits and remove stale legacy dependency data.
  • Add regression coverage for format compatibility, metadata lookup behavior, and legacy output; update six raw-metadata snapshots.
  • Document commit() and toJSON() as ways to obtain data in the selected lockfile format.
File Description
workspaces/​arborist/​test/​shrinkwrap.js Tests skipped traversal, legacy compatibility, and changed v3 lookup behavior.
workspaces/​arborist/​tap-snapshots/​test/​shrinkwrap.js.test.cjs Removes legacy dependency blocks from six v3 metadata snapshots.
workspaces/​arborist/​README.md Clarifies format-specific metadata access.
workspaces/​arborist/​lib/​shrinkwrap.js Stops legacy traversal for v3 after root processing and deletes legacy dependencies.

Review effort: Balanced

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 change breaks documented meta.get() lookups for versionless inert optional nodes after commit() or toJSON(), requiring preserved lookup behavior or an explicit compatibility plan.

1 open issue

Review effort: Max

Comment thread workspaces/arborist/lib/shrinkwrap.js Resolved
@jeffwidman
jeffwidman force-pushed the perf/skip-v3-legacy-metadata branch from 4174b03 to fae00da Compare November 11, 2026 03:38
@jeffwidman jeffwidman changed the title fix(arborist): skip legacy dependency generation for v3 lockfiles fix(arborist): skip legacy metadata work for v3+ lockfiles Nov 11, 2026
@jeffwidman
jeffwidman force-pushed the perf/skip-v3-legacy-metadata branch from fae00da to d1251fc Compare November 11, 2026 03:59
@jeffwidman
jeffwidman requested a max review from Copilot November 11, 2026 04:07

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

Shrinkwrap handling can return metadata for the wrong node, fail to restore legacy resolution and integrity values, and discard legacy dependency data before a tree is attached.

3 open issues
1 resolved since last review

Review effort: Max

Comment thread workspaces/arborist/lib/shrinkwrap.js Resolved
Comment thread workspaces/arborist/lib/shrinkwrap.js Resolved
Comment thread workspaces/arborist/lib/shrinkwrap.js Resolved
@jeffwidman
jeffwidman force-pushed the perf/skip-v3-legacy-metadata branch from d1251fc to 98c5f89 Compare November 11, 2026 05:25
Co-authored-by: martin <43253759+martinrrm@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.

🟡 Changes recommended

Incorrect link-path mapping and metadata selection cause valid dependency lookups to return empty or incomplete data, omitting stored resolved and integrity values.

3 open issues
3 resolved since last review

Review effort: Max

Comment on lines +699 to +702
const inventoryLocation = useTree && this.tree &&
relpath(this.tree.root.realpath, nodePathResolved)
const node = inventoryLocation &&
this.tree.root.inventory.get(inventoryLocation)
Comment on lines +699 to +702
const inventoryLocation = useTree && this.tree &&
relpath(this.tree.root.realpath, nodePathResolved)
const node = inventoryLocation &&
this.tree.root.inventory.get(inventoryLocation)
Comment on lines +703 to +704
if (node) {
return Shrinkwrap.metaFromNode(node, this.path, this.resolveOptions)
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