Bazel 的并行评估和增量模型。
数据模型
数据模型包含以下项:
SkyValue
。也称为节点。SkyValues
是不可变的对象,包含在 build 过程中构建的所有数据以及 build 的输入。例如:输入文件、输出文件、目标和配置的目标。SkyKey
。用于引用SkyValue
的简短不可变名称,例如FILECONTENTS:/tmp/foo
或PACKAGE://foo
。SkyFunction
。根据节点及其依赖节点构建节点。- 节点图。一种包含节点之间依赖关系的数据结构。
Skyframe
. Bazel 所基于的增量评估框架的代码名称。
评估
通过评估表示 build 请求的节点来实现 build。
首先,Bazel 会找到与顶级 SkyKey
的键对应的 SkyFunction
。然后,该函数会请求评估需要评估顶级节点的节点,这反过来会导致其他 SkyFunction
调用,直到到达叶节点。叶节点通常表示文件系统中的输入文件。最后,Bazel 会得到顶级 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 可以构建从输入文件到输出文件的完整数据流图,并使用此信息仅重建那些实际上需要重建的节点:更改的输入文件集的反向传递闭包。
具体而言,增量分析策略有两种:自下而上和自上而下。哪个是最佳选择取决于依赖关系图的结构。
在自下而上的失效过程中,构建图表并确定更改的输入集后,所有传递性依赖于更改文件的节点都会失效。如果将再次构建相同的顶级节点,则此方法是最佳选择。请注意,自下而上的失效需要对之前构建的所有输入文件运行
stat()
,以确定它们是否已更改。可以使用inotify
或类似机制来了解已更改的文件,从而改进此流程。在自上而下的失效期间,系统会检查顶级节点的传递闭包,并仅保留传递闭包干净的节点。如果节点图较大,但下一个 build 只需要其中的一小部分,那么这种方式会更好:自下而上的失效会使第一个 build 的较大图失效,而自上而下的失效只会遍历第二个 build 的较小图。
Bazel 仅执行自下而上的失效。
为了进一步提高增量性,Bazel 使用了更改剪枝:如果某个节点失效,但在重新构建时发现其新值与旧值相同,则因该节点发生更改而失效的节点会被“复活”。
例如,如果有人更改了 C++ 文件中的注释,那么由此生成的 .o
文件将保持不变,因此无需再次调用链接器。
增量链接 / 编译
此模型的主要限制是,节点失效是一个全有或全无的问题:当依赖项发生变化时,即使存在更好的算法可以根据变化来改变节点的旧值,依赖节点也始终会从头开始重建。以下是一些可能需要使用此功能的示例:
- 增量关联
- 如果 JAR 文件中的单个类文件发生更改,可以就地修改 JAR 文件,而不是从头开始重新构建。
Bazel 不以原则性方式支持这些内容的原因有两方面:
- 性能提升有限。
- 难以验证变异的结果是否与干净重建的结果相同,而 Google 非常重视可逐位重复的 build。
到目前为止,通过分解开销大的 build 步骤并以这种方式实现部分重新评估,可以获得足够好的性能。例如,在 Android 应用中,您可以将所有类拆分为多个组,并分别对它们进行 dex 处理。这样一来,如果某个组中的类未发生变化,则无需重新执行 dexing。
与 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 实现之间的关系: