Ohhnews

分类导航

$ cd ..
DZone Java原文

Jeffrey Microscope 中的 Java 差分火焰图

#差分火焰图#jeffrey microscope#java#性能分析#代码优化

第一篇文章中,我们介绍了 Jeffrey Microscope,并学会了如何阅读单一火焰图——时序图、搜索、工具提示,以及分配和挂钟变体。这次我们将直接基于这些基础,探索 Jeffrey 在真实性能工作中最强大的功能之一:差分火焰图,它可以比较两次记录,精确显示两者之间的变化。

单一火焰图能告诉你应用程序的时间消耗在哪里。但在实践中,最关键的问题往往是比较型的:

  • 我的优化真的有效吗?
  • 这次重构让什么变慢了?
  • 额外的分配来自哪里?

盯着两张火焰图并排查看,用肉眼找出差异既慢又容易出错——图形很大,而有趣的变化往往深埋在栈帧中。Jeffrey Microscope 的 差分火焰图 解决了这个问题:它将两次记录叠加到一个图形中,并根据每个帧的变化着色:

  • 红色 —— 主 Profile 比基线花费 更多(回归)。
  • 绿色 —— 花费 更少(改进)。
  • 深色 —— 全新或完全消失的帧,被明显标注出来。

在本文中,我们将沿用上一篇文章中的两次记录 —— 优化过的 直接 序列化路径和垃圾密集的 DOM 路径 —— 将其中一个设为次要 Profile,让差分视图精确定位哪些方法造成了差异。

我们直接从第一篇文章结束的地方开始。打开优化后的记录 jeffrey-persons-direct-serde-cpu.jfr.lz4,进入 可视化 选项卡 —— 这是我们的 Profile,即我们上次探索过的 CPU 火焰图。单独看,它显示了直接序列化路径的时间消耗,但要进行比较,我们需要第二条记录来对照。这就是顶部栏中的 次要 Profile 插槽的作用 —— 当前显示为 未设置。下一步,我们将指向 DOM 版本的记录,并解锁侧边栏中的 差分 视图。

[LOADING...] [LOADING...]

支持的事件类型

设置好次要 Profile 后,差分 页面会镜像主页面 —— 每个事件类型一张卡片 —— 但每张卡片同时显示 两侧 的数据。左边的值是 基线(次要 Profile),右边的值是 主 Profile,徽章显示两者的相对变化:红色 +N% 表示主 Profile 的该事件比基线 更多(增长),绿色 −N% 表示 更少(减少)。这让你在打开图形之前就能评估总体变化 —— 无论变化是四舍五入的微小波动,还是值得调查的真正回归。

Jeffrey 支持为其能正常渲染的任何基于采样的事件生成差分火焰图:

  • 执行样本 —— 总的 CPU 工作量。样本越多表示在 CPU 上花费的时间越多(此处为 37.3K → 39.7K+6.4%)。
  • 挂钟样本 —— 包括等待和阻塞的经过时间,其变化可能独立于 CPU(5.0M → 4.4M−12.4%)。
  • 分配样本 —— 内存压力;切换 使用总分配 以比较字节而非样本计数,并查看真实的分配成本(27.47 GiB → 30.45 GiB+10.9%)。
  • CPU 时间样本方法追踪 —— 此处为空,但当记录包含它们时,差分方式相同。

[LOADING...]

每个数字只是概览;下面的火焰图逐帧细化同一个增量,让你看到哪些方法驱动了它。点击 执行样本 卡片上的 查看火焰图,打开 CPU 差分视图。

读取差分火焰图

打开差分视图感觉很熟悉 —— 和普通火焰图一样有序列、搜索和工具提示 —— 但现在每条信息都同时编码了两个 Profile:

  • 顶部摘要栏 并排显示总数:基线 35,472 vs 主 39,668,净 +4,196 (+11.83%),标记为 回归。这是概览 —— 主运行在 CPU 上的总工作量更多。
  • 时序图 叠加了两条线 —— 主 Profile 为蓝色,次要(基线) 为红色 —— 让你看到两个 Profile 在时间上的分歧点,而不仅仅是知道它们不同。
  • 火焰图颜色 编码了每帧的变化:浅粉/绿色表示变化较小的帧,而饱和的 深红/深绿 表示仅存在于 一个 Profile 中的帧 —— 全新出现的工作和完全消失的工作。

亮点在最后两张截图中。由于优化和未优化的路径使用不同名称的类,差分将它们渲染为一对:深红色EfficientPersonService.getNPersons 子树(主 Profile 中新增)紧邻深绿色InefficientPersonService 子树(主 Profile 中消失)。你实际上看到了从上到下的代码替换。悬停在一个 共享 帧上可以精确量化变化 —— PersonController.getNPersons 上的工具提示显示 基线 854 → 主 525,该端点的路径 改进了 −329 (−38.52%)

[LOADING...]

差分 CPU 火焰图叠加了两个记录:时序图绘制了主 Profile(蓝色)和次要基线(红色),摘要栏显示 基线 35,472 → 主 39,668,净 +4,196 (+11.83%) 标记为 回归

[LOADING...]

合并后的火焰图根据每个帧的变化着色。共享的 Tomcat、Coyote 和 Spring 层大多保持浅粉色 —— 小幅变化 —— 而摘要栏始终显示总体 +11.83% 的增量。火焰图还包括 JVM 自身的线程,而不仅仅是你的请求路径 —— 左侧的 CompileBroker/C2Compiler 栈是 JIT 编译,垃圾回收活动也以同样方式显示。在两个记录之间进行比较,可以告诉你哪个版本触发了额外的 JIT 或 GC 活动高峰,这是当一个版本分配更多或导致更多代码变更时常见的隐藏成本。

[LOADING...]

更深入地栈中,两个实现分离出来:饱和的 红色 列表示主 Profile 中新增的工作,而 深绿色 列表示仅存在于基线中的路径,在主 Profile 中消失。

[LOADING...]

优化的 EfficientPersonService 路径(红色,新增)紧挨着已删除的 InefficientPersonService 路径(绿色)。悬停共享的 PersonController.getNPersons 帧精确量化了变化:基线 854 → 主 525改进了 −329 (−38.52%)

总结

接下来,请尝试对 挂钟分配 差分火焰图执行相同操作 —— 步骤完全相同,每个维度都揭示了变化的不同方面:等待时间和分配的字节数。

感谢阅读!想要进一步了解,请访问 Jeffrey 页面,或通过 LinkedIn 直接联系我 —— 期待您的反馈。敬请关注:下一篇文章中,我们将离开火焰图,探索 Jeffrey 的 JVM 内部 视图之一,深入研究运行时底层工作原理。

DZone 贡献者的观点仅代表其个人。