perf(resolving-npm-resolver): probe deprecation without hydrating version manifests

With minimumReleaseAge active (the default), every range pick runs
filter_pkg_metadata_by_publish_date, and any dist-tag pointing at a
version outside the maturity cutoff repopulates by scanning all
candidate versions, reading each candidate's deprecated flag. Reading
that flag through PackageVersions::get hydrated the full manifest
fragment (including the flattened catch-all map), which a time profile
of a warm-cache fresh resolve of the babylon fixture showed as the
single largest CPU consumer (~10 thread-seconds, all on the resolve
task's critical path).

PackageVersions::is_deprecated answers the same question from the raw
fragment: a substring pre-check for the key text, then a single-field
deserialize using the same normalization as PackageVersion::deprecated.
The tag-repopulation loop now also parses candidate versions once per
filter call instead of once per tag, matching the parsedSemverCache in
pnpm's filterPkgMetadataByPublishDate, and the deprecated-pick fallback
in pick_version_by_version_range uses the probe instead of hydrating
every version.

Warm-cache fresh resolve of the babylon fixture: resolve_workspace
7.5s -> 2.6s. Resolved version sets are byte-identical before/after
across the vue and babylon fixtures.
This commit is contained in:
Zoltan Kochan
2026-06-11 17:17:34 +02:00
parent cb18695c5b
commit 9a3cfa40fd
5 changed files with 200 additions and 25 deletions

View File

@@ -151,7 +151,7 @@ where
/// A bool `true` becomes `Some("")`, a bool `false` becomes `None`;
/// a string stays as `Some(s)`. Missing field defaults to `None` via
/// the `#[serde(default)]` on the field itself.
fn deserialize_deprecated_field<'de, Deser>(
pub(crate) fn deserialize_deprecated_field<'de, Deser>(
deserializer: Deser,
) -> Result<Option<String>, Deser::Error>
where

View File

