Repository: vercel/next.js
Pull request: Turbopack: Split Dirty into Dirtyness and CleanInSession

### What?

Split Dirty into Dirtyness and CleanInSession

This is in preparation to make CleanInSession transient and only for the current session.

Changed files:
filename: turbopack/crates/turbo-tasks-backend/src/backend/mod.rs
patch: @@ -61,7 +61,7 @@ use crate::{
     backing_storage::BackingStorage,
     data::{
         ActivenessState, AggregationNumber, CachedDataItem, CachedDataItemKey, CachedDataItemType,
-        CachedDataItemValueRef, CellRef, CollectibleRef, CollectiblesRef, DirtyState,
+        CachedDataItemValueRef, CellRef, CollectibleRef, CollectiblesRef, Dirtyness,
         InProgressCellState, InProgressState, InProgressStateInner, OutputValue, RootType,
     },
     utils::{
@@ -567,8 +567,7 @@ impl<B: BackingStorage> TurboTasksBackendInner<B> {
                 }
             }
 
-            let is_dirty =
-                get!(task, Dirty).map_or(false, |dirty_state| dirty_state.get(self.session_id));
+            let is_dirty = task.is_dirty(self.session_id);
 
             // Check the dirty count of the root node
             let dirty_tasks = get!(task, AggregatedDirtyContainerCount)
@@ -624,8 +623,7 @@ impl<B: BackingStorage> TurboTasksBackendInner<B> {
                             visited: &mut FxHashSet<TaskId>,
                         ) -> String {
                             let task = ctx.task(task_id, TaskDataCategory::Data);
-                            let is_dirty = get!(task, Dirty)
-                                .map_or(false, |dirty_state| dirty_state.get(ctx.session_id()));
+                            let is_dirty = task.is_dirty(ctx.session_id());
                             let in_progress =
                                 get!(task, InProgress).map_or("not in progress", |p| match p {
                                     InProgressState::InProgress(_) => "in progress",
@@ -2381,57 +2379,60 @@ impl<B: BackingStorage> TurboTasksBackendInner<B> {
         ));
 
         // Update the dirty state
-        let old_dirty_state = get!(task, Dirty).copied();
+        let old_dirtyness = task.dirtyness_and_session();
 
-        let new_dirty_state = if session_dependent {
-            Some(DirtyState {
-                clean_in_session: Some(self.session_id),
-            })
+        let new_dirtyness = if session_dependent {
+            Some((Dirtyness::SessionDependent, Some(self.session_id)))
         } else {
             None
         };
 
-        let dirty_changed = old_dirty_state != new_dirty_state;
+        let dirty_changed = old_dirtyness != new_dirtyness;
         let data_update = if dirty_changed {
-            if let Some(new_dirty_state) = new_dirty_state {
-                task.insert(CachedDataItem::Dirty {
-                    value: new_dirty_state,
-                });
-            } else {
+            if let Some((value, _)) = new_dirtyness {
+                task.insert(CachedDataItem::Dirty { value });
+            } else if old_dirtyness.is_some() {
                 task.remove(&CachedDataItemKey::Dirty {});
             }
+            if let Some(session_id) = new_dirtyness.and_then(|t| t.1) {
+                task.insert(CachedDataItem::CleanInSession { value: session_id });
+            } else if old_dirtyness.is_some_and(|t| t.1.is_some()) {
+                task.remove(&CachedDataItemKey::CleanInSession {});
+            }
 
-            if old_dirty_state.is_some() || new_dirty_state.is_some() {
-                let mut dirty_containers = get!(task, AggregatedDirtyContainerCount)
-                    .cloned()
-                    .unwrap_or_default();
-                if let Some(old_dirty_state) = old_dirty_state {
-                    dirty_containers.update_with_dirty_state(&old_dirty_state);
+            let mut dirty_containers = get!(task, AggregatedDirtyContainerCount)
+                .cloned()
+                .unwrap_or_default();
+            if let Some((old_dirtyness, old_clean_in_session)) = old_dirtyness {
+                dirty_containers
+                    .update_with_dirtyness_and_session(old_dirtyness, old_clean_in_session);
+            }
+            let aggregated_update = match (old_dirtyness, new_dirtyness) {
+                (None, None) => unreachable!(),
+                (Some(old), None) => {
+                    dirty_containers.undo_update_with_dirtyness_and_session(old.0, old.1)
                 }
-                let aggregated_update = match (old_dirty_state, new_dirty_state) {
-                    (None, None) => unreachable!(),
-                    (Some(old), None) => dirty_containers.undo_update_with_dirty_state(&old),
-                    (None, Some(new)) => dirty_containers.update_with_dirty_state(&new),
-                    (Some(old), Some(new)) => dirty_containers.replace_dirty_state(&old, &new),
-                };
-                if !aggregated_update.is_zero() {
-                    if aggregated_update.get(self.session_id) < 0
-                        && let Some(activeness_state) = get_mut!(task, Activeness)
-                    {
-                        activeness_state.all_clean_event.notify(usize::MAX);
-                        activeness_state.unset_active_until_clean();
-                        if activeness_state.is_empty() {
-                            task.remove(&CachedDataItemKey::Activeness {});
-                        }
+                (None, Some(new)) => {
+                    dirty_containers.update_with_dirtyness_and_session(new.0, new.1)
+                }
+                (Some(old), Some(new)) => {
+                    dirty_containers.replace_dirtyness_and_session(old.0, old.1, new.0, new.1)
+                }
+            };
+            if !aggregated_update.is_zero() {
+                if aggregated_update.get(self.session_id) < 0
+                    && let Some(activeness_state) = get_mut!(task, Activeness)
+                {
+                    activeness_state.all_clean_event.notify(usize::MAX);
+                    activeness_state.unset_active_until_clean();
+                    if activeness_state.is_empty() {
+                        task.remove(&CachedDataItemKey::Activeness {});
                     }
-                    AggregationUpdateJob::data_update(
-                        &mut task,
-                        AggregatedDataUpdate::new()
-                            .dirty_container_update(task_id, aggregated_update),
-                    )
-                } else {
-                    None
                 }
+                AggregationUpdateJob::data_update(
+                    &mut task,
+                    AggregatedDataUpdate::new().dirty_container_update(task_id, aggregated_update),
+                )
             } else {
                 None
             }
@@ -2900,7 +2901,7 @@ impl<B: BackingStorage> TurboTasksBackendInner<B> {
 
         let mut ctx = self.execute_context(turbo_tasks);
         let mut task = ctx.task(task_id, TaskDataCategory::All);
-        let is_dirty = get!(task, Dirty).map_or(false, |dirty| dirty.get(self.session_id));
+        let is_dirty = task.is_dirty(self.session_id);
         let has_dirty_containers = get!(task, AggregatedDirtyContainerCount)
             .map_or(false, |dirty_containers| {
                 dirty_containers.get(self.session_id) > 0
@@ -2993,7 +2994,7 @@ impl<B: BackingStorage> TurboTasksBackendInner<B> {
                     }
                 }
 
-                let is_dirty = get!(task, Dirty).is_some_and(|dirty| dirty.get(self.session_id));
+                let is_dirty = task.is_dirty(self.session_id);
                 let has_dirty_container = get!(task, AggregatedDirtyContainerCount)
                     .is_some_and(|count| count.get(self.session_id) > 0);
                 let should_be_in_upper = is_dirty || has_dirty_container;

filename: turbopack/crates/turbo-tasks-backend/src/backend/operation/aggregation_update.rs
patch: @@ -249,8 +249,8 @@ impl AggregatedDataUpdate {
                 collectibles_update.push((collectible, 1));
             }
         }
-        if let Some(dirty) = get!(task, Dirty) {
-            dirty_container_count.update_with_dirty_state(dirty);
+        if let Some((dirtyness, clean_in_session)) = task.dirtyness_and_session() {
+            dirty_container_count.update_with_dirtyness_and_session(dirtyness, clean_in_session);
         }
 
         let mut result = Self::new().collectibles_update(collectibles_update);
@@ -323,18 +323,18 @@ impl AggregatedDataUpdate {
             );
 
             if !aggregated_update.is_zero() {
-                let dirty_state = get!(task, Dirty).copied();
+                let dirtyness_and_session = task.dirtyness_and_session();
                 let task_id = task.id();
                 update!(task, AggregatedDirtyContainerCount, |old: Option<
                     DirtyContainerCount,
                 >| {
                     let mut new = old.unwrap_or_default();
-                    if let Some(dirty_state) = dirty_state {
-                        new.update_with_dirty_state(&dirty_state);
+                    if let Some((dirtyness, clean_in_session)) = dirtyness_and_session {
+                        new.update_with_dirtyness_and_session(dirtyness, clean_in_session);
                     }
                     let aggregated_update = new.update_count(&aggregated_update);
-                    if let Some(dirty_state) = dirty_state {
-                        new.undo_update_with_dirty_state(&dirty_state);
+                    if let Some((dirtyness, clean_in_session)) = dirtyness_and_session {
+                        new.undo_update_with_dirtyness_and_session(dirtyness, clean_in_session);
                     }
                     if !aggregated_update.is_zero() {
                         result.dirty_container_update = Some((task_id, aggregated_update));
@@ -1209,7 +1209,7 @@ impl AggregationUpdateQueue {
     ) {
         let session_id = ctx.session_id();
         // Task need to be scheduled if it's dirty or doesn't have output
-        let dirty = get!(task, Dirty).map_or(false, |d| d.get(session_id));
+        let dirty = task.is_dirty(session_id);
         let should_schedule = if dirty {
             Some(TaskExecutionReason::ActivateDirty)
         } else if !task.has_key(&CachedDataItemKey::Output {}) {

filename: turbopack/crates/turbo-tasks-backend/src/backend/operation/invalidate.rs
patch: @@ -11,10 +11,10 @@ use crate::{
                 AggregatedDataUpdate, AggregationUpdateJob, AggregationUpdateQueue,
             },
         },
-        storage::{get, get_mut},
+        storage::{get, get_mut, remove},
     },
     data::{
-        CachedDataItem, CachedDataItemKey, CachedDataItemValue, DirtyState, InProgressState,
+        CachedDataItem, CachedDataItemKey, CachedDataItemValue, Dirtyness, InProgressState,
         InProgressStateInner,
     },
 };
@@ -232,15 +232,11 @@ pub fn make_task_dirty_internal(
         *stale = true;
     }
     let old = task.insert(CachedDataItem::Dirty {
-        value: DirtyState {
-            clean_in_session: None,
-        },
+        value: Dirtyness::Dirty,
     });
     let mut dirty_container = match old {
         Some(CachedDataItemValue::Dirty {
-            value: DirtyState {
-                clean_in_session: None,
-            },
+            value: Dirtyness::Dirty,
         }) => {
             #[cfg(feature = "trace_task_dirty")]
             let _span = tracing::trace_span!(
@@ -254,16 +250,30 @@ pub fn make_task_dirty_internal(
             return;
         }
         Some(CachedDataItemValue::Dirty {
-            value: DirtyState {
-                clean_in_session: Some(session_id),
-            },
+            value: Dirtyness::SessionDependent,
         }) => {
-            // Got dirty in that one session only
-            let mut dirty_container = get!(task, AggregatedDirtyContainerCount)
-                .cloned()
-                .unwrap_or_default();
-            dirty_container.update_session_dependent(session_id, 1);
-            dirty_container
+            let old = remove!(task, CleanInSession);
+            match old {
+                None => {
+                    #[cfg(feature = "trace_task_dirty")]
+                    let _span = tracing::trace_span!(
+                        "session-dependent task already dirty",
+                        name = ctx.get_task_description(task_id),
+                        cause = %TaskDirtyCauseInContext::new(&cause, ctx)
+                    )
+                    .entered();
+                    // already dirty
+                    return;
+                }
+                Some(session_id) => {
+                    // Got dirty in that one session only
+                    let mut dirty_container = get!(task, AggregatedDirtyContainerCount)
+                        .cloned()
+                        .unwrap_or_default();
+                    dirty_container.update_session_dependent(session_id, 1);
+                    dirty_container
+                }
+            }
         }
         None => {
             // Get dirty for all sessions
@@ -284,9 +294,8 @@ pub fn make_task_dirty_internal(
     .entered();
 
     let should_schedule = {
-        let aggregated_update = dirty_container.update_with_dirty_state(&DirtyState {
-            clean_in_session: None,
-        });
+        let aggregated_update =
+            dirty_container.update_with_dirtyness_and_session(Dirtyness::Dirty, None);
         if !aggregated_update.is_zero() {
             queue.extend(AggregationUpdateJob::data_update(
                 &mut task,

filename: turbopack/crates/turbo-tasks-backend/src/backend/operation/mod.rs
patch: @@ -20,12 +20,12 @@ use crate::{
     backend::{
         OperationGuard, TaskDataCategory, TransientTask, TurboTasksBackend, TurboTasksBackendInner,
         TurboTasksBackendJob,
-        storage::{SpecificTaskDataCategory, StorageWriteGuard, iter_many},
+        storage::{SpecificTaskDataCategory, StorageWriteGuard, get, iter_many},
     },
     backing_storage::BackingStorage,
     data::{
         CachedDataItem, CachedDataItemKey, CachedDataItemType, CachedDataItemValue,
-        CachedDataItemValueRef, CachedDataItemValueRefMut,
+        CachedDataItemValueRef, CachedDataItemValueRefMut, Dirtyness,
     },
 };
 
@@ -415,6 +415,21 @@ pub trait TaskGuard: Debug {
     fn invalidate_serialization(&mut self);
     fn prefetch(&mut self) -> Option<FxIndexMap<TaskId, bool>>;
     fn is_immutable(&self) -> bool;
+    fn is_dirty(&self, session_id: SessionId) -> bool {
+        get!(self, Dirty).is_some_and(|dirtyness| match dirtyness {
+            Dirtyness::Dirty => true,
+            Dirtyness::SessionDependent => get!(self, CleanInSession).copied() != Some(session_id),
+        })
+    }
+    fn dirtyness_and_session(&self) -> Option<(Dirtyness, Option<SessionId>)> {
+        match get!(self, Dirty)? {
+            Dirtyness::Dirty => Some((Dirtyness::Dirty, None)),
+            Dirtyness::SessionDependent => Some((
+                Dirtyness::SessionDependent,
+                get!(self, CleanInSession).copied(),
+            )),
+        }
+    }
 }
 
 pub struct TaskGuardImpl<'a, B: BackingStorage> {

filename: turbopack/crates/turbo-tasks-backend/src/data.rs
patch: @@ -144,14 +144,9 @@ transient_traits!(ActivenessState);
 impl Eq for ActivenessState {}
 
 #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
-pub struct DirtyState {
-    pub clean_in_session: Option<SessionId>,
-}
-
-impl DirtyState {
-    pub fn get(&self, session: SessionId) -> bool {
-        self.clean_in_session != Some(session)
-    }
+pub enum Dirtyness {
+    Dirty,
+    SessionDependent,
 }
 
 fn add_with_diff(v: &mut i32, u: i32) -> i32 {
@@ -251,34 +246,47 @@ impl DirtyContainerCount {
         diff
     }
 
-    /// Applies a dirty state to the count. Returns an aggregated count that represents the change.
-    pub fn update_with_dirty_state(&mut self, dirty: &DirtyState) -> DirtyContainerCount {
-        if let Some(clean_in_session) = dirty.clean_in_session {
-            self.update_session_dependent(clean_in_session, 1)
+    /// Applies a dirtyness to the count. Returns an aggregated count that represents the change.
+    pub fn update_with_dirtyness_and_session(
+        &mut self,
+        dirtyness: Dirtyness,
+        clean_in_session: Option<SessionId>,
+    ) -> DirtyContainerCount {
+        if let (Dirtyness::SessionDependent, Some(session_id)) = (dirtyness, clean_in_session) {
+            self.update_session_dependent(session_id, 1)
         } else {
             self.update(1)
         }
     }
 
-    /// Undoes the effect of a dirty state on the count. Returns an aggregated count that represents
+    /// Undoes the effect of a dirtyness on the count. Returns an aggregated count that represents
     /// the change.
-    pub fn undo_update_with_dirty_state(&mut self, dirty: &DirtyState) -> DirtyContainerCount {
-        if let Some(clean_in_session) = dirty.clean_in_session {
-            self.update_session_dependent(clean_in_session, -1)
+    pub fn undo_update_with_dirtyness_and_session(
+        &mut self,
+        dirtyness: Dirtyness,
+        clean_in_session: Option<SessionId>,
+    ) -> DirtyContainerCount {
+        if let (Dirtyness::SessionDependent, Some(session_id)) = (dirtyness, clean_in_session) {
+            self.update_session_dependent(session_id, -1)
         } else {
             self.update(-1)
         }
     }
 
-    /// Replaces the old dirty state with the new one. Returns an aggregated count that represents
+    /// Replaces the old dirtyness with the new one. Returns an aggregated count that represents
     /// the change.
-    pub fn replace_dirty_state(
+    pub fn replace_dirtyness_and_session(
         &mut self,
-        old: &DirtyState,
-        new: &DirtyState,
+        old_dirtyness: Dirtyness,
+        old_clean_in_session: Option<SessionId>,
+        new_dirtyness: Dirtyness,
+        new_clean_in_session: Option<SessionId>,
     ) -> DirtyContainerCount {
-        let mut diff = self.undo_update_with_dirty_state(old);
-        diff.update_count(&self.update_with_dirty_state(new));
+        let mut diff =
+            self.undo_update_with_dirtyness_and_session(old_dirtyness, old_clean_in_session);
+        diff.update_count(
+            &self.update_with_dirtyness_and_session(new_dirtyness, new_clean_in_session),
+        );
         diff
     }
 
@@ -412,37 +420,33 @@ mod dirty_container_count_tests {
     }
 
     #[test]
-    fn test_update_with_dirty_state() {
+    fn test_update_with_dirtyness_and_session() {
         let mut count = DirtyContainerCount::default();
-        let dirty = DirtyState {
-            clean_in_session: None,
-        };
-        let diff = count.update_with_dirty_state(&dirty);
+        let diff = count.update_with_dirtyness_and_session(Dirtyness::Dirty, None);
         assert!(!count.is_zero());
         assert_eq!(count.get(SESSION_1), 1);
         assert_eq!(diff.get(SESSION_1), 1);
         assert_eq!(count.get(SESSION_2), 1);
         assert_eq!(diff.get(SESSION_2), 1);
 
-        let diff = count.undo_update_with_dirty_state(&dirty);
+        let diff = count.undo_update_with_dirtyness_and_session(Dirtyness::Dirty, None);
         assert!(count.is_zero());
         assert_eq!(count.get(SESSION_1), 0);
         assert_eq!(diff.get(SESSION_1), -1);
         assert_eq!(count.get(SESSION_2), 0);
         assert_eq!(diff.get(SESSION_2), -1);
 
         let mut count = DirtyContainerCount::default();
-        let dirty = DirtyState {
-            clean_in_session: Some(SESSION_1),
-        };
-        let diff = count.update_with_dirty_state(&dirty);
+        let diff =
+            count.update_with_dirtyness_and_session(Dirtyness::SessionDependent, Some(SESSION_1));
         assert!(!count.is_zero());
         assert_eq!(count.get(SESSION_1), 0);
         assert_eq!(diff.get(SESSION_1), 0);
         assert_eq!(count.get(SESSION_2), 1);
         assert_eq!(diff.get(SESSION_2), 1);
 
-        let diff = count.undo_update_with_dirty_state(&dirty);
+        let diff = count
+            .undo_update_with_dirtyness_and_session(Dirtyness::SessionDependent, Some(SESSION_1));
         assert!(count.is_zero());
         assert_eq!(count.get(SESSION_1), 0);
         assert_eq!(diff.get(SESSION_1), 0);
@@ -451,31 +455,29 @@ mod dirty_container_count_tests {
     }
 
     #[test]
-    fn test_replace_dirty_state() {
+    fn test_replace_dirtyness_and_session() {
         let mut count = DirtyContainerCount::default();
-        let old = DirtyState {
-            clean_in_session: None,
-        };
-        let new = DirtyState {
-            clean_in_session: Some(SESSION_1),
-        };
-        count.update_with_dirty_state(&old);
-        let diff = count.replace_dirty_state(&old, &new);
+        count.update_with_dirtyness_and_session(Dirtyness::Dirty, None);
+        let diff = count.replace_dirtyness_and_session(
+            Dirtyness::Dirty,
+            None,
+            Dirtyness::SessionDependent,
+            Some(SESSION_1),
+        );
         assert!(!count.is_zero());
         assert_eq!(count.get(SESSION_1), 0);
         assert_eq!(diff.get(SESSION_1), -1);
         assert_eq!(count.get(SESSION_2), 1);
         assert_eq!(diff.get(SESSION_2), 0);
 
         let mut count = DirtyContainerCount::default();
-        let old = DirtyState {
-            clean_in_session: Some(SESSION_1),
-        };
-        let new = DirtyState {
-            clean_in_session: None,
-        };
-        count.update_with_dirty_state(&old);
-        let diff = count.replace_dirty_state(&old, &new);
+        count.update_with_dirtyness_and_session(Dirtyness::SessionDependent, Some(SESSION_1));
+        let diff = count.replace_dirtyness_and_session(
+            Dirtyness::SessionDependent,
+            Some(SESSION_1),
+            Dirtyness::Dirty,
+            None,
+        );
         assert!(!count.is_zero());
         assert_eq!(count.get(SESSION_1), 1);
         assert_eq!(diff.get(SESSION_1), 1);
@@ -563,7 +565,10 @@ pub enum CachedDataItem {
 
     // State
     Dirty {
-        value: DirtyState,
+        value: Dirtyness,
+    },
+    CleanInSession {
+        value: SessionId,
     },
 
     // Children
@@ -695,6 +700,7 @@ impl CachedDataItem {
                 !collectible.cell.task.is_transient()
             }
             CachedDataItem::Dirty { .. } => true,
+            CachedDataItem::CleanInSession { .. } => true,
             CachedDataItem::Child { task, .. } => !task.is_transient(),
             CachedDataItem::CellData { .. } => true,
             CachedDataItem::CellTypeMaxIndex { .. } => true,
@@ -777,6 +783,7 @@ impl CachedDataItem {
             | Self::Output { .. }
             | Self::AggregationNumber { .. }
             | Self::Dirty { .. }
+            | Self::CleanInSession { .. }
             | Self::Follower { .. }
             | Self::Child { .. }
             | Self::Upper { .. }
@@ -811,6 +818,7 @@ impl CachedDataItemKey {
                 !collectible.cell.task.is_transient()
             }
             CachedDataItemKey::Dirty { .. } => true,
+            CachedDataItemKey::CleanInSession { .. } => true,
             CachedDataItemKey::Child { task, .. } => !task.is_transient(),
             CachedDataItemKey::CellData { .. } => true,
             CachedDataItemKey::CellTypeMaxIndex { .. } => true,
@@ -861,6 +869,7 @@ impl CachedDataItemType {
             | Self::Output { .. }
             | Self::AggregationNumber { .. }
             | Self::Dirty { .. }
+            | Self::CleanInSession { .. }
             | Self::Follower { .. }
             | Self::Child { .. }
             | Self::Upper { .. }
@@ -887,6 +896,7 @@ impl CachedDataItemType {
             Self::Output
             | Self::Collectible
             | Self::Dirty
+            | Self::CleanInSession
             | Self::Child
             | Self::CellData
             | Self::CellTypeMaxIndex