VUE组件——FlameGraph火焰图

先贴个图

火焰图的应用场景

       用于显示复杂的函数调用链,特点在于:数据量较大。

       详细介绍可以参考阮大师的博客:http://www.ruanyifeng.com/blog/2017/09/flame-graph.html

本文主要在vue框架的基础上实现函数调用链火焰图,其它框架的实现方式类似,由于应用场景中的数据量较大,使用了三种方式来实现并测试该功能。

数据源格式:

      函数调用链实质就是一棵树,最下层节点是根;上层节点的位置及宽度依赖于下层节点。

{
id: {
id: 'id_*', //使用字符串id,避免json.parse自动排序
name: 函数名, percentage: 百分比, parentId: 父节点id, prevId: 兄弟节点id, level: 层级,
}
}

  

计算方式:

     根节点的宽度默认为100%宽度;

    子节点宽度=调用百分比*父节点宽度;

    位置信息x 从父节点的x开始,若前面有兄弟节点prev,则x = prev.x + prev.width;

    位置信息y 从最底端开始,y = level * 16(每层火焰高度为15,间隔1像素)。

方法一:SVG多组件渲染方式

将一个节点作为一个组件实现

<svg width="100%" :height="height">
   <g v-show="!loading" id="graphContainer">
      <call-item v-for="item in items" :item="item" :key = "item.id" @mouseover="displayItem(item)" @mouseout="display=''" 
        :parent="items['id_' + item.parentId]" @click="repaint(item)"
        :prev="items['id_' + item.preId]" :height="cHeight" :cur = "curItem"></call-item>
      <text x="15" :y="cHeight - 15" >{{display}}</text>
    </g>
</svg>

mouseover事件,在下册text区域显示鼠标滑动所至节点的详细信息

click事件,以被点击的节点为最大节点重绘调用链,其父节点均半透明,子节点根据当前节点宽度及位置信息重绘

单组件渲染方式在节点个数过大时效果较差

方法二:SVG单组件渲染方式

使用js拼凑火焰图部分作为模板来渲染,绘制部分和渲染部分如下:

   drawOneCallItem (item, frag, items) {
      let g = '', color = this.calcItemColor(),
        title = `${item.name}, ${item.percentage}%)`, display = title
      display = this.calcDisplay(title, item.width)
      g = `<g class="func_g">
      <title>${title}</title>
      <rect x="${item.pos.x}" y="${item.pos.y}" width="${item.width}" height="15.0" fill="${color}" rx="2" ry="2"
      orig-x="${item.pos.x}" orig-y="${item.pos.y}" orig-width="${item.width}" ></rect>
      <text x="${item.pos.x + 5}" y="${item.pos.y + 12}" >${display}</text>
      </g>`
      frag.push(g)
    },
    initFlameGraph () {
      console.info('starting getData:', new Date().getTime())
      if (this.id > 0) {
        this.depth = 0
        let frag = []
        service.getTree().then(({data}) => {
          let res = data.data
          let graph = document.getElementById('graphContainer')
          graph.innerHTML = ''
          for (let key of Object.keys(res.records)) {
            let item = res.records[key]
            this.drawOneCallItem(item, frag, res.records)
          }
          var MyAppendTo = Vue.extend({
            template: frag.join('')
          })
          let app = new MyAppendTo()
          app.$mount().$appendTo('#graphContainer')
          frag = []
          console.info('draw finish:', new Date().getTime())
        })
      }
    }

此方案在绘制1万+个节点时加上网络消耗大致需要2秒左右,基本符合需求。

方法三:Canvas实现方式

canvas 方式绘制性能非常好,实验过12万+的节点比方法二快3倍左右。但事件处理流程比较复杂,尤其是hover事件的模拟,计算量也比较大。

计算方式与方法二类似,预先计算好节点位置,然后从最底层按照宽度和位置信息按序绘制即可。

RoundedRect 类是canvas的单节点绘制类,原本是需要绘制圆矩形的,但宽度低时有问题,后面修改后再更新。
export default class RoundedRect {
  constructor (ctx, x, y, width, fill, text) {
    this.ctx = ctx
    this.x = x
    this.y = y
    this.width = width
    this.fill = fill
    this.text = text
  }

    //
  draw () {
    let ctx = this.ctx
    this.drawRoundedRect(ctx, this.x, this.y, this.width, 15, 0, this.fill, this.text)
  }

  calcDisplay (title, width) {
    let cLen = Math.ceil(width / 8) - 2,
      disStr = title
    if (title.length > cLen) {
      disStr = title.substring(0, cLen - 2)
      disStr = disStr.length > 0 ? disStr + '..' : disStr
    }
    return cLen <= 4 ? '' : disStr
  }

  drawRoundedRect (ctx, x, y, w, h, r, bgcolor, text) {
    ctx.beginPath()
    ctx.fillStyle = bgcolor
    ctx.fillRect(x, y, w, h)
    ctx.strokeText(this.calcDisplay(text, w), x + 5, y + 12)
    ctx.closePath()
  }
}

canvas方案的hover和click事件需要通过鼠标位置信息找到hover或click的是哪个节点,在节点很多时计算量也是相当的大啊,最后不得不放弃canvas方案。选择方法二。

原文地址:https://www.cnblogs.com/yiyitong/p/8182416.html