可视化整个 Chromium include 图

发布时间:2025年5月21日 10:00 AM

在这篇文章中,我将描述如何借助我的一个副项目——clang-include-graph来可视化 Chromium 的 include 图。

这项工作的主要动机是使用一个大型代码库来测试新发布的 clang-include-graph 版本。

在接下来的章节中,我将描述重现最终图形所需的所有步骤,包括构建 Chromium 以获取 compile_commands.json,以及使用 clang-include-graph 生成一个 GraphML 格式的 include 图表示,然后使用 Gephi 可视化该图形。当然,如果你赶时间:

TL;DR -> 完整的图形

目录

clang-include-graph 概述

clang-include-graph 是一个简单的基于 Clang 的命令行工具,用于分析 C/C++ 项目的 include 图。从 0.2.0 版本开始,它提供以下特性:

在本文中,我们将重点介绍 clang-include-graph 的 GraphML 输出特性,以及如何使用现有的开源软件来可视化和分析 Chromium 源代码的 include 图。

生成 include 图

构建 Chromium

由于 clang-include-graph 是一个基于 Clang 的工具,在生成 GraphML 文件格式的 include 图之前,我们需要为 Chromium 源代码生成 compile_commands.json 文件。

为了可重现性,我创建了一个包含一些辅助脚本的 Docker 镜像,用于获取和构建 Chromium,以及生成 GraphML 文件。你可以在这里找到它。

不过,下面我将介绍如何手动执行这些步骤。我们假设我们将在 /build 目录中工作,这对应于 Docker 容器中的卷挂载。

获取 Chromium 源码

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
mkdir -p chromium
cd chromium
export PATH=$PATH:/build/depot_tools
fetch --nohooks --no-history chromium
gclient runhooks

作为参考,我们将使用的特定 Chromium commit 的日期是 2025 年 5 月 13 日:

$ git -C chromium/src log
commit 48d682cce8e29049011b34f8e753b9dc4f73181e (grafted, HEAD, origin/main, origin/HEAD)
Author: Michael Slutskii <slutskii@google.com>
Date:  Tue May 13 05:06:05 2025 -0700
...

生成 compile_commands.json

现在,通常对于 C/C++ 项目,实际上不需要构建项目来生成 compile_commands.json。例如,CMake 可以在构建文件生成阶段生成一个。然而,Chromium 包含大量自动生成的代码(不,我不是指 LLM,而是像 Protobuf 和 Mojo stubs 这样的东西...),这些代码必须可用,Clang 才能正确解析所有翻译单元。由于我找不到一种无需实际构建整个 Chromium 即可生成所有这些文件的方法,因此我们只需这样做并等待:

cd chromium/src
gn gen out/Default
tools/clang/scripts/generate_compdb.py -p out/Default > compile_commands.json
ninja -C out/Default chrome unit_tests browser_tests
# Remove .o files to reduce volume size - we only need sources and compile_commands.json
find . -type f -name '*.o' -exec rm -- '{}' \;

我们可以检查 compile_commands.json 中的编译命令数量和唯一的翻译单元:

$ jq length compile_commands.json
71289
$ jq '.[] | .file' compile_commands.json | sort | uniq | wc -l
68505

作为参考,我可以从这里下载我得到的 compile_command.json

如果任何这些步骤对你不起作用,或者你需要调整某些内容,请参阅原始的 Chromium 构建文档

生成图形

现在我们有了 compile_commands.json 文件,我们可以使用 clang-include-graph 生成包含 include 图的 GraphML:

cd chromium/src/out/Default
clang-include-graph -v 1 --graphml \
 --compilation-database-dir /build/chromium/src \
 --relative-to /build/chromium/src --relative-only \
 --remove-compile-flag "-fextend-variable-liveness=none" \
 --remove-compile-flag -Wno-nontrivial-memcall \
 --add-compile-flag -Wno-unknown-pragmas \
 --remove-compile-flag -fcomplete-member-pointers \
 --remove-compile-flag -MMD --jobs 32 \
 --add-compile-flag -fsyntax-only \
 -o /build/graphml/chromium_include_graph_full.graphml

大多数选项希望是不言自明的,但以防万一:

在我的系统上 - 一台带有 AMD Ryzen,16 个核心(32 个线程)和 64GB RAM 的台式机 - 生成花费了 3 小时 13 分钟。生成的 GraphML 文件本身是 150M,从头到尾看起来像这样:

<?xml version="1.0" encoding="UTF-8"?>
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
 <key id="key0" for="node" attr.name="file" attr.type="string" />
 <key id="key1" for="edge" attr.name="is_system" attr.type="boolean" />
 <graph id="G" edgedefault="directed" parse.nodeids="canonical" parse.edgeids="canonical" parse.order="nodesfirst">
  <node id="n0">
   <data key="key0">apps/switches.h</data>
  </node>
  <node id="n1">
   <data key="key0">apps/switches.cc</data>
  </node>
  ...
  <edge id="e1310549" source="n141247" target="n141245">
   <data key="key1">0</data>
  </edge>
  <edge id="e1310550" source="n141247" target="n141232">
   <data key="key1">0</data>
  </edge>
 </graph>
