Download PDF version of this article PDF

GitHub 的静态分析

实践报告

Timothy Clem 和 Patrick Thomson

GitHub 是一个代码托管网站,建立在 Git 版本控制系统之上,托管着数亿个代码仓库,由超过 6500 万开发者上传。GitHub 的语义代码团队构建并运营一套技术,为 github.com 上的符号代码导航提供支持。符号代码导航让开发者可以通过点击源代码中的命名标识符来导航到该实体的定义,反之亦然:给定一个标识符,他们可以列出该标识符在项目中的所有使用位置。

该系统由云对象存储服务支持,已从多 TB 分片关系数据库迁移而来,每分钟处理超过 40,000 个请求,涵盖读取和写入操作。静态分析阶段本身建立在一个名为 Tree-sitter 的开源解析工具包之上,实现了一些知名的计算机科学研究,并与 github.com 基础设施集成,以便从源代码中提取名称绑定信息。

该系统支持六百万个仓库中的九种流行编程语言。即使是将最简单的程序分析扩展到这种规模也需要大量的工程努力,本文将对此进行回顾,希望能为那些将静态分析扩展到大型且快速变化的代码库的人提供有用的指导。

 

动机:见树木又见森林(解析树)

代码导航是阅读、编写和理解程序的基本组成部分。Unix 工具,如 grep(1),允许开发者搜索文本模式,但程序员的需求范围更广:他们最感兴趣的是程序各部分如何拼接在一起——给定一个函数,它在哪里被调用,又在哪里被定义?对这些查询的快速和高质量的回答,可以让程序员建立起程序结构的心理模型;反过来,这可以有效地进行修改或故障排除。诸如 grep 这样的工具,仅限于文本匹配,并且对程序结构一无所知,通常提供的信息太少或太多。

流畅的代码导航也是研究错误的一个宝贵工具。错误报告系统中的堆栈跟踪开启了理解导致该错误的程序状态的旅程;以符号方式导航代码减轻了在上下文中理解代码的负担。因此,大多数 IDE(集成开发环境)都广泛支持代码导航和其他静态分析,以减轻用户的负担。

语义代码团队希望将这种 IDE 风格的符号代码导航引入到 github.com 的 Web 端。该团队受到了诸如 source.dot.netMozillaChromium 代码搜索 等单用途站点的启发,这些站点提供了全面的浏览器内代码导航。问题是如何大规模地做到这一点:GitHub 为超过 6500 万开发者提供服务,他们贡献了超过 2 亿个仓库,涵盖 370 种编程语言。仅在过去的 12 个月里,github.com 上就有 22 亿次贡献。这是大量的代码和大量的更改。

 

理念:树与非树

语义代码团队实现代码导航的方法围绕以下核心思想展开。

1. 零配置

最终用户无需进行任何设置或配置即可利用代码导航,只需将代码推送到 GitHub 即可。没有设置或自定义或选择加入功能——如果仓库的语言受支持,它就应该正常工作。这对于这个特定的用例至关重要,因为如果您在 GitHub 上查看受支持语言的源代码文件,那么期望是代码导航应该正常工作。如果每个开源项目都必须做哪怕一点额外的工作来配置其仓库或设置构建来发布此信息,那么在 GitHub 上浏览代码的体验将因项目而异,并且从推送代码到能够使用代码导航的时间可能取决于缓慢而复杂的构建过程。

仅仅要求开发者克隆并启动他们自己的 IDE(或等待像 GitHub Codespaces 这样的浏览器内 IDE 加载)是不够的;开发者希望能够在不下载代码及其相关工具的情况下快速阅读和浏览代码。为了使此功能能够扩展并服务于所有 GitHub 用户,它必须在任何地方和每个项目中都可用。目标是让开发者专注于他们的程序和他们试图解决的问题,而不是配置 GitHub 以使其与他们的项目正常工作,或说服另一个项目所有者设置正确的设置。

2. 增量性

