@@ -47,7 +47,7 @@ use turbo_tasks::{
4747
4848pub use self :: {
4949 operation:: AnyOperation ,
50- storage:: { SpecificTaskDataCategory , TaskDataCategory } ,
50+ storage:: { EvictionCounts , SpecificTaskDataCategory , TaskDataCategory } ,
5151} ;
5252#[ cfg( feature = "trace_task_dirty" ) ]
5353use crate :: backend:: operation:: TaskDirtyCause ;
@@ -86,13 +86,13 @@ const DEPENDENT_TASKS_DIRTY_PARALLIZATION_THRESHOLD: usize = 10000;
8686const SNAPSHOT_REQUESTED_BIT : usize = 1 << ( usize:: BITS - 1 ) ;
8787
8888/// Configurable idle timeout for snapshot persistence.
89- /// Defaults to 2 seconds if not set or if the value is invalid.
89+ /// Defaults to 10 seconds if not set or if the value is invalid.
9090static IDLE_TIMEOUT : LazyLock < Duration > = LazyLock :: new ( || {
9191 std:: env:: var ( "TURBO_ENGINE_SNAPSHOT_IDLE_TIMEOUT_MILLIS" )
9292 . ok ( )
9393 . and_then ( |v| v. parse :: < u64 > ( ) . ok ( ) )
9494 . map ( Duration :: from_millis)
95- . unwrap_or ( Duration :: from_secs ( 2 ) )
95+ . unwrap_or ( Duration :: from_secs ( 10 ) )
9696} ) ;
9797
9898struct SnapshotRequest {
@@ -143,6 +143,11 @@ pub struct BackendOptions {
143143
144144 /// Avoid big preallocations for faster startup. Should only be used for testing purposes.
145145 pub small_preallocation : bool ,
146+
147+ /// When enabled, evict all evictable tasks from in-memory storage after every snapshot.
148+ /// This reclaims memory by clearing persisted data that can be re-loaded from disk on demand.
149+ /// This is an EXPERIMENTAL FEATURE under development
150+ pub evict_after_snapshot : bool ,
146151}
147152
148153impl Default for BackendOptions {
@@ -153,6 +158,7 @@ impl Default for BackendOptions {
153158 storage_mode : Some ( StorageMode :: ReadWrite ) ,
154159 num_workers : None ,
155160 small_preallocation : false ,
161+ evict_after_snapshot : false ,
156162 }
157163 }
158164}
@@ -220,6 +226,19 @@ impl<B: BackingStorage> TurboTasksBackend<B> {
220226 pub fn backing_storage ( & self ) -> & B {
221227 & self . 0 . backing_storage
222228 }
229+
230+ /// Perform a snapshot and then evict all evictable tasks from memory.
231+ ///
232+ /// This is exposed for integration tests that need to verify the
233+ /// snapshot → evict → restore cycle works correctly.
234+ ///
235+ /// Returns `(snapshot_had_new_data, eviction_counts)`.
236+ pub fn snapshot_and_evict (
237+ & self ,
238+ turbo_tasks : & dyn TurboTasksBackendApi < TurboTasksBackend < B > > ,
239+ ) -> ( bool , EvictionCounts ) {
240+ self . 0 . snapshot_and_evict ( turbo_tasks)
241+ }
223242}
224243
225244impl < B : BackingStorage > TurboTasksBackendInner < B > {
@@ -339,6 +358,38 @@ impl<B: BackingStorage> TurboTasksBackendInner<B> {
339358 )
340359 }
341360
361+ fn should_evict ( & self ) -> bool {
362+ self . options . evict_after_snapshot && self . should_persist ( )
363+ }
364+
365+ /// Perform a snapshot and then evict all evictable tasks from memory.
366+ ///
367+ /// This is exposed for integration tests that need to verify the
368+ /// snapshot → evict → restore cycle works correctly.
369+ ///
370+ /// Returns `(snapshot_had_new_data, eviction_counts)`.
371+ pub fn snapshot_and_evict (
372+ & self ,
373+ turbo_tasks : & dyn TurboTasksBackendApi < TurboTasksBackend < B > > ,
374+ ) -> ( bool , EvictionCounts ) {
375+ assert ! (
376+ self . should_persist( ) ,
377+ "snapshot_and_evict requires persistence"
378+ ) ;
379+ let snapshot_result = self . snapshot_and_persist ( None , "test" , turbo_tasks) ;
380+ let had_new_data = match snapshot_result {
381+ Some ( ( _, new_data) ) => new_data,
382+ None => {
383+ // Snapshot/persist failed — skip eviction since the data may not
384+ // be on disk yet. Evicting now could lose in-memory state that
385+ // can't be restored.
386+ return ( false , EvictionCounts :: default ( ) ) ;
387+ }
388+ } ;
389+ let counts = self . storage . evict_after_snapshot ( ) ;
390+ ( had_new_data, counts)
391+ }
392+
342393 fn should_restore ( & self ) -> bool {
343394 self . options . storage_mode . is_some ( )
344395 }
@@ -2775,6 +2826,8 @@ impl<B: BackingStorage> TurboTasksBackendInner<B> {
27752826 let mut last_snapshot = self . start_time + Duration :: from_millis ( last_snapshot) ;
27762827 let mut idle_start_listener = self . idle_start_event . listen ( ) ;
27772828 let mut idle_end_listener = self . idle_end_event . listen ( ) ;
2829+ // Whether to immediately set an idle timeout if possible
2830+ // set to false if we don't persist anything in a cycle.
27782831 let mut fresh_idle = true ;
27792832 loop {
27802833 const FIRST_SNAPSHOT_WAIT : Duration = Duration :: from_secs ( 300 ) ;
@@ -2809,7 +2862,7 @@ impl<B: BackingStorage> TurboTasksBackendInner<B> {
28092862 idle_start_listener = self . idle_start_event. listen( )
28102863 } ,
28112864 _ = & mut idle_end_listener => {
2812- idle_time = until + idle_timeout ;
2865+ idle_time = far_future ( ) ;
28132866 idle_end_listener = self . idle_end_event. listen( )
28142867 } ,
28152868 _ = tokio:: time:: sleep_until( until) => {
@@ -2836,6 +2889,41 @@ impl<B: BackingStorage> TurboTasksBackendInner<B> {
28362889 if let Some ( ( snapshot_start, new_data) ) = snapshot {
28372890 last_snapshot = snapshot_start;
28382891
2892+ // Evict persisted tasks from memory to reclaim space.
2893+ // Like compaction, this runs after snapshot_and_persist
2894+ // as a separate concern.
2895+
2896+ // TODO: should we only run if we stored new data? syncing data to disk
2897+ // implies that some of it is eligible for eviction, but if nothing was
2898+ // stored then that isn't true. on the other hand pre-fetching might
2899+ // bring unused data into the heap.
2900+ if this. should_evict ( ) && new_data {
2901+ let idle_ended = tokio:: select! {
2902+ biased;
2903+ _ = & mut idle_end_listener => {
2904+ idle_end_listener = self . idle_end_event. listen( ) ;
2905+ true
2906+ } ,
2907+ _ = std:: future:: ready( ( ) ) => false ,
2908+ } ;
2909+ if !idle_ended {
2910+ let evict_span = tracing:: info_span!(
2911+ parent: background_span. id( ) ,
2912+ "evict tasks" ,
2913+ full = tracing:: field:: Empty ,
2914+ data_and_meta = tracing:: field:: Empty ,
2915+ data_only = tracing:: field:: Empty ,
2916+ meta_only = tracing:: field:: Empty ,
2917+ ) ;
2918+ let _guard = evict_span. enter ( ) ;
2919+ let counts = this. storage . evict_after_snapshot ( ) ;
2920+ evict_span. record ( "full" , counts. full ) ;
2921+ evict_span. record ( "data_and_meta" , counts. data_and_meta ) ;
2922+ evict_span. record ( "data_only" , counts. data_only ) ;
2923+ evict_span. record ( "meta_only" , counts. meta_only ) ;
2924+ }
2925+ }
2926+
28392927 // Compact while idle (up to limit), regardless of
28402928 // whether the snapshot had new data.
28412929 // `background_span` is not entered here because
0 commit comments