天空框架

报告问题 查看源代码

Bazel 的并行评估和增量模型。

数据模型

该数据模型包含以下几项:

  • SkyValue。也称为节点。SkyValues 是不可变的对象,包含构建过程中构建的所有数据以及构建的输入。例如:输入文件、输出文件、目标和配置的目标。
  • SkyKey:用于引用 SkyValue 的不可变的短名称,例如 FILECONTENTS:/tmp/fooPACKAGE://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 用于执行构建的关键 SkyFunctionSkyValue 实现:

  • FileStateValuelstat() 的结果。对于现有文件,该函数还会计算其他信息以检测对文件所做的更改。这是 Skyframe 图中最低级别的节点,没有依赖项。
  • FileValue。由所有关心文件实际内容或解析路径的内容使用。依赖于相应的 FileStateValue 和任何需要解析的符号链接(例如,a/bFileValue 需要 a 的解析路径和 a/b 的解析路径)。FileValueFileStateValue 之间的区别很重要,因为在实际不需要文件内容时可以使用后者。例如,在评估文件系统 glob(例如 srcs=glob(["*/*.java"]))时,文件内容不相关。
  • DirectoryListingStateValuereaddir() 的结果。与 FileStateValue 一样,它是最低级别的节点,没有依赖项。
  • DirectoryListingValue。由所有关心目录条目的用户使用。依赖于相应的 DirectoryListingStateValue 以及目录的关联 FileValue
  • PackageValue。表示 BUILD 文件的解析版本。依赖于关联的 BUILD 文件的 FileValue,以及用于解析软件包中的 glob 的任何 DirectoryListingValue(在内部表示 BUILD 文件的内容的数据结构)。
  • ConfiguredTargetValue。表示配置的目标,这是在分析目标期间生成的操作集的元组,以及提供给从属配置目标的信息。根据相应目标所在的 PackageValue、直接依赖项的 ConfiguredTargetValues 以及表示 build 配置的特殊节点。
  • ArtifactValue。表示 build 中的文件,可以是源文件,也可以是输出工件。工件几乎等同于文件,用于在实际执行构建步骤期间引用文件。源文件取决于关联节点的 FileValue,而输出工件取决于生成工件的任何操作的 ActionExecutionValue
  • ActionExecutionValue。表示操作的执行。依赖于其输入文件的 ArtifactValues。它执行的操作包含在其 SkyKey 中,这与 SkyKey 应较小的概念相反。请注意,如果执行阶段未运行,则未使用 ActionExecutionValueArtifactValue

此图显示了构建 Bazel 本身之后的 SkyFunction 实现之间的关系,您可以直观地看到:

SkyFunction 实现关系图