成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

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]。

 

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

 

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

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

本地調(diào)試

react-flame-graph 這個庫本身是由 rollup 負(fù)責(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 模式,進(jìn)入示例網(wǎng)站,通過編寫 React App Demo 進(jìn)行調(diào)試。

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

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

 

組件揭秘

使用

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

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

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)計(jì)到總耗時,這個值就可以用作為 value。

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

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

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

整個火焰圖的渲染,其實(shí)就是每個層級對應(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é)點(diǎn) id,nodes 則是 id 所對應(yīng)的節(jié)點(diǎn)數(shù)據(jù)。

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

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

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

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

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

  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é)點(diǎn) 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 計(jì)數(shù)器 + 1 
  47.     uidCounter++; 
  48.  
  49.     if (Array.isArray(children)) { 
  50.       children.forEach((sourceChildNode) => { 
  51.         // 進(jìn)一步遞歸 
  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ā)生改變的情況下,直接返回上一次計(jì)算的值。

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

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

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

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

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

focuesdNode為根節(jié)點(diǎn)時:

 

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

 

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

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

而 ItemRenderer 組件其實(shí)就負(fù)責(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ù)容器寬度算出來的真實(shí) DOM 寬度。

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

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

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

 

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

 

 

 

 

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

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

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

總結(jié)

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

短短一篇文章下來,我們已經(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)載自微信公眾號「前端從進(jìn)階到入院」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系前端從進(jìn)階到入院眾號。

 

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

2019-07-22 10:42:11

React組件前端

2019-07-20 23:30:48

開發(fā)技能代碼

2010-07-07 18:00:44

UML類圖建模

2020-08-13 06:43:41

React前端開發(fā)

2011-04-06 11:21:25

PHPPython

2021-01-19 09:59:02

招聘管理團(tuán)隊(duì)

2020-10-12 10:06:26

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

2023-05-30 09:07:06

CPU性能火焰圖

2009-11-23 20:37:45

ibmdwRational

2025-04-07 08:25:01

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

2017-02-28 21:57:05

React組件

2017-08-24 09:19:20

分解技術(shù)揭秘

2020-01-07 15:40:43

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

2009-12-17 16:53:13

.NET Framew

2009-12-16 09:29:13

VS Team Sys

2016-11-25 13:50:15

React組件SFC

2022-05-13 08:48:50

React組件TypeScrip

2023-12-21 10:26:30

??Prettier

2010-06-02 09:31:43

Linux core

2015-11-15 17:22:25

微軟硬件創(chuàng)新
點(diǎn)贊
收藏

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

主站蜘蛛池模板: 91在线视频国产 | 成人高清在线视频 | 色播久久| 天天操天天摸天天爽 | www.99热.com| 欧美区日韩区 | 综合成人在线 | 黄色片网站在线观看 | 中文字幕视频在线免费 | 国产日韩av一区二区 | 色毛片| 久草免费在线 | 中文字幕一区二区三区四区 | 国产精品永久免费观看 | 精品久久久久久红码专区 | 国产99精品 | 国产精品一区在线 | 91精品久久久 | www.日韩| 欧美中文一区 | 女人牲交视频一级毛片 | 亚洲成人国产 | 99久久久国产精品 | 国产综合视频 | 亚洲精品久久久9婷婷中文字幕 | 99久热 | 91色在线 | 日韩欧美精品一区 | 91精品久久久 | 亚洲自拍偷拍免费视频 | 欧美一二区 | 狠狠狠色丁香婷婷综合久久五月 | 天天干狠狠干 | 日韩一区二区三区视频在线观看 | 日韩欧美国产一区二区 | 午夜影院 | 九九热这里只有精品在线观看 | 日韩午夜在线观看 | 中文字幕专区 | 91偷拍精品一区二区三区 | 水蜜桃亚洲一二三四在线 |