perf(layered): dynamic service does not allocate anymore#480
Conversation
There was a problem hiding this comment.
Pull request overview
This PR reduces per-request allocations when using layered’s DynamicService (used by fetch pipelines) by replacing dynosaur-based type erasure with an infinity_pool::BlindPool-backed future pooling approach.
Changes:
- Reworks
DynamicServiceto execute via a pooled future handle (BlindPooledMut), avoiding repeated heap allocations during dynamic dispatch. - Removes the dynosaur-based
DynServicegeneration path fromlayeredand switches thedynamic-servicefeature to depend oninfinity_pool. - Updates
layeredcrate dependencies accordingly (including lockfile updates).
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| crates/layered/src/service.rs | Removes dynosaur attribute previously used to generate an internal dynamic service wrapper. |
| crates/layered/src/dynamic.rs | Implements pooled dynamic dispatch using BlindPool and a custom Future wrapper. |
| crates/layered/Cargo.toml | Switches dynamic-service feature from dynosaur to infinity_pool and updates deps. |
| Cargo.lock | Updates the lockfile to reflect the dependency change. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| impl<In: Send, Out: Send> Service<In> for DynamicService<In, Out> { | ||
| type Out = Out; | ||
|
|
||
| async fn execute(&self, input: In) -> Self::Out { | ||
| self.0.execute(input).await | ||
| fn execute(&self, input: In) -> impl Future<Output = Self::Out> + Send { | ||
| ServiceFuture { | ||
| handle: self.exec.as_ref()(input), | ||
| } | ||
| } | ||
| } |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #480 +/- ##
=======================================
Coverage 100.0% 100.0%
=======================================
Files 335 335
Lines 25586 25600 +14
=======================================
+ Hits 25586 25600 +14 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
considering that @sandersaares, do you have any recommendations here? |
|
Also worried about potential unsafety this might bring. How risky is this @sandersaares? |
There is a When do we even need to release capacity? There are two reasons that immediately come to mind:
It is an area that could benefit from exploration and data from real customer scenarios. It is not obvious to me that shrinking resource pools is at all necessary/appropriate.
There are no open bugs that I am aware of. More risky than safe Rust, less risky than skydiving. |
is it better to have one shared pool for all instances of |
sandersaares
left a comment
There was a problem hiding this comment.
How is the change in terms of processor time spent?
| // future and inserts it into the pool | ||
| let exec = move |input: In| { | ||
| let cloned = Arc::clone(&service); | ||
| let fut = async move { cloned.execute(input).await }; |
There was a problem hiding this comment.
The generics are blinding my eyes today, so I cannot identify it myself but is it possible here to get a type for the thing being inserted, or at least a Layout? If so, switching to OpaquePool would eliminate some overhead. OpaquePool vs BlindPool is basically "one type/layout" versus "multiple types/layouts", with the BlindPool acting as a router over a pool of OpaquePool instances.
Unclear - I do not have a good understanding of the actual usage patterns. Sharing a single pool across many callers improves resource utilization, of course. What I have sometimes done is to have a |
I got quite surprised how much allocations are happening in fetch with standard pipeline. This change leverages
BlindPoolthat allows memory and future pooling, thus eliminating the memory allocations.Before:
After:
no changes in public API