【轉(zhuǎn)】推薦一個檢測 JS 內(nèi)存泄漏的神器

最近,Meta 開源了一款檢測 员咽,我們來一起看看這個框架有啥神奇之處吧~

2020 年赫段,Meta 的工程師將 Facebook.com 重構(gòu)為了單頁應(yīng)用(SPA),程序的大部分渲染和導(dǎo)航都會在客戶端使用 JavaScript 完成肪虎。后來他們又使用類似的架構(gòu)來重構(gòu)了 Meta 的大多數(shù)其他流行的網(wǎng)絡(luò)應(yīng)用程序,包括 Instagram 和 Workplace惧蛹。雖然這種架構(gòu)能夠提供更快的用戶交互扇救、更好的開發(fā)者體驗和更像原生應(yīng)用程序的感覺,但是在客戶端維護(hù) Web 應(yīng)用的狀態(tài)會讓內(nèi)存的管理變得更加復(fù)雜香嗓。

使用 Meta 網(wǎng)站的用戶經(jīng)常會快速注意到一些性能和功能正常使用的問題迅腔。然而,內(nèi)存泄漏就是另一回事了靠娱。它不會立即被察覺出來钾挟,因為它一次會占用一大塊內(nèi)存 — 然后逐漸影響整個 Web 會話并讓后續(xù)的交互和響應(yīng)變得更慢。

Meta 的工程師花費了大量時間來測試饱岸、優(yōu)化和控制頁面加載和交互時間,以及 JavaScript 的代碼大小徽千。相比之下苫费,他們在管理 Web 瀏覽器內(nèi)存方面做的工作并不多。當(dāng)分析新 Facebook.com 的內(nèi)存使用情況時双抽,發(fā)現(xiàn)客戶端的內(nèi)存使用情況和內(nèi)存不足 (OOM) 崩潰的數(shù)量一直在攀升百框。較高的內(nèi)存使用對頁面加載、交互性能牍汹、用戶參與度等核心指標(biāo)都有負(fù)面影響铐维。

為了幫助開發(fā)者解決這個問題柬泽,Meta 的工程師構(gòu)建了 MemLab,這是一個 JavaScript 內(nèi)存測試框架嫁蛇,可以自動進(jìn)行內(nèi)存泄漏檢測锨并,并且更容易找到內(nèi)存泄漏的根本原因。Meta 使用 MemLab 成功地控制了不可持續(xù)的內(nèi)存增長睬棚,并識別出了產(chǎn)品和基礎(chǔ)設(shè)施中的內(nèi)存泄漏和內(nèi)存優(yōu)化的一些手段第煮。

導(dǎo)致 Web 應(yīng)用內(nèi)存過高的原因

因為內(nèi)存泄漏通常不是很明顯,在開發(fā)過程中抑党,以及做 Code Review 的時候都很難發(fā)現(xiàn)包警,而且在生產(chǎn)環(huán)境中通常也很難找到根本原因。雖然主流的 JavaScript 運行時都有垃圾回收機(jī)制底靠,那么為什么還會有內(nèi)存泄漏呢害晦?

JavaScript 代碼中可能會有很多隱藏對象的引用,而隱藏的引用會以許多意想不到的方式導(dǎo)致內(nèi)存泄漏暑中。

例如:

var obj = {};
console.log(obj);
obj = null;

在 Chrome 中壹瘟,即使我們將引用設(shè)置為 null ,這段代碼也會泄漏 obj 痒芝。發(fā)生這種情況是因為 Chrome 需要保留對打印對象的內(nèi)部引用俐筋,以便以后可以在 Web 控制臺中對其進(jìn)行檢查(即使在 Web 控制臺沒打開的情況下)。

在某些情況下严衬,內(nèi)存在技術(shù)上并沒有發(fā)生泄漏澄者,而是在用戶會話期間線性增長而且沒有限制。最常見的原因是客戶端緩存沒有內(nèi)置任何釋放的邏輯请琳,無限滾動列表沒有任何虛擬化的功能粱挡,無法在添加新內(nèi)容時從列表中刪除較早的內(nèi)容。

