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