天空框架

报告问题 查看源代码 每夜版 · 8.4 · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

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

数据模型

数据模型包含以下项:

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

评估

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

如果 SkyFunction 无法预先知道完成工作所需的所有节点,则可以多次请求 SkyKeys。一个简单的示例是评估一个输入文件节点,该节点最终是一个符号链接:该函数尝试读取文件,发现它是一个符号链接,因此会提取表示符号链接目标的相应文件系统节点。但该文件本身也可以是符号链接,在这种情况下,原始函数也需要获取其目标。

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

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

SkyFunction 实现不应以请求依赖项以外的任何其他方式(例如直接读取文件系统)访问数据,因为这会导致 Bazel 未注册对所读取文件的依赖项,从而导致增量构建不正确。

当函数有足够的数据来完成其工作时,应返回一个非 null 值来指示完成。

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

  • 封闭性。如果函数仅通过依赖其他节点来请求输入数据,则 Bazel 可以保证,如果输入状态相同,则返回的数据也相同。如果所有 Sky 函数都是确定性的,这意味着整个 build 也是确定性的。
  • 正确且完美的增量效果。如果记录了所有函数的所有输入数据,那么当输入数据发生变化时,Bazel 可以仅使需要失效的确切节点集失效。
  • 并行性。由于函数只能通过请求依赖项的方式相互交互,因此不相互依赖的函数可以并行运行,并且 Bazel 可以保证结果与按顺序运行的结果相同。

增量

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

具体而言,增量分析策略有两种:自下而上和自上而下。哪个最佳取决于依赖关系图的形状。

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

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

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

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

例如,如果有人更改了 C++ 文件中的注释,那么由此生成的 .o 文件将保持不变,因此我们无需再次调用链接器。

增量链接 / 编译

此模型的主要限制是,节点失效是一个全有或全无的过程:当依赖项发生变化时,即使存在更好的算法可以根据变化来更改节点的旧值,依赖节点也始终会从头开始重建。以下是一些可使用此功能的示例:

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

Bazel 目前之所以无法以原则性的方式支持这些功能(我们对增量链接提供了一定程度的支持,但它并未在 Skyframe 中实现),原因有两方面:我们仅获得了有限的性能提升,并且很难保证突变的结果与干净重建的结果相同,而 Google 非常重视可逐位重复的 build。

到目前为止,我们始终可以通过简单地分解开销大的 build 步骤来实现足够好的性能,从而实现部分重新评估:它将应用中的所有类拆分为多个组,并分别对它们进行 dexing。这样一来,如果组中的类没有变化,就不必重新执行 dexing。

与 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 文件内容的数据结构)的任何 DirectoryListingValue
  • ConfiguredTargetValue。表示一个已配置的目标,它是指在分析目标期间生成的一组操作,以及提供给依赖于此目标的已配置目标的信息。取决于相应目标所在的 PackageValue、直接依赖项的 ConfiguredTargetValues 以及表示 build 配置的特殊节点。
  • ArtifactValue。表示 build 中的文件,无论是源文件还是输出工件(工件几乎等同于文件,用于在实际执行 build 步骤期间引用文件)。对于源文件,它取决于关联节点的 FileValue;对于输出制品,它取决于生成制品的任何操作的 ActionExecutionValue
  • ActionExecutionValue。表示操作的执行。取决于输入文件的 ArtifactValues。目前,该操作的执行包含在其 sky 密钥中,这与 sky 密钥应较小的概念相悖。我们正在努力解决这一差异(请注意,如果我们不在 Skyframe 上运行执行阶段,则 ActionExecutionValueArtifactValue 未使用)。