天空框架

报告问题 查看源代码

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 可以保证在输入状态相同时返回相同的数据。如果所有 Sky 函数都是确定性的,这意味着整个 build 也将是确定性的。
  • 正确且完美的增量。如果记录了所有函数的所有输入数据,则当输入数据发生更改时,Bazel 只能使需要失效的那组确切节点失效。
  • 并行处理。由于函数只能通过请求依赖项相互交互,因此可以并行运行不相互依赖的函数,并且 Bazel 可以保证结果与按顺序运行一样。

增量

由于函数只能依赖其他节点来访问输入数据,因此 Bazel 可以构建从输入文件到输出文件的完整数据流图,并使用此信息仅重新构建实际需要重新构建的节点:更改的输入文件集的反向传递闭包。

特别是,存在两种可能的增量策略:自下而上的和自上而下策略。哪种方式最合适取决于依赖关系图的外观。

  • 在自下而上的失效过程中,在构建好图且了解更改的输入集之后,所有节点都会失效,以传递方式依赖于已更改的文件。如果我们知道要再次构建相同的顶级节点,这是最佳选择。请注意,自下而上的失效需要对上一个 build 的所有输入文件运行 stat(),以确定它们是否已更改。使用 inotify 或类似机制来了解已更改的文件,对此进行改进。

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

我们目前仅执行自下而上的失效操作。

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

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

增量关联 / 编译

此模型的主要限制是,节点失效是一劳永逸:当依赖项发生变化时,依赖节点总是从头开始重新构建,即使存在可根据更改改变节点旧值的更好的算法。下面列举了一些实用的示例:

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

Bazel 目前在原则上不支持这些功能的原因包括两方面:我们仅获得了有限的性能提升,并且难以保证改变的结果与干净的重新构建的结果相同,Google 重视的“逐位重复”构建在一定程度上支持增量链接。

直到现在,我们始终可以通过简单地分解开销大的构建步骤并通过这种方式实现部分重新评估来实现足够好的性能:它将一个应用中的所有类拆分为多个组,并分别对其进行 dex 处理。这样,如果组中的类未发生变化,就无需重复执行 dexing。

映射到 Bazel 概念

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

  • FileStateValue 中的字段。lstat() 的结果。对于现有文件,我们还会计算其他信息,以检测文件的更改。这是 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。它执行的操作目前包含在其 Sky 键中,这与 Sky 键应较小的概念相反。我们正在努力解决此差异(请注意,如果不在 Skyframe 上运行执行阶段,系统不会使用 ActionExecutionValueArtifactValue)。