Skip to content

Replace ingredient cache with faster ingredient map #921

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

ibraheemdev
Copy link
Contributor

@ibraheemdev ibraheemdev commented Jun 20, 2025

An alternative to #919, this uses a lock-free map for ingredient access instead of DashMap. It looks like this isn't fast enough to get rid of the IngredientCache entirely, but it's pretty close. I also added a fast-path that avoids checking for the view downcaster if the ingredient has already been registered. This will make the slow-path slower, but that only matters the first time the query is called on a given database.

Copy link

netlify bot commented Jun 20, 2025

Deploy Preview for salsa-rs canceled.

Name Link
🔨 Latest commit 578d464
🔍 Latest deploy log https://app.netlify.com/projects/salsa-rs/deploys/685db34b88ced80008a38764

Copy link

codspeed-hq bot commented Jun 20, 2025

CodSpeed Performance Report

Merging #921 will degrade performances by 11.19%

Comparing ibraheemdev:ibraheem/remove-ingredient-cache (578d464) with master (0666e20)

Summary

❌ 5 (👁 5) regressions
✅ 7 untouched benchmarks

Benchmarks breakdown

Benchmark BASE HEAD Change
👁 amortized[Input] 3.2 µs 3.6 µs -11.19%
👁 amortized[InternedInput] 3.2 µs 3.5 µs -6.53%
👁 amortized[SupertypeInput] 3.9 µs 4.2 µs -7.96%
👁 new[InternedInput] 5.4 µs 5.7 µs -4.86%
👁 new[SupertypeInput] 16.8 µs 17.7 µs -5.44%

@ibraheemdev
Copy link
Contributor Author

ibraheemdev commented Jun 20, 2025

Hmm looks like this may not be good enough... I guess we could keep the IngredientCache and take the 10% hit for the multiple-database path. 10% seems a lot better than #919 at least (which was ~50%).

Copy link
Contributor

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

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

This is great and I prefer it over the other PR not just because it's much faster but also because it doesn't require using raw-api.

I do think it makes sense to keep the secondary. A 10% regression for the most common case seems a lot.

Main

single

ty_walltime     fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ small                      │               │               │               │         │
   ╰─ pydantic  296.6 ms      │ 305.5 ms      │ 299.5 ms      │ 300.5 ms      │ 3       │ 3

multi

ty_walltime     fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ small                      │               │               │               │         │
   ╰─ pydantic  69.42 ms      │ 612.2 ms      │ 605.8 ms      │ 429.1 ms      │ 3       │ 3

This PR

single

ty_walltime     fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ small                      │               │               │               │         │
   ╰─ pydantic  303.8 ms      │ 322.4 ms      │ 305.9 ms      │ 310.7 ms      │ 3       │ 3

multi

ty_walltime     fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ small                      │               │               │               │         │
   ╰─ pydantic  55.83 ms      │ 79.95 ms      │ 57.73 ms      │ 64.5 ms       │ 3       │ 3

Overall: The multi-threading regression is now about the same (~10%) as the single threaded regression when using multiple databases

@MichaReiser
Copy link
Contributor

Hmm, it does seem that shuttle got stuck somewhere....

@ibraheemdev
Copy link
Contributor Author

because it doesn't require using raw-api

The other PR doesn't actually need the raw-api, that just made it easier to make the shuttle shim.. it looks like we'll need a shuttle shim for this one as well. I now realized the shim can just return a copy of the value instead of having to mimic the guard API.

@ibraheemdev
Copy link
Contributor Author

It might also be worth adding specific benchmarks to compare running with a second database, because right now (for ty_walltime) we are assuming that the fastest run is the one with the first database. At least on my machine it wasn't clear that was the case; the gap was a lot closer and running into noise.

@MichaReiser
Copy link
Contributor

Did you change the sample count to 1?

@ibraheemdev
Copy link
Contributor Author

Yes. Did you run those benchmarks on this PR directly? Because with the IngredientCache removed there should be no difference between the runs using the first database, so your numbers might just indicate noise?

