羔迪 发表于 2025-6-29 08:13:37

抛砖系列之性能分析工具火焰图


 
六月进入了紧张的996冲刺节奏,一个月下来周围的兄弟们看着都眼神迷离,面色阴沉,着实疲惫。幸运的是最后一个周末通知大家不用继续加班,我也好腾出手来写点东西,长时间停更让人脑袋空空,思来想去决定探索一个之前听说过觉得好玩,但是又没有实践过的东西-火焰图。
何为火焰图

火焰图将一组堆栈跟踪(又名调用堆栈)可视化,以倒置冰柱布局的邻接图形式呈现。火焰图通常用于可视化 CPU 分析器的输出,其中堆栈跟踪是通过采样收集的。
火焰图具有以下特点:
・一个堆栈跟踪由一列方框表示,每个方框代表一个函数(一个堆栈帧)。
・y 轴表示堆栈深度,从底部的根到顶部的叶依次排列。顶部的方框显示收集堆栈跟踪时正在使用 CPU 的函数,其下方的所有内容都是它的祖先。一个函数下方的函数是它的父函数。
・x 轴涵盖堆栈跟踪的收集范围。它不表示时间的流逝,因此从左到右的顺序没有特殊意义。堆栈跟踪的从左到右顺序是按照函数名称从每个堆栈的根到叶按字母顺序排列的。这样可以最大限度地合并方框:当相同的函数方框水平相邻时,它们会被合并。
・每个函数方框的宽度表示该函数在堆栈跟踪中出现的频率,或者是堆栈跟踪祖先的一部分。与窄方框的函数相比,宽方框的函数在堆栈跟踪中出现的频率更高,与它们的宽度成比例。
・如果方框的宽度足够,则显示完整的函数名称。如果不够,则显示带省略号的截断函数名称,或者不显示任何内容。
・每个方框的背景颜色并不重要,随机选择为暖色调。这种随机性有助于人眼区分方框,特别是对于相邻的细 “塔” 状方框。稍后会讨论其他配色方案。
・可视化的分析数据可能涵盖单个线程、多个线程、多个应用程序或多个主机。如果需要,可以生成单独的火焰图,特别是用于研究单个线程。
・堆栈跟踪可能从不同的分析目标收集,并且宽度可以反映除样本计数之外的其他度量。例如,一个分析器(或跟踪器)可以测量一个线程被阻塞的时间以及它的堆栈跟踪。这可以可视化为一个火焰图,其中 x 轴涵盖总阻塞时间,火焰图显示阻塞代码路径。
由于整个分析器输出一次性可视化,最终用户可以直观地导航到感兴趣的区域。火焰图中的形状和位置成为软件执行的可视化地图。
(来自于火焰图作者在acm的英文翻译)
通过一个Demo认识火焰图

本地写一段简单的代码来看看生成的火焰图长什么样子,代码如下:
import java.io.IOException;
import java.util.concurrent.TimeUnit;

/**
* func_c
* func_b
* func_a
* start_thread
*
* func_d
* func_a
* start_thread
*
* func_d
* func_a
* start_thread
*/
public class ThreadProfileTest {

    // 模拟 func_a 方法
    public static void func_a(int i) throws InterruptedException {
      System.out.println("Executing func_a");

      if (i <=1 ) {
            func_d();
      } else {
            func_b();
      }
    }

    // 模拟 func_b 方法
    public static void func_b() throws InterruptedException {
      System.out.println("Executing func_b");
      func_c();
    }

    // 模拟 func_c 方法
    public static void func_c() throws InterruptedException {
      long start = System.currentTimeMillis();
      while(true){
            System.out.println("Executing func_c");
            Thread.sleep(10);
            if(System.currentTimeMillis()- start > TimeUnit.MINUTES.toMillis(1)){
                break;
            }
      }

    }

    // 模拟 func_d 方法
    public static void func_d() throws InterruptedException {
      long start = System.currentTimeMillis();
      while(true){
            System.out.println("Executing func_d");
            Thread.sleep(10);
            if(System.currentTimeMillis()- start > TimeUnit.MINUTES.toMillis(1)){
                break;
            }
      }

    }

