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 变换成流式读取,这样可以去复用中间的结构体。

相关推荐
凡人的AI工具箱41 分钟前
AI教你学Python 第11天 : 局部变量与全局变量
开发语言·人工智能·后端·python
是店小二呀1 小时前
【C++】C++ STL探索:Priority Queue与仿函数的深入解析
开发语言·c++·后端
canonical_entropy1 小时前
金蝶云苍穹的Extension与Nop平台的Delta的区别
后端·低代码·架构
我叫啥都行2 小时前
计算机基础知识复习9.7
运维·服务器·网络·笔记·后端
无名指的等待7122 小时前
SpringBoot中使用ElasticSearch
java·spring boot·后端
.生产的驴3 小时前
SpringBoot 消息队列RabbitMQ 消费者确认机制 失败重试机制
java·spring boot·分布式·后端·rabbitmq·java-rabbitmq
AskHarries4 小时前
Spring Boot利用dag加速Spring beans初始化
java·spring boot·后端
苹果酱05674 小时前
一文读懂SpringCLoud
java·开发语言·spring boot·后端·中间件
掐指一算乀缺钱4 小时前
SpringBoot 数据库表结构文档生成
java·数据库·spring boot·后端·spring
计算机学姐7 小时前
基于python+django+vue的影视推荐系统
开发语言·vue.js·后端·python·mysql·django·intellij-idea