</graphml>

它本质上是一个 <node> 元素列表,表示源文件和头文件,以及 <edge> 元素,表示从 sourcetarget 节点的 include 指令。每个节点都包含一个 key0 属性,其中包含文件的相对路径,每个 edge 都有一个布尔属性 key1,表示 include 是否是系统 include (即 #include <...>),或者常规 include (即 #include "...")。

现在,帖子的标题说这是整个 Chromium include 图,但有一些注意事项:

图形统计

在进行可视化之前,让我们计算一下这个图的一些基本图形统计信息。为此,我基于 NetworkX 图库编写了一个小的 Python 脚本 - 你可以在这里找到它。

运行脚本会产生以下输出:

$ python3 calculate_statistics.py ./chromium_include_graph_full.graphml
Loading graph from chromium_include_graph_full.graphml...
Calculating statistics...
Calculating basic metrics (nodes, edges)...
Calculating degree metrics (in/out degrees)...
Calculating degree centrality...
Finding strongly connected components...
Finding simple cycles...
Calculating average directed clustering coefficient...
Statistics calculation complete.
Graph Statistics:
- Number of nodes: 141248
- Number of edges: 1310551
- Maximum in-degree (most included): 23399
- Maximum out-degree (most including): 1131
Top 10 most included files (in-degree):
 third_party/libc++/src/include/utility: 23399
 third_party/libc++/src/include/memory: 21168
 third_party/libc++/src/include/string: 20801
 third_party/libc++/src/include/vector: 16486
 third_party/libc++/src/include/optional: 13053
 testing/gtest/include/gtest/gtest.h: 11635
 base/memory/raw_ptr.h: 9382
 build/build_config.h: 8993
 third_party/libc++/src/include/type_traits: 8170
 third_party/libc++/src/include/algorithm: 7738
Top 10 most including files (out-degree):
 out/Default/gen/third_party/blink/renderer/bindings/modules/v8/v8_window.cc: 1131
 chrome/browser/chrome_content_browser_client.cc: 498
 chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc: 399
 third_party/blink/renderer/core/dom/document.cc: 368
 third_party/pdfium/xfa/fxfa/parser/cxfa_node.cpp: 357
 out/Default/gen/third_party/blink/renderer/bindings/modules/v8/v8_dedicated_worker_global_scope.cc: 328
 out/Default/gen/v8/torque-generated/exported-macros-assembler.cc: 319
 chrome/browser/ui/views/frame/browser_view.cc: 311
 out/Default/gen/third_party/blink/renderer/bindings/modules/v8/v8_service_worker_global_scope.cc: 309
 content/browser/renderer_host/render_frame_host_impl.cc: 307
Top 10 nodes by degree centrality:
 third_party/libc++/src/include/utility: 0.1658
 third_party/libc++/src/include/memory: 0.1501
 third_party/libc++/src/include/string: 0.1478
 third_party/libc++/src/include/vector: 0.1168
 third_party/libc++/src/include/optional: 0.0928
 testing/gtest/include/gtest/gtest.h: 0.0824
 base/memory/raw_ptr.h: 0.0664
 build/build_config.h: 0.0637
 third_party/libc++/src/include/type_traits: 0.0584
 third_party/libc++/src/include/algorithm: 0.0561
Number of strongly connected components: 140874
Size of largest strongly connected component: 92
Largest strongly connected component nodes:
 v8/src/sandbox/js-dispatch-table-inl.h
 v8/src/objects/tagged-impl-inl.h
 v8/src/objects/map-inl.h
 v8/src/objects/struct-inl.h
 v8/src/objects/transitions-inl.h
 v8/src/objects/objects-inl.h
 v8/src/handles/maybe-handles-inl.h
 # ... skipped a bunch of v8 headers
 v8/src/objects/heap-number-inl.h
 v8/src/objects/property-cell-inl.h
 v8/src/sandbox/cppheap-pointer-inl.h
Number of simple cycles: 7809709
Average clustering coefficient: 0.0842

从这些信息中可以得出一些关键的结论:

统计 | 值 | 注释 ---|---|--- 节点数量 | 141,248 | 每个节点都是一个源文件或头文件 边数量 | 1,310,551 | 每条边代表一个 #include 指令 被 include 最多的文件的 include 数量 | 23399 | 有一个文件被直接 include 了 23,399 次。事实上,排名前 10 的 include 最多的文件中有一半以上来自 third_party,我们是否应该将其作为 Chromium 源代码可视化的一部分包括在内是一个问题,但由于我们的目标是整个代码库,所以让我们暂时保留它,看看结果如何。 具有最多 #include 的文件的 include 数量 | 1131 | 有一个文件具有 1131 个 #include 指令(相对于当前的编译标志) 循环数量 | 7,809,709 | 听起来很多?我们稍后再研究... 平均聚类系数 | 0.0842 | 表明该图不包含太多的集群,而是几个大的集群 强连通分量数量 | 140874 | 强连通分量的数量几乎与节点的数量一样多。据我所知,这对于具有大多数树状层次结构的定向图来说是很典型的。大多数 include 路径不会创建双向连接单独组件的循环。 最大强连通分量的大小 | 92 | 出于某种原因,最大的强连通分量似乎在 v8 子目录中。

可视化

现在我们或多或少地了解了我们正在处理的内容,让我们看看这个东西。经过一些实验,我选择了一个开源项目 Gephi 来生成可视化(Linux 的 0.10.1 版本)。

第一次尝试,在以默认设置运行 YifanHu 布局引擎后:

Gephi Yifan Hu

是的... 这不会很有用。我们需要以某种方式添加一些附加信息,并使用更多可用的布局引擎。显然,我们可以添加带有 include 文件路径的标签,但是在这个规模上,这只会使图像更加模糊。让我们尝试其他方法:

使用 NetworkX 注解 include 图

下面是一个简单的脚本,它将上述注解添加到使用 clang-include-graph 获取的 GraphML 文件中:

import networkx as nx
import argparse
def get_component(file_path):
  parts = file_path.split('/')
  return parts[0]
COLOR_MAP = {
  'third_party': '#33CC33',
  'chrome':    '#FF33FF',
  'components':  '#FF9900',
  'out':     '#00CCCC',
  'content':   '#800000',
  'ui':      '#808000',
  'net':     '#7B68EE',
  'services':   '#0000FF',
  'media':    '#FF66CC',
  'extensions':  '#FF3399',
  'base':     '#20B2AA',
  'remoting':   '#8B4513',
  'cc':      '#87CEEB',
  'gpu':     '#228B22',
  'device':    '#006400',
  'mojo':     '#2F4F4F',
  'v8':      '#800080',
  'storage':   '#008080',
  'google_apis': '#7FFF00',
  'sandbox':   '#556B2F',
  'pdf':     '#DAA520',
  'ppapi':    '#FF8C00',
  'headless':   '#008B8B',
  'ipc':     '#CD853F',
  'printing':   '#696969',
  'crypto':    '#E0FFFF',
  'gin':     '#FF0000',
  'tools':    '#A9A9A9',
  'skia':     '#DDA0DD',
  'url':     '#008B8B',
  'sql':     '#90EE90',
  'dbus':     '#D3D3D3',
  'testing':   '#C0C0C0',
  'apps':     '#4169E1',
  'build':    '#FF1493',
  'codelabs':   '#FFDAB9',
  'chromeos':   '#FA8072',
  'ash':     '#FF00FF',
}
def add_component_color_and_labels(graphml_file, output_graphml_file):
  G = nx.read_graphml(graphml_file)
  degree_map = dict(G.in_degree())
  top10 = sorted(degree_map, key=lambda n: degree_map[n], reverse=True)[:10]
  # Attach label properties to nodes
  for node, data in G.nodes(data=True):
    file_path = data.get('file', '')
    component = get_component(file_path)
    G.nodes[node]['component'] = component
    G.nodes[node]['color'] = COLOR_MAP.get(component, '#000000')
    if node in top10:
      G.nodes[node]['label'] = G.nodes[node].get('file', '')
    else:
      # This is a hack to force Gephi to not render node id when
      # label is empty
      G.nodes[node]['label'] = '____'
  # Ensure each component has at least one labeled node
  components = {data['component'] for _, data in G.nodes(data=True) if data.get('component')}
  for comp in components:
    comp_nodes = [n for n, d in G.nodes(data=True) if d.get('component') == comp]
    if not comp_nodes:
      continue
    best = max(comp_nodes, key=lambda n: degree_map.get(n, 0))
    G.nodes[best]['label'] = G.nodes[best].get('file', '')
  print(f"Nodes: {G.number_of_nodes()}")
  print(f"Edges: {G.number_of_edges()}")
  print(f"Dependencies: {', '.join(sorted(components))}")
  nx.write_graphml(G, output_graphml_file)
def main():
  parser = argparse.ArgumentParser(
    description='Annotate include graph nodes'
  )
  parser.add_argument('input_graphml', type=str, help='Path to the input GraphML file')
  parser.add_argument('output_graphml', type=str, help='Path for the updated GraphML output')
  args = parser.parse_args()
  add_component_color_and_labels(args.input_graphml, args.output_graphml)
  print(f"Updated GraphML with component, color, and labels saved to {args.output_graphml}")
if __name__ == '__main__':
  main()

下面是颜色映射的参考:

组件 | 颜色 | 组件 | 颜色 ---|---|---|--- third_party | sandbox | google_apis | ash chrome | pdf | sandbox | apps components | ppapi | pdf | build out | headless | ppapi | codelabs content | ipc | headless | chromeos ui | printing | ipc | ash net | crypto | printing | services | gin | crypto | media | tools | gin | extensions | skia | tools | base | url | skia | remoting | sql | url | cc | dbus | sql | gpu | testing | dbus | device | apps | testing | mojo | build | v8 | codelabs | storage | chromeos |

base 子目录

首先是 base 子目录。对于像我这样对 Chromium 代码库一无所知的人来说,这似乎是一个不错的起点。名称表明它可能是项目中其余部分通用的基本类和实用程序。

我们将通过向 clang-include-graph 添加 2 个标志来生成单独的 GraphML 文档:

--translation-unit "/build/chromium/src/base/**/*.cc" --output /build/graphml/chromium_include_graph_base.graphml

这意味着我们只访问 base 子目录下的翻译单元,并将输出写入 chromium_include_graph_base.graphml 文件。然后,要生成带注解的图形,我们只需调用:

$ python3 annotate_include_graph.py graphml/chromium_include_graph_base.graphml annotate_include_graph.py graphml/chromium_include_graph_base_annotated.graphml

GraphML 文件链接:raw, annotated

此文件的基本统计信息相当适中:

节点 | 边 | 依赖 ---|---|--- 3899 | 28811 | base, build, buildtools, out, testing, third_party

让我们看看这个图的一些布局。Gephi 提供了几个布局引擎可供选择,但对于最终图形,实际上只有少数几个能够处理它的大小,特别是:

因此,让我们坚持使用这些布局,无论对于所有组件还是完整图形。

边的颜色是源节点和目标节点颜色的混合,因此我们可以看到哪些箭头表示给定组件内部的 include(与组件节点颜色相同),哪些表示组件间的依赖关系(混合颜色)。

Yifan Hu 布局

base_yifanhu_labels

高分辨率图像:labels, no labels

好的,现在至少我们可以看到一些东西了。首先,我们可以看到较大的节点(被许多文件直接 include 的头文件)位于图形的中心,并形成 2 个单独的集群(左侧 base 的蓝绿色,右侧 third_party 的绿色)。

另一件事是,几乎所有节点和边都包含在这 2 个集群内(basethird_party 子目录),basethird_party 之间有一些互连(表示从 basethird_party 的依赖关系)。

圆形布局

base_circular_labels

高分辨率图像:labels, no labels

在这种情况下,圆形布局没有提供更多的信息,虽然从边的颜色中我们可以看到 buildtestingthird_party 组件在 base 翻译单元中的总体贡献。

圆形打包布局

base_circularpack_labels

高分辨率图像:labels, no labels

现在,这个图表虽然可能在视觉效果上稍逊一筹,但可能是信息量最大的。它清楚地显示了来自每个组件的文件(头文件或源文件)的数量,以及它们在 base 翻译单元中的受欢迎程度(节点半径)。

net 子目录

让我们继续到另一个子目录 - net。我们可以像以前一样生成相应的 GraphML 文件。

GraphML 文件链接:raw, annotated

基于图形统计信息,它比 base 大约 2 倍:

节点 | 边 | 依赖 ---|---|--- 6116 | 55594 | base, build, buildtools, components, crypto, mojo, net, out, sql, testing, third_party, ui, url

基于 net 翻译单元所依赖的组件列表,应该还有更多的颜色。同样,我们有 3 种布局可供选择:

Yifan Hu 布局

net_yifanhu_labels

高分辨率图像:labels, no labels

net 子目录的情况下,我们可以看到它的翻译单元不太直接依赖于 third_party 子目录,因为它的绝大部分在顶部形成一个单独的集群(请注意,此图不是建立在前一个图的基础之上的,它的边表示通过分析 net 子目录中的翻译单元发现的 include 指令)。此外,net 头文件似乎紧密耦合到 2 个集群中。另外,有趣的是 build 头文件是如何分布在图形的外边缘的,这意味着它们之间的互连非常松散。

圆形布局

net_circular_labels

高分辨率图像:labels, no labels

从这个布局中,我们可以看出,在这个图中,超过一半的节点来自 net 子目录(基于紫色弧的长度),并且基于内圆的颜色,我们可以看出 net 文件主要 include 来自 net 子目录的其他文件,其中一部分是来自 basethird_party 目录的头文件,只有微量的其他组件。

圆形打包布局

net_circularpack_labels

高分辨率图像:[labels](https://bkryza.org/