Skip to content

Commit 0555379

Browse files
authored
turbo-tasks: task-storage memory wins (#93720)
## Summary Four small, independent changes that shrink `TaskStorage` and the data it owns: Recommend reviewing commit-by-commit 1. **`Arc<CachedTaskType>` → `triomphe::Arc<CachedTaskType>`.** `triomphe::Arc` is already a workspace dep used in `ReadRef` / `SharedReference`. `CachedTaskType` never appears in a `Weak<...>`, so we can drop the weak count and the CAS in `drop_slow`. Saves one `usize` per allocation. Migrated via a `CachedTaskTypeArc` newtype so the bincode `Encode`/`Decode` impls don't need to cross the orphan rule. 2. **Niche-encode `CellDependency`.** The `cell_dependencies` / `cell_dependents` sets used to hold `(CellRef, Option<u64>)` tuples — `Option<u64>` cost a full 16 B (8 B discriminant + 8 B value, aligned), making each element 32 B. A `CellDependency` enum with two variants (`All(CellRef)` / `Hash(CellRef, u64)`) lets the layout algorithm reuse the niche on `ValueTypeId` (`NonZero<u16>`) inside `CellRef.cell.type_id` for the variant tag. Element size drops 32 → 24 B; `LazyField` from 56 → 48 B. The same enum backs both forward and reverse edges — for `cell_dependents` we re-point `CellRef.task` at the dependent task. Added `CellDependency::into_parts()` and use it in `iter_cell_dependents` / `iter_cell_dependencies` hot loops so the discriminant is checked once instead of twice via back-to-back `cell_ref()` + `key()` calls. 3. **`TaskStorage::lazy: Vec<LazyField>` → `TinyVec<LazyField>`.** The lazy vec only ever holds ~25 elements (one per declared lazy field in the schema). Swapping `Vec`'s 24 B `(ptr, len, cap)` header for `(ptr, len: u8, cap: u8)` + 6 B padding gives 16 B. Drops `size_of::<TaskStorage>()` from 136 → 128 B. `TinyVec` is hand-rolled so I added a push/iter micro-benchmark to confirm it doesn't lose performance vs std `Vec`. Results below. 4. **Rightsize collections** → Explore the `AutoSet`/`AutoMap` types in storage_schema and ensure each one is maximally sized for its natural alignment. ## Benchmark results ### `next build` on a representative app (15 runs each, M4 Pro, `caffeinate -dimsu nice -n -20`) Fresh same-day baseline against branch: | metric | canary | branch | Δ | 95% CI | significant? | |---|---:|---:|---:|---|:---:| | wall time | 40.83s | 41.12s | +0.7% | [−1.07s, +1.64s] | no | | user time | 282.27s | 283.21s | +0.3% | [−1.02s, +2.89s] | no | | sys time | 69.38s | 71.26s | +2.7% | [−1.54s, +5.32s] | no | | **MaxRSS** | **12.47 GB** | **12.04 GB** | **−3.4%** | **[−0.48 GB, −0.38 GB]** | **yes** | **MaxRSS is the headline.** −0.43 GB on a 12.5 GB working set, with t=−17.86 (every branch run lower than every canary run, CV ≤ 0.6% on both sides). Wall / user / sys are all within noise — this PR is a memory win with no measurable timing impact. ### `TinyVec` vs `Vec` micro-bench (`turbo-tasks/benches/tiny_vec.rs`, 200 samples each) | n | Vec push | TinyVec push | Δ% | Vec iter | TinyVec iter | Δ% | |---:|---:|---:|---:|---:|---:|---:| | 0 | 1.31ns | 894ps | **−31.8%** | 598ps | 596ps | −0.4% | | 1 | 16.92ns | 14.75ns | **−12.9%** | 964ps | 952ps | −1.2% | | 4 | 17.93ns | 15.93ns | **−11.1%** | 1.49ns | 1.50ns | +0.5% | | 8 | 63.13ns | 45.24ns | **−28.3%** | 1.97ns | 1.96ns | −0.2% | | 16 | 97.35ns | 79.91ns | **−17.9%** | 3.16ns | 3.14ns | −0.5% | | 24 | 137.41ns | 119.88ns | **−12.8%** | 4.30ns | 4.30ns | +0.0% | TinyVec push is 11–32% faster than Vec push across all realistic sizes; iter is identical. Run with `cargo bench -p turbo-tasks --bench tiny_vec`. ### `task_overhead/turbo` Criterion bench (M4 Pro, `--sample-size 200`) | variant | dur | canary | branch | Δ | significant? | |---|---:|---:|---:|---:|:---:| | turbo-uncached | 1µs | 9.77 µs | 9.68 µs | −1.0% | yes | | turbo-uncached | 1000µs | 1.01 ms | 1.01 ms | −0.1% | yes | | turbo-cached-same-keys | 1µs | 198.6 ns | 191.9 ns | −3.4% | yes | | turbo-cached-same-keys | 100µs | 226.5 ns | 208.1 ns | −8.1% | yes | | turbo-cached-different-keys | 1µs | 233.8 ns | 224.1 ns | −4.2% | yes | | turbo-cached-different-keys | 100µs | 305.3 ns | 246.9 ns | −19.1% | yes | | turbo-uncached-parallel | 10µs | 1.63 µs | 1.54 µs | −5.8% | yes | | turbo-uncached-parallel | 100µs | 8.41 µs | 7.88 µs | −6.3% | yes | <!-- NEXT_JS_LLM_PR -->
1 parent dadc154 commit 0555379

14 files changed

Lines changed: 1094 additions & 155 deletions

File tree

turbopack/crates/turbo-tasks-backend/src/backend/counter_map.rs

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,34 +12,37 @@ use bincode::{
1212
};
1313
use rustc_hash::FxHasher;
1414

15-
type InnerMap<K, V> = AutoMap<K, V, BuildHasherDefault<FxHasher>, 1>;
15+
type InnerMap<K, V, const I: usize> = AutoMap<K, V, BuildHasherDefault<FxHasher>, I>;
1616

1717
/// A map optimized for reference counting, backed by AutoMap.
1818
///
1919
/// Entries are automatically removed when their count reaches zero.
2020
/// This provides memory-efficient storage for sparse counter data.
21+
///
22+
/// The `I` const generic forwards the inline capacity to the backing `AutoMap`
23+
/// — see the schema field-by-field sizing for the chosen values.
2124
#[derive(Debug, Clone)]
22-
pub struct CounterMap<K, V>(InnerMap<K, V>);
25+
pub struct CounterMap<K, V, const I: usize>(InnerMap<K, V, I>);
2326

24-
impl<K, V> Default for CounterMap<K, V> {
27+
impl<K, V, const I: usize> Default for CounterMap<K, V, I> {
2528
fn default() -> Self {
2629
Self(InnerMap::default())
2730
}
2831
}
2932

30-
impl<K: Eq + Hash, V: Eq> PartialEq for CounterMap<K, V> {
33+
impl<K: Eq + Hash, V: Eq, const I: usize> PartialEq for CounterMap<K, V, I> {
3134
fn eq(&self, other: &Self) -> bool {
3235
self.0 == other.0
3336
}
3437
}
3538

36-
impl<K: Encode, V: Encode> Encode for CounterMap<K, V> {
39+
impl<K: Encode, V: Encode, const I: usize> Encode for CounterMap<K, V, I> {
3740
fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
3841
self.0.encode(encoder)
3942
}
4043
}
4144

42-
impl<Context, K, V> Decode<Context> for CounterMap<K, V>
45+
impl<Context, K, V, const I: usize> Decode<Context> for CounterMap<K, V, I>
4346
where
4447
K: Decode<Context> + Eq + Hash,
4548
V: Decode<Context>,
@@ -80,7 +83,7 @@ impl CounterValue for i32 {
8083
}
8184
}
8285

83-
impl<K, V> CounterMap<K, V> {
86+
impl<K, V, const I: usize> CounterMap<K, V, I> {
8487
pub fn new() -> Self {
8588
Self(AutoMap::default())
8689
}
@@ -138,16 +141,16 @@ impl<K, V> CounterMap<K, V> {
138141
}
139142
}
140143

141-
impl<K, V> IntoIterator for CounterMap<K, V> {
144+
impl<K, V, const I: usize> IntoIterator for CounterMap<K, V, I> {
142145
type Item = (K, V);
143-
type IntoIter = <InnerMap<K, V> as IntoIterator>::IntoIter;
146+
type IntoIter = <InnerMap<K, V, I> as IntoIterator>::IntoIter;
144147

145148
fn into_iter(self) -> Self::IntoIter {
146149
self.0.into_iter()
147150
}
148151
}
149152

150-
impl<K: Hash + Eq, V: CounterValue> CounterMap<K, V> {
153+
impl<K: Hash + Eq, V: CounterValue, const I: usize> CounterMap<K, V, I> {
151154
/// Insert a key-value pair. Panics if value is zero (invariant: zero values are not stored).
152155
pub fn insert(&mut self, key: K, value: V) -> Option<V> {
153156
debug_assert!(
@@ -297,15 +300,15 @@ mod tests {
297300

298301
#[test]
299302
fn test_update_count_new_entry() {
300-
let mut map: CounterMap<u32, u32> = CounterMap::new();
303+
let mut map: CounterMap<u32, u32, 1> = CounterMap::new();
301304
// Adding new entry crosses zero (from nothing to something)
302305
assert!(map.update_count(1, 5));
303306
assert_eq!(map.get(&1), Some(&5));
304307
}
305308

306309
#[test]
307310
fn test_update_count_increment() {
308-
let mut map: CounterMap<u32, u32> = CounterMap::new();
311+
let mut map: CounterMap<u32, u32, 1> = CounterMap::new();
309312
map.update_count(1, 5);
310313
// Incrementing existing entry doesn't cross zero
311314
assert!(!map.update_count(1, 3));
@@ -314,7 +317,7 @@ mod tests {
314317

315318
#[test]
316319
fn test_update_count_removal_on_zero() {
317-
let mut map: CounterMap<u32, i32> = CounterMap::new();
320+
let mut map: CounterMap<u32, i32, 1> = CounterMap::new();
318321
map.update_count(1, 5);
319322
// Subtracting to zero removes entry and crosses zero
320323
assert!(map.update_count(1, -5));
@@ -324,69 +327,69 @@ mod tests {
324327

325328
#[test]
326329
fn test_update_count_zero_delta_on_empty() {
327-
let mut map: CounterMap<u32, u32> = CounterMap::new();
330+
let mut map: CounterMap<u32, u32, 1> = CounterMap::new();
328331
// Adding zero to non-existent entry doesn't create it
329332
assert!(!map.update_count(1, 0));
330333
assert!(map.is_empty());
331334
}
332335

333336
#[test]
334337
fn test_update_and_get_new_entry() {
335-
let mut map: CounterMap<u32, u32> = CounterMap::new();
338+
let mut map: CounterMap<u32, u32, 1> = CounterMap::new();
336339
assert_eq!(map.update_and_get(1, 5), 5);
337340
assert_eq!(map.get(&1), Some(&5));
338341
}
339342

340343
#[test]
341344
fn test_update_and_get_increment() {
342-
let mut map: CounterMap<u32, u32> = CounterMap::new();
345+
let mut map: CounterMap<u32, u32, 1> = CounterMap::new();
343346
map.update_and_get(1, 5);
344347
assert_eq!(map.update_and_get(1, 3), 8);
345348
assert_eq!(map.get(&1), Some(&8));
346349
}
347350

348351
#[test]
349352
fn test_update_and_get_removal() {
350-
let mut map: CounterMap<u32, i32> = CounterMap::new();
353+
let mut map: CounterMap<u32, i32, 1> = CounterMap::new();
351354
map.update_and_get(1, 5);
352355
assert_eq!(map.update_and_get(1, -5), 0);
353356
assert!(map.is_empty());
354357
}
355358

356359
#[test]
357360
fn test_add_entry() {
358-
let mut map: CounterMap<u32, u32> = CounterMap::new();
361+
let mut map: CounterMap<u32, u32, 1> = CounterMap::new();
359362
map.add_entry(1, 10);
360363
assert_eq!(map.get(&1), Some(&10));
361364
}
362365

363366
#[test]
364367
#[should_panic(expected = "Entry already exists")]
365368
fn test_add_entry_panics_on_duplicate() {
366-
let mut map: CounterMap<u32, u32> = CounterMap::new();
369+
let mut map: CounterMap<u32, u32, 1> = CounterMap::new();
367370
map.add_entry(1, 10);
368371
map.add_entry(1, 20); // Should panic
369372
}
370373

371374
#[test]
372375
fn test_update_positive_crossing_new_positive() {
373-
let mut map: CounterMap<u32, i32> = CounterMap::new();
376+
let mut map: CounterMap<u32, i32, 1> = CounterMap::new();
374377
// From nothing to positive - crosses positive boundary
375378
assert!(map.update_positive_crossing(1, 5));
376379
assert_eq!(map.get(&1), Some(&5));
377380
}
378381

379382
#[test]
380383
fn test_update_positive_crossing_new_negative() {
381-
let mut map: CounterMap<u32, i32> = CounterMap::new();
384+
let mut map: CounterMap<u32, i32, 1> = CounterMap::new();
382385
// From nothing to negative - doesn't cross positive boundary
383386
assert!(!map.update_positive_crossing(1, -5));
384387
assert_eq!(map.get(&1), Some(&-5));
385388
}
386389

387390
#[test]
388391
fn test_update_positive_crossing_stay_positive() {
389-
let mut map: CounterMap<u32, i32> = CounterMap::new();
392+
let mut map: CounterMap<u32, i32, 1> = CounterMap::new();
390393
map.update_positive_crossing(1, 5);
391394
// Staying positive doesn't cross boundary
392395
assert!(!map.update_positive_crossing(1, 3));
@@ -395,7 +398,7 @@ mod tests {
395398

396399
#[test]
397400
fn test_update_positive_crossing_to_non_positive() {
398-
let mut map: CounterMap<u32, i32> = CounterMap::new();
401+
let mut map: CounterMap<u32, i32, 1> = CounterMap::new();
399402
map.update_positive_crossing(1, 5);
400403
// Crossing to non-positive
401404
assert!(map.update_positive_crossing(1, -8));
@@ -404,7 +407,7 @@ mod tests {
404407

405408
#[test]
406409
fn test_update_positive_crossing_to_zero_removes() {
407-
let mut map: CounterMap<u32, i32> = CounterMap::new();
410+
let mut map: CounterMap<u32, i32, 1> = CounterMap::new();
408411
map.update_positive_crossing(1, 5);
409412
// Crossing to zero removes and crosses boundary
410413
assert!(map.update_positive_crossing(1, -5));
@@ -413,37 +416,37 @@ mod tests {
413416

414417
#[test]
415418
fn test_update_with_create() {
416-
let mut map: CounterMap<u32, u32> = CounterMap::new();
419+
let mut map: CounterMap<u32, u32, 1> = CounterMap::new();
417420
map.update_with(1, |_| Some(10));
418421
assert_eq!(map.get(&1), Some(&10));
419422
}
420423

421424
#[test]
422425
fn test_update_with_modify() {
423-
let mut map: CounterMap<u32, u32> = CounterMap::new();
426+
let mut map: CounterMap<u32, u32, 1> = CounterMap::new();
424427
map.update_with(1, |_| Some(10));
425428
map.update_with(1, |v| v.map(|x| x + 5));
426429
assert_eq!(map.get(&1), Some(&15));
427430
}
428431

429432
#[test]
430433
fn test_update_with_remove() {
431-
let mut map: CounterMap<u32, u32> = CounterMap::new();
434+
let mut map: CounterMap<u32, u32, 1> = CounterMap::new();
432435
map.update_with(1, |_| Some(10));
433436
map.update_with(1, |_| None);
434437
assert!(map.is_empty());
435438
}
436439

437440
#[test]
438441
fn test_update_with_no_op() {
439-
let mut map: CounterMap<u32, u32> = CounterMap::new();
442+
let mut map: CounterMap<u32, u32, 1> = CounterMap::new();
440443
map.update_with(1, |_| None);
441444
assert!(map.is_empty());
442445
}
443446

444447
#[test]
445448
fn test_len_and_is_empty() {
446-
let mut map: CounterMap<u32, u32> = CounterMap::new();
449+
let mut map: CounterMap<u32, u32, 1> = CounterMap::new();
447450
assert!(map.is_empty());
448451
assert_eq!(map.len(), 0);
449452

@@ -457,7 +460,7 @@ mod tests {
457460

458461
#[test]
459462
fn test_iter() {
460-
let mut map: CounterMap<u32, u32> = CounterMap::new();
463+
let mut map: CounterMap<u32, u32, 1> = CounterMap::new();
461464
map.update_count(1, 5);
462465
map.update_count(2, 10);
463466

turbopack/crates/turbo-tasks-backend/src/backend/mod.rs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ use turbo_tasks::{
3434
TaskId, TaskPersistence, TaskPriority, TraitTypeId, TurboTasksBackendApi, TurboTasksPanic,
3535
ValueTypeId,
3636
backend::{
37-
Backend, CachedTaskType, CellContent, CellHash, TaskExecutionSpec, TransientTaskType,
38-
TurboTaskContextError, TurboTaskLocalContextError, TurboTasksError,
37+
Backend, CachedTaskType, CachedTaskTypeArc, CellContent, CellHash, TaskExecutionSpec,
38+
TransientTaskType, TurboTaskContextError, TurboTaskLocalContextError, TurboTasksError,
3939
TurboTasksExecutionError, TurboTasksExecutionErrorMessage, TypedCellContent,
4040
VerificationMode,
4141
},
@@ -70,8 +70,8 @@ use crate::{
7070
},
7171
backing_storage::{BackingStorage, SnapshotItem, SnapshotMeta, compute_task_type_hash},
7272
data::{
73-
ActivenessState, CellRef, CollectibleRef, CollectiblesRef, Dirtyness, InProgressCellState,
74-
InProgressState, InProgressStateInner, OutputValue, TransientTask,
73+
ActivenessState, CellDependency, CellRef, CollectibleRef, CollectiblesRef, Dirtyness,
74+
InProgressCellState, InProgressState, InProgressStateInner, OutputValue, TransientTask,
7575
},
7676
error::TaskError,
7777
utils::{
@@ -785,7 +785,8 @@ impl<B: BackingStorage> TurboTasksBackendInner<B> {
785785
&& (!task.immutable() || cfg!(feature = "verify_immutable"))
786786
{
787787
let reader = reader.unwrap();
788-
let _ = task.add_cell_dependents((cell, key, reader));
788+
let _ = task
789+
.add_cell_dependents(CellDependency::new(CellRef { task: reader, cell }, key));
789790
drop(task);
790791

791792
// Note: We use `task_pair` earlier to lock the task and its reader at the same
@@ -797,8 +798,9 @@ impl<B: BackingStorage> TurboTasksBackendInner<B> {
797798
task: task_id,
798799
cell,
799800
};
800-
if !reader_task.remove_outdated_cell_dependencies(&(target, key)) {
801-
let _ = reader_task.add_cell_dependencies((target, key));
801+
let dep = CellDependency::new(target, key);
802+
if !reader_task.remove_outdated_cell_dependencies(&dep) {
803+
let _ = reader_task.add_cell_dependencies(dep);
802804
}
803805
drop(reader_task);
804806
}
@@ -1545,7 +1547,7 @@ impl<B: BackingStorage> TurboTasksBackendInner<B> {
15451547
// Only now do we force the allocation.
15461548
// NOTE: if our caller had to perform resolution, then this will have already
15471549
// been boxed and take_box just takes it.
1548-
let task_type = Arc::new(CachedTaskType {
1550+
let task_type = CachedTaskTypeArc::new(CachedTaskType {
15491551
native_fn,
15501552
this,
15511553
arg: arg.take_box(),
@@ -1776,7 +1778,7 @@ impl<B: BackingStorage> TurboTasksBackendInner<B> {
17761778
}
17771779
}
17781780

1779-
fn debug_get_cached_task_type(&self, task_id: TaskId) -> Option<Arc<CachedTaskType>> {
1781+
fn debug_get_cached_task_type(&self, task_id: TaskId) -> Option<CachedTaskTypeArc> {
17801782
let task = self.storage.access_mut(task_id);
17811783
task.get_persistent_task_type().cloned()
17821784
}
@@ -2216,7 +2218,7 @@ impl<B: BackingStorage> TurboTasksBackendInner<B> {
22162218
Some(
22172219
// Collect all dependencies on tasks to check if all dependencies are immutable
22182220
task.iter_output_dependencies()
2219-
.chain(task.iter_cell_dependencies().map(|(target, _key)| target.task))
2221+
.chain(task.iter_cell_dependencies().map(|dep| dep.cell_ref().task))
22202222
.collect::<FxHashSet<_>>(),
22212223
)
22222224
} else {
@@ -2255,7 +2257,7 @@ impl<B: BackingStorage> TurboTasksBackendInner<B> {
22552257
// breaking dependency tracking.
22562258
old_edges.extend(
22572259
task.iter_outdated_cell_dependencies()
2258-
.map(|(target, key)| OutdatedEdge::CellDependency(target, key)),
2260+
.map(OutdatedEdge::CellDependency),
22592261
);
22602262
old_edges.extend(
22612263
task.iter_outdated_output_dependencies()

turbopack/crates/turbo-tasks-backend/src/backend/operation/cleanup_old_edges.rs

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::{
1717
},
1818
storage_schema::TaskStorageAccessors,
1919
},
20-
data::{CellRef, CollectibleRef, CollectiblesRef},
20+
data::{CellDependency, CellRef, CollectibleRef, CollectiblesRef},
2121
};
2222

2323
#[derive(Encode, Decode, Clone)]
@@ -48,7 +48,7 @@ impl Default for CleanupOldEdgesOperation {
4848
pub enum OutdatedEdge {
4949
Child(TaskId),
5050
Collectible(CollectibleRef, i32),
51-
CellDependency(CellRef, Option<u64>),
51+
CellDependency(CellDependency),
5252
OutputDependency(TaskId),
5353
CollectiblesDependency(CollectiblesRef),
5454
}
@@ -166,27 +166,28 @@ impl CleanupOldEdgesOperation {
166166
AggregatedDataUpdate::new().collectibles_update(collectibles),
167167
));
168168
}
169-
OutdatedEdge::CellDependency(
170-
CellRef {
171-
task: cell_task_id,
172-
cell,
173-
},
174-
key,
175-
) => {
169+
OutdatedEdge::CellDependency(dep) => {
170+
let (
171+
CellRef {
172+
task: cell_task_id,
173+
cell,
174+
},
175+
key,
176+
) = dep.into_parts();
176177
{
177178
let mut task = ctx.task(cell_task_id, TaskDataCategory::Data);
178-
task.remove_cell_dependents(&(cell, key, task_id));
179-
}
180-
{
181-
let mut task = ctx.task(task_id, TaskDataCategory::Data);
182-
task.remove_cell_dependencies(&(
179+
task.remove_cell_dependents(&CellDependency::new(
183180
CellRef {
184-
task: cell_task_id,
181+
task: task_id,
185182
cell,
186183
},
187184
key,
188185
));
189186
}
187+
{
188+
let mut task = ctx.task(task_id, TaskDataCategory::Data);
189+
task.remove_cell_dependencies(&dep);
190+
}
190191
}
191192
OutdatedEdge::OutputDependency(output_task_id) => {
192193
#[cfg(feature = "trace_task_output_dependencies")]

0 commit comments

Comments
 (0)