@@ -29,7 +29,7 @@ use turbo_tasks::{
2929} ;
3030
3131use crate :: {
32- backend:: counter_map:: CounterMap ,
32+ backend:: { counter_map:: CounterMap , operation :: LEAF_NUMBER } ,
3333 data:: {
3434 ActivenessState , AggregationNumber , CellRef , CollectibleRef , CollectiblesRef , Dirtyness ,
3535 InProgressCellState , InProgressState , LeafDistance , OutputValue , RootType , TransientTask ,
@@ -384,11 +384,21 @@ impl TaskFlags {
384384// Eviction
385385// =============================================================================
386386
387+ #[ derive( Debug , Clone , Copy , PartialEq , Eq , Hash ) ]
388+ pub enum UnevictableReason {
389+ InProgress ,
390+ TransientDependents ,
391+ TransientData ,
392+ TransientUppers ,
393+ SessionState ,
394+ Modified ,
395+ }
396+
387397/// Eviction level for a task after a snapshot.
388398#[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
389399pub enum Evictability {
390400 /// Task cannot be evicted.
391- No ,
401+ No ( UnevictableReason ) ,
392402 /// Only the data category can be evicted (meta is still in use).
393403 DataOnly ,
394404 /// The entire task can be evicted (removed from the storage map).
@@ -426,7 +436,7 @@ impl TaskStorage {
426436 || self . get_activeness ( ) . is_some ( )
427437 || self . get_transient_task_type ( ) . is_some ( )
428438 {
429- return Evictability :: No ;
439+ return Evictability :: No ( UnevictableReason :: InProgress ) ;
430440 }
431441
432442 // Check if full eviction is possible
@@ -436,28 +446,71 @@ impl TaskStorage {
436446 let data_evictable = flags. data_restored ( )
437447 && !flags. data_modified ( )
438448 && !flags. data_modified_during_snapshot ( ) ;
449+ if !data_evictable {
450+ return Evictability :: No ( UnevictableReason :: Modified ) ;
451+ }
439452
440- 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 ) ) {
453+ // Data-category fields with `filter_transient` lose entries referencing transient
454+ // tasks when serialized to disk. If the in-memory copy has such entries, evicting
455+ // (and later restoring from disk) would silently drop those reverse-dependency
456+ // edges, causing transient tasks (e.g., HMR update streams) to never be notified
457+ // when cells/outputs change. Prevent data eviction in this case.
458+ let has_transient_dependents = self
459+ . output_dependent ( )
460+ . iter ( )
461+ . any ( |task_id| task_id. is_transient ( ) )
462+ || self
463+ . cell_dependents ( )
464+ . is_some_and ( |deps| deps. iter ( ) . any ( |( _, _, task_id) | task_id. is_transient ( ) ) )
465+ || self
466+ . collectibles_dependents ( )
467+ . is_some_and ( |deps| deps. iter ( ) . any ( |( _, id) | id. is_transient ( ) ) ) ;
468+ // If any transient tasks are reading this one we need to not evict so the notifications
469+ // still work
470+ if has_transient_dependents {
471+ return Evictability :: No ( UnevictableReason :: TransientDependents ) ;
472+ }
473+ // Check for non-serializable cell data (transient category, while this would be preserved
474+ // by only
475+ if self . transient_cell_data ( ) . is_some_and ( |m| !m. is_empty ( ) )
476+ || self . get_output ( ) . is_some_and ( |o| o. is_transient ( ) )
477+ {
478+ return Evictability :: No ( UnevictableReason :: TransientData ) ;
479+ }
480+ // Meta fields with filter_transient (children, upper, followers, output,
481+ // collectibles_dependents, etc.) lose transient entries when serialized.
482+ // If the task participates in the aggregation graph with transient nodes,
483+ // full eviction would break those relationships. Check key meta fields.
484+ debug_assert ! (
485+ !self
486+ . children( )
487+ . is_some_and( |c| c. iter( ) . any( |id| id. is_transient( ) ) ) ,
488+ "persistent tasks cannot have transient children"
489+ ) ;
490+ if self . upper ( ) . iter ( ) . any ( |( id, _) | id. is_transient ( ) ) {
491+ return Evictability :: No ( UnevictableReason :: TransientUppers ) ;
492+ }
493+
494+ if self
495+ . get_dirty ( )
496+ . is_some_and ( |d| matches ! ( d, Dirtyness :: SessionDependent ) )
497+ {
498+ return Evictability :: No ( UnevictableReason :: SessionState ) ;
499+ }
500+ if meta_evictable {
501+ // Session-dependent tasks have transient state (current_session_clean flag,
502+ // Dirtyness::SessionDependent) that would be lost on full eviction.
503+
504+ // Aggregating nodes carry transient session-clean container counts that would
505+ // be lost on full eviction, breaking has_dirty_containers() checks.
506+ if self . aggregation_number . effective >= LEAF_NUMBER {
451507 return Evictability :: DataOnly ;
452508 }
453- return Evictability :: Full ;
454- }
455509
456- if data_evictable {
457- return Evictability :: DataOnly ;
510+ return Evictability :: Full ;
458511 }
459512
460- Evictability :: No
513+ return Evictability :: DataOnly ;
461514 }
462515}
463516
0 commit comments