天空框架

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

数据模型

数据模型包含以下项:

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

评估

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

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

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

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

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

一旦函数有足够的数据来完成其工作,它就应该返回一个非null值,表示完成。

这种评估策略具有许多优势:

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

增量

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

具体来说,存在两种可能的增量策略:自下而上的策略和自上而下的策略。哪种策略最佳取决于依赖关系图的外观。

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

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

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

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

例如,如果更改 C++ 文件中的注释,则由此生成的 .o 文件将相同,因此,我们无需再次调用链接器。

增量链接 / 编译

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

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

Bazel 目前没有以原则性的方式支持这些功能(我们对增量链接提供了一定的支持,但它不是在 Skyframe 中实现的),原因有两方面:我们只获得了有限的性能提升,并且很难保证更改的结果与干净重建的结果相同,而 Google 非常重视按位重复的 build。

到目前为止,我们始终可以通过简单地分解开销高昂的 build 步骤并以这种方式实现部分重新评估来获得足够好的性能:它将应用中的所有类拆分为多个组,并分别对它们执行 dexing。这样,如果组中的类没有更改,则无需重新执行 dexing。

映射到 Bazel 概念

以下是 Bazel 用于执行 build 的一些 SkyFunction 实现的粗略概览:

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