Bazel 的并行评估和增量模型。
数据模型
数据模型由以下各项组成:
SkyValue
。也称为节点。SkyValues
是不可变对象,包含构建过程中构建的所有数据和构建的输入。示例包括:输入文件、输出文件、目标和配置的目标。SkyKey
。用于引用SkyValue
(例如FILECONTENTS:/tmp/foo
或PACKAGE://foo
)的不可变短名称。SkyFunction
。根据节点的键和依赖节点构建节点。- 节点图。一种数据结构,包含节点之间的依赖关系。
Skyframe
。Bazel 所依赖的增量评估框架的代码名称。
评估
build 包括评估代表 build 请求的节点(这是我们努力实现的状态,但有大量旧代码阻碍我们实现这一目标)。首先,系统会使用顶级 SkyKey
的键找到并调用其 SkyFunction
。然后,该函数会请求评估其需要评估顶级节点的节点,这反过来会导致其他函数调用,以此类推,直到到达叶节点(通常是表示文件系统中输入文件的节点)。最后,我们得到顶级 SkyValue
的值、一些副作用(例如文件系统中的输出文件),以及构建过程中涉及的节点之间的依赖项的有向无环图。
如果 SkyFunction
无法预先确定执行其工作所需的所有节点,则可以多次请求 SkyKeys
。一个简单的示例是评估最终被判定为符号链接的输入文件节点:该函数会尝试读取文件,发现它是符号链接,然后提取表示符号链接目标的文件系统节点。但它本身可以是符号链接,在这种情况下,原始函数也需要提取其目标。
这些函数在代码中由接口 SkyFunction
表示,并由名为 SkyFunction.Environment
的接口向其提供服务。函数可以执行以下操作:
- 通过调用
env.getValue
请求对另一个节点进行评估。如果节点可用,则返回其值;否则,返回null
,并且函数本身应返回null
。在后一种情况下,系统会评估依赖项节点,然后再次调用原始节点构建器,但这次相同的env.getValue
调用将返回非null
值。 - 通过调用
env.getValues()
请求评估多个其他节点。这在本质上与前面的方法相同,只不过依赖节点是并行求值的。 - 在调用期间执行计算
- 具有副作用,例如将文件写入文件系统。需要注意,两个不同的函数不能互相干扰。一般来说,写入副作用(数据从 Bazel 流出)是可以的,读取副作用(数据在没有注册依赖项的情况下流入 Bazel)则不行,因为它们是未注册的依赖项,因此可能会导致增量 build 不正确。
SkyFunction
实现不应以请求依赖项以外的任何方式访问数据(例如直接读取文件系统),因为这会导致 Bazel 未注册对所读取文件的数据依赖项,从而导致增量 build 不正确。
当函数有足够的数据来执行其工作后,应返回一个非 null
值来指示已完成。
这种评估策略具有多项优势:
- 密封性。如果函数仅通过依赖于其他节点来请求输入数据,Bazel 可以保证,如果输入状态相同,则会返回相同的数据。如果所有 Sky 函数都是确定性的,则表示整个 build 也是确定性的。
- 正确且完美的增量效果。如果记录所有函数的所有输入数据,Bazel 只能在输入数据发生变化时使确切的一组节点失效。
- 并行处理。由于函数只能通过请求依赖项相互交互,因此彼此不依赖的函数可以并行运行,并且 Bazel 可以保证其结果与顺序运行时相同。
增量
由于函数只能通过依赖于其他节点来访问输入数据,因此 Bazel 可以构建从输入文件到输出文件的完整数据流图,并使用此信息仅重新构建实际需要重新构建的节点:一组已更改输入文件的反向传递闭包。
具体而言,有两种可能的增量策略:自下而上和自上而下。哪种方法最优取决于依赖项图的具体情况。
在自底向上失效期间,构建图表并确定一组更改的输入后,所有传递依赖于更改的文件的节点都会失效。如果我们知道将重新构建相同的顶级节点,则此方法是最佳选择。请注意,自下而上失效需要对上一个 build 的所有输入文件运行
stat()
,以确定这些文件是否发生了更改。您可以使用inotify
或类似机制来了解更改后的文件,从而改进此问题。在自顶向下失效期间,系统会检查顶级节点的传递闭包,并仅保留传递闭包为空的节点。如果我们知道当前的节点图很大,但在下一个 build 中只需要其中的一小部分,则采用这种方法更好:自底向上失效会使第一个 build 中较大的图失效,而自顶向下失效只会遍历第二个 build 中较小的图。
我们目前仅执行自下而上的失效处理。
为了进一步实现增量更新,我们使用了更改修剪:如果某个节点失效,但在重新构建时发现其新值与旧值相同,则因该节点发生更改而失效的节点会被“复活”。
例如,如果您更改了 C++ 文件中的注释,那么从中生成的 .o
文件将保持不变,因此我们无需再次调用链接器。
增量链接 / 编译
此模型的主要限制是,节点失效是完全失效或完全有效的:当依赖项发生变化时,依赖节点始终会从头重建,即使存在更好的算法可以根据更改更改节点的旧值也是如此。以下是此功能的一些实用场景:
- 增量链接
- 当
.jar
中的单个.class
文件发生更改时,理论上,我们可以修改.jar
文件,而不是从头开始构建它。
Bazel 目前不以原则性的方式支持这些功能(我们对增量链接提供了一定程度的支持,但并未在 Skyframe 中实现)的原因有两点:我们获得的性能提升有限,并且很难保证变更的结果与完整重建的结果相同,而 Google 重视可按位重复的 build。
到目前为止,我们一直可以通过简单地分解耗时的构建步骤并以此方式实现部分重新评估,从而获得足够好的性能:它会将应用中的所有类拆分为多个组,并分别对它们进行 dex 处理。这样一来,如果某个组中的类没有更改,则无需重新进行 dex 处理。
对应到 Bazel 概念
下面简要介绍了 Bazel 用于执行构建的一些 SkyFunction
实现:
- FileStateValue。
lstat()
的结果。对于现有文件,我们还会计算其他信息,以检测文件的更改。这是 Skyframe 图中的最低级别节点,没有任何依赖项。 - FileValue。由关心文件的实际内容和/或解析路径的任何内容使用。取决于相应的
FileStateValue
和需要解析的任何符号链接(例如,a/b
的FileValue
需要a
的解析路径和a/b
的解析路径)。区分FileStateValue
很重要,因为在某些情况下(例如,评估文件系统正则表达式 [例如srcs=glob(["*/*.java"])
]),实际上不需要文件的内容。 - DirectoryListingValue。实质上是
readdir()
的结果。取决于与目录关联的FileValue
。 - PackageValue。表示 BUILD 文件的解析版本。取决于关联的
BUILD
文件的FileValue
,还会传递性地取决于用于解析软件包中的正则表达式(在内部表示BUILD
文件内容的数据结构)的任何DirectoryListingValue
- ConfiguredTargetValue。表示已配置的目标,它是分析目标期间生成的一组操作和提供给依赖于此目标的已配置目标的信息的元组。取决于相应目标所在的
PackageValue
、直接依赖项的ConfiguredTargetValues
,以及表示 build 配置的特殊节点。 - ArtifactValue。表示 build 中的文件,无论是源文件还是输出工件(工件几乎等同于文件,在实际执行 build 步骤期间用于引用文件)。对于源文件,它取决于关联节点的
FileValue
;对于输出工件,它取决于生成工件的任何操作的ActionExecutionValue
。 - ActionExecutionValue。表示操作的执行。取决于其输入文件的
ArtifactValues
。它执行的操作目前包含在其 Sky 键中,这与 Sky 键应较小这一概念相悖。我们正在努力解决这一差异(请注意,如果我们不对 Skyframe 运行执行阶段,则ActionExecutionValue
和ArtifactValue
将不会被使用)。