@MichaReiser
Copy link
Contributor

MichaReiser commented Jun 22, 2025

Yes. Did you run those benchmarks on this PR directly? Because with the IngredientCache removed there should be no difference between the runs using the first database, so your numbers might just indicate noise?

I did see #921 (review). The results are fairly consistent, with the first run being slightly slower. It was mainly apparent in the multi-threaded case.

But you can try running some end-to-end benchmarks (that call the CLI) to get a better sense for the regression

@Veykril
Copy link
Member

Veykril commented Jun 25, 2025

I'd expect that removing IngredientCache will always hurt performance for the happy one database case, given that instead of a mere Nonce check we now need to go through a lock and a map.

I'd really prefer to keep the fast path for the happy case, given that is what usually matters for applications here (at least for r-a, we only ever have one database in production) 😕

@ibraheemdev ibraheemdev force-pushed the ibraheem/remove-ingredient-cache branch 2 times, most recently from e402d0c to c944589 Compare June 25, 2025 22:41
@ibraheemdev
Copy link
Contributor Author

ibraheemdev commented Jun 25, 2025

I'd expect that removing IngredientCache will always hurt performance for the happy one database case, given that instead of a mere Nonce check we now need to go through a lock and a map.

There's no lock for the read path here, but yeah I agree. I was just curious to see how close we could get without special casing the single-database case (and it seems like we got pretty close!). I updated the code and added back the IngredientCache.

@ibraheemdev
Copy link
Contributor Author

ibraheemdev commented Jun 25, 2025

Hmm the benchmark create a new database on every run, so they are measuring the slow path here, so I guess the regression is expected.

@ibraheemdev ibraheemdev force-pushed the ibraheem/remove-ingredient-cache branch from 36ec61f to 32be70b Compare June 26, 2025 02:45
src/zalsa.rs Outdated
Comment on lines 300 to 305
#[doc(hidden)]
#[inline]
pub fn lookup_jar_by_type<J: Jar>(&self) -> Option<IngredientIndex> {
let jar_type_id = TypeId::of::<J>();
self.jar_map.get(&jar_type_id, &self.jar_map.guard())
}
Copy link
Contributor

@MichaReiser MichaReiser Jun 26, 2025

Choose a reason for hiding this comment

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

This is a bit unfortunate that we need a new method for this. I guess it shouldn't matter in the fast-path where we always exit early but it does mean that self.jar_map.get is called three times in a slow path of a query (and that might affect inlining).

Could we rewrite this to an entry-style API where jar_by_type returns an Entry struct that has an is_add function (or similar) and a get to get the value (which might insert it).

Entry api

