相约 2023 年 BazelCon 将于 10 月 24 日至 25 日在 Google 慕尼黑举办!报名现已开放! 了解详情

Skyframe

报告问题 查看源代码

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

数据模型

数据模型包含以下各项:

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

评估

构建包括评估代表构建请求的节点(我们力求达到此状态,但也存在大量旧代码)。首先,使用顶级 SkyKey 的键找到并调用其 SkyFunction。然后,该函数会请求评估所需的节点以评估顶级节点,这进而会导致其他函数调用,以此类推,直到到达叶节点(通常是代表文件系统中的输入文件的节点)。最后,我们得到顶层 SkyValue 的值、一些附带效应(例如文件系统中的输出文件)以及构建涉及的节点之间的依赖关系的有向无环图。

如果 SkyFunction 无法提前告知其需要执行其工作所需的所有节点,则可以通过多次遍历请求 SkyKeys。一个简单的例子是评估一个输入文件节点,该节点实际上是符号链接:该函数尝试读取文件,意识到它是符号链接,因此提取表示符号链接的目标的文件系统节点。但它本身可以是符号链接,在这种情况下,原始函数也需要提取其目标。

在代码中,函数由接口 SkyFunction 表示,而服务由称为 SkyFunction.Environment 的接口提供。函数可以执行的操作:

  • 通过调用 env.getValue 请求评估另一个节点。如果节点可用,则返回其值;否则,返回 null,函数本身应返回 null。在后一种情况下,系统会对从属节点进行评估,然后再次调用原始节点构建器,但这次相同的 env.getValue 调用将返回非 null 值。
  • 通过调用 env.getValues() 请求评估多个其他节点。除了从属节点并行求值之外,这基本上是相同的。
  • 在调用期间执行计算
  • 有副作用,例如将文件写入文件系统。需要注意的是,两个不同的功能不会相互冲突。一般来说,写入副作用(数据从 Bazel 向外流动)是可以接受的,读取副作用(数据在没有注册依赖项的情况下流向 Bazel)则不行,因为它们是未注册的依赖项,因而可能会导致增量构建不正确。

SkyFunction 实现不得通过任何其他方式(例如直接读取文件系统)访问数据,因为这会导致 Bazel 未在已读取的文件上注册数据依赖项,从而导致增量构建不正确。

如果函数有足够的数据来完成其工作,则应返回表示完成情况的非 null 值。

此评估策略具有诸多优势:

  • 封闭性如果函数仅根据其他节点请求输入数据,Bazel 可以保证在输入状态相同时返回相同的数据。如果所有星空函数都是确定性的,这意味着整个构建也将具有确定性。
  • 正确且完美的增量方法。如果记录了所有函数的所有输入数据,则 Bazel 只能使需要在输入数据更改时失效的确切节点集无效。
  • 并行处理。由于函数只能通过请求依赖项的方式相互交互,因此不相互依赖的函数可以并行运行,并且 Bazel 可以保证结果与依序运行的结果相同。

增量

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

具体而言,有两种可能的增量策略:自下而上和自上而下。最适合的因素取决于依赖关系图。

  • 在自下而上的失效期间,在构建图并且了解更改的输入集之后,所有节点都会失效,传递性依赖于转换的文件。如果我们知道将再次构建相同的顶级节点,则会采用最佳配置。请注意,自下而上的失效需要对上一个 build 的所有输入文件运行 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 的数据结构(在内部表示 BUILD 文件内容的数据结构)
  • ConfiguredTargetValue。表示已配置目标,目标组是在分析目标时生成的一组操作元组,以及提供给已配置目标的已配置目标的信息。取决于相应的目标所在的 PackageValue、直接依赖项的 ConfiguredTargetValues 以及表示 build 配置的特殊节点。
  • ArtifactValue。表示 build 中的文件,无论是源工件还是输出工件(工件几乎等同于文件,并用于在实际执行构建步骤期间引用文件)。对于源文件,该文件取决于关联节点的 FileValue;对于输出工件,该文件依赖于生成工件的任何操作的 ActionExecutionValue
  • ActionExecutionValue。表示操作的执行。依赖于其输入文件的 ArtifactValues。它执行的操作目前包含在天空键中,这与天空键应该很小的概念相悖。我们正在努力解决此差异(请注意,如果未在 Skyframe 上运行执行阶段,未使用 ActionExecutionValueArtifactValue)。