自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

React Core Team 成員開發(fā)的「火焰圖組件」技術(shù)揭秘

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

[[379900]]

前言

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

根據(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

火焰圖是一種可視化分析軟件,讓我們可以快速準(zhǔn)確的發(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 負責(zé)構(gòu)建,而 react-flame-graph 的示例網(wǎng)站[4]則是用 webpack 構(gòu)建。

所以本地想要調(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 格式則是樹形結(jié)構(gòu):

  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. }; 

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

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

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

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

 

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

數(shù)據(jù)轉(zhuǎn)換

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

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

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

我們的目標(biāo)是把數(shù)據(jù)整理成這樣的結(jié)構(gòu):

  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 對應(yīng)層級關(guān)系和每層的節(jié)點 id,nodes 則是 id 所對應(yīng)的節(jié)點數(shù)據(jù)。

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

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

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

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

轉(zhuǎn)換部分其實就是一次遞歸,代碼如下:

  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 屬性是(當(dāng)前節(jié)點 value / 根元素的 value) 
  37.       width: value / maxValue, 
  38.     }); 
  39.  
  40.     // 記錄每個 level 對應(yīng)的 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.   }; 

渲染列表

轉(zhuǎn)換好數(shù)據(jù)結(jié)構(gòu)后,就要開始渲染部分了。這里作者 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. <List 
  10.   height={height} 
  11.   innerTagName="svg" 
  12.   itemCount={data.height} 
  13.   itemData={itemData} 
  14.   itemSize={rowHeight} 
  15.   width={width} 
  16.   {ItemRenderer} 
  17. </List>; 

這里需要注意的是把外部傳入的一些數(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, 

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

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

focuesdNode為根節(jié)點時:

 

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

 

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

  1. <List>{(props) => <ItemRenderer {...props} />}</List> 

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

每一次都可以拿到對應(yīng)層級的 uids,通過 uid 又可以拿到 node 相關(guān)的信息,完成渲染。

  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.     <LabeledRect 
  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 像素。

 

 

 

 

也許有同學(xué)會疑惑,在 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 渲染出矩形,沒什么特殊的。

總結(jié)

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

短短一篇文章下來,我們已經(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

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

 

責(zé)任編輯:武曉燕 來源: 前端從進階到入院
相關(guān)推薦

2019-07-22 10:42:11

React組件前端

2020-08-13 06:43:41

React前端開發(fā)

2019-07-20 23:30:48

開發(fā)技能代碼

2010-07-07 18:00:44

UML類圖建模

2023-05-30 09:07:06

CPU性能火焰圖

2020-10-12 10:06:26

技術(shù)React代數(shù)

2021-01-19 09:59:02

招聘管理團隊

2009-11-23 20:37:45

ibmdwRational

2011-04-06 11:21:25

PHPPython

2017-02-28 21:57:05

React組件

2025-04-07 08:25:01

React復(fù)合組件組件模式

2020-01-07 15:40:43

React前端技術(shù)準(zhǔn)則

2009-12-16 09:29:13

VS Team Sys

2017-08-24 09:19:20

分解技術(shù)揭秘

2010-06-02 09:31:43

Linux core

2016-11-25 13:50:15

React組件SFC

2022-05-13 08:48:50

React組件TypeScrip

2021-03-18 08:00:55

組件Hooks React

2023-12-31 19:41:04

PHP性能終端

2010-09-16 15:51:02

數(shù)據(jù)屏蔽技術(shù)加密
點贊
收藏

51CTO技術(shù)棧公眾號