Skip to main content

turbo_tasks/
invalidation.rs

1use std::{fmt::Display, mem::replace, sync::Arc};
2
3use bincode::{Decode, Encode};
4use indexmap::map::Entry;
5use rustc_hash::FxHashSet;
6use turbo_dyn_eq_hash::{
7    DynEq, DynHash, impl_eq_for_dyn, impl_hash_for_dyn, impl_partial_eq_for_dyn,
8};
9
10use crate::{
11    FxIndexMap, FxIndexSet, NonLocalValue, OperationValue, TaskId, TurboTasksApi,
12    manager::{current_task_if_available, mark_invalidator},
13    util::StaticOrArc,
14};
15
16/// Get an [`Invalidator`] that can be used to invalidate the current task
17/// based on external events.
18/// Returns `None` if called outside of a task context.
19pub fn get_invalidator() -> Option<Invalidator> {
20    if let Some(task) = current_task_if_available("turbo_tasks::get_invalidator()") {
21        mark_invalidator();
22        Some(Invalidator { task })
23    } else {
24        None
25    }
26}
27
28/// A lightweight handle to invalidate a task. Only stores the task ID.
29/// The caller must provide the `TurboTasksApi` when calling invalidation methods.
30#[derive(Clone, Copy, Hash, PartialEq, Eq, Encode, Decode, Debug)]
31pub struct Invalidator {
32    task: TaskId,
33}
34
35impl Invalidator {
36    pub fn invalidate(self, turbo_tasks: &dyn TurboTasksApi) {
37        turbo_tasks.invalidate(self.task);
38    }
39
40    pub fn invalidate_with_reason<T: InvalidationReason>(
41        self,
42        turbo_tasks: &dyn TurboTasksApi,
43        reason: T,
44    ) {
45        turbo_tasks.invalidate_with_reason(
46            self.task,
47            (Arc::new(reason) as Arc<dyn InvalidationReason>).into(),
48        );
49    }
50
51    /// Invalidates all `invalidators` together, as one batch. Duplicates are fine, every task is
52    /// invalidated once. Large batches may be split into chunks that are invalidated in parallel.
53    pub fn invalidate_all(
54        invalidators: impl IntoIterator<Item = Invalidator>,
55        turbo_tasks: &dyn TurboTasksApi,
56    ) {
57        let tasks = Self::unique_tasks(invalidators);
58        if !tasks.is_empty() {
59            turbo_tasks.invalidate_many(&tasks);
60        }
61    }
62
63    /// Like [`Invalidator::invalidate_all`], recording `reason` for the batch.
64    pub fn invalidate_all_with_reason<T: InvalidationReason>(
65        invalidators: impl IntoIterator<Item = Invalidator>,
66        turbo_tasks: &dyn TurboTasksApi,
67        reason: T,
68    ) {
69        Self::invalidate_all_with_reasons(invalidators, turbo_tasks, [reason]);
70    }
71
72    /// Like [`Invalidator::invalidate_all`], recording all `reasons` for the batch. The reasons
73    /// are not tied to individual tasks, they are only reported in the aggregated update info.
74    pub fn invalidate_all_with_reasons<T: InvalidationReason>(
75        invalidators: impl IntoIterator<Item = Invalidator>,
76        turbo_tasks: &dyn TurboTasksApi,
77        reasons: impl IntoIterator<Item = T>,
78    ) {
79        let tasks = Self::unique_tasks(invalidators);
80        if tasks.is_empty() {
81            return;
82        }
83        let reasons = reasons
84            .into_iter()
85            .map(|reason| (Arc::new(reason) as Arc<dyn InvalidationReason>).into())
86            .collect();
87        turbo_tasks.invalidate_many_with_reasons(&tasks, reasons);
88    }
89
90    /// The backend requires unique task ids in a batch.
91    fn unique_tasks(invalidators: impl IntoIterator<Item = Invalidator>) -> Vec<TaskId> {
92        let invalidators = invalidators.into_iter();
93        let capacity = invalidators.size_hint().0;
94        let mut seen = FxHashSet::with_capacity_and_hasher(capacity, Default::default());
95        let mut tasks = Vec::with_capacity(capacity);
96        for invalidator in invalidators {
97            if seen.insert(invalidator.task) {
98                tasks.push(invalidator.task);
99            }
100        }
101        tasks
102    }
103}
104
105unsafe impl OperationValue for Invalidator {}
106// Safety: Invalidator only contains a TaskId (a NonZero<u32> wrapper) and does not contain any
107// local Vc references.
108unsafe impl NonLocalValue for Invalidator {}
109
110/// A user-facing reason why a task was invalidated. This should only be used
111/// for invalidation that were triggered by the user.
112///
113/// Reasons are deduplicated, so this need to implement [Eq] and [Hash]
114pub trait InvalidationReason: DynEq + DynHash + Display + Send + Sync + 'static {
115    fn kind(&self) -> Option<StaticOrArc<dyn InvalidationReasonKind>> {
116        None
117    }
118}
119
120/// Invalidation reason kind. This is used to merge multiple reasons of the same
121/// kind into a combined description.
122///
123/// Reason kinds are used a hash map key, so this need to implement [Eq] and
124/// [Hash]
125pub trait InvalidationReasonKind: DynEq + DynHash + Send + Sync + 'static {
126    /// Displays a description of multiple invalidation reasons of the same
127    /// kind. It is only called with two or more reasons.
128    fn fmt(
129        &self,
130        data: &FxIndexSet<StaticOrArc<dyn InvalidationReason>>,
131        f: &mut std::fmt::Formatter<'_>,
132    ) -> std::fmt::Result;
133}
134
135impl_partial_eq_for_dyn!(dyn InvalidationReason);
136impl_eq_for_dyn!(dyn InvalidationReason);
137impl_hash_for_dyn!(dyn InvalidationReason);
138
139impl_partial_eq_for_dyn!(dyn InvalidationReasonKind);
140impl_eq_for_dyn!(dyn InvalidationReasonKind);
141impl_hash_for_dyn!(dyn InvalidationReasonKind);
142
143#[derive(PartialEq, Eq, Hash)]
144enum MapKey {
145    Untyped {
146        unique_tag: usize,
147    },
148    Typed {
149        kind: StaticOrArc<dyn InvalidationReasonKind>,
150    },
151}
152
153enum MapEntry {
154    Single {
155        reason: StaticOrArc<dyn InvalidationReason>,
156    },
157    Multiple {
158        reasons: FxIndexSet<StaticOrArc<dyn InvalidationReason>>,
159    },
160}
161
162/// A set of [InvalidationReason]s. They are automatically deduplicated and
163/// merged by kind during insertion. It implements [Display] to get a readable
164/// representation.
165#[derive(Default)]
166pub struct InvalidationReasonSet {
167    next_unique_tag: usize,
168    // We track typed and untyped entries in the same map to keep the occurrence order of entries.
169    map: FxIndexMap<MapKey, MapEntry>,
170}
171
172impl InvalidationReasonSet {
173    pub(crate) fn insert(&mut self, reason: StaticOrArc<dyn InvalidationReason>) {
174        if let Some(kind) = reason.kind() {
175            let key = MapKey::Typed { kind };
176            match self.map.entry(key) {
177                Entry::Occupied(mut entry) => {
178                    let entry = &mut *entry.get_mut();
179                    match replace(
180                        entry,
181                        MapEntry::Multiple {
182                            reasons: FxIndexSet::default(),
183                        },
184                    ) {
185                        MapEntry::Single {
186                            reason: existing_reason,
187                        } => {
188                            if reason == existing_reason {
189                                *entry = MapEntry::Single {
190                                    reason: existing_reason,
191                                };
192                                return;
193                            }
194                            let mut reasons = FxIndexSet::default();
195                            reasons.insert(existing_reason);
196                            reasons.insert(reason);
197                            *entry = MapEntry::Multiple { reasons };
198                        }
199                        MapEntry::Multiple { mut reasons } => {
200                            reasons.insert(reason);
201                            *entry = MapEntry::Multiple { reasons };
202                        }
203                    }
204                }
205                Entry::Vacant(entry) => {
206                    entry.insert(MapEntry::Single { reason });
207                }
208            }
209        } else {
210            let key = MapKey::Untyped {
211                unique_tag: self.next_unique_tag,
212            };
213            self.next_unique_tag += 1;
214            self.map.insert(key, MapEntry::Single { reason });
215        }
216    }
217
218    pub fn is_empty(&self) -> bool {
219        self.map.is_empty()
220    }
221
222    pub fn len(&self) -> usize {
223        self.map.len()
224    }
225}
226
227impl Display for InvalidationReasonSet {
228    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229        let count = self.map.len();
230        for (i, (key, entry)) in self.map.iter().enumerate() {
231            if i > 0 {
232                write!(f, ", ")?;
233                if i == count - 1 {
234                    write!(f, "and ")?;
235                }
236            }
237            match entry {
238                MapEntry::Single { reason } => {
239                    write!(f, "{reason}")?;
240                }
241                MapEntry::Multiple { reasons } => {
242                    let MapKey::Typed { kind } = key else {
243                        unreachable!("An untyped reason can't collect more than one reason");
244                    };
245                    kind.fmt(reasons, f)?
246                }
247            }
248        }
249        Ok(())
250    }
251}