日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關咨詢
選擇下列產(chǎn)品馬上在線溝通
服務時間:8:30-17:00
你可能遇到了下面的問題
關閉右側工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
ReactCoreTeam成員開發(fā)的「火焰圖組件」技術揭秘

前言

最近在業(yè)務的開發(fā)中,業(yè)務方需要我們性能監(jiān)控平臺提供火焰圖來展示函數(shù)堆棧以及相關的耗時信息。

根據(jù) Brendan Gregg 在 FlameGraph[1] 主頁中的定義:

Flame graphs are a visualization of profiled software, allowing the most frequent code-paths to be identified quickly and accurately

火焰圖是一種可視化分析軟件,讓我們可以快速準確的發(fā)現(xiàn)調(diào)用頻繁的函數(shù)堆棧。

可以在這里查看火焰圖的示例[2]。

 

其實不光是調(diào)用頻率,火焰圖也同樣適合描述函數(shù)調(diào)用的堆棧以及耗時頻率,比如 Chrome DevTools 中的火焰圖:

 

其實根節(jié)點在頂部,葉子節(jié)點在底部的這種圖形稱為 Icicle charts(冰柱圖)更合適,不過為了理解方便,下文還是統(tǒng)一稱為火焰圖。

本文想要分析的源碼并不是上面的任意一種,而是 React 瀏覽器插件中使用的火焰圖組件,它是由 React 官方成員 Brian Vaughn 開發(fā)的 react-flame-graph[3]。

本地調(diào)試

react-flame-graph 這個庫本身是由 rollup 負責構建,而 react-flame-graph 的示例網(wǎng)站[4]則是用 webpack 構建。

所以本地想要調(diào)試的話,clone 這個庫以后:

  1. 分別在根目錄和 website 目錄安裝依賴。
  2. 在根目錄執(zhí)行 npm link 鏈接到全局,再去 website 目錄 npm link react-flame-graph 建立軟鏈接。
  3. 在根目錄執(zhí)行 npm run start 開啟 rollup 的 watch 編譯模式,把 react-flame-graph 編譯到 dist 目錄。
  4. 在 website 目錄執(zhí)行 npm run start 開啟 webpack dev 模式,進入示例網(wǎng)站,通過編寫 React App Demo 進行調(diào)試。

由于這個庫比較老,最好用 nrm 把 node 版本調(diào)整到 10.15.0,我是在這個版本下才成功安裝了依賴。

先來簡單看一下火焰圖的效果:

 

組件揭秘

使用

想要使用這個組件,必須傳入的數(shù)據(jù)是 width 和 data,

width 是指整個火焰圖容器的寬度,后續(xù)計算每個的寬度都需要用到。

data 格式則是樹形結構:

 
 
 
 
  1. const simpleData = { 
  2.   name: "foo", 
  3.   value: 5, 
  4.   children: [ 
  5.     { 
  6.       name: "custom tooltip", 
  7.       value: 1, 
  8.       tooltip: "Custom tooltip shown on hover", 
  9.     }, 
  10.     { 
  11.       name: "custom background color", 
  12.       value: 3, 
  13.       backgroundColor: "#35f", 
  14.       color: "#fff", 
  15.       children: [ 
  16.         { 
  17.           name: "leaf", 
  18.           value: 2, 
  19.         }, 
  20.       ], 
  21.     }, 
  22.   ], 
  23. }; 

除了標準樹的 name, children 外,這里還有一個必須的屬性 value,根據(jù)每一層的 value 也就決定了每一個火焰圖塊的寬度。

比如這個數(shù)據(jù)的寬度樹是

 
 
 
 
  1. width: 5 
  2.  - width 1 
  3.  - width 3 
  4.   - width 2 

那么生成的火焰圖也會遵循這個寬度比例:

 

而在業(yè)務場景中,這里一般每個矩形塊對應一次函數(shù)調(diào)用,它會統(tǒng)計到總耗時,這個值就可以用作為 value。

數(shù)據(jù)轉換

這個組件的第一步,是把這份遞歸的數(shù)據(jù)轉化為拉平的數(shù)組。

遞歸數(shù)據(jù)雖然比較直觀的展示了層級,但是用作渲染卻比較麻煩。

整個火焰圖的渲染,其實就是每個層級對應的所有矩形塊逐行渲染而已,所以平級的數(shù)組更適合。

我們的目標是把數(shù)據(jù)整理成這樣的結構:

 
 
 
 
  1. levels: [ 
  2.   ["_0"], 
  3.   ["_1", "_2"], 
  4.   ["_3"], 
  5. ], 
  6. nodes: { 
  7.   _0: { width: 1, depth: 0, left: 0, name: "foo", …} 
  8.   _1: { width: 0.2, depth: 1, left: 0, name: "custom tooltip", …} 
  9.   _2: { width: 0.6, depth: 1, left: 0.2, name: "custom background color", …} 
  10.   _3: { width: 0.4, depth: 2, left: 0.2, name: "leaf", …} 

一目了然,levels 對應層級關系和每層的節(jié)點 id,nodes 則是 id 所對應的節(jié)點數(shù)據(jù)。

其實這一步很關鍵,這個數(shù)據(jù)基本把渲染的層級和樣式?jīng)Q定好了。