Subject: [PATCH] Only yield if all heads result in a cycle. Retry if even just one inner cycle made progress (in which case there's a probably a new memo)
---
Index: src/accumulator.rs
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/accumulator.rs b/src/accumulator.rs
--- a/src/accumulator.rs	(revision 32be70bbbf53822e97077c22549c917318056051)
+++ b/src/accumulator.rs	(date 1750920458682)
@@ -64,7 +64,7 @@
 impl<A: Accumulator> IngredientImpl<A> {
     /// Find the accumulator ingredient for `A` in the database, if any.
     pub fn from_zalsa(zalsa: &Zalsa) -> Option<&Self> {
-        let index = zalsa.add_or_lookup_jar_by_type::<JarImpl<A>>();
+        let index = zalsa.lookup_jar_by_type::<JarImpl<A>>().get();
         let ingredient = zalsa.lookup_ingredient(index).assert_type::<Self>();
         Some(ingredient)
     }
Index: src/zalsa.rs
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/zalsa.rs b/src/zalsa.rs
--- a/src/zalsa.rs	(revision 32be70bbbf53822e97077c22549c917318056051)
+++ b/src/zalsa.rs	(date 1750920358014)
@@ -299,23 +299,19 @@
     /// **NOT SEMVER STABLE**
     #[doc(hidden)]
     #[inline]
-    pub fn lookup_jar_by_type<J: Jar>(&self) -> Option<IngredientIndex> {
-        let jar_type_id = TypeId::of::<J>();
-        self.jar_map.get(&jar_type_id, &self.jar_map.guard())
-    }
-
-    /// **NOT SEMVER STABLE**
-    #[doc(hidden)]
-    #[inline]
-    pub fn add_or_lookup_jar_by_type<J: Jar>(&self) -> IngredientIndex {
+    pub fn lookup_jar_by_type<J: Jar>(&self) -> LookupJarByType<'_, J> {
         let jar_type_id = TypeId::of::<J>();
-
         let guard = self.jar_map.guard();
         if let Some(index) = self.jar_map.get(&jar_type_id, &guard) {
-            return index;
+            return LookupJarByType::Registered(index);
         };
 
-        self.add_or_lookup_jar_by_type_slow::<J>(jar_type_id, guard)
+        LookupJarByType::Missing {
+            jar_type_id,
+            zalsa: self,
+            guard,
+            _jar: PhantomData::default(),
+        }
     }
 
     #[cold]
@@ -567,3 +563,33 @@
     // SAFETY: the caller must guarantee that `T` is a wide pointer for `U`
     unsafe { &mut *u }
 }
+
+#[doc(hidden)]
+pub enum LookupJarByType<'a, Jar> {
+    Registered(IngredientIndex),
+    Missing {
+        jar_type_id: TypeId,
+        zalsa: &'a Zalsa,
+        guard: papaya::LocalGuard<'a>,
+        _jar: PhantomData<Jar>,
+    },
+}
+
+impl<J: Jar> LookupJarByType<'_, J> {
+    pub const fn is_missing(&self) -> bool {
+        matches!(self, LookupJarByType::Missing { .. })
+    }
+
+    #[inline]
+    pub fn get(self) -> IngredientIndex {
+        match self {
+            LookupJarByType::Registered(index) => index,
+            LookupJarByType::Missing {
+                jar_type_id,
+                zalsa,
+                guard,
+                _jar,
+            } => zalsa.add_or_lookup_jar_by_type_slow::<J>(jar_type_id, guard),
+        }
+    }
+}
Index: components/salsa-macro-rules/src/setup_tracked_struct.rs
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/components/salsa-macro-rules/src/setup_tracked_struct.rs b/components/salsa-macro-rules/src/setup_tracked_struct.rs
--- a/components/salsa-macro-rules/src/setup_tracked_struct.rs	(revision 32be70bbbf53822e97077c22549c917318056051)
+++ b/components/salsa-macro-rules/src/setup_tracked_struct.rs	(date 1750920452744)
@@ -188,7 +188,7 @@
                         $zalsa::IngredientCache::new();
 
                     CACHE.get_or_create(zalsa, || {
-                        zalsa.add_or_lookup_jar_by_type::<$zalsa_struct::JarImpl<$Configuration>>()
+                        zalsa.lookup_jar_by_type::<$zalsa_struct::JarImpl<$Configuration>>().get()
                     })
                 }
             }
@@ -211,7 +211,7 @@
                 type MemoIngredientMap = $zalsa::MemoIngredientSingletonIndex;
 
                 fn lookup_or_create_ingredient_index(aux: &$zalsa::Zalsa) -> $zalsa::IngredientIndices {
-                    aux.add_or_lookup_jar_by_type::<$zalsa_struct::JarImpl<$Configuration>>().into()
+                    aux.lookup_jar_by_type::<$zalsa_struct::JarImpl<$Configuration>>().get().into()
                 }
 
                 #[inline]
