前言在最近的业务开发中,业务方需要我们的性能监控平台提供火焰图来展示函数栈和相关耗时信息。根据BrendanGregg在FlameGraph[1]主页中的定义:Flamegraphs是一种可视化的异形软件,可以让最频繁的代码路径快速准确地识别出来Flamegraph是一种可视化分析软件,可以让我们快速准确地找到频繁出现的代码路径称为函数栈。可以在此处查看火焰图的示例[2]。其实不仅仅是调用频率,火焰图也适合用来描述函数调用的栈和耗时频率,比如ChromeDevTools中的火焰图:其实就是根节点在顶部和底部的叶子节点称为冰柱图(icecolumnFigure)更为合适,但为了便于理解,下文将统称为火焰图。本文要分析的源码不是以上任何一个,而是React浏览器插件中使用的火焰图组件,它是由React官方成员BrianVaughn开发的react-flame-graph[3].本地调试react-flame-graph库本身是通过rollup搭建的,react-flame-graph[4]的示例网站是用webpack搭建的。所以如果你想在本地调试,克隆这个库后:分别在根目录和网站目录安装依赖。在根目录执行npmlink链接到全局,然后到网站目录npmlinkreact-flame-graph建立软链接。在根目录下执行npmrunstart开启rollup的watch编译模式,将react-flame-graph编译到dist目录下。在网站目录下执行npmrunstart,开启webpackdev模式,进入示例网站,通过编写ReactAppDemo进行调试。由于这个库比较老,最好用nrm把node版本调整到10.15.0。我在这个版本下成功安装了依赖。先简单看一下火焰图的效果:组件揭秘要使用这个组件,必须传入的数据是width和data。宽度指的是整个火焰图容器的宽度,后续计算每个宽度都需要这个宽度。数据格式为树形结构:constsimpleData={name:"foo",value:5,children:[{name:"customtooltip",value:1,tooltip:"Customtooltipshownonhover",},{name:"custombackgroundcolor",value:3,backgroundColor:"#35f",color:"#fff",children:[{name:"leaf",value:2,},],},],};除了标准树的name和children之外,还有一个必要的属性值,它根据每一层的值来决定每一个火焰块的宽度。比如这个数据的宽度树是width:5-width1-width3-width2,那么生成的火焰图也会遵循这个宽度比例:在业务场景中,每个矩形块一般对应一个函数调用,它会counttotal耗时,这个值可以作为值。数据转换组件的第一步是将递归数据转换为展平数组。递归数据虽然更直观地展现了层级,但是使用起来渲染起来比较麻烦。整个火焰图的渲染其实就是逐行渲染每一层对应的所有矩形块,所以层数相等的数组比较合适。我们的目标是将数据组织成这种结构:levels:[["_0"],["_1","_2"],["_3"],],nodes:{_0:{width:1,depth:0,left:0,name:"foo",...}_1:{width:0.2,depth:1,left:0,name:"customtooltip",...}_2:{width:0.6,depth:1,left:0.2,name:"custombackgroundcolor",...}_3:{width:0.4,depth:2,left:0.2,name:"leaf",...}}一目了然,层级对应层级关系和每一层的节点id,nodes是id对应的节点数据。其实这一步很关键。这个数据基本上决定了渲染的层次和风格。这里节点中的宽度已经通过width:value/maxValue进行了处理,maxValue其实就是根节点定义的宽度。在这个例子中,对应的值为5,所以:第一层节点宽度为5/5=1第二层节点宽度自然为1/5=0.2,3/5=0.6。这里处理的好处是渲染时可以直接乘以火焰图容器的宽度,即真实dom节点的宽度,得到矩形块的真实宽度。转换部分其实是一个递归,代码如下::number,leftOffset:number):ChartNode{const{backgroundColor,children,color,id,name,tooltip,value,}=sourceNode;constuidOrCounter=id||`_${uidCounter}`;//把这个节点放入地图consttargetNode=(nodes[uidOrCounter]={backgroundColor:backgroundColor||getNodeBackgroundColor(value,maxValue),color:color||getNodeColor(value,maxValue),depth,left:leftOffset,name,source:sourceNode,tooltip,//widthpropertyis(currentnodevalue/valueoftherootelement)width:value/maxValue,});//记录每一层对应的uid列表if(levels.length<=depth){levels.push([]);}levels[depth].push(uidOrCounter);//放入全局UID计数器+1uidCounter++;if(Array.isArray(children)){children.forEach((sourceChildNode)=>{//进一步递归consttargetChildNode=convertNode(sourceChildNode,depth+1,leftOffset);leftOffset+=targetChildNode.width;});}returntargetNode;}convertNode(rawData,0,0);constrootUid=rawData.id||"_0";return{height:levels.length,levels,nodes,root:rootUid,};}数据结构转换完成后,开始渲染部分了作者BrianVaughn在这里使用了他编写的React虚拟滚动库react-window[5]来优化长列表的性能。//FlamGraph.jsconstitemData=this.getItemData(data,focusedNode,...,width);{(props)=>
ItemRenderer组件实际上负责渲染每一行通过数据的矩形块。由于数据中有3个级别,因此该组件将被调用3次。每次都可以获取到对应层级的uid,通过uid可以获取节点相关信息,完成渲染。//ItemRendererconstfocusedNodeLeft=scale(focusedNode.left);constfocusedNodeWidth=scale(focusedNode.width);consttop=parseInt(style.top,10);constuids=data.levels[index];returnuids.map((uid)=>{constnode=data.nodes[uid];constnodeLeft=scale(node.left);constnodeWidth=scale(node.width);//太小的矩形块不渲染if(nodeWidth
