天空框架

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

数据模型

数据模型包含以下几项:

  • SkyValue。也称为节点。SkyValues 是不可变对象,包含构建过程中构建的所有数据以及构建流程的输入。示例包括:输入文件、输出文件、目标和配置的目标。
  • SkyKey - 用于引用 SkyValue 的不可变的简短名称,例如 FILECONTENTS:/tmp/fooPACKAGE://foo
  • SkyFunction:根据节点的键和从属节点构建节点。
  • 节点图。包含节点之间依赖关系的数据结构。
  • Skyframe。增量评估框架 Bazel 的代号。

评估

构建包括评估代表构建请求的节点(这是我们所追求的状态,但是有很多旧版代码)。首先,系统会查找其 SkyFunction,并使用顶级 SkyKey 的键进行调用。然后,该函数会请求评估其在评估顶层节点所需的节点,而这些节点又会引发其他函数调用,以此类推,直到叶节点(通常为代表文件系统中的输入文件的节点)为止。最后,我们最终会得到顶级 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 或类似机制了解已更改的文件,即可改进这一点。

  • 在自上而下失效期间,系统会检查顶级节点的传递闭包,并且只保留其传递闭包干净的那些节点。如果我们知道当前节点图较大,但下次构建中只需要它的一小部分,效果会更好:自下而上失效会导致第一个构建的较大图失效,而自上而下失效只会遍历第二次构建的小图。

我们目前只执行自下而上的失效。

为了进一步提高增量,我们使用更改剪枝:如果某个节点失效,但在重新构建时,系统会发现其新值与其旧值相同,因此因此节点发生变化而失效的节点将“恢复”。

这非常有用,例如,如果您更改了 C++ 文件中的某条注释,那么根据该文件生成的 .o 文件将是相同的,因此,我们无需再次调用链接器。

增量链接 / 编译

此模型的主要限制在于,节点的失效是一刀切的事情:当依赖项发生变化时,依赖节点总是从头开始重新构建,即使存在更好的算法,可以基于这些变化改变节点的旧值。下面是一些可能会对此有所帮助的示例:

  • 增量关联
  • .jar 中的单个 .class 文件发生更改时,理论上我们可以修改 .jar 文件,而不是再次从头开始构建。

Bazel 目前无法在原则上支持这些功能的原因(我们已衡量对增量链接的支持,但未在 Skyframe 中实现)是双重原因:我们只能获得有限的性能提升,并且很难保证变更的结果与干净重建的结果相同,Google 价值的构建可以点对点重复。

到目前为止,我们始终都可以通过分解开销很高的构建步骤并以这种方式实现部分重新评估,从而实现足够出色的性能:它将应用中的所有类拆分为多个组,分别对这些类执行 dex 处理。这样,如果组中的类未更改,则无需重复执行 dex 处理。

Bazel 概念的对应关系

下面简要介绍了 Bazel 执行构建时使用的一些 SkyFunction 实现:

  • FileStateValuelstat() 的结果。对于现有文件,我们还会计算其他信息,以检测对文件所做的更改。这是 Skyframe 图表中最低级别的节点,没有依赖关系。
  • FileValue。供任何需要实际内容和/或文件解析路径的处理程序使用。依赖于相应的 FileStateValue 和需要解析的任何符号链接(例如,a/bFileValue 需要 a 的解析路径和 a/b 的解析路径)。FileStateValue 之间的区别非常重要,因为在某些情况下(例如,评估文件系统 glob(例如 srcs=glob(["*/*.java"])),实际上并不需要文件的内容)。
  • DirectoryListingValue。实质上是 readdir() 的结果。依赖于与目录关联的关联 FileValue
  • PackageValue。表示 BUILD 文件的解析版本。依赖于关联 BUILD 文件的 FileValue,以及以传递方式依赖于用于解析软件包中的 glob 的任何 DirectoryListingValue(在内部表示 BUILD 文件内容的数据结构)
  • ConfiguredTargetValue。表示已配置的目标,它是在分析目标期间生成的一组操作的元组,以及向依赖于该目标的已配置目标提供的信息。依赖于相应目标所在的 PackageValue、直接依赖项的 ConfiguredTargetValues 以及表示 build 配置的特殊节点。
  • ArtifactValue。表示构建中的文件,可以是源文件,也可以是输出工件(工件几乎等同于文件,用于在实际执行构建步骤期间引用文件)。对于源文件,它依赖于关联节点的 FileValue;对于输出工件,它依赖于生成工件的任何操作的 ActionExecutionValue
  • ActionExecutionValue。表示操作的执行。依赖于其输入文件的 ArtifactValues。它执行的操作目前包含在它的天空键中,这与天空键应该很小的概念背道而驰。我们正在努力解决此差异(请注意,如果未在 Skyframe 上运行执行阶段,则不使用 ActionExecutionValueArtifactValue)。