对于推送到仓库的每个更改,后端处理应该只对已更改的文件进行工作。这与建立持续集成工作流程不同,在持续集成工作流程中,用户可能特别希望获得一个全新的环境来进行可重复的构建。这也暗示了结果将在推送后更快地可用——以秒为单位,而不是分钟为单位。等待整个构建周期才能显示代码导航数据对于期望的用户体验来说是不可接受的;开发者期望导航功能能够跟上他们的更改。

3. 语言无关性

无论分析的语言是什么,都应该运行和操作相同的后端处理代码。因此,团队决定不运行特定于语言的工具,例如 C# 的 Roslyn 项目或 Python 的 Jedi,因为这将需要为每种语言(有时甚至为每种语言版本)操作不同的技术栈。

尽管这意味着语言语法可能接受给定语言的超集,但这种理念可以更快地扩展和交付结果。基础设施可以运行单一代码栈,无需管理多个容器和相关的资源成本,无需启动工具的冷启动时间,也无需尝试检测项目的结构、配置或目标语言版本。

这也意味着分析的主要方面可以在一个地方实现,从而提供诸如机器可读语法和树查询语言之类的抽象。只有某些特定于编程语言的东西必须先开发出来,然后才能在其余技术栈中启用该语言。

4. 渐进式保真度

足够好的结果永远不应该等待完美的结果。该系统的设计使其产生的结果可以随着时间的推移进行改进和完善。团队没有试图为 GitHub 上所有语言构建和展示完美的系统,而是选择发布增量改进:逐个添加语言,并通过进一步的技术投入来改进导航结果。

5. 人机协作

优秀的软件工具可以增强和补充人类的能力。在这种情况下,GitHub 的语义代码团队希望促进软件的阅读、编写和理解过程,并以这样一种方式进行,即开发者可以为他们最关心的语言的工具做出贡献。该系统应尽可能避免软件开发的围墙花园模式,因为语言社区通常随时准备修复与其选择的语言相关的错误和疏忽。

 

方法论:标记 AST

GitHub 代码导航功能所基于的静态分析称为标签分析。标签分析查看函数、变量和数据类型的定义和用法,将它们整理成适合查看给定实体的定义以及完全查询代码库以确定该实体所有使用位置的格式。

此分析由一个名为 ctags 的程序引入,该程序由 Ken Arnold 开发,并于 1979 年作为 BSD Unix 3.0 的一部分发布。顾名思义,ctags 仅支持 C 编程语言;尽管如此,它立即获得了成功,最终被纳入 Single Unix Specification。ctags 的进一步后代添加了对更多编程语言的支持,现在绝大多数文本编辑器和 IDE 都提供了 ctags 集成。尽管标签分析对于静态程序分析的最新技术来说是微不足道的,但在 GitHub 规模和 GitHub 的分布式架构中实现这样的分析并非易事。

Tree-Sitter

任何类型的静态分析的第一步都是将文本源代码解析为机器可读的数据结构。这通过使用 Tree-sitter(一个解析器生成器工具和增量解析库)生成具体的语法树来完成。

Tree-sitter 允许指定编程语言的语法,然后生成解析器,这是一个无依赖的 C 程序。给定一个解析器和一些源代码,系统会生成该源代码的语法树。标记代码的行为包括遍历树并执行过滤器映射操作。使用类似 s-expression 的 DSL(领域特定语言),Tree-sitter 提供了树查询,允许指定要在语法树中匹配的节点(例如,表示函数名称的标识符)。Tree-sitter 生态系统中的工具允许快速迭代语法开发和树匹配,从而可以快速支持新的语言语法或识别用于代码导航的新结构。

