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