@@ -24,7 +24,16 @@ use std::{
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::value::RawValue;
use crate::package_version::PackageVersion;
use crate::package_version::{PackageVersion, deserialize_deprecated_field};
/// Single-field view of a version manifest for
/// [`PackageVersions::is_deprecated`] — same normalization as
/// [`PackageVersion::deprecated`], every other field skipped.
#[derive(Deserialize)]
struct DeprecatedProbe {
#[serde(default, deserialize_with = "deserialize_deprecated_field")]
deprecated: Option<String>,
}
#[derive(Debug, Default, Clone)]
pub struct PackageVersions {
@@ -141,6 +150,36 @@ impl PackageVersions {
self.slots.contains_key(version)
}
/// Whether `version` is marked deprecated, equivalent to
/// `get(version).is_some_and(|manifest| manifest.deprecated.is_some())`
/// but without hydrating the full manifest: an unhydrated fragment
/// is probed with a single-field deserialize (skipped entirely when
/// the fragment text doesn't contain the `"deprecated"` key).
/// An absent version or an undecodable fragment reads as not
/// deprecated, matching the "undecodable fragment behaves as if
/// the version were absent" contract of [`PackageVersions::get`].
///
/// The pick paths consult deprecation for *many* candidate
/// versions per packument (dist-tag repopulation, the
/// deprecated-pick fallback); hydrating each candidate parses its
/// whole manifest — including the flattened catch-all map — and
/// dominated warm-resolve CPU.
#[must_use]
pub fn is_deprecated(&self, version: &str) -> bool {
let Some(slot) = self.slots.get(version) else { return false };
if let Some(parsed) = slot.parsed.get() {
return parsed.as_ref().is_some_and(|manifest| manifest.deprecated.is_some());
}
let Some(json) = slot.source.json() else { return false };
// Absence of the key text anywhere in the fragment proves the
// field is absent; presence (possibly in an unrelated nested
// position) falls through to the real parse.
if !json.contains(r#""deprecated""#) {
return false;
}
serde_json::from_str::<DeprecatedProbe>(&json).is_ok_and(|probe| probe.deprecated.is_some())
}
/// Version strings, in `HashMap` order. Never hydrates.
pub fn keys(&self) -> impl Iterator<Item = &String> {
self.slots.keys()

View File

@@ -126,3 +126,60 @@ fn latest_returns_none_for_dangling_or_undecodable_tag() {
parse_package(r#"{"name": "foo", "dist-tags": {"latest": "9.9.9"}, "versions": {}}"#);
assert!(dangling.latest().is_none());
}
#[test]
fn is_deprecated_probes_without_hydrating() {
let package = parse_package(
r#"{
"name": "foo",
"dist-tags": {},
"versions": {
"1.0.0": {"name": "foo", "version": "1.0.0", "dist": {"integrity": "sha512-a", "tarball": "https://r/foo-1.0.0.tgz"}},
"1.1.0": {"name": "foo", "version": "1.1.0", "deprecated": "use 2.x", "dist": {"integrity": "sha512-b", "tarball": "https://r/foo-1.1.0.tgz"}},
"1.2.0": {"name": "foo", "version": "1.2.0", "deprecated": false, "dist": {"integrity": "sha512-c", "tarball": "https://r/foo-1.2.0.tgz"}},
"1.3.0": {"name": "foo", "version": "1.3.0", "deprecated": true, "dist": {"integrity": "sha512-d", "tarball": "https://r/foo-1.3.0.tgz"}},
"1.4.0": {"name": "foo", "version": "1.4.0", "deprecated": "", "dist": {"integrity": "sha512-e", "tarball": "https://r/foo-1.4.0.tgz"}},
"1.9.0": {"corrupt": "fragment", "deprecated": 1}
}
}"#,
);
assert!(!package.versions.is_deprecated("1.0.0"));
assert!(package.versions.is_deprecated("1.1.0"));
assert!(!package.versions.is_deprecated("1.2.0"));
assert!(package.versions.is_deprecated("1.3.0"));
assert!(package.versions.is_deprecated("1.4.0"));
// Absent version and undecodable-probe fragments read as not
// deprecated, matching the absent-version contract of `get`.
assert!(!package.versions.is_deprecated("9.9.9"));
assert!(!package.versions.is_deprecated("1.9.0"));
// The probe must agree with the hydrated field on every slot it
// can hydrate, and must not have hydrated anything itself: `get`
// still parses fresh (no cached Arc identity from the probe).
for version in ["1.0.0", "1.1.0", "1.2.0", "1.3.0", "1.4.0"] {
let manifest = package.versions.get(version).expect("hydrate");
assert_eq!(
package.versions.is_deprecated(version),
manifest.deprecated.is_some(),
"probe vs hydrated disagree for {version}",
);
}
}
/// A fragment that mentions `"deprecated"` only in an unrelated nested
/// position (a dependency literally named `deprecated`) must fall
/// through to the real parse and report not-deprecated.
#[test]
fn is_deprecated_ignores_unrelated_key_text() {
let package = parse_package(
r#"{
"name": "foo",
"dist-tags": {},
"versions": {
"1.0.0": {"name": "foo", "version": "1.0.0", "dependencies": {"deprecated": "^0.0.2"}, "dist": {"integrity": "sha512-a", "tarball": "https://r/foo-1.0.0.tgz"}}
}
}"#,
);
assert!(!package.versions.is_deprecated("1.0.0"));
}

View File

@@ -27,6 +27,7 @@
//! module pulls in no I/O.
use std::{
cell::OnceCell,
collections::BTreeMap,
sync::{Arc, LazyLock},
};
@@ -345,15 +346,12 @@ pub fn pick_version_by_version_range(
// non-deprecated subset. Matches upstream's loop at
// pickPackageFromMeta.ts#L194-L201.
if let Some(ref picked) = max_pick {
let picked_meta = opts.meta.versions.get(picked);
let picked_is_deprecated = picked_meta.is_some_and(|version| version.deprecated.is_some());
let picked_is_deprecated = opts.meta.versions.is_deprecated(picked);
if picked_is_deprecated && all_versions.len() > 1 {
let non_deprecated: Vec<&str> = opts
.meta
.versions
let non_deprecated: Vec<&str> = all_versions
.iter()
.filter(|(_, manifest)| manifest.deprecated.is_none())
.map(|(name, _)| name.as_str())
.copied()
.filter(|version| !opts.meta.versions.is_deprecated(version))
.collect();
if let Some(non_deprecated_max) = max_satisfying(&non_deprecated, opts.version_range) {
return Some(non_deprecated_max);
@@ -431,6 +429,14 @@ pub fn filter_pkg_metadata_by_publish_date(
});
let mut dist_tags_within_date = std::collections::HashMap::new();
// Candidate versions parsed once per filter call and shared by
// every repopulated tag, with the deprecation flag resolved
// lazily per candidate — mirrors upstream's `parsedSemverCache`.
// Deprecation goes through [`PackageVersions::is_deprecated`]
// instead of hydrating each candidate's manifest; the hydration
// per comparison dominated warm-resolve CPU on packuments with
// out-of-cutoff dist-tags.
let mut parsed_candidates: Option<Vec<(Version, &String, OnceCell<bool>)>> = None;
for (tag, version) in &meta.dist_tags {
if versions_within_date.contains_key(version) {
dist_tags_within_date.insert(tag.clone(), version.clone());
@@ -438,35 +444,43 @@ pub fn filter_pkg_metadata_by_publish_date(
}
let Ok(original) = Version::parse(version) else { continue };
let original_is_prerelease = !original.pre_release.is_empty();
let mut best_version: Option<(Version, &String)> = None;
for candidate_raw in versions_within_date.keys() {
let Ok(candidate) = Version::parse(candidate_raw) else { continue };
let candidates = parsed_candidates.get_or_insert_with(|| {
versions_within_date
.keys()
.filter_map(|raw| {
Version::parse(raw).ok().map(|parsed| (parsed, raw, OnceCell::new()))
})
.collect()
});
let deprecated = |slot: &(Version, &String, OnceCell<bool>)| -> bool {
*slot.2.get_or_init(|| versions_within_date.is_deprecated(slot.1))
};
let mut best_index: Option<usize> = None;
for (index, slot) in candidates.iter().enumerate() {
let (candidate, _, _) = slot;
if tag != "latest" && candidate.major != original.major {
continue;
}
if candidate.pre_release.is_empty() == original_is_prerelease {
continue;
}
match best_version {
None => best_version = Some((candidate, candidate_raw)),
Some((ref best, best_raw)) => {
let best_deprecated = versions_within_date
.get(best_raw)
.is_some_and(|manifest| manifest.deprecated.is_some());
let candidate_deprecated = versions_within_date
.get(candidate_raw)
.is_some_and(|manifest| manifest.deprecated.is_some());
let candidate_wins = (candidate > *best
match best_index {
None => best_index = Some(index),
Some(best) => {
let best_slot = &candidates[best];
let best_deprecated = deprecated(best_slot);
let candidate_deprecated = deprecated(slot);
let candidate_wins = (*candidate > best_slot.0
&& best_deprecated == candidate_deprecated)
|| (best_deprecated && !candidate_deprecated);
if candidate_wins {
best_version = Some((candidate, candidate_raw));
best_index = Some(index);
}
}
}
}
if let Some((_, best_raw)) = best_version {
dist_tags_within_date.insert(tag.clone(), best_raw.clone());
if let Some(best) = best_index {
dist_tags_within_date.insert(tag.clone(), candidates[best].1.clone());
}
}

View File

@@ -602,3 +602,68 @@ fn pick_from_meta_returns_none_for_undecodable_exact_version() {
.expect("ok");
assert!(picked.is_none());
}
/// The dist-tag repopulation tie-break prefers a non-deprecated
/// candidate over a higher deprecated one (and only falls back to
/// deprecated candidates when nothing else matches the tag family).
#[test]
fn filter_tag_rewrite_prefers_non_deprecated_candidate() {
let mut pkg = make_package(
"acme",
&[
("2.1.0", None),
("2.2.0", Some("use 3.x")),
("2.5.0", None), // dropped by the cutoff
],
&[("old", "2.5.0")],
);
pkg.time = Some(make_time_map(&[
("2.1.0", "2024-01-01T00:00:00.000Z"),
("2.2.0", "2024-06-01T00:00:00.000Z"),
("2.5.0", "2025-06-01T00:00:00.000Z"),
]));
let cutoff = parse_iso("2025-01-01T00:00:00.000Z");
let filtered = filter_pkg_metadata_by_publish_date(&pkg, cutoff, None);
assert_eq!(
filtered.dist_tag("old"),
Some("2.1.0"),
"non-deprecated 2.1.0 must beat the higher deprecated 2.2.0",
);
}
/// The repopulation tie-break must read deprecation from the raw
/// fragments without full-manifest hydration. The fragments here are
/// valid JSON but not decodable as complete version manifests, so a
/// tie-break that hydrates to check `deprecated` sees every candidate
/// as undecodable (= not deprecated) and picks the higher deprecated
/// version instead. Guards the no-hydration deprecation probe the
/// publish-date filter relies on — also matching pnpm, which reads
/// `versions[candidate].deprecated` off plain parsed JSON without
/// validating the rest of the entry.
#[test]
fn filter_tag_rewrite_reads_deprecation_from_raw_fragments() {
let mut pkg: Package = serde_json::from_str(
r#"{
"name": "acme",
"dist-tags": {"old": "2.5.0"},
"versions": {
"2.1.0": {"name": "acme"},
"2.2.0": {"name": "acme", "deprecated": "use 3.x"},
"2.5.0": {"name": "acme"}
}
}"#,
)
.expect("parse package");
pkg.time = Some(make_time_map(&[
("2.1.0", "2024-01-01T00:00:00.000Z"),
("2.2.0", "2024-06-01T00:00:00.000Z"),
("2.5.0", "2025-06-01T00:00:00.000Z"),
]));
let cutoff = parse_iso("2025-01-01T00:00:00.000Z");
let filtered = filter_pkg_metadata_by_publish_date(&pkg, cutoff, None);
assert_eq!(
filtered.dist_tag("old"),
Some("2.1.0"),
"deprecation must be read from the raw fragment, not via hydration",
);
}