Index: components/salsa-macro-rules/src/setup_input_struct.rs
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/components/salsa-macro-rules/src/setup_input_struct.rs b/components/salsa-macro-rules/src/setup_input_struct.rs
--- a/components/salsa-macro-rules/src/setup_input_struct.rs	(revision 32be70bbbf53822e97077c22549c917318056051)
+++ b/components/salsa-macro-rules/src/setup_input_struct.rs	(date 1750920570382)
@@ -101,14 +101,14 @@
                         $zalsa::IngredientCache::new();
 
                     CACHE.get_or_create(zalsa, || {
-                        zalsa.add_or_lookup_jar_by_type::<$zalsa_struct::JarImpl<$Configuration>>()
+                        zalsa.lookup_jar_by_type::<$zalsa_struct::JarImpl<$Configuration>>().get()
                     })
                 }
 
                 pub fn ingredient_mut(db: &mut dyn $zalsa::Database) -> (&mut $zalsa_struct::IngredientImpl<Self>, &mut $zalsa::Runtime) {
                     let zalsa_mut = db.zalsa_mut();
                     zalsa_mut.new_revision();
-                    let index = zalsa_mut.add_or_lookup_jar_by_type::<$zalsa_struct::JarImpl<$Configuration>>();
+                    let index = zalsa_mut.lookup_jar_by_type::<$zalsa_struct::JarImpl<$Configuration>>().get();
                     let (ingredient, runtime) = zalsa_mut.lookup_ingredient_mut(index);
                     let ingredient = ingredient.assert_type_mut::<$zalsa_struct::IngredientImpl<Self>>();
                     (ingredient, runtime)
@@ -150,7 +150,7 @@
                 type MemoIngredientMap = $zalsa::MemoIngredientSingletonIndex;
 
                 fn lookup_or_create_ingredient_index(aux: &$zalsa::Zalsa) -> $zalsa::IngredientIndices {
-                    aux.add_or_lookup_jar_by_type::<$zalsa_struct::JarImpl<$Configuration>>().into()
+                    aux.lookup_jar_by_type::<$zalsa_struct::JarImpl<$Configuration>>().get().into()
                 }
 
                 #[inline]
Index: tests/interned-structs_self_ref.rs
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/tests/interned-structs_self_ref.rs b/tests/interned-structs_self_ref.rs
--- a/tests/interned-structs_self_ref.rs	(revision 32be70bbbf53822e97077c22549c917318056051)
+++ b/tests/interned-structs_self_ref.rs	(date 1750920478928)
@@ -87,7 +87,9 @@
 
             let zalsa = db.zalsa();
             CACHE.get_or_create(zalsa, || {
-                zalsa.add_or_lookup_jar_by_type::<zalsa_struct_::JarImpl<Configuration_>>()
+                zalsa
+                    .lookup_jar_by_type::<zalsa_struct_::JarImpl<Configuration_>>()
+                    .get()
             })
         }
     }
@@ -114,7 +116,8 @@
         type MemoIngredientMap = zalsa_::MemoIngredientSingletonIndex;
 
         fn lookup_or_create_ingredient_index(aux: &Zalsa) -> salsa::plumbing::IngredientIndices {
-            aux.add_or_lookup_jar_by_type::<zalsa_struct_::JarImpl<Configuration_>>()
+            aux.lookup_jar_by_type::<zalsa_struct_::JarImpl<Configuration_>>()
+                .get()
                 .into()
         }
 
