From ad335434e5376d29f38882d0dceb2461bfd8d5c6 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Thu, 20 Aug 2026 01:31:03 +0900 Subject: perf(semver): memoize Intervals::isSubsetOf on its operand pair isSubsetOf builds a throwaway `MultiConstraint([candidate, constraint])` and hands it to `Intervals::get`, whose cache key is the constraint's string form. That MultiConstraint is fresh on every call, so its memoized string form is always cold and the whole intersection has to be stringified recursively -- profiling put `AnyConstraint: Display::fmt` at 3.6 % of self time, more than the interval computation the cache exists to skip. Cache the answer on the pair of operand strings instead. Both operands are long-lived, so each one's own string memo stays warm and the throwaway intersection is never built on a hit. laravel/framework require --no-install (warm cache, network disabled): instructions:u 9885142257 -> 9142874710 (-7.5 %) cycles:u 4858265879 -> 4444135669 (-8.5 %) wall (hyperfine, 20 runs) 1.322 s +- 0.018 s -> 1.224 s +- 0.011 s (-7.4 %) monolog/monolog is unchanged (101.6 ms -> 100.3 ms, within noise). composer.lock is byte-identical for both packages. Co-Authored-By: Claude Opus 5 (1M context) --- crates/shirabe-semver/src/intervals.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) (limited to 'crates') diff --git a/crates/shirabe-semver/src/intervals.rs b/crates/shirabe-semver/src/intervals.rs index 101ba05c..ba574814 100644 --- a/crates/shirabe-semver/src/intervals.rs +++ b/crates/shirabe-semver/src/intervals.rs @@ -23,6 +23,12 @@ fn intervals_cache() -> &'static Mutex> INTERVALS_CACHE.get_or_init(|| Mutex::new(IndexMap::new())) } +static SUBSET_CACHE: OnceLock>> = OnceLock::new(); + +fn subset_cache() -> &'static Mutex> { + SUBSET_CACHE.get_or_init(|| Mutex::new(IndexMap::new())) +} + fn op_sort_order(op: &str) -> i64 { match op { ">=" => -3, @@ -39,6 +45,7 @@ pub struct Intervals; impl Intervals { pub fn clear() { *intervals_cache().lock().unwrap() = IndexMap::new(); + *subset_cache().lock().unwrap() = IndexMap::new(); } pub fn is_subset_of( @@ -53,6 +60,24 @@ impl Intervals { return Ok(false); } + // Keying on the two operands keeps the memoized string form of each long-lived constraint + // warm. Building the intersection first would instead stringify a throwaway + // MultiConstraint on every call. + let key = (candidate.to_string(), constraint.to_string()); + if let Some(cached) = subset_cache().lock().unwrap().get(&key) { + return Ok(*cached); + } + + let result = Self::compute_is_subset_of(candidate, constraint)?; + subset_cache().lock().unwrap().insert(key, result); + + Ok(result) + } + + fn compute_is_subset_of( + candidate: &AnyConstraint, + constraint: &AnyConstraint, + ) -> anyhow::Result { let multi = MultiConstraint::new(vec![candidate.clone(), constraint.clone()], true, None).into(); let intersection_intervals = Self::get(&multi)?; -- cgit v1.3.1-4-g156e