jfr to pprof 性能优化

最近需要将 jfr 转换成 pprof,发现转换太耗费性能了,遂做一些性能优化。

将一份 jfr 文件转成 lock,memory, cpu, wall 四种类型的 pprof 的开销:

初始性能:

bash 复制代码
Benchmark-8            2         833094945 ns/op        461013108 B/op   8881948 allocs/op

实现

代码中需要通过 java 的 class name 得到 filename,我们给它加上一层缓存。

go 复制代码
func (b *pprofBuilder) getFileName(s string) string {
   k := s
   if res, ok := b.classNameCache[k]; ok {
      return res
   }
   if i := strings.Index(s, "$"); i != -1 {
      s = s[:i]
   }
   res := s + ".java"
   b.classNameCache[k] = res
   return res
}

有一块需要将 jvm 的 name 抓换成类名, 比如 [C 需要转换成 char[]。

我们给 dimension 加上 cache。

go 复制代码
var dimensions = []string{"", "[]", "[][]", "[][][]"}

func getDimension(dimension int) string {
   if dimension < len(dimensions) {
      return dimensions[dimension]
   }
   return strings.Repeat("[]", dimension)
}

对切片进行复用:

css 复制代码
b.results = b.results[:0]

成果:

arduino 复制代码
// with classname cache and dimension cache and results cache
// Benchmark-8           10         668776975 ns/op        369239214 B/op   6147112 allocs/op

我们原先通过 classname + "." + methodName + args 的形式拼接成 java name。

转换成通过 bytes.buffer 尝试去减小分配:

css 复制代码
b.b.Reset()
b.b.WriteString(className)
b.b.WriteString(".")
b.b.WriteString(f.Method.Name.String)
if f.Method.Descriptor != nil {
   if args := b.parseArgs(f.Method.Descriptor.String); args != "()" {
      b.b.WriteString(args)
   }
}
name = b.b.String()

成果:

bash 复制代码
// use bytes.buffer to write it
// Benchmark-8           10         641657292 ns/op        338757497 B/op   5566944 allocs/op

java 中带有 args,我们需要做一层转换

arduino 复制代码
(ZJ)V -> (boolean, long)

同样的也做一层 cache:

bash 复制代码
Benchmark-8           10         493109508 ns/op        315108003 B/op   4101661 allocs/op

在生成 args 的时候需要使用字符串拼接:

vbnet 复制代码
 "(" + strings.Join(results, ",") + ")"

这里有两次内存分配,我们可以去除 + 带来的一次内存分配:

go 复制代码
func (b *pprofBuilder) Join(elems []string, sep string) string {
   switch len(elems) {
   case 0:
      return ""
   case 1:
      return elems[0]
   }
   n := len(sep) * (len(elems) - 1)
   for i := 0; i < len(elems); i++ {
      n += len(elems[i])
   }

   n += 2
   b.b.Reset()
   b.b.Grow(n)
   b.b.WriteString("(")
   b.b.WriteString(elems[0])
   for _, s := range elems[1:] {
      b.b.WriteString(sep)
      b.b.WriteString(s)
   }
   b.b.WriteString(")")
   return b.b.String()
}

但是由于 argscache 的存在,对性能影响不大,去除掉避免让代码逻辑变得复杂:

bash 复制代码
Benchmark-8           10         498650422 ns/op        312121734 B/op   4101807 allocs/op

我们之前使用 bytes.buffer 去拼接字符串,然后用于 map 中的 key:

ini 复制代码
name  = b.b.String()
if result, ok := b.functionTable[name]; ok {
   return result
}
 b.functionTable = name

事实上我们在 map 中寻找的时候,只需要一个临时的 string 去计算一个 hash 就行了,因此可以改写成这样:

ini 复制代码
name  = BytesToString(b.b.Bytes())
if result, ok := b.functionTable[name]; ok {
   return result
}
name  = b.b.String()
 b.functionTable = name

完成后:

bash 复制代码
Benchmark-8           10         438528324 ns/op        239495232 B/op   2975963 allocs/op

原先我们使用 func name + line number 作为 location 的 key:

go 复制代码
line64 := int64(line)
key := fun.Name + ":" + strconv.FormatInt(line64, 10)
if l, ok := b.locationTable[key]; ok {

但如果我们假设 line number 不会大于 10000 的话,那么 key 可以改为 uint64:

go 复制代码
key := fun.ID*10000 + uint64(line)
if l, ok := b.locationTable[key]; ok {
   return l
}

结果:

bash 复制代码
Benchmark-8           10         204552827 ns/op        34932547 B/op     459624 allocs/op

对于每一个调用栈, 我们要生成一个 key 用于确认唯一一个调用栈,跟生成 function key 时候的一样,我们也使用 unsafe 去获取 hash。 同时, 给 java 的 pid 套一层 cache:

go 复制代码
func (b *pprofBuilder) getPid(JavaThreadID int64) string {
   return b.pidCache[JavaThreadID]
}

结果:

bash 复制代码
Benchmark-8           10         161653003 ns/op         3604637 B/op      25385 allocs/op

随后,我们合并 github.com/grafana/jfr... 库的这个 mr:

减少内存分配:github.com/grafana/jfr...

总结

通过将内存分配减少为原先的 1%,我们成功的优化了 80% 的 cpu 消耗。

对比优化前后的内存分配次数火焰图,jfr to pprof 的部分几乎完全抹除了,剩下的部分是读取 jfr 文件本身。

对于这部分,有一个优化思路是将 api 变换成流式读取,这样可以去复用中间的结构体。

相关推荐
uzong24 分钟前
软件架构指南 Software Architecture Guide
后端
又是忙碌的一天24 分钟前
SpringBoot 创建及登录、拦截器
java·spring boot·后端
勇哥java实战分享1 小时前
短信平台 Pro 版本 ,比开源版本更强大
后端
学历真的很重要1 小时前
LangChain V1.0 Context Engineering(上下文工程)详细指南
人工智能·后端·学习·语言模型·面试·职场和发展·langchain
计算机毕设VX:Fegn08951 小时前
计算机毕业设计|基于springboot + vue二手家电管理系统(源码+数据库+文档)
vue.js·spring boot·后端·课程设计
上进小菜猪2 小时前
基于 YOLOv8 的智能杂草检测识别实战 [目标检测完整源码]
后端
韩师傅3 小时前
前端开发消亡史:AI也无法掩盖没有设计创造力的真相
前端·人工智能·后端
栈与堆3 小时前
LeetCode-1-两数之和
java·数据结构·后端·python·算法·leetcode·rust
superman超哥3 小时前
双端迭代器(DoubleEndedIterator):Rust双向遍历的优雅实现
开发语言·后端·rust·双端迭代器·rust双向遍历
1二山似3 小时前
crmeb多商户启动swoole时报‘加密文件丢失’
后端·swoole