Index: components/salsa-macro-rules/src/setup_tracked_fn.rs
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/components/salsa-macro-rules/src/setup_tracked_fn.rs b/components/salsa-macro-rules/src/setup_tracked_fn.rs
--- a/components/salsa-macro-rules/src/setup_tracked_fn.rs	(revision 32be70bbbf53822e97077c22549c917318056051)
+++ b/components/salsa-macro-rules/src/setup_tracked_fn.rs	(date 1750920531455)
@@ -151,21 +151,23 @@
                 fn fn_ingredient(db: &dyn $Db) -> &$zalsa::function::IngredientImpl<$Configuration> {
                     let zalsa = db.zalsa();
                     $FN_CACHE.get_or_create(zalsa, || {
+                        let lookup = zalsa.lookup_jar_by_type::<$Configuration>();
+
                         // If the ingredient has already been inserted, we know that the downcaster
                         // has also been registered. This is a fast-path for multi-database use cases
                         // that bypass the ingredient cache and will always execute this closure.
-                        zalsa.lookup_jar_by_type::<$Configuration>()
-                            .unwrap_or_else(|| {
-                                <dyn $Db as $Db>::zalsa_register_downcaster(db);
-                                zalsa.add_or_lookup_jar_by_type::<$Configuration>()
-                            })
+                        if lookup.is_missing() {
+                            <dyn $Db as $Db>::zalsa_register_downcaster(db);
+                        }
+
+                        lookup.get()
                     })
                 }
 
                 pub fn fn_ingredient_mut(db: &mut dyn $Db) -> &mut $zalsa::function::IngredientImpl<Self> {
                     <dyn $Db as $Db>::zalsa_register_downcaster(db);
                     let zalsa_mut = db.zalsa_mut();
-                    let index = zalsa_mut.add_or_lookup_jar_by_type::<$Configuration>();
+                    let index = zalsa_mut.lookup_jar_by_type::<$Configuration>().get();
                     let (ingredient, _) = zalsa_mut.lookup_ingredient_mut(index);
                     ingredient.assert_type_mut::<$zalsa::function::IngredientImpl<Self>>()
                 }
@@ -177,7 +179,7 @@
                         let zalsa = db.zalsa();
                         $INTERN_CACHE.get_or_create(zalsa, || {
                             <dyn $Db as $Db>::zalsa_register_downcaster(db);
-                            zalsa.add_or_lookup_jar_by_type::<$Configuration>().successor(0)
+                            zalsa.lookup_jar_by_type::<$Configuration>().get().successor(0)
                         })
                     }
                 }
Index: components/salsa-macro-rules/src/setup_interned_struct.rs
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/components/salsa-macro-rules/src/setup_interned_struct.rs b/components/salsa-macro-rules/src/setup_interned_struct.rs
--- a/components/salsa-macro-rules/src/setup_interned_struct.rs	(revision 32be70bbbf53822e97077c22549c917318056051)
+++ b/components/salsa-macro-rules/src/setup_interned_struct.rs	(date 1750920422317)
@@ -149,7 +149,7 @@
 
                     let zalsa = db.zalsa();
                     CACHE.get_or_create(zalsa, || {
-                        zalsa.add_or_lookup_jar_by_type::<$zalsa_struct::JarImpl<$Configuration>>()
+                        zalsa.lookup_jar_by_type::<$zalsa_struct::JarImpl<$Configuration>>().get()
                     })
                 }
             }
@@ -182,7 +182,7 @@
                 type MemoIngredientMap = $zalsa::MemoIngredientSingletonIndex;
 
                 fn lookup_or_create_ingredient_index(aux: &$zalsa::Zalsa) -> $zalsa::IngredientIndices {
-                    aux.add_or_lookup_jar_by_type::<$zalsa_struct::JarImpl<$Configuration>>().into()
+                    aux.lookup_jar_by_type::<$zalsa_struct::JarImpl<$Configuration>>().get().into()
                 }
 
                 #[inline]

@ibraheemdev
Copy link
Contributor Author

Should the benchmarks be updated to reuse the same database and not measure the ingredient registration?

@MichaReiser
Copy link
Contributor

MichaReiser commented Jun 27, 2025

Should the benchmarks be updated to reuse the same database and not measure the ingredient registration?

I think this is already the case, no?

        b.iter_batched_ref(
            || {
                let db = salsa::DatabaseImpl::default();
                // we can't pass this along otherwise, and the lifetime is generally informational
                let input: InternedInput<'static> =
                    unsafe { transmute(InternedInput::new(&db, "hello, world!".to_owned())) };
                let interned_len = interned_length(black_box(&db), black_box(input));
                assert_eq!(black_box(interned_len), 13);
                (db, input)
            },
            |&mut (ref db, input)| {
                let interned_len = interned_length(black_box(db), black_box(input));
                assert_eq!(black_box(interned_len), 13);
            },
            BatchSize::SmallInput,
        )