我們也沒有適當(dāng)?shù)淖詣踊到y(tǒng)和流程來控制內(nèi)存俄精,因此防止此類問題的唯一防御措施就是專家通過 Chrome DevTools 定期挖掘內(nèi)存泄漏询筏,一些大型的項目幾乎每天都會有發(fā)布和變更,這樣的工作方式是不可持續(xù)的竖慧。

MemLab 的工作原理

MemLab 通過預(yù)定義的測試場景運行無頭瀏覽器并比較和分析 JavaScript 堆快照來發(fā)現(xiàn)內(nèi)存泄漏的問題嫌套。


這個過程可以分為下面六個步驟:

  • 1.「瀏覽器交互」:MemLab 使用 Puppeteer 自動化瀏覽器,在目標(biāo)頁面上查找泄露的對象圾旨;

  • 2.「區(qū)分堆」:導(dǎo)航到一個頁面然后離開它踱讨,正常情況下該頁面分配的大部分內(nèi)存也應(yīng)該被釋放,如果沒有砍的,可能暗示著存在內(nèi)存泄漏痹筛。MemLab 通過區(qū)分 JavaScript 堆并記錄在頁面 B 上分配的一組對象,這些對象沒有在頁面 A 上分配,但在重新加載頁面 A 時仍然存在帚稠,從而發(fā)現(xiàn)潛在的內(nèi)存泄漏谣旁;

  • 3.「細(xì)化內(nèi)存泄漏列表」:內(nèi)存泄漏檢測器進(jìn)一步結(jié)合了特定框架的知識來細(xì)化泄漏對象的列表。例如滋早,React 分配的 Fiber 節(jié)點(React 用于渲染虛擬 DOM 的內(nèi)部數(shù)據(jù)結(jié)構(gòu))應(yīng)該在我們訪問多個選項卡后清理時釋放榄审。

  • 4.「生成 retainer traces」:遍歷堆并為每個泄漏的對象生成 retainer traces 。trace 顯示了泄漏對象為何以及如何在內(nèi)存中保持活動狀態(tài)馆衔。打破引用鏈意味著泄漏的對象將不再可以從 GC 的根訪問瘟判,因此可以進(jìn)行垃圾回收。通過一步步地跟蹤角溃,就可以找到應(yīng)該設(shè)置為 null 的引用拷获;

  • 5.「聚合 retainer traces」:將所有 retainer traces 聚集在一起,并為每個共享相似 retainer traces 的泄漏對象聚合顯示為一個跟蹤减细,其中還包括調(diào)試信息匆瓜,例如支配節(jié)點和保留大小。

  • 6.「報告泄漏」:定期運行 MemLab未蝌,以持續(xù)收集 retainer traces驮吱,任何新的 traces 都會記錄到內(nèi)部儀表板,開發(fā)者可以查看每個內(nèi)存泄漏的 retainer traces 上的對象屬性萧吠。

MemLab 有哪些能力

「內(nèi)存泄漏檢測」

對于瀏覽器內(nèi)存泄漏的檢測左冬,MemLab 需要開發(fā)者提供的唯一輸入就是一個測試場景文件,這個文件定義了如何通過使用 Puppeteer API 和 CSS 選擇器覆蓋三個回調(diào)來與網(wǎng)頁交互纸型。MemLab 會自動區(qū)分 JavaScript 堆拇砰、優(yōu)化內(nèi)存泄漏并聚合結(jié)果。


「JavaScript 堆的 Graph-view API」

MemLab 支持一個自定義的泄漏檢測器狰腌,作為篩選器回調(diào)除破,應(yīng)用于每個由目標(biāo)交互分配的泄漏候選對象,但之后從不釋放琼腔。泄漏過濾器回調(diào)函數(shù)可以遍歷堆并確定哪些對象是內(nèi)存泄漏瑰枫。例如,我們的內(nèi)置檢漏器會跟蹤 React Fiber 節(jié)點的返回鏈路丹莲,檢查 Fiber 節(jié)點是否與 React Fiber 樹分離光坝。


為了分析每個可能內(nèi)存泄漏的上下文,MemLab 提供了一個 JavaScript 堆的內(nèi)存效率圖甥材。這可以在不了解 V8 堆快照文件結(jié)構(gòu)的任何領(lǐng)域知識的情況下查詢和遍歷 JavaScript 堆盯另。