    // 启动新线程来执行 func_a
    public static void start_thread(int i) {
      System.out.println("Starting new thread...");

      Thread thread = new Thread(() -> {
            try {
                func_a(i);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
      });

      thread.start();
    }

    public static void main(String[] args) throws InterruptedException, IOException {
      //为命令行生成火焰图预留时间
      Thread.sleep(20000);

      // 主线程启动多个线程来模拟不同的调用路径
      for (int i = 0; i < 3; i++) {
            start_thread(i);
            Thread.sleep(100);
      }
      System.in.read();
    }
}java可以使用async-profiler工具采集cpu的使用情况,看看这个demo最终形成的cpu火焰图长什么样子

基本结构与颜色含义


[*]分层结构:从上到下是调用栈层级,上层是更基础的系统 / 库函数,下层是业务代码,反映 “谁调用了谁” 及调用深度。
[*]颜色区分:不同颜色无严格固定语义,主要辅助区分不同调用栈分支。
关键调用链与耗时热点


[*]系统调用 & 基础库:顶部大量futex_wait do_futex x64_sys_call等,是 Linux 系统调用(用于线程同步、系统交互),说明程序有频繁线程等待 / 同步操作,可能因多线程协作、锁竞争产生。
[*]JVM & 标准库:os::PlatformEvent::park java/lang/Thread.sleep ,体现 JVM 内部线程调度(如线程阻塞)和 Java 标准库的线程休眠逻辑,说明部分线程在主动 “睡眠” 或被 JVM 调度挂起。
[*]业务代码:底部ThreadProfileTest相关方法(func_a/func_b/func_c/func_d等),是应用自定义逻辑。这些方法在栈底,说明是业务执行入口,需结合上层调用看其实际耗时占比。
性能分析方向


[*]线程同步开销:系统调用层大量futex相关操作,需检查业务是否有过度锁竞争(如多线程争抢同一把锁),或不必要的线程等待逻辑。
[*]线程休眠合理性:Thread.sleep调用若频繁 / 时长不合理,可能影响程序吞吐量,需确认业务场景下线程休眠是否必要(如是否可优化为异步、事件驱动 )。
[*]业务方法占比:观察ThreadProfileTest方法在火焰图中的 “宽度”(宽度对应 CPU 耗时占比),若某些方法占比较大,聚焦优化其内部逻辑(如算法效率、冗余计算 )。
不难发现fun_c和func_d占用了较多的cpu时间,可能存在性能问题,需要结合代码去做优化。
 
应该关注火焰图的哪些方面

Flame graphs can be interpreted as follows:
• The top edge of the flame graph shows the function that was running on the CPU when the stack trace was collected. For CPU profiles, this is the function that is directly consuming CPU cycles. For other profile types, this is the function that directly led to the instrumented event.
• Look for large plateaus along the top edge, as these show a single stack trace was frequently present in the profile. For CPU profiles, this means a single function was frequently running on-CPU.
• Reading top down shows ancestry. A function was called by its parent, which is shown directly below it; the parent was called by its parent shown below it, and so on. A quick scan downward from a function identifies why it was called.
• Reading bottom up shows code flow and the bigger picture. A function calls any child functions shown above it, which, in turn, call functions shown above them. Reading bottom up also shows the big picture of code flow before various forks split execution into smaller towers.
• The width of function boxes can be directly compared: wider boxes mean a greater presence in the profile and are the most important to understand first.
• For CPU profiles that employ timed sampling of stack traces, if a function box is wider than another, this may be because it consumes more CPU per function call or that the function was simply called more often. The function-call count is not shown or known via sampling.
• Major forks in the flame graph, spotted as two or more large towers atop a single function, can be useful to study. They can indicate a logical grouping of code, where a function processes work in stages, each with its own function. It can also be caused by a conditional statement, which chooses which function to call.
The Flame Graph - ACM Queue
大致意思是:
[*]顶部函数意义:顶部边缘函数是采集栈跟踪时 CPU 上运行(CPU 分析场景)或直接引发监测事件(其他场景)的函数,是直接关联核心行为(CPU 消耗 / 事件触发)的入口 。
[*]顶部 “大平台” 价值:顶部出现大面积连续区域(大平台),说明对应单一栈跟踪在分析数据里频繁出现,反映某函数常占用 CPU(CPU 分析时) 。
[*]调用关系阅读:

[*]自顶向下:看函数 “血统”,上层函数由正下方父函数调用,逐层下探能明白 “为何被调用” 。
[*]自底向上:展现代码流向与宏观逻辑,底层函数调用上方子函数,能梳理分叉前整体执行路径 。

[*]函数框宽度作用:宽度直观体现函数在分析数据里的 “存在感”,越宽越关键,优先聚焦分析 。
[*]CPU 采样场景特殊点:CPU 采样分析中,函数框宽可能因 “单次调用耗 CPU 多” 或 “调用次数多”,但采样无法知晓实际调用次数 。
[*]火焰图分叉价值:多个 “高塔” 从单一函数分叉,可研究代码逻辑分组(如函数分阶段处理任务)或条件分支(选择调用不同函数)情况 。
 
当你手握这把“手术刀”以后,是不是迫不及待想对自己眼前的代码来个庖丁解牛,那我就再把自己练手的准备工作跟大家伙啰嗦一下,希望能少走弯路。
选一个趁手的工具

如果你要分析运行在linux服务器上的java程序时,我推荐直接使用arthas工具(基于async-profile),但是我相信有不少同学可能和我一样想在自己的windows pc机上写一点简单的Demo来帮助自己入门,目前async-profile还不支持windows,紧接着我就搜索各种在windows上生成火焰图的方法,也跟着下了不少的工具,但是和async-profile呈现的效果还是有不少区别,最终决定基于windows的WSL来模拟linux环境快速验证。
WSL是适用于 Linux 的 Windows 子系统(WSL)是 Windows 的一项功能,可用于在 Windows 计算机上运行 Linux 环境,而无需单独的虚拟机或双重启动。 WSL 旨在为想要同时使用 Windows 和 Linux 的开发人员提供无缝高效的体验。
在使用WSL之前差不多浪费了几个小时找各种工具,用了WSL也就前后不到半小时,简单描述下步骤,希望看到这里的你少走弯路:
1.根据推荐阅读最后的链接安装WSL,安装完以后WSL中会默认安装Ubuntu
2.在cmd中启动Ubuntu
wsl -d Ubuntu3.在Ubuntu中安装jdk
apt update
apt install openjdk-11-jdk4.下载async-profiler&解压缩
wget https://github.com/async-profiler/async-profiler/releases/download/v4.0/async-profiler-4.0-linux-x64.tar.gz<br>tar -xvf async-profiler-4.0-linux-x64.tar.gz<br>5.在Ubuntu中编写测试代码(也可以从自己的ide中把编译好的class直接拷贝到Ubuntu运行,我嫌麻烦就直接在Ubuntu写了)
可以基于vim编辑器,然后复制进来。
6.手动编译测试代码
javac ThreadProfileTest.java这一步估计好多年轻的程序员都没见过,毕竟现在ide太强大了,根本用不到,编译完以后会有一个.class文件。

 
7.运行编译好的class
java ThreadProfileTest8.找到java进程的pid然后使用async-profile的命令生成火焰图  
./asprof -e cpu -d 120 -f /workspace/flamegraph_2029.html 2029asprof在async-profiler解压缩以后的bin下
9.我上一步指定了将火焰图放在/workspace目录,所以在自己pc机的文件资源管理器中输入\\wsl$\Ubuntu\workspace然后就可以看到生成的火焰图,直接使用浏览器打开即可


 
周六没写完,周日大清单爬起来继续写,终于完事,我要出去浪了。
推荐阅读

The Flame Graph - ACM Queue
GitHub - async-profiler/async-profiler: Sampling CPU and HEAP profiler for Java featuring AsyncGetCallTrace + perf_events
Flame Graphs
profiler | arthas
安装 WSL | Microsoft Learn
 


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 抛砖系列之性能分析工具火焰图