Skip to content

Commit fa50df9

Browse files
committed
improve eviction semantics and add tests
1 parent c207991 commit fa50df9

7 files changed

Lines changed: 425 additions & 37 deletions

File tree

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { nextTestSetup } from 'e2e-utils'
2+
import { retry, waitFor } from 'next-test-utils'
3+
4+
describe('evict-after-snapshot', () => {
5+
const envVars = [
6+
'ENABLE_CACHING=1',
7+
'TURBO_ENGINE_IGNORE_DIRTY=1',
8+
'TURBO_ENGINE_SNAPSHOT_IDLE_TIMEOUT_MILLIS=1000',
9+
'TURBO_ENGINE_EVICT_AFTER_SNAPSHOT=1',
10+
].join(' ')
11+
12+
const { skipped, next } = nextTestSetup({
13+
files: __dirname,
14+
skipDeployment: true,
15+
patchFileDelay: 500,
16+
packageJson: {
17+
scripts: {
18+
dev: `${envVars} next dev`,
19+
},
20+
},
21+
installCommand: 'npm i',
22+
startCommand: 'npm run dev',
23+
})
24+
25+
if (skipped) {
26+
return
27+
}
28+
29+
async function waitForSnapshotAndEviction() {
30+
// The idle timeout is 1s, give extra time for snapshot + eviction to complete
31+
await waitFor(5000)
32+
}
33+
34+
// Turbopack-only: eviction requires persistent caching
35+
;(process.env.IS_TURBOPACK_TEST ? it : it.skip)(
36+
'should serve correct content after eviction and HMR',
37+
async () => {
38+
const browser = await next.browser('/')
39+
await retry(async () => {
40+
expect(await browser.elementByCss('p').text()).toBe('hello world')
41+
})
42+
43+
for (let cycle = 1; cycle <= 3; cycle++) {
44+
await waitForSnapshotAndEviction()
45+
46+
await next.patchFile(
47+
'app/page.tsx',
48+
(content) => content.replace('hello world', `cycle ${cycle}`),
49+
async () => {
50+
await retry(async () => {
51+
expect(await browser.elementByCss('p').text()).toBe(
52+
`cycle ${cycle}`
53+
)
54+
}, 10000)
55+
}
56+
)
57+
}
58+
59+
await browser.close()
60+
},
61+
90000
62+
)
63+
;(process.env.IS_TURBOPACK_TEST ? it : it.skip)(
64+
'should handle client component HMR after eviction',
65+
async () => {
66+
const browser = await next.browser('/client')
67+
await retry(async () => {
68+
expect(await browser.elementByCss('p').text()).toBe('hello world')
69+
})
70+
71+
await waitForSnapshotAndEviction()
72+
73+
await next.patchFile(
74+
'app/client/page.tsx',
75+
(content) => content.replace('hello world', 'hello eviction'),
76+
async () => {
77+
await retry(async () => {
78+
expect(await browser.elementByCss('p').text()).toBe(
79+
'hello eviction'
80+
)
81+
}, 10000)
82+
}
83+
)
84+
85+
await browser.close()
86+
},
87+
90000
88+
)
89+
})

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,19 @@ impl<B: BackingStorage> TurboTasksBackend<B> {
236236
pub fn backing_storage(&self) -> &B {
237237
&self.0.backing_storage
238238
}
239+
240+
/// Perform a snapshot and then evict all evictable tasks from memory.
241+
///
242+
/// This is exposed for integration tests that need to verify the
243+
/// snapshot → evict → restore cycle works correctly.
244+
///
245+
/// Returns `(snapshot_had_new_data, full_evicted, data_only_evicted)`.
246+
pub fn snapshot_and_evict(
247+
&self,
248+
turbo_tasks: &dyn TurboTasksBackendApi<TurboTasksBackend<B>>,
249+
) -> (bool, usize, usize) {
250+
self.0.snapshot_and_evict(turbo_tasks)
251+
}
239252
}
240253

