ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

深入理解 Next.js 的 Turbo Tasks:增量计算缓存系统的架构、原语与用法

深入理解 Next.js 的 Turbo Tasks:增量计算缓存系统的架构、原语与用法 深入理解 Next.js 的 Turbo Tasks增量计算缓存系统的架构、原语与用法【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.jsTurbo Tasksturbo-tasks是 Turbopack 与 Next.js 底层构建体系next.js 仓库中turbopack/crates/turbo-tasks所依赖的增量计算框架它通过 Rust 宏与类型系统把缓存、失效与重建自动化。本文以其在仓库中的官方文档为主体结合 crate 源码系统讲解 Turbo Tasks 的 4 个核心原语、任务图与失效传播、增量构建原理以及#[turbo_tasks::function]宏的属性、签名改写规则、单例模式Singleton Pattern与 Vc 读写机制帮助你理解 Turbopack 为什么能少做工作却得到正确答案。1. Turbo Tasks 是什么用类型和宏自动化的增量计算在开始阅读本文之前你可以先了解 Turbopack 的整体设计仓库根目录的 readme.md 定位了项目为 The React Framework而本模块则位于 turbopack/crates/turbo-tasks。该目录下的 README.md 开门见山地给出定义An incremental computation system that uses macros and types to automate the caching process. 一个增量计算系统使用宏和类型来自动化缓存过程。理解这句话的关键在于一般的缓存方案需要开发者手动决定缓存什么、何时失效、何时重算而 Turbo Tasks 尝试把这件事交给编译器宏和类型系统。开发者只需要用宏标注函数、用类型声明数据结构依赖追踪与失效重建就会自动发生。文档同时建议想要获得高层概览可以先阅读官方博客Inside Turbopack: Building Faster by Building Less博客原文通过文档末尾的[blog-post]链接给出。本文后续内容以该 crate 文档和源码为准。四个核心原语PrimitivesREADME.md 明确列出 Turbo Tasks 定义的四类基础构造原语宏/类型职责Functions函数#[turbo_tasks::function]执行、失效与重新执行的单元Values值#[turbo_tasks::value]由函数创建、存储并返回的数据Traits特质#[turbo_tasks::value_trait]在值之上定义一组函数的 traitCollectibles可收集对象TurboTasks::emit_collectible在函数中发射、沿调用图向上冒泡、可在父函数中收集的值其中 Collectibles 具有一个重要的去重特性Collectibles 依据 cell id 相等性cell id equality进行去重。可以把它理解为构建过程产生的副作用/告警/错误等带类型的消息它们在任务调用图内向上冒泡供上层任务统一收集处理。对应的能力定义在 src/collectibles.rs 的CollectiblesSourcetrait 中提供了drop_collectibles、take_collectibles、peek_collectibles三种操作。由原语派生的两个元素Tasks任务一个函数与其参数的实例。换言之#[turbo_tasks::function]函数 一组具体参数 一个运行时任务任务是构建系统中工作的最小单位。VcValue Cell值单元指向任务中值存储位置的引用。一次函数重执行后cell 的内容可能因失效而变化对一个Vc执行读取.await会得到该时刻 cell 数据的一个快照引用ReadRef只读。常见设计模式单例模式文档末尾专门指出Turbo Tasks 常用的设计模式是Singleton Pattern单例模式其作用是保证值与值 cell之间的1:1 映射。该模式的完整说明与示例见 singleton_pattern.md具体内容在第 6 节展开。2. 函数与任务Functions and TasksREADME.md 用一段示意图展示了一个示例 turbo-task 函数的形态随后说明#[turbo_tasks::function]函数是构建过程中的记忆化memoized函数。一个函数配合参数构成一个task任务负责三件事追踪依赖Tracking Dependencies每个任务都会记录它的依赖用来判断何时需要重算。依赖在一个VcT被.await时被记录。重算变更Recomputing Changes当一个依赖发生变化时受影响的任务会被自动重算。并行执行Parallel Execution每个任务都会被派生为一个 [Tokio task]文档中链接到 Tokio 官方文档的 spawning 教程跑在 Tokio 多线程 work-stealing 执行器上。需要强调的一点是记忆化memoized并不是永久缓存。它更像是惰性 依赖感知同一个函数用相同参数调用时复用上次结果但一旦其读取过的Vc失效就会自动重新执行。这样保证缓存自动失效无需手写版本号或清理逻辑。任务的外部签名External Signature完整规范位于 function.md。要点如下定义一个任务就是在 Rust 函数上标注#[turbo_tasks::function]并调用它函数 参数的每种唯一组合都会在运行时创建一个新任务。任务函数可以是同步或异步函数。参数必须实现TaskInputtrait通常是原始类型如i32或包在VcT中的类型。任务函数的外部签名总是返回VcT或OperationVcT声明时可以是VcT或ResolvedVcT可再包一层Result...若声明为ResolvedVcT外部签名会被改写回VcT。任务函数不支持泛型类型参数或生命周期参数。#[turbo_tasks::function]宏会改写函数的参数与返回值改写后的签名称为外部签名external signature。参数改写规则类型为ResolvedVcT的参数改写为VcT调用函数时会自动 resolve 值 cell减少在VcT与ResolvedVcT之间来回转换的工作量该规则也适用于嵌套在OptionResolvedVcT与VecResolvedVcT内的场景详见FromTaskInputtrait。方法参数中的self被改写为self: VcSelf。返回值改写规则ResultVcT改写为VcT这样允许在任务函数体内惯用地使用?操作符。ResolvedVcT改写为VcT允许任务直接返回一个已 resolve 的 cell例如来自ResolvedVc::cell或.resolved_cell()而无需显式转回VcT包在Result中的ResultResolvedVcT同样改写为VcT。无返回值的函数改写为返回Vc()。异步函数隐含的impl FutureOutput VcT被**压平flatten**成VcT类型——VcT实现了IntoFuture因此可以直接.await。这些逻辑的一部分由TaskOutputtrait 及其关联类型Return表示见src/task/下的源码。外部签名改写示例摘自 function.md// 声明形式 #[turbo_tasks::function] async fn foo( self, a: i32, b: Vci32, c: ResolvedVci32, d: OptionVecResolvedVci32, ) - ResultResolvedVci32 { // ... } // 改写后的外部签名 fn foo( self: VcSelf, // 原来是 self a: i32, b: Vci32, c: Vci32, // 原来是 ResolvedVci32 d: OptionVecVci32, // 原来是 OptionVecResolvedVci32 ) - Vci32; // 原来是 impl FutureOutput ResultResolvedVci32这条规则的价值在于实现任务的人可以自由选择最方便的签名形式而调用方看到的始终是一致、友好的VcT世界。#[turbo_tasks::function]支持的属性Attributes宏可以接受若干可选属性来改变任务行为多个属性用逗号分隔组合例如#[turbo_tasks::function(fs, session_dependent)] async fn read_file(path: RcStr) - ResultVcFileContent { // ... }各属性语义整理如下依据 function.md属性含义与约束operation把任务标记为operation外部签名将返回OperationVcT而非VcT且所有参数必须实现OperationValue。Operation 任务是任务图的显式入口可用于把非响应式代码接入响应式计算图。与selfreceiver 互斥。root把任务标记为聚合图中的根节点。根任务从最大聚合号u32::MAX开始因此位于聚合树顶部用于表示计算的顶层入口点。fsI/O 标记声明任务直接执行文件系统操作。只应标注在直接执行 I/O 的任务上而不是传递调用它的任务。networkI/O 标记声明任务直接执行网络操作。与fs一样只标注在直接执行 I/O 的任务上。session_dependent标记任务依赖会话session。这类任务从持久化缓存恢复时会被重新执行因为它们依赖外部状态文件系统、环境变量、网络而这些状态在不同会话之间可能已经改变。任务完成时不会被标记为完全干净而是保留一种特殊的 session dependent 脏状态若之后从持久缓存恢复该状态会促使它重新执行而不是复用缓存结果。典型用例是文件读取、环境变量读取、网络请求。session_dependent的示例摘自文档清楚地展示了应用位置的原则#[turbo_tasks::function(fs, session_dependent)] async fn read(self, path: FileSystemPath) - ResultVcFileContent { // 文件内容在上个会话后可能已变化因此缓存恢复时始终重新执行。 // ... } #[turbo_tasks::function(session_dependent)] fn read_all_env(self) - VcTransientEnvMap { // 环境变量在不同会话之间可能不同。 Vc::cell(self.vars.clone()) }注意session_dependent应标注在直接读取外部状态的叶子任务上传递依赖它的任务无需标注——当被依赖的 session-dependent 任务产出新结果时它们会自然重执行。方法与self为值实现方法、为 trait 实现任务任务可以作为某个值或 trait 实现上的方法存在依托 Rust 的arbitrary_self_typesnightly 特性见 function.md 指向 RFC 3519 的链接。**固有实现inherent implementation**示例#[turbo_tasks::value_impl] impl Something { #[turbo_tasks::function] fn method(self: VcSelf, a: i32) - VcSomethingElse { // 收到完整的 VcSelf需要 .await 才能得到 ReadRefSelf。 let value self.await?; // Vc 类型适合继续调用声明在 VcSelf 上的其他方法 self.method_resolved(a) } #[turbo_tasks::function] fn method_resolved(self: ResolvedVcSelf, a: i32) - VcSomethingElse { // 收到 ResolvedVcSelf可以 .await 为 ReadRef也可以隐式或通过 *解引用为 Vc。 let value self.await?; self.method_ref(a) } #[turbo_tasks::function] fn method_ref(self, a: i32) - VcSomethingElse { // 便捷形式收到 self 的已 resolve 版本读取时无需 .await。 // 可以直接访问结构体字段但不能调用声明在 VcSelf 上的其他方法。 Vc::cell(SomethingElse::new(self.some_field, a)) } }声明位置这些方法定义在VcT上即Vc::Something::method而不是内部类型上。self语法糖#[turbo_tasks::function]的self参数会隐式地从self: VcSelf读取值。签名改写所有外部签名改写规则在此同样适用self可以是ResolvedVcT也支持async与ResultVcT返回值。trait 实现与固有实现几乎相同只是 trait 中只有外部签名改写后需要与 trait 定义对齐#[turbo_tasks::value_impl] impl Trait for Something { #[turbo_tasks::function] fn method(self: VcSelf, a: i32) - VcSomethingElse { // self: ResolvedVcSelf 和 self 也是合法的参数类型 } }与这些方法/值定义配套的还有#[turbo_tasks::value_trait]用于让 trait 可作为VcBoxdyn MyTrait存入 cell方法默认实现可用支持no_debug、operation等参数与#[turbo_tasks::value_impl]作用于任何VcValueType的 impl 块两者的详细文档均以 rustdoc 形式内嵌在 src/lib.rs 中。3. 任务图Task Graph所有任务及其依赖共同构成一张任务图task graph。README.md 用第二张示意图展示了一个任务图的例子其图注明确说明了这张图表达的三类调用树初始冷执行一次全新的完整执行标记脏mark dirty操作某个文件发生变化时对该任务的标记从叶子到根部的传播脏状态如何由叶子逐级传播到根任务。这张图对**失效传播invalidation propagation**至关重要当一个任务被失效时变更会沿任务图传播在必要处触发重建。可以把任务图理解为构建系统的因果网络——只要记录下谁读了谁的依赖关系系统就能在源头变化后精准定位哪些下游需要重算。任务图形态的一个关键注解从任务的角度#[turbo_tasks::function]的执行依赖在读取Vc时即.await时被记录。这意味着依赖记录不是静态推断而是动态追踪同一个任务函数在不同输入下可能读取不同的Vc任务图也会相应地记录不同的依赖边。这种按需记录依赖的机制是 Turbo Tasks 实现精确增量重建的基础。4. 增量构建Incremental Builds这是本模块的核心价值所在。执行函数时turbo-tasks会记录读取过哪些Vc一旦其中任何一个变化系统就会使由该函数执行所创建的任务失效任务随后会被调度并重新执行。自底向上的重建策略首次执行之后Turbo Tasks 对增量重建采用**自底向上bottom-up**的策略。流程可以概括为某个源头例如一个文件发生变化 → 直接依赖它的任务被标记失效失效沿任务图传播只有被影响的任务才会被标记通过重建失效任务只有图中受变更影响的部分被重建未受影响的部分保持原样。文档中有一句关键总结值得原样保留By rebuilding invalidated tasks, only the parts of the graph affected by changes are rebuilt, leaving untouched parts intact.No work is done for unchanged parts.通过重建失效任务只有图中受变更影响的部分被重建未受影响的部分保持完好。对于未变化的部分不执行任何工作。这就是 Turbopack 之所以能够实现毫秒级热更新体验的架构根源它不是在每次变更时全量重算而是在一张精确的依赖图上执行最小重建。也可以从结构层面印证任务调度与优先级逻辑集中在 src/manager.rs 的TurboTasksB: Backend结构与priority_runner中后台会持续把失效任务排队、调度交由 Tokio 多线程执行器处理。5. 深入 Vccell 的创建、读取、更新与执行模型VcValue Cell是理解整个系统的一把钥匙。turbo-tasks在 src/vc/README.md 中对 cell 的语义做了最系统的阐述本节将其完整内容与源码互相印证。5.1 cell 的定义像电子表格里的单元格Value cells 表示一个计算的结果可能是未完成的、pending 的就像电子表格中的一个单元格。当某个Vc的内容变化时变化会通过失效依赖任务来传播。要获得指向值的引用需要对VcT执行.await得到ReadRefTlet some_vc: VcT; let some_ref: ReadRefT some_vc.await?; some_ref.some_method_on_t();返回的ReadRefT是某个时刻 cell 值的一个引用计数快照基于triomphe::Arc。这正是文档反复强调的快照语义的来源。5.2 cell 的四个特性cell 是与任务关联的数据存储位置提供不可变性Immutability值一旦存入 cell在该任务重执行之前保持不变。可重算性Recomputability如果 cell 被失效或缓存被驱逐可以通过重执行其所属任务来重新计算内容。依赖追踪Dependency Tracking当 cell 的内容被读取.await时读取任务会被标记为该 cell 的依赖者。持久化Persistence由持久化任务拥有的 cell 可以通过bincodecrate 序列化。存储模型cell 存放在构造它的任务所关联的数组里。一个Vc既可以指向某个特定 cell即 resolved cell也可以指向某个函数的返回值即 unresolved cell。src/vc/README.md 中配图说明TaskOutput指向一个具体任务及其输出 cellTaskCell指向任务内的某个具体 cell。Task cell 存储在一张表概念上等价于MapValueTypeId, VecSharedReference中由一个类型 id 顺序分配的索引对来引用。5.3 构造 cell大多数使用#[turbo_tasks::value]宏的类型都会获得一个.cell()方法该方法返回该类型的Vc。使用#[turbo_tasks::value(transparent)]的透明包装类型无法在包装类型上定义方法因此需要用Vc::cell函数来构造。5.4 更新 cellcompare-then-update 与确定性执行任务每次运行时其 cell 都会被重建。.cell()或Vc::cell被调用时该ValueTypeId的 cell 计数器递增用PartialEq将新值与上一次执行的值比较若该索引的值不同cell 被更新所有依赖任务被失效。先比较再更新compare-then-update的行为可以通过#[turbo_tasks::value(cell new)]覆盖为总是更新并失效。在 src/lib.rs 的#[turbo_tasks::value]宏文档中对该参数有更细的说明cell new总是覆盖 cell 中的值并使所有依赖任务失效。cell compare默认覆盖前先与现有值比较要求类型实现Eq。cell keyed类似compare但对透明 map 类型使用逐 key 失效。因为 cell 由类型 构造顺序共同定位任务函数应当保证确定的执行顺序。顺序不一致的函数虽然结果依然正确但可能因失效额外 cell 而造成无谓的重算。文档给出了两条具体建议尽量使用行为确定的类型迭代集合时用IndexMap、BTreeMap、FrozenMap替代HashMap后者的迭代顺序是随机的如果在单个 turbo-task 内部并行执行工作注意不要在跨线程执行的部分里构造 cell可能导致意外的非确定性行为。正确的做法是先在并行中收集结果回到主线程排序后再构造 cell。这也解释了为何 src/lib.rs 会导出FxIndexMap、FxIndexSet等基于indexmap、使用FxHasher的确定顺序集合类型——它们是保证 cell 构造顺序确定性的基础设施。5.5 读取Vc与普通 Future 的三点差异Vc实现了IntoFuture可以.await但相比普通Future有几个关键差异Vc指向的值可能因依赖变化而失效、或被缓存驱逐因此多次.await同一个Vc可能得到不同结果ReadRef只是某一时刻 cell 值的快照。读取.awaitVc会让当前任务被记录为该Vc所属任务或 task cell 的依赖者当被读的任务/cell 变化时当前任务可能被重执行。Vc类型总是Copy的大多数Future不是。这得益于Vc内部只是指向 turbo-tasks 框架所管理数据结构的一小组 id 或索引不做引用计数并支持为假设的、未实现的垃圾回收器做 tracing。5.6 一致性、执行模型与 Local Output 优化执行模型虽然任务函数被期望无副作用但其执行时机仍对性能或使用 collectibles 表达问题/副作用很重要。即使未被.await未缓存的函数调用也保证在根任务结束前或包含该调用的任何强一致读取完成前被执行可能发出 collectibles。但其确切开始执行的时间是实现细节若某依赖失效函数可能执行不止一次。最终一致性由于 turbo-tasks 是**最终一致eventually consistent**的连续两次对同一VcT的.await可能返回不同值。若发生这种情况任务最终会被失效并由强一致根任务OperationVc::read_strongly_consistent重新执行。顶层任务若尝试对Vc做最终一致性读取会 panic。任务对潜在不一致的值不应 panic它们可以返回错误错误会被强一致根任务丢弃。当前所有不一致任务都会被轮询至完成未来版本可能在一段时间后丢弃已被判定为不一致的任务。Local Outputs 优化除显式的 resolved 与 operation 表示外Vc还存在第三种内部表示 LocalVcRawVcUnpacked::LocalOutput它是函数部分参数尚未 resolve时同步返回值的特例存储在任务本地状态中待父级非本地任务退出后释放。系统用NonLocalValue标记 trait 与运行时回退检查来防止潜在的本地Vc逃逸出函数生命周期从而避免在Vc上引入生命周期标注的语法负担。5.7Vc的三种子类型对比Vc有两个显式子类型都可廉价转回VcResolvedVc内部即RawVcUnpacked::TaskCell对任务内通过Vc::cell/.cell()构造的 cell 的引用。因为 cell 至少被构造过一次其具体类型已知支持便宜的下转型downcast。存储为 task id type id cell id 的组合。OperationVc内部即RawVcUnpacked::TaskOutput#[turbo_tasks::function]的同步返回值内部以 task id 存储读取前必须先用connect连接。在涉及 collectibles、需要以强一致方式读取函数结果、或使用State时更有用。三者的能力对比依据 src/vc/README.md 的表格类型内部表示相等性下转型强一致性Collectibles非本地值Vc多种之一RawVc不可靠brokenresolve 后可最终一致不支持不支持ResolvedVcTask Id Type Id Cell Id支持支持且便宜最终一致不支持支持OperationVcTask Id支持resolve 后可支持支持支持这些表示在内部统一使用类型擦除的RawVc存储以减少为支持Vc及其子类型所需的单态化膨胀进而减小二进制体积、缩短编译时间。这意味着Vc常常与ResolvedVc/OperationVc有相同的内存表示但因静态类型未固定而不暴露相同方法如下转型。5.8 相等性Equality与哈希由于等价的两个Vc可能拥有不同表示不推荐直接用相等性比较Vc而是应当先转换为显式子类型首选ResolvedVc再比较。未来版本中Vc可能不再实现Eq、PartialEq或Hash。src/vc/resolved.rs 补充说明两个ResolvedVc相等意味着二者内存表示完全相同而Vc相比之下只多了一个潜在表示可能指向任务本地信息这正是文档中 collectibles are deduplicated by cell id equality 那句论断能够成立的前提。6. 单例模式Singleton Pattern保证值到 Vc 的 1:1 映射README.md 中提到的单例模式在 singleton_pattern.md 中有完整说明。6.1 用途在 turbo-tasks 的语境里单例模式用于把一个值 intern 进一个Vc保证对于一个值恰好存在一个已 resolve 的Vc。这使ResolvedVc的相等性比较更加安全也适合作为IndexMap或HashMap的键。6.2 使用步骤把.cell()方法设为私有使用#[turbo_tasks::value]而非#[turbo_tasks::value(shared)]后者的shared标志会把宏生成的.cell()方法公开给所有人。只在唯一的#[turbo_tasks::function]中调用.cell()该函数扮演构造器角色。构造器参数充当键构造任务的参数作为键保证相同值总是在同一任务中 cell 化。只比较ResolvedVc的相等性未 resolve 的Vc之间可能不相等。6.3 完整示例#[turbo_tasks::value] struct SingletonString { value: String, } #[turbo_tasks::value_impl] impl SingletonString { #[turbo_tasks::function] fn new(value: String) - VcSingletonString { Self { value }.cell() } } #[test] fn test_singleton() { let a1 SingletonString::new(a.to_string()).to_resolved().await?; let a2 SingletonString::new(a.to_string()).to_resolved().await?; let b SingletonString::new(b.to_string()).to_resolved().await?; assert_eq!(a1, a2); // 相同的 ResolvedVc 相等 assert_ne!(a1, b); let set HashSet::from([a1, a2, b]); assert_eq!(set.len(), 2); // 只有两个不同的值 }在这个示例中SingletonString是包装单个String的结构体new作为其构造器函数保证相同字符串值始终被 cell 化到同一个任务里——因此相同内容的值会复用同一个已 resolve 的Vc在作为哈希键、做相等判断时既高效又安全。7. 小结读懂 Turbo Tasks 的三个层次把上面的内容收束起来可以从三个层次把握 Turbo Tasks使用层你写#[turbo_tasks::function]执行单元、#[turbo_tasks::value]数据、#[turbo_tasks::value_trait]/#[turbo_tasks::value_impl]方法、emit_collectible副作用/消息调用函数得到VcT.await读取得到快照ReadRefT。为了正确与高效需要遵循三条铁律任务函数无副作用、cell 构造顺序确定、Vc比较前先 resolve。机制层每次.await都记录依赖边全部任务形成任务图变更触发标记脏→自底向上传播→最小重建冷启动后的一切工作都是增量的。语义层系统是最终一致的顶层由强一致根任务兜底cell 更新采用compare-then-update可用cell new覆盖session_dependent等属性用于精确刻画外部世界状态持久化缓存通过bincode序列化跨会话复用。这套文档与 src/vc/README.md、function.md、singleton_pattern.md 及 src/lib.rs 宏文档互为表里。如果你想继续深入建议从 src/vc/Vc/ResolvedVc/OperationVc 的实现、src/manager.rsTurboTasks调度器与 src/collectibles.rscollectibles 语义读起——它们正是 Turbo Tasks 用宏和类型自动化缓存这句话的代码级答案。【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表