perf(pacquet/resolving-deps-resolver): share ResolveResult via Arc on tree + graph nodes

`ResolveResult.clone()` ran at every visit in `resolve_node`
(into `ResolvedPackage.result`) and again per
`(dep_path, peer-suffix)` slot when `resolve_peers` carved the
graph nodes out of the resolved tree (into
`DependenciesGraphNode.resolve_result`). Both clones deep-copied
every `String` field on `ResolveResult` (`id`, `alias`,
`resolved_via`, `name_ver`, etc.) — 2× per resolved package ×
~1.3k packages = ~2.7k full struct clones on the resolve hot
path.

Wrap once at the resolver boundary so both stores share one
heap allocation:

- `ResolvedPackage.result` and
  `DependenciesGraphNode.resolve_result` re-typed as
  `Arc<ResolveResult>`.
- `resolve_node` wraps the resolver-returned `ResolveResult` in
  `Arc::new` once and uses `Arc::clone` for the store + later
  reads. The `extract_children` / `extract_peer_dependencies`
  borrows work unchanged via `Arc`'s `Deref`.
- `resolve_peers`'s `.clone()` of `pkg.result` is now an
  `Arc::clone` (refcount bump), not a deep copy.

Read sites in `dependencies_graph_to_lockfile` and
`install_with_fresh_lockfile` work unchanged — `&Arc<T>` derefs
to `&T` at the field-init coercion site and at every
`.field`-style access.

Tests: 348/348 in `pacquet-resolving-deps-resolver` +
`pacquet-package-manager` pass. Clippy clean.
This commit is contained in:
Zoltan Kochan
2026-05-22 00:46:26 +02:00
parent a7c94a905d
commit 5d6a420777
4 changed files with 22 additions and 6 deletions

View File

@@ -74,7 +74,7 @@ fn make_node_with_optional(
DependenciesGraphNode {
dep_path: dep_path.clone(),
resolved_package_id: format!("{name}@{version}"),
resolve_result: make_resolve_result(name, version, manifest),
resolve_result: std::sync::Arc::new(make_resolve_result(name, version, manifest)),
children,
peer_dependencies,
transitive_peer_dependencies,
@@ -265,7 +265,7 @@ fn peer_suffixed_dep_path_splits_into_distinct_snapshot_and_package_keys() {
let react_dom = DependenciesGraphNode {
dep_path: react_dom_dep_path.clone(),
resolved_package_id: "react-dom@17.0.2".to_string(),
resolve_result: make_resolve_result(
resolve_result: std::sync::Arc::new(make_resolve_result(
"react-dom",
"17.0.2",
json!({
@@ -273,7 +273,7 @@ fn peer_suffixed_dep_path_splits_into_distinct_snapshot_and_package_keys() {
"version": "17.0.2",
"peerDependencies": { "react": "17.0.2" },
}),
),
)),
children: react_dom_children,
peer_dependencies: react_dom_peers,
transitive_peer_dependencies: HashSet::new(),

View File

@@ -23,7 +23,12 @@ pub struct DependenciesGraphNode {
/// by reference value (cloned) so consumers don't need a separate
/// map lookup.
pub resolved_package_id: String,
pub resolve_result: ResolveResult,
/// Held as `Arc` so the graph's per-occurrence clones (one per
/// `(dep_path, peer-suffix)` slot) reuse the same heap-allocated
/// `ResolveResult` instead of deep-copying each field. The graph
/// is built once and read by the install dispatch; nothing
/// mutates the inner `ResolveResult` after `resolve_peers`.
pub resolve_result: std::sync::Arc<ResolveResult>,
/// `alias → DepPath` edges to children + resolved peers. Children
/// inherited from the per-occurrence tree node, peers added during
/// peer resolution.

View File

@@ -348,6 +348,11 @@ where
specifier: render_specifier(&wanted),
});
};
// Wrap in `Arc` once so the two store sites below (the per-id
// `ResolvedPackage` envelope and the later peer-resolved graph
// node) share one heap-allocated `ResolveResult` instead of
// cloning every `String` field per occurrence.
let result = Arc::new(result);
if let Some(violation) = result.policy_violation.clone() {
lock_recoverable(&ctx.policy_violations).push(violation);
@@ -403,7 +408,7 @@ where
id.clone(),
ResolvedPackage {
id: id.clone(),
result: result.clone(),
result: Arc::clone(&result),
peer_dependencies,
optional: current_is_optional,
},

View File

@@ -80,7 +80,13 @@ pub struct DirectDep {
#[derive(Debug, Clone)]
pub struct ResolvedPackage {
pub id: String,
pub result: ResolveResult,
/// Held as `Arc` so cloning a `ResolvedPackage` (which the
/// per-occurrence tree walk does on every snapshot, and which
/// the peer-resolution pass does when it carves
/// `DependenciesGraphNode`s out of the resolved tree) is an
/// `Arc::clone` instead of a deep copy of every `String` field
/// on `ResolveResult` (id, alias, resolved_via, name_ver, …).
pub result: std::sync::Arc<ResolveResult>,
/// `peerDependencies` from the package's manifest, with names that
/// also appear in the package's own `dependencies` /
/// `optionalDependencies` filtered out (mirrors upstream's