241254
impl<B: BackingStorage> TurboTasksBackendInner<B> {
@@ -371,6 +384,26 @@ impl<B: BackingStorage> TurboTasksBackendInner<B> {
371384
self.options.evict_after_snapshot && self.should_persist()
372385
}
373386

387+
/// Perform a snapshot and then evict all evictable tasks from memory.
388+
///
389+
/// This is exposed for integration tests that need to verify the
390+
/// snapshot → evict → restore cycle works correctly.
391+
///
392+
/// Returns `(snapshot_had_new_data, full_evicted, data_only_evicted)`.
393+
pub fn snapshot_and_evict(
394+
&self,
395+
turbo_tasks: &dyn TurboTasksBackendApi<TurboTasksBackend<B>>,
396+
) -> (bool, usize, usize) {
397+
assert!(
398+
self.should_persist(),
399+
"snapshot_and_evict requires persistence"
400+
);
401+
let snapshot_result = self.snapshot_and_persist(None, "test", turbo_tasks);
402+
let had_new_data = snapshot_result.map_or(false, |(_, new_data)| new_data);
403+
let (full, data_only) = self.storage.evict_after_snapshot();
404+
(had_new_data, full, data_only)
405+
}
406+
374407
fn should_restore(&self) -> bool {
375408
self.options.storage_mode.is_some()
376409
}
@@ -2798,6 +2831,7 @@ impl<B: BackingStorage> TurboTasksBackendInner<B> {
27982831
// Evict persisted tasks from memory to reclaim space.
27992832
// Like compaction, this runs after snapshot_and_persist
28002833
// as a separate concern.
2834+
28012835
if this.should_evict() {
28022836
let evict_span = tracing::info_span!(
28032837
parent: background_span.id(),

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,11 +203,15 @@ impl Storage {
203203
}
204204
});
205205

206-
// We also need to unset all the modified flags.
206+
// We also need to unset the modified flags and mark data as restored
207+
// (on disk and recoverable). This enables eviction of freshly created
208+
// tasks that have just been snapshotted.
207209
for key in removed_modified {
208210
if let Some(mut inner) = self.map.get_mut(&key) {
209211
inner.flags.set_data_modified(false);
210212
inner.flags.set_meta_modified(false);
213+
inner.flags.set_data_restored(true);
214+
inner.flags.set_meta_restored(true);
211215
}
212216
}
213217

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ struct TaskStorageSchema {
294294
#[field(storage = "auto_map", category = "transient", shrink_on_completion)]
295295
in_progress_cells: AutoMap<CellId, InProgressCellState>,
296296

297-
#[field(storage = "direct", category = "data", inline, keep_on_restore)]
297+
#[field(storage = "direct", category = "data", inline)]
298298
pub persistent_task_type: Option<Arc<CachedTaskType>>,
299299

300300
#[field(storage = "direct", category = "transient")]
@@ -438,6 +438,18 @@ impl TaskStorage {
438438
&& !flags.data_modified_during_snapshot();
439439

440440
if meta_evictable && data_evictable {
441+
// Non-serializable cell data (e.g. process pool handles) cannot be restored from
442+
// disk. Full eviction would permanently lose it. Downgrade to data-only eviction
443+
// which preserves transient fields.
444+
if self.transient_cell_data().is_some_and(|m| !m.is_empty()) {
445+
return Evictability::DataOnly;
446+
}
447+
// Session-dependent tasks have transient `current_session_clean` state that cannot
448+
// be restored from disk. Losing it would make the task appear dirty in the current
449+
// session, causing redundant re-execution. Downgrade to data-only eviction.
450+
if matches!(self.get_dirty(), Some(Dirtyness::SessionDependent)) {
451+
return Evictability::DataOnly;
452+
}
441453
return Evictability::Full;
442454
}
443455

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ impl<T: KeyValueDatabase + Send + Sync + 'static> BackingStorageSealed
246246

247247
{
248248
let _span = tracing::trace_span!("update task data").entered();
249-
let counts: (usize, usize) =
249+
let (meta_count, data_count) =
250250
parallel::map_collect_owned::<_, _, Result<Vec<_>>>(snapshots, |tasks| {
251251
let mut local_meta = 0usize;
252252
let mut local_data = 0usize;
@@ -280,8 +280,8 @@ impl<T: KeyValueDatabase + Send + Sync + 'static> BackingStorageSealed
280280
.into_iter()
281281
.fold((0, 0), |(am, ad), (m, d)| (am + m, ad + d));
282282

283-
span.record("meta", counts.0);
284-
span.record("data", counts.1);
283+
span.record("meta", meta_count);
284+
span.record("data", data_count);
285285
let flush_span = tracing::trace_span!("flush task data").entered();
286286
parallel::try_for_each(&[KeySpace::TaskMeta, KeySpace::TaskData], |&key_space| {
287287
let _span = flush_span.clone().entered();

0 commit comments

Comments
 (0)