可视化整个 Chromium 的 include 图
可视化整个 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 可视化该图形。当然,如果你赶时间:
目录
clang-include-graph
概述- 生成 include 图
- 图形统计
- 可视化
- 结论
- 链接
clang-include-graph
概述
clang-include-graph 是一个简单的基于 Clang 的命令行工具,用于分析 C/C++ 项目的 include 图。从 0.2.0
版本开始,它提供以下特性:
- 以多种格式生成 include 图:
- 拓扑排序的 include 列表
- Include 树和反向 include 树
- Include 图的循环检测
- 列出指定头文件的所有依赖项
- 并行处理翻译单元
在本文中,我们将重点介绍 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
大多数选项希望是不言自明的,但以防万一:
--graphml
- 我们想要以 GraphML 格式打印 include 图--compilation-database-dir /build/chromium/src
- Clang 应该在哪里查找compile_commands.json
--relative-to /build/chromium/src
- 输出图中的所有呈现路径都应该相对于此路径--relative-only
- 我们只想在输出图中包含相对于/build/chromium/src
的文件(即,图中没有系统头文件,但是由于 Chromium 在third-party
目录中自带 C++ 头文件,因此这并没有太大改变)--remove-compile-flag
和--add-compile-flag
- 与基于 Clang 的工具一样,我们经常需要稍微调整compile_commands.json
中现有的编译标志。
在我的系统上 - 一台带有 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>
元素,表示从 source
到 target
节点的 include 指令。每个节点都包含一个 key0
属性,其中包含文件的相对路径,每个 edge
都有一个布尔属性 key1
,表示 include 是否是系统 include (即 #include <...>
),或者常规 include (即 #include "..."
)。
现在,帖子的标题说这是整个 Chromium include 图,但有一些注意事项:
- 我们只包括存在于
chromium/src
目录下的源文件和头文件,不包括外部系统头文件 - 我们只处理包含在生成的
compile_commands.json
中的翻译单元,这主要取决于平台 (x84_64 Linux),因此任何特定于其他平台 (Windows, macos, Android) 的文件都不会被包括在内 - 我们只处理未被
#ifdef
宏因默认编译标志而排除的 include 指令 - 任何在
chromium/src
目录下没有任何 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 布局引擎后:
是的... 这不会很有用。我们需要以某种方式添加一些附加信息,并使用更多可用的布局引擎。显然,我们可以添加带有 include 文件路径的标签,但是在这个规模上,这只会使图像更加模糊。让我们尝试其他方法:
- 对于每个节点,我们可以添加一个属性,表示
chromium/src
子目录中的顶级目录名称,并将其称为component
(例如,base/memory/raw_ptr.h
节点的base
) - 对于每个节点,我们可以根据组件名称添加颜色属性
- 对于具有最高入度(被 include 最多的文件)的前 10 个节点,我们将添加一个单独的
label
属性,以便能够看到哪些文件被 include 最多 - 确保每个组件至少标记一个头文件(入度最高的那个)
- 我们将从小处着手,基于分析所选组件(子目录)中的翻译单元来生成单独的图形,然后使用
compile_commands.json
中的所有翻译单元生成一个图形
使用 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
此文件的基本统计信息相当适中:
节点 | 边 | 依赖 ---|---|--- 3899 | 28811 | base, build, buildtools, out, testing, third_party
让我们看看这个图的一些布局。Gephi 提供了几个布局引擎可供选择,但对于最终图形,实际上只有少数几个能够处理它的大小,特别是:
- YifanHu 布局 - 高效的力导向布局算法(参见原始论文)
- 圆形布局 - 将节点组织在一个圆上,按其在
chromium/src
中的相对路径排序(即,来自相同组件的文件彼此靠近),并且边包含在圆内 - 圆形打包布局 - 节点根据多个因素聚类到层次结构中,在我们的例子中,第一个层次结构基于
component
属性,一个子层次结构基于入度指标
因此,让我们坚持使用这些布局,无论对于所有组件还是完整图形。
边的颜色是源节点和目标节点颜色的混合,因此我们可以看到哪些箭头表示给定组件内部的 include(与组件节点颜色相同),哪些表示组件间的依赖关系(混合颜色)。
Yifan Hu 布局
好的,现在至少我们可以看到一些东西了。首先,我们可以看到较大的节点(被许多文件直接 include 的头文件)位于图形的中心,并形成 2 个单独的集群(左侧 base
的蓝绿色,右侧 third_party
的绿色)。
另一件事是,几乎所有节点和边都包含在这 2 个集群内(base
和 third_party
子目录),base
和 third_party
之间有一些互连(表示从 base
到 third_party
的依赖关系)。
圆形布局
在这种情况下,圆形布局没有提供更多的信息,虽然从边的颜色中我们可以看到 build
、testing
和 third_party
组件在 base
翻译单元中的总体贡献。
圆形打包布局
现在,这个图表虽然可能在视觉效果上稍逊一筹,但可能是信息量最大的。它清楚地显示了来自每个组件的文件(头文件或源文件)的数量,以及它们在 base
翻译单元中的受欢迎程度(节点半径)。
net 子目录
让我们继续到另一个子目录 - net
。我们可以像以前一样生成相应的 GraphML 文件。
基于图形统计信息,它比 base
大约 2 倍:
节点 | 边 | 依赖 ---|---|--- 6116 | 55594 | base, build, buildtools, components, crypto, mojo, net, out, sql, testing, third_party, ui, url
基于 net
翻译单元所依赖的组件列表,应该还有更多的颜色。同样,我们有 3 种布局可供选择:
Yifan Hu 布局
在 net
子目录的情况下,我们可以看到它的翻译单元不太直接依赖于 third_party
子目录,因为它的绝大部分在顶部形成一个单独的集群(请注意,此图不是建立在前一个图的基础之上的,它的边表示通过分析 net
子目录中的翻译单元发现的 include 指令)。此外,net
头文件似乎紧密耦合到 2 个集群中。另外,有趣的是 build
头文件是如何分布在图形的外边缘的,这意味着它们之间的互连非常松散。
圆形布局
从这个布局中,我们可以看出,在这个图中,超过一半的节点来自 net
子目录(基于紫色弧的长度),并且基于内圆的颜色,我们可以看出 net
文件主要 include 来自 net
子目录的其他文件,其中一部分是来自 base
和 third_party
目录的头文件,只有微量的其他组件。
圆形打包布局
高分辨率图像:[labels](https://bkryza.org/