在視圖中,堆中的每個 JavaScript 對象或原生對象都是一個圖節(jié)點擂达,堆中的每個 JavaScript 引用都是一個圖的邊。實際應(yīng)用程序的堆大小通常很大,因此圖視圖需要在提供直觀的面向?qū)ο蠖驯闅v API 的同時提高內(nèi)存效率板鬓。因此悲敷,圖節(jié)點被設(shè)計成了虛擬的,不通過 JavaScript 引用進(jìn)行連接俭令。當(dāng)分析代碼遍歷堆時后德,虛擬圖會部分地即時構(gòu)建圖的接觸部分。圖的任何部分都可以很容易地釋放抄腔,因為這些虛擬節(jié)點彼此之間沒有 JavaScript 引用瓢湃。

堆視圖可以從基于 Chromium 的瀏覽器、Node.js赫蛇、Electron 和 Hermes 獲取的 JavaScript 堆快照加載绵患。這允許分析復(fù)雜的模式并回答諸如 “有多少 React Fiber 節(jié)點是備用的 Fiber 節(jié)點,它們用于不完整的并發(fā)渲染悟耘?”之類的問題落蝙。

import {getHeapFromFile} from '@memlab/heap-analysis';
const heapGraph = await getHeapFromFile(heapFile);
heapGraph.nodes.forEach(node => {
  // heap node traversal
  node.type
  node.references
);
「內(nèi)存斷言」

Node.js 程序或 Jest 測試也可以使用 graph-view API 來獲取其自身狀態(tài)的堆視圖,進(jìn)行自內(nèi)存檢查暂幼,并編寫各種內(nèi)存斷言筏勒。

import type {IHeapSnapshot} from '@memlab/core';
import {config, takeNodeMinimalHeap, tagObject} from '@memlab/core';

test('memory test', async () => {
  config.muteConsole = true;
  const o1 = {};
  let o2 = {};

  // tag o1 with marker: "memlab-mark-1", does not modify o1 in any way
  tagObject(o1, 'memlab-mark-1');
  // tag o2 with marker: "memlab-mark-2", does not modify o2 in any way
  tagObject(o2, 'memlab-mark-2');

  o2 = null;

  const heap: IHeapSnapshot = await takeNodeMinimalHeap();

  // expect object with marker "memlab-mark-1" exists
  expect(heap.hasObjectWithTag('memlab-mark-1')).toBe(true);

  // expect object with marker "memlab-mark-2" can be GCed
  expect(heap.hasObjectWithTag('memlab-mark-2')).toBe(false);

}, 30000);
「內(nèi)存工具箱」

除了內(nèi)存泄漏檢測,MemLab 還包括一組內(nèi)置的 CLI 命令和 API旺嬉,用于尋找可能的內(nèi)存優(yōu)化機(jī)會:



Meta 使用 MemLab 的實踐

在過去的幾年中管行,Meta 一直在使用 MemLab 檢測和診斷內(nèi)存泄漏,并收集了很多有助于優(yōu)化內(nèi)存邪媳、減少 OOM 崩潰并改善用戶體驗的手段捐顷。

在 2021 年上半年, Facebook.com 上的 OOM 崩潰減少了 50%悲酷。


「React Fiber 節(jié)點清理」

為了渲染組件套菜,React 構(gòu)建了 Fiber 樹 — 一個 React 用于渲染虛擬 DOM 的內(nèi)部數(shù)據(jù)結(jié)構(gòu)。雖然 Fiber 樹看起來像一棵樹设易,但它是一個雙向圖逗柴,將所有 Fiber 節(jié)點、React 組件實例和關(guān)聯(lián)的 HTML DOM 元素強(qiáng)連接起來顿肺。理想情況下戏溺,React 維護(hù)對組件 Fiber 樹的根的引用,并防止 Fiber 樹被垃圾回收屠尊。當(dāng)一個組件被卸載時旷祸,React 會斷開組件的根與 Fiber 樹的其余部分之間的連接,然后這些部分就可以被垃圾回收了讼昆。

擁有這樣的強(qiáng)連接圖的缺點是托享,如果有任何外部引用指向圖的任何部分,就無法對整個圖進(jìn)行垃圾回收。例如闰围,下面 export 語句在模塊范圍級別緩存 React 組件赃绊,因此相關(guān)的 Fiber 樹和分離的 DOM 元素永遠(yuǎn)不會被釋放。

export const Component = (( 
  <List> ... </List> 
): React.Element<typeof List>);

也不僅僅是 React 數(shù)據(jù)結(jié)構(gòu)要 keep alive 羡榴,Hooks 和它們的閉包也可以讓各種其他對象北滩椋活。這意味著單個 React 組件泄漏可能會導(dǎo)致頁面對象的重要部分泄漏校仑,從而導(dǎo)致巨大的內(nèi)存泄漏忠售。


為了防止 Fiber 樹中內(nèi)存泄漏的級聯(lián)效應(yīng),MemLab 添加了一個樹的完整遍歷迄沫,當(dāng)組件在 React 18 中卸載時會進(jìn)行清理稻扬。這可以讓垃圾回收器在清理未掛載的樹方面做得更好一點。這個優(yōu)化將 Facebook 上的平均內(nèi)存使用量減少了近 25%邢滑,其他使用 React 的站點在升級時也有了很大的改進(jìn)腐螟。你可能會擔(dān)心這種比較激進(jìn)的清理方式可能會減慢 React 組件的卸載速度,但令人驚訝的是困后,由于內(nèi)存的減少乐纸,性能也有顯著的提升。

「string interning」

通過利用 MemLab 中的 heap analysis API摇予,Meta 團(tuán)隊發(fā)現(xiàn)字符串占據(jù)了 70% 的堆內(nèi)存汽绢,其中一半的字符串至少有一個重復(fù)的實例。(V8 對 string interning 支持的不是很好侧戴,這是一種對具有相同值的字符串實例進(jìn)行重復(fù)數(shù)據(jù)刪除的優(yōu)化宁昭。)

另外很大一部分字符串內(nèi)存被 Relay 中緩存的鍵字符串消耗。通過與 Relay 和 React Apps 團(tuán)隊合作酗宋,可以在客戶端插入和縮短過長的字符串鍵來優(yōu)化 Relay 緩存鍵字符串积仗。

這種優(yōu)化使 Relay 能夠緩存更多數(shù)據(jù),允許站點向用戶顯示更多內(nèi)容蜕猫,尤其是在客戶端 RAM 有限的情況下寂曹。內(nèi)存 p99 和 OOM 崩潰減少了 20%,頁面渲染速度更快回右,用戶體驗得到改善隆圆,在收入上也有一定提升。

試用 MemLab:

npm i -g memlab

最后
MemLab Github:https://github.com/facebookincubator/memlab

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末翔烁,一起剝皮案震驚了整個濱河市渺氧,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蹬屹,老刑警劉巖侣背,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件白华,死亡現(xiàn)場離奇詭異,居然都是意外死亡贩耐,警方通過查閱死者的電腦和手機(jī)衬鱼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來憔杨,“玉大人,你說我怎么就攤上這事蒜胖∠穑” “怎么了?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵台谢,是天一觀的道長寻狂。 經(jīng)常有香客問我,道長朋沮,這世上最難降的妖魔是什么蛇券? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮樊拓,結(jié)果婚禮上纠亚,老公的妹妹穿的比我還像新娘。我一直安慰自己筋夏,他們只是感情好蒂胞,可當(dāng)我...
    茶點故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著条篷,像睡著了一般骗随。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上赴叹,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天鸿染,我揣著相機(jī)與錄音,去河邊找鬼乞巧。 笑死涨椒,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的摊欠。 我是一名探鬼主播丢烘,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼些椒!你這毒婦竟也來了播瞳?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤免糕,失蹤者是張志新(化名)和其女友劉穎赢乓,沒想到半個月后忧侧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡牌芋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年蚓炬,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片躺屁。...
    茶點故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡肯夏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出犀暑,到底是詐尸還是另有隱情驯击,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布耐亏,位于F島的核電站徊都,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏广辰。R本人自食惡果不足惜暇矫,卻給世界環(huán)境...
    茶點故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望择吊。 院中可真熱鬧李根,春花似錦、人聲如沸几睛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽枉长。三九已至冀续,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間必峰,已是汗流浹背洪唐。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留吼蚁,地道東北人凭需。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像肝匆,于是被迫代替她去往敵國和親粒蜈。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內(nèi)容