@@ -171,7 +180,7 @@ macro_rules! setup_tracked_fn {
let zalsa = db.zalsa();
$INTERN_CACHE.get_or_create(zalsa, || {
<dyn $Db as $Db>::zalsa_register_downcaster(db);
zalsa.add_or_lookup_jar_by_type::<$Configuration>().successor(0)
zalsa.lookup_jar_by_type::<$Configuration>().get_or_create().successor(0)
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Should we use the same two-pased registration here to only register the downcasters in the create branch?

Copy link
Contributor

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

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

I think what we see in the benchmarks is that the many-databases case is now slightly slower when running in single-threaded mode. I don't know papaya enough to say if that's likely but it doesn't seem unreasonable to me that a papaya lookup is more expensive than a mutex lock followed by a fx hash map lookup.

I think this is good to merge as it fixes a significant perf regression in multi-threaded mode, most production cases use a single database, and the single-threaded regression isn't that significant.

@MichaReiser
Copy link
Contributor

Yes, this seems about right. I modified your papaya single_thread benchmark to use a mutex wrapped hash map

    group.bench_function("std-mutex", |b| {
        let mut m = HashMap::<usize, usize>::default();
        for i in RandomKeys::new().take(SIZE) {
            m.insert(i, i);
        }

        let m = std::sync::Mutex::new(m);

        b.iter(|| {
            for i in RandomKeys::new().take(SIZE) {
                black_box(assert_eq!(m.lock().unwrap().get(&i), Some(&i)));
            }
        });
    });

and it shows that papaya is ~10% slower:

read/papaya             time:   [117.36 µs 118.19 µs 119.11 µs]
                        change: [-0.8586% +0.0348% +1.0098%] (p = 0.93 > 0.05)
                        No change in performance detected.
Found 3 outliers among 100 measurements (3.00%)
  2 (2.00%) high mild
  1 (1.00%) high severe
read/std                time:   [74.150 µs 74.533 µs 74.910 µs]
                        change: [+0.7745% +1.3484% +1.9011%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 9 outliers among 100 measurements (9.00%)
  9 (9.00%) low mild
read/std-mutex          time:   [106.87 µs 107.11 µs 107.36 µs]
                        change: [-1.2686% -0.9412% -0.6028%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 1 outliers among 100 measurements (1.00%)
  1 (1.00%) low mild
read/dashmap            time:   [143.05 µs 143.41 µs 143.78 µs]
                        change: [+0.3474% +0.6783% +1.0208%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 3 outliers among 100 measurements (3.00%)
  1 (1.00%) low mild
  2 (2.00%) high mild

@MichaReiser MichaReiser added this pull request to the merge queue Jun 27, 2025
@MichaReiser
Copy link
Contributor

@ibraheemdev I go ahead and merge this, considering that we understand the regression. Would you mind updating Salsa in ty?

Merged via the queue into salsa-rs:master with commit d44f638 Jun 27, 2025
12 checks passed
This was referenced Jun 27, 2025
ibraheemdev pushed a commit to astral-sh/ruff that referenced this pull request Jul 2, 2025
## Summary

This PR updates Salsa to pull in Ibraheem's multithreading improvements (salsa-rs/salsa#921).

## Performance

A small regression for single-threaded benchmarks is expected because
papaya is slightly slower than a `Mutex<FxHashMap>` in the uncontested
case (~10%). However, this shouldn't matter as much in practice because:

1. Salsa has a fast-path when only using 1 DB instance which is the
common case in production. This fast-path is not impacted by the changes
but we measure the slow paths in our benchmarks (because we use multiple
db instances)
2. Fixing the 10x slowdown for the congested case (multi threading)
outweights the downsides of a 10% perf regression for single threaded
use cases, especially considering that ty is heavily multi threaded.

## Test Plan

`cargo test`
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