這里的 nodes 中的 width 經(jīng)過了 width: value / maxValue 這樣的處理,而 maxValue其實就是根節(jié)點定義的那個 width,本例中對應數(shù)值為 5,所以:

  • 第一層的節(jié)點寬度是 5 / 5 = 1
  • 第二層的節(jié)點的寬度自然就是 1 / 5 = 0.2, 3 / 5 = 0.6。

在這里處理的好處是渲染的時候可以直接通過和火焰圖容器的寬度,也就是真實 dom 節(jié)點的寬度相乘,得到矩形塊真實寬度。

轉換部分其實就是一次遞歸,代碼如下:

 
 
 
 
  1. export function transformChartData(rawData: RawData): ChartData { 
  2.   let uidCounter = 0; 
  3.  
  4.   const maxValue = rawData.value; 
  5.  
  6.   const nodes = {}; 
  7.   const levels = []; 
  8.  
  9.   function convertNode( 
  10.     sourceNode: RawData, 
  11.     depth: number, 
  12.     leftOffset: number 
  13.   ): ChartNode { 
  14.     const { 
  15.       backgroundColor, 
  16.       children, 
  17.       color, 
  18.       id, 
  19.       name, 
  20.       tooltip, 
  21.       value, 
  22.     } = sourceNode; 
  23.  
  24.     const uidOrCounter = id || `_${uidCounter}`; 
  25.  
  26.     // 把這個 node 放到 map 中 
  27.     const targetNode = (nodes[uidOrCounter] = { 
  28.       backgroundColor: 
  29.         backgroundColor || getNodeBackgroundColor(value, maxValue), 
  30.       color: color || getNodeColor(value, maxValue), 
  31.       depth, 
  32.       left: leftOffset, 
  33.       name, 
  34.       source: sourceNode, 
  35.       tooltip, 
  36.       // width 屬性是(當前節(jié)點 value / 根元素的 value) 
  37.       width: value / maxValue, 
  38.     }); 
  39.  
  40.     // 記錄每個 level 對應的 uid 列表 
  41.     if (levels.length <= depth) { 
  42.       levels.push([]); 
  43.     } 
  44.     levels[depth].push(uidOrCounter); 
  45.  
  46.     // 把全局的 UID 計數(shù)器 + 1 
  47.     uidCounter++; 
  48.  
  49.     if (Array.isArray(children)) { 
  50.       children.forEach((sourceChildNode) => { 
  51.         // 進一步遞歸 
  52.         const targetChildNode = convertNode( 
  53.           sourceChildNode, 
  54.           depth + 1, 
  55.           leftOffset 
  56.         ); 
  57.         leftOffset += targetChildNode.width; 
  58.       }); 
  59.     } 
  60.  
  61.     return targetNode; 
  62.   } 
  63.  
  64.   convertNode(rawData, 0, 0); 
  65.  
  66.   const rootUid = rawData.id || "_0"; 
  67.  
  68.   return { 
  69.     height: levels.length, 
  70.     levels, 
  71.     nodes, 
  72.     root: rootUid, 
  73.   }; 

渲染列表

轉換好數(shù)據(jù)結構后,就要開始渲染部分了。這里作者 Brian Vaughn 用了他寫的 React 虛擬滾動庫 react-window[5] 去優(yōu)化長列表的性能。

 
 
 
 
  1. // FlamGraph.js 
  2. const itemData = this.getItemData( 
  3.   data, 
  4.   focusedNode, 
  5.   ..., 
  6.   width 
  7. ); 
  8.  
  9.   height={height} 
  10.   innerTagName="svg" 
  11.   itemCount={data.height} 
  12.   itemData={itemData} 
  13.   itemSize={rowHeight} 
  14.   width={width} 
  15.   {ItemRenderer} 

這里需要注意的是把外部傳入的一些數(shù)據(jù)整合成了虛擬列表組件所需要的 itemData,方法如下:

 
 
 
 
  1. import memoize from "memoize-one"; 
  2.  
  3. getItemData = memoize( 
  4.   ( 
  5.     data: ChartData, 
  6.     disableDefaultTooltips: boolean, 
  7.     focusedNode: ChartNode, 
  8.     focusNode: (uid: any) => void, 
  9.     handleMouseEnter: (event: SyntheticMouseEvent<*>, node: RawData) => void, 
  10.     handleMouseLeave: (event: SyntheticMouseEvent<*>, node: RawData) => void, 
  11.     handleMouseMove: (event: SyntheticMouseEvent<*>, node: RawData) => void, 
  12.     width: number 
  13.   ) => 
  14.     ({ 
  15.       data, 
  16.       disableDefaultTooltips, 
  17.       focusedNode, 
  18.       focusNode, 
  19.       handleMouseEnter, 
  20.       handleMouseLeave, 
  21.       handleMouseMove, 
  22.       scale: (value) => (value / focusedNode.width) * width, 
  23.     }: ItemData) 
  24. ); 

memoize-one 是一個用來做函數(shù)緩存的庫,它的作用是傳入的參數(shù)不發(fā)生改變的情況下,直接返回上一次計算的值。

對于新版的 React 來說,直接用 useMemo 配合依賴也可以達到類似的效果。

這里就是簡單的把數(shù)據(jù)保存了一下,唯一不同的就是新定義了一個方法 scale:

 
 
 
 
  1. scale: value => (value / focusedNode.width) * width, 

它是負責計算真實 DOM 寬度的,所有節(jié)點的寬度都會參照 focuesdNode 的寬度再乘以火焰圖容易的真實 DOM 寬度來計算。

所以點擊了某個節(jié)點聚焦它后,它的子節(jié)點寬度也會發(fā)生變化。

focuesdNode為根節(jié)點時:

 

點擊 custom background color 這個節(jié)點后:

 

這里 children 的位置用花括號的方式放了一個組件引用 ItemRenderer,其實這是 render props 的用法,相當于:

 
 
 
 
  1. {(props) => } 

而 ItemRenderer 組件其實就負責通過數(shù)據(jù)來渲染每一行的矩形塊,由于數(shù)據(jù)中有 3 層 level,所以這個組件會被調(diào)用 3 次。

每一次都可以拿到對應層級的 uids,通過 uid 又可以拿到 node 相關的信息,完成渲染。

 
 
 
 
  1. // ItemRenderer 
  2. const focusedNodeLeft = scale(focusedNode.left); 
  3. const focusedNodeWidth = scale(focusedNode.width); 
  4.  
  5. const top = parseInt(style.top, 10); 
  6.  
  7. const uids = data.levels[index]; 
  8.  
  9. return uids.map((uid) => { 
  10.   const node = data.nodes[uid]; 
  11.   const nodeLeft = scale(node.left); 
  12.   const nodeWidth = scale(node.width); 
  13.  
  14.   // 太小的矩形塊不渲染 
  15.   if (nodeWidth < minWidthToDisplay) { 
  16.     return null; 
  17.   } 
  18.  
  19.   // 超出視圖的部分就直接不渲染了 
  20.   if ( 
  21.     nodeLeft + nodeWidth < focusedNodeLeft || 
  22.     nodeLeft > focusedNodeLeft + focusedNodeWidth 
  23.   ) { 
  24.     return null; 
  25.   } 
  26.  
  27.   return ( 
  28.     
  29.       ... 
  30.       onClick={() => itemData.focusNode(uid)} 
  31.       x={nodeLeft - focusedNodeLeft} 
  32.       y={top} 
  33.     /> 
  34.   ); 
  35. }); 

這里所有的數(shù)值量都是通過 scale 根據(jù)容器寬度算出來的真實 DOM 寬度。

這里計算偏移量比較巧妙的點在于,最終傳遞給矩形塊組件LabeledRect的 x 也就是橫軸的偏移量,是根據(jù) focusedNode 的 left 值計算出來的。

如果父節(jié)點被 focus 后,它是占據(jù)整行的,子節(jié)點的 x 也會緊隨父節(jié)點偏移到最左邊去。

比如這個圖中聚焦的節(jié)點是 foo,那么最底下的 leaf 節(jié)點計算偏移量時,focusedNodeLeft 就是 0,它的偏移量就保持自身的 left 不變。

 

而聚焦的節(jié)點變成 custom background color 時,由于聚焦節(jié)點的 left 是 200,所以leaf 節(jié)點也會左移 200 像素。

也許有同學會疑惑,在 custom background color 聚焦時,它的父節(jié)點 foo 節(jié)點本身偏移量就是 0 了,再減去 200,不是成負數(shù)了嘛,那能父節(jié)點的矩形塊保證占據(jù)一整行嗎?

這里再回顧 scale 的邏輯:value => (value / focusedNode.width) * width,計算父節(jié)點的寬度時是 scale(父節(jié)點的寬度),而此時父節(jié)點的 width 是大于聚焦的節(jié)點的,所以最終的寬度能保證在偏移一定程度的負數(shù)時,父節(jié)點還是占滿整行。

最后 LabeledRect 就是用 svg 渲染出矩形,沒什么特殊的。

總結

看似復雜的火焰圖,在設計了良好的數(shù)據(jù)結構以及組件結構以后,一層層梳理下來,其實也并不難。

短短一篇文章下來,我們已經(jīng)完整解析了 react-devtools 中被大家廣泛使用的火焰圖組件,這種性能分析的利器也就這樣掌握了原理。

參考資料

[1]FlameGraph: http://www.brendangregg.com/flamegraphs.html[2]火焰圖的示例: http://www.brendangregg.com/FlameGraphs/cpu-mysql-updated.svg[3]react-flame-graph: react-flame-graph[4]react-flame-graph 的示例網(wǎng)站: https://react-flame-graph.now.sh/[5]react-window: https://github.com/bvaughn/react-window

本文轉載自微信公眾號「前端從進階到入院」,可以通過以下二維碼關注。轉載本文請聯(lián)系前端從進階到入院眾號。


當前標題:ReactCoreTeam成員開發(fā)的「火焰圖組件」技術揭秘
分享地址:http://m.5511xx.com/article/dpegcji.html