Tree-sitter 从多个学术研究领域汲取灵感,但最突出的是 GLR(广义 LR)解析。GLR 算法是众所周知的 LR(从左到右推导)解析算法的扩展,可以处理现代编程语言中常见的歧义或非确定性语法。现实世界的语言使用许多不同的解析算法:Python 使用 LL(1) 算法,Ruby 使用 LALR,GCC(GNU 编译器集合)项目使用自定义递归下降解析器。GLR 算法由 Bernard Lang 于 1974 年引入,并由 Masaru Tomita 于 1984 年实现,它足够灵活,可以解析所有这些语言;依赖于不太复杂的算法(如 LR(1))将过于受限,因为此类解析器仅限于查看输入中的一个 token。

 

构建解析器

每种编程语言都必须有一个解析器,以将其文本表示形式转换为可以被机器执行的形式。然而,语义代码团队没有采用受支持语言的规范解析器,而是在 Tree-sitter 之上重新实现了这些解析器。与使用语言的内置工具相比,这提供了许多优势

   • 常见的编程语言有许多版本,它们的语法兼容性级别各不相同。通常没有信息可以区分 Python 2 文件和 Python 3 文件,因此使用语言的内置解析器将需要启发式方法或猜测来找到合适的解析器。

   • 一些语言工具需要代码本身不存在的外部信息或项目级配置。为了充分分析此类项目,分析本质上必须运行软件的构建。GitHub 通过 GitHub Actions 提供构建基础设施,并通过 CodeQL 提供构建后分析,但用户必须选择加入此功能,并且结果通常在几分钟内可用,而不是代码导航所依赖的几秒钟。

   • 运行各个特定于语言的工具在操作上过于复杂而不可行。您不仅最终会得到混合平台 VM(C# 工具必须在 Windows 上运行,Swift 在 macOS 上运行,大多数其他语言将需要 Unix 系统),您还需要一种方法在语言及其核心组件的各种版本之间切换。GitHub Actions 出于 CI(持续集成)领域的需要而解决了这个问题,但这需要用户和实现者付出努力来选择语言、框架、编译器和操作系统的正确版本。

通过开发通用的解析框架,语义代码团队创建了标准的机器可读语法,可以解析语言所有版本的超集。这产生了有趣的后果,例如能够通过组合 HTML 和 Ruby 语法来轻松支持嵌入式 Ruby 模板语言。此外,这项工作使得可以通过一个工具支持多种语言。Tree-sitter 解析器是零依赖的 C 库,使其易于嵌入和绑定到其他语言。

标记树

下一个挑战是对解析树进行一些基本分析。首先,团队决定仅基于已知语法结构的标识符提供朴素的名称绑定信息。这最初是作为原型设计的,但效果非常好,以至于在开始进一步改进工作的同时就发布了。基本思路是

1. 解析源文件。

2. 遍历解析树,捕获某些语法结构的标识符,例如函数定义。

3. 保留这些标签的数据库表,以及它们在原始源代码中出现的行/列位置以及标识符是某物的定义还是引用(例如,函数调用将是引用)等信息。

事实证明,对于许多语言来说,仅凭名称绑定信息就提供了引人注目的产品体验。它并不完美,但与没有任何信息相比,这是一个巨大的改进,因此团队使用了这个原型来充实生产规模的系统,同时研究如何改进结果的精度。

为了遍历解析树,使用 Tree-sitter 对查询的支持就足够了。通过使用 s-expression 语法指定包含有用标识符信息的语法节点类型的结构,遍历解析树可以仅产生指定的节点。例如,给定识别 Ruby 中的方法定义的用例,让我们解析一段定义了方法 add 的 Ruby 代码片段

 

# test.rb
def add(a, b)
 a + b
end

 

使用 Tree-sitter,解析树看起来像这样,以 s-expression 形式,包含节点名称和与该节点关联的行/列/跨度信息

 

> tree-sitter parse test.rb
(program [0, 0] - [3, 0]
  (method [0, 0] - [2, 3]
    name: (identifier [0, 4] - [0, 7])
    parameters: (method_parameters [0, 7] - [0, 13]
      (identifier [0, 8] - [0, 9])
      (identifier [0, 11] - [0, 12]))
    (binary [1, 2] - [1, 7]
      left: (identifier [1, 2] - [1, 3])
      right: (identifier [1, 6] - [1, 7]))))

 

Ruby 方法(如 add)的标记操作使用 Tree-sitter 查询语法进行编码

 

(method name: (_) @name) @definition.method

 

@ 捕获并标记树中匹配的部分,而 _ 表示我们不关心树的结构,超出此点。Tree-sitter 现在可以使用此查询来提供所需的匹配

 

> tree-sitter tags test.rb
add | method   def (0, 4) - (0, 7) `def add(a, b)`

 

在更复杂的情况下,Tree-sitter 树查询可以在查询操作期间维护自定义状态。这对于像 Ruby 这样的语言非常重要,Ruby 在语法上不区分变量和不带参数调用的函数。为了产生有用的结果,您必须记录给定 Ruby 作用域中任何局部变量的名称,这允许消除语法歧义。

 

实现:架构

GitHub 的代码导航管道建立在开源软件和标准之上

 

• Apache Kafka。用于处理高吞吐量数据流(例如提交到仓库)的平台。数据流中的单个数据单元是消息。

• Git。分布式版本控制系统。程序员将更改上传到 GitHub 上托管的代码仓库。每个更改都会影响该仓库中的一个或多个文件(称为 blobs)。对一个或多个 blob 的更改单元称为提交,上传一个或多个提交的行为称为推送。一旦提交被推送,其他程序员就可以看到这些提交并将它们合并到他们计算机上存在的仓库副本中。程序员可以通过定位分支(已命名的提交集合)来上传他们的代码,而不会影响其他人。Git 项目从单个分支开始,通常称为“main”,然后通过直接推送提交或通过审查和集成其他人的更改(拉取请求)来更改该分支。Git 仓库中的实体根据 SHA-1 哈希算法获得唯一的标签。

• Kubernetes。用于部署和操作应用程序服务的编排系统,Kubernetes 提供 pod,即计算的隔离单元,可以在其上部署给定应用程序的一个或多个副本。

• Semantic。GitHub 的开源程序分析工具。

• Tree-sitter。这个建立在 GLR 算法之上的解析工具包,生成高效解析源代码的代码:解析行为将人类可读的源代码文本表示形式转换为树数据结构(通常称为语法树),该结构适合机器使用和分析。

• Twirp。RPC(远程过程调用)标准,Twirp 定义了系统中实体可以通信的方式。

该系统由部署在 Kubernetes 上的三个独立服务组成,这些服务与主要的 github.com Ruby on Rails 应用程序(因其规模和复杂性而被亲切地称为“巨石”)集成:(1)索引器 消费 Git 推送的 Kafka 消息,并协调特定仓库在提交 SHA 处的解析、标记和保存结果的过程;(2)标记器 是一项服务,它接受原始源代码内容,解析和标记此代码,并返回结果;(3)查询服务 为符号数据库提供 Twirp RPC 接口,允许 github.com 的前端查找定义和引用。

图 1 显示了各种索引器序列组件之间的关系。

Static Analysis at GitHub

索引

开发者通过 Git 推送或通过网站界面编辑文件的方式将代码上传到 GitHub。收到新的推送后,GitHub 服务器会向 Kafka 发送一条消息,描述有关推送的元数据(项目位置、更改作者、目标分支)。系统运行一个 Kubernetes pod 池,分布在多个集群和物理数据中心,监听这些消息。由于消息根据仓库的唯一标识符分布在 pod 池中,因此给定的 pod 通常会重复消费同一仓库的消息,从而允许在索引管道中进行有效的本地缓存。

当消息被消费时,索引器进程会聚合它们,确保在时间窗口内对同一仓库的推送作为一个请求进行处理。随后,它会过滤掉无效消息;由于规模原因,推送仅索引到默认分支;它们必须仅涉及系统支持的编程语言。然后,它限制索引速率,以便在处理后续推送之前允许初始索引完全完成。Kafka 记录给定消息何时被传递以进行处理,并且对同时处理的上限会对 Kafka 施加背压。这意味着在容量过载的情况下,索引器不会尝试消费过多的消息,从而在系统中提供弹性和弹性。

在处理推送时,索引器会对仓库进行浅克隆到临时目录。聚合阶段意味着单个下载可以在未来的索引中重用。

索引涉及确定在分析的更改中仓库中的哪些 Git blob 尚未被索引。blob 的 OID(对象标识符)是其内容的 SHA-1 哈希值;此标识符可以轻松地用于跟踪需要处理的文件。未更改的 blob 在提交之间共享,不需要额外的工作,尽管特定提交中可访问的路径和 blob 的集合会被记录下来。

对于确实需要解析和标记的 blob,索引器会调用在另一组 pod 上运行的单独服务。这种分离的原因是并非每个推送都代表相同量的解析工作。可能有 1 到 20,000 个文件需要处理。索引器工作负载主要受 I/O 限制:等待套接字(用于 Kafka、数据存储、仓库克隆)以及读取/写入文件系统。然而,解析主要受计算限制,因此拥有单独的 pod 池(请求在这些 pod 池之间进行负载均衡)意味着可以适当地扩展资源,并且系统不会遇到单个索引器 pod 因处理大型或活跃仓库而变得的情况。

该系统每天部署多达数十次,并且它通过停止消费任何新的 Kafka 消息来优雅地处理这些部署,允许正在进行的索引有 30 秒的窗口来完成处理,并中止和重新排队任何无法在该窗口中完成的索引。由于每个 Git blob 的处理都是增量的,因此拾取该消息的新 pod 将只需要解析和标记原始运行期间未完成的 blob。

查询

当标签结果被完全处理后,它们会被存储在数据库中,以及足够的信息来服务于未来的查询:在提交 Y 的仓库 X 中,foo在哪里定义? 系统提供了一些 Twirp RPC(远程过程调用)端点,允许 github.com 前端用户界面根据用户的交互和上下文查找定义和引用。过去,团队选择的数据库是一个大型 MySQL 实例,通过 Vitess(一种 MySQL 分片技术)跨多台机器进行分区。在撰写本文时,标签数据存储在 Microsoft Azure Blob Storage 中,这是一个支持上传文件的云存储系统。

图 2 显示了各种查询序列组件之间的关系。

Static Analysis at GitHub

 

运维历程

该系统的第一个原型直接使用了 ctags 命令行工具:ctags 的调用将生成的标签转储到与标记仓库关联的 Git 存储中,并且还尝试在每次推送到该仓库时执行此操作。这对于演示来说很棒,但无法大规模运行:CPU 资源在 Git 存储服务器上非常宝贵;对多个 refs 的多次推送使管理标签文件变得具有挑战性;并且在这些服务器上处理查找请求并不理想。Ctags 没有利用 Git 数据结构所需的 API,并且以网络服务可以提供其操作的方式使用命令行程序并不容易。

进入 Tree-Sitter

当语义代码团队接手该项目时,下一个迭代使用了 Tree-sitter 而不是 ctags,但大多数索引逻辑仍然在主要的 github.com Ruby on Rails 应用程序中实现。在推送时排队,用 Ruby 编写的索引作业用于通过内部 RPC 获取 Git 内容,检测源文件的语言,过滤掉不相关的内容(例如,二进制文件,不受支持的语言的代码),调用服务进行解析/标记,并保存生成的标签。一个专用的 MySQL 数据库用于存储和检索标签数据,查询路径基本上直接从 Rails 控制器到这个数据库。

这解决了一系列问题,并证明 Tree-sitter 解析器和初始名称绑定分析可以提供解析/标记即服务,易于迭代语法,并且易于水平扩展解析计算工作负载。最重要的是,这个原型使领导层相信这是一组有价值的功能,可以构建,并且它以有意义的方式增强了 GitHub 体验。它还表明,我们有一个扩展、支持 GitHub 上所有仓库和支持新语言的计划。

脱离巨石

下一个迭代进一步脱离了 github.com Ruby on Rails 应用程序,原因如下

• GitHub 中的作业架构不是很健壮,并且具有“尝试一次,尽力而为”的语义,这导致了不可靠性。此外,这些操作需要在与 GitHub 上正在处理的无数后台作业(例如,Webhooks、发送电子邮件和更新拉取请求)共享的作业工作器上进行大量的资源争用。

• 随着更多仓库、用户和语言的添加,标签数据库的增长损害了扩展能力。这很快成为 GitHub 上最大的 MySQL 实例之一。

从工程组织的角度来看,在主要的 Ruby on Rails 应用程序代码库中工作速度很慢,并且伴随着许多限制。GitHub 的主要部署管道的许多部分今天都已精简,但在当时,部署单个拉取请求可能需要一天以上的时间。为了应对这些挑战,实现了额外的服务

• 使用 GitHub 的开源语义语言工具包重写了解析/标记服务。

• 索引器服务,它从 Kafka 消费推送信息,克隆和解析仓库,并将结果插入到专用的 MySQL 数据库中。

• 查询服务,它执行数据库查找并将结果返回给 Ruby on Rails 应用程序。

这些模式在很长一段时间内为系统提供了良好的服务,允许快速增长,为代码导航功能发布了几种新语言,并简化了开发和部署。

扩展 MySQL

在那之后,下一个迭代需要将数据库从单个 MySQL 实例迁移到由开源项目 Vitess 驱动的水平分布式分区 MySQL 实例集群。迁移到 Vitess 需要工程复杂性:团队需要编写新的模式,选择新的分片策略,并在该集群内部署主 MySQL 实例和辅助 MySQL 实例。在此期间,团队从基于 Semantic 的标记服务迁移到完全使用(新开发的)Tree-sitter 查询的服务。尽管 Semantic 性能良好,但使用 Tree-sitter 查询语言可以实现更快的迭代,并避免了程序分析框架的操作开销。

迁移到 Blob 存储

最新的迭代,也是在撰写本文时部署到 github.com 的迭代,涉及数据库格式的巨大变化。虽然 Vitess 分片 MySQL 数据库在理论上为水平扩展提供了重要的跑道(只需添加更多分片),但几个新的约束变得明显

• 金钱。使用 Vitess 集群,从技术上讲,水平扩展 MySQL 没有限制,但足够强大的硬件需要大量的前期和维护成本。虽然这些成本受到仅索引仓库的主分支的限制,但预测解析 GitHub 上所有代码的所有分支的成本简直是令人望而却步的。

• 并非 MySQL 的所有功能都可以使用,特别是考虑到 SQL 的容量受到 Vitess 为了正确分片数据库而施加的约束的限制。

• 对数据库的写入次数压倒了 MySQL:为了保持 MySQL 和 Vitess 的平稳运行,流量必须通过限制服务,这导致索引器性能出现重大瓶颈。对 MySQL 的写入根据复制延迟进行限制。当延迟(写入主数据库和应用于副本之间的时间)超过某个阈值时,索引器进程会退避插入。通常,这些是由于不健康的硬件、配置差异或只是数据模式(例如,开发者在星期一早上和假期后的 1 月份格外忙碌)引起的临时性波动。

尽管此迁移主要是关于成本结构的,但数据存储的更改在整个过程中产生了一些有趣的性能优势。但在架构上,系统中的一切都保持不变:只需将写着“MySQL”的盒子换成写着“Azure”的不同盒子——至少想法是这样的。

实际上,这涉及到大量的工作。团队不仅需要并行维护旧有的 MySQL 系统和新的 Azure 系统,而且还必须为标签数据设计一个兼容 Azure 的模式,该模式允许对存储库进行增量索引,并以与基于 MySQL 的系统大致相同的性能特征查询标签。这意味着要优化空间、时间和成本。(存储需要成本,但每次操作也需要成本:例如,读取操作比写入操作便宜,而写入操作又比列表查询操作便宜。)

数据存储设计

团队最终采用的存储结构如下,以类似文件系统的形式表示。整体路径结构包含一个 <版本> 前缀,以便允许对数据存储进行主要的结构和格式更改。然后,每个存储库的数据都以其 ID 作为前缀

 

/<版本>/<repo_id>/
  blobs/
    <blob_sha>.tsv.gz
    ...
  commits/
    <commit_sha>/
      index.tsv.gz
      symbols/
        <hash_prefix>.tsv.gz
        ...
    ...
  refs/
    DEFAULT.tsv
    heads/
      <branch_name>.tsv
      ...
    tags/
      ...

 

这种数据库内的文件层次结构提供了访问所有不同数据的途径,这些数据是提供有用的代码导航信息所必需的。在索引编制期间,适当的文件被上传到 blob 存储,用于正在分析的存储库。在查询时,存储库、提交 SHA 和符号名称提供了足够的信息来获取适当的文件并计算结果。所有文件都是不可变的。

 

commits/<commit_sha>/symbols/<hash_prefix>.tsv — 提交中所有共享两位 SHA1 哈希前缀的符号名称,以 TSV 格式存储。此前缀允许通过基于此两位前缀进行过滤来加速数据库查询。这是在执行符号查找时读取的第一个文件,目的是获取定义 (D) 或引用 (R) 符号的 blob SHA 列表。

 

blob_sha <D|R>   <符号名称>

 

commits/<commit_sha>/index.tsv.gz — 表示提交已被索引,并保存提交中所有 blob SHA 的映射,以及这些 blob 的路径。读取此文件,并将 blob SHA 与上一步中的 blob SHA 相交,即可得到要在步骤 3 中获取的 blob 列表。


此文件包含一个标头和两个 TSV 内容部分。标头给出了每个部分开始处的字节偏移量(从文件开头算起)。每个部分包含针对每个已索引路径的一行。第一部分包含 <路径>\t<blob_sha> 字段,并按路径排序。这允许对特定路径进行二进制搜索,以查找其在提交中的 blob SHA。第二部分中的行包含一个 <字节偏移量> 字段,该字段是第 1 部分中某行的偏移量(从文件开头算起)。此部分按 blob SHA 排序,并允许对一组 blob SHA 进行二进制搜索,以查找它们在提交中出现的路径。

 

# 内容开始
 <内容开始的字节偏移量>
# oid 索引开始
 <辅助索引开始的字节偏移量>
# 列:filepath blob_sha
###################
<路径> <blob_sha>
...

<blob_sha 的字节偏移量>
...

 

blobs/<blob_sha>.tsv.gz — 特定 git blob 的所有定义和引用。这是符号查找的最后一步,提供返回给前端的信息。许多定义查找在此步骤中只需读取一个文件。由于引用可能分散在多个文件中,因此读取是并行完成的,并设置了上限。

定义首先存储,以便查找操作可以在到达第一个引用后尽早返回。请注意,由于这是 blob 内容的 SHA1 哈希值,因此一旦解析了 blob,就无需再次解析,并且可以在包含该 blob 的任何提交中共享。

 

<符号名称> <D|R>   <语法类型>   <行>   <列> <结束列>   <源代码行>

 

其他文件用于快速判断特定分支或标签是否已被索引。

 

refs/(heads|tags)/<branch_name>.tsv — 已索引的分支顶端的单个提交 SHA。

DEFAULT.tsv 包含默认分支的提交 SHA 和 ref

 

系统的每次迭代,甚至数据存储的迁移,都是在完整的生产负载下进行的,对最终用户的影响极小甚至没有影响。blob 存储的更改显著节省了成本,提高了索引编制吞吐量,并略微缩短了查询请求时间。

 

结论

最终的代码导航系统每分钟处理和索引超过 1,000 次推送,每分钟生成超过 200 万个新的标识符名称。存储库首次索引的 p99 值(第 99 百分位数;即最慢的 1% 索引所需的时间)约为 60 秒(第 50 百分位数约为 1.1 秒)。增量索引的 p99 值约为 10 秒(p50 约为 1.3 秒)。该系统每分钟为符号查找提供 30,000 个请求,p99 请求时间约为 90 毫秒。它支持九种语言:C#、CodeQL、Go、Java、JavaScript、PHP、Python、Ruby 和 TypeScript,并且正在开发更多语言的语法。

然而,我们了解到,规模——与其说是关于大量的用户和存储库以及 HTTP 请求,以及 GitHub 上托管的代码库的惊人规模——不如说是关于采用、用户行为、渐进式改进和实用性。特别是静态分析,在人类行为方面很难扩展;我们经常认为复杂的分析工具旨在查找代码中潜在的问题模式,然后尝试说服人类修复它们。我们的方法采取了不同的策略:使用基本分析技术,快速将增强我们理解程序能力的信息呈现在每个在 GitHub 上阅读代码的人面前,无需任何配置,并且在代码更改后几乎立即可用。结果是在 GitHub 上阅读、导航、共享和理解代码的愉快体验。

 

Timothy Clem 自 2011 年以来一直在构建 GitHub,他从分布式系统、编程语言的设计和应用以及将研究理念扩展到大规模应用中获得极大的乐趣。他与一群主要是家养哺乳动物的小型动物居住在旧金山。

Patrick Thomson 是 GitHub 的一名工程师、函数式程序员和编程语言爱好者。他住在纽约皇后区,在那里他喜欢吃小笼包和撰写关于音乐的文章。

 

版权所有 © 2021,归所有者/作者所有。出版权已许可给 。

acmqueue

最初发表于 Queue vol. 19, no. 4
数字图书馆 中评论本文





更多相关文章

Catherine Hayes, David Malone - 质疑非加密哈希函数的评估标准
尽管加密和非加密哈希函数无处不在,但在它们的设计方式上似乎存在差距。存在许多由各种安全需求驱动的加密哈希标准,但在非加密方面,存在一定的民间传统,尽管哈希函数历史悠久,但尚未得到充分探索。虽然针对现实世界数据集的均匀分布很有意义,但在面对具有特定模式的数据集时,这可能是一个挑战。


Nicole Forsgren, Eirini Kalliamvakou, Abi Noda, Michaela Greiler, Brian Houck, Margaret-Anne Storey - DevEx 实践
随着领导者寻求在财政紧缩和人工智能等变革性技术的背景下优化软件交付,DevEx(开发者体验)在许多软件组织中越来越受到关注。技术领导者凭直觉接受,良好的开发者体验能够实现更有效的软件交付和开发者幸福感。然而,在许多组织中,旨在改善 DevEx 的拟议倡议和投资难以获得支持,因为业务利益相关者质疑改进的价值主张。


João Varajão, António Trigo, Miguel Almeida - 低代码开发生产力
本文旨在通过展示使用基于代码、低代码和极限低代码技术进行的实验室实验结果来研究生产力差异,从而为该主题提供新的见解。低代码技术已清楚地显示出更高的生产力水平,为低代码在短期/中期内主导软件开发主流提供了强有力的论据。本文报告了程序和协议、结果、局限性和未来研究的机会。


Ivar Jacobson, Alistair Cockburn - 用例至关重要
虽然软件行业是一个快节奏且令人兴奋的世界,其中不断开发新的工具、技术和技巧来服务于商业和社会,但它也很健忘。在其快速前进的步伐中,它容易受到时尚的摆布,并且可能会忘记或忽略针对其面临的一些永恒问题的成熟解决方案。用例于 1986 年首次引入,后来得到普及,就是这些成熟的解决方案之一。





© 保留所有权利。

© . All rights reserved.