1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        我對(duì)GitHub 8.3k Star項(xiàng)目貢獻(xiàn)了一次5倍性能提升的PR!

        共 7237字,需瀏覽 15分鐘

         ·

        2024-05-08 10:10


        ??目錄


        1 qs 庫(kù)簡(jiǎn)介

        2 優(yōu)化過(guò)程

        3 開(kāi)源貢獻(xiàn)

        4 總結(jié)




        本文是一次優(yōu)秀的反哺開(kāi)源社區(qū)貢獻(xiàn)實(shí)踐,騰訊工程師在日常工作中基于對(duì)開(kāi)源庫(kù) qs 的使用,發(fā)現(xiàn)了其在某些業(yè)務(wù)場(chǎng)景下存在的瓶頸問(wèn)題。通過(guò)對(duì)測(cè)試數(shù)據(jù)的復(fù)現(xiàn),debug 定位了真正的問(wèn)題原因,并基于對(duì)問(wèn)題的分析提出了一個(gè)穩(wěn)定提升 5 倍性能的調(diào)優(yōu)方案。

        本次調(diào)優(yōu)方案在他發(fā)起 pull request 后,僅耗時(shí) 34 小時(shí)便被開(kāi)源庫(kù)作者合入主線并發(fā)布新版本,成為截至目前唯一的性能優(yōu)化更新。他是怎么做到的,一起來(lái)看看吧!





        01



        qs 庫(kù)簡(jiǎn)介


        qs 是 JavaScript 領(lǐng)域最流行的解析和序列化 URL 查詢字符串開(kāi)源庫(kù)。


        GitHub 上依賴 qs 的代碼庫(kù)超過(guò)2760萬(wàn),npm 上每周下載量超過(guò)7000萬(wàn)。


        作者 Jordan Harband 自2014年以來(lái)一直是 TC39(JavaScript 標(biāo)準(zhǔn)委員會(huì))成員,并在18-21年擔(dān)任編輯。




        02



        優(yōu)化過(guò)程


           2.1 發(fā)現(xiàn)問(wèn)題


        • 實(shí)際業(yè)務(wù)場(chǎng)景中使用包含了 30M+ 的中文文本的數(shù)據(jù)在 Windows 上 node 進(jìn)程出現(xiàn)了 crash 的現(xiàn)象。

        • 好在該問(wèn)題有相關(guān)的測(cè)試數(shù)據(jù)可以在特定環(huán)境下穩(wěn)定復(fù)現(xiàn),debug 發(fā)現(xiàn)導(dǎo)致 crash 的原因是 JavaScript heap out of memory。


           2.2 定位問(wèn)題


        1. 解決 OOM 問(wèn)題的主要挑戰(zhàn)在于定位內(nèi)存泄漏的源頭。在龐大且復(fù)雜的項(xiàng)目中,追蹤內(nèi)存泄漏的線索尤其困難,但是一旦準(zhǔn)確地定位到位置,通常該問(wèn)題就解決了99%。

        2. 基于以往處理 Node 內(nèi)存泄漏的經(jīng)驗(yàn),問(wèn)題往往出現(xiàn)在某些變量的生命周期管理不當(dāng),導(dǎo)致它們持續(xù)占用了大量?jī)?nèi)存。因此,我最初嘗試使用 Chrome 的開(kāi)發(fā)者工具來(lái)對(duì) Node.js 進(jìn)程進(jìn)行內(nèi)存快照分析,以便發(fā)現(xiàn)是否有堆棧占用了異常的內(nèi)存量。遺憾的是,這次嘗試并未成功,未能發(fā)現(xiàn)任何某些變量?jī)?nèi)存占用特別大的情況。

        3. 既然能穩(wěn)定復(fù)現(xiàn)該 OOM 的環(huán)境,那么就直接在相關(guān)業(yè)務(wù)流程的關(guān)鍵點(diǎn)打內(nèi)存變化日志,最終通過(guò)日志逐步排查定位到內(nèi)存激增的地方是 qs.stringify 附近。

        4. qs 庫(kù)已經(jīng)持續(xù)十多年更新上百版本,所以一開(kāi)始是不太敢確認(rèn)一定是 qs 庫(kù)處理大數(shù)據(jù)性能有問(wèn)題,還是項(xiàng)目復(fù)雜的環(huán)境干擾到了。那就單獨(dú)起一個(gè)獨(dú)立干凈的測(cè)試 Demo,引入與項(xiàng)目相同的 qs 庫(kù),構(gòu)造同級(jí)數(shù)據(jù)量測(cè)試內(nèi)存和耗時(shí),發(fā)現(xiàn)確實(shí)是 qs 的問(wèn)題。

        5. 看到項(xiàng)目中 qs 不是最新版本,那么就升級(jí)下 qs 看看性能問(wèn)題是否已經(jīng)優(yōu)化了,可惜最新版本測(cè)試效果一樣。既然這樣,那就自己動(dòng)手豐衣足食,去尋找 qs 性能瓶頸并嘗試優(yōu)化。



           2.3 原因分析


        • 直接下載 qs 庫(kù)源代碼進(jìn)行 debug,在關(guān)鍵路徑上輸出耗時(shí)和內(nèi)存。

          最終發(fā)現(xiàn) encode 函數(shù)性能不佳,encode qs 庫(kù)核心功能的底層函數(shù)。

          把這個(gè)函數(shù)單獨(dú)拿出來(lái)簡(jiǎn)化后使用 30M 中文測(cè)試耗時(shí)8092ms,內(nèi)存2.369G。

        • 以下代碼是encode函數(shù)的性能不佳的部分,分析可知:

          當(dāng) string 的長(zhǎng)度為 30M,那么 for 循環(huán)需要遍歷30 ?1024 ?1024,超過(guò)3000萬(wàn)次。

          JavaScript 中,字符串是不可變的,每次字符串拼接操作都會(huì)創(chuàng)建一個(gè)新的字符串,這會(huì)導(dǎo)致大量的內(nèi)存分配和垃圾回收,從而增加內(nèi)存占用和處理時(shí)間。



           2.4 解決思路


        1、通過(guò)以上分析,優(yōu)化一個(gè)方向在于通過(guò)減少字符串拼接的次數(shù)而減少臨時(shí)變量的產(chǎn)生以降低內(nèi)存的消耗。

        2、如何減少字符串的拼接呢?考慮換一種數(shù)據(jù)結(jié)構(gòu)來(lái)存放這些被 encode 后的字符,最終再把這些字符一次性轉(zhuǎn)成字符串。

        3、首先嘗試把 encode 后的字符放在一個(gè)數(shù)組中,這樣就不會(huì)產(chǎn)生臨時(shí)的字符串變量了,等 string 的每個(gè)字符都處理完成,再把數(shù)組轉(zhuǎn)成最終結(jié)果的字符串。


        // 簡(jiǎn)化代碼示意var out = [];out.push(c);out.join('');


        然而,經(jīng)過(guò)測(cè)試發(fā)現(xiàn)該方法:

        • 字符放入數(shù)組,耗時(shí)5168ms,內(nèi)存1.928G。

        • 數(shù)組轉(zhuǎn)字符串,總耗時(shí)7040ms,內(nèi)存3.535G。

        耗時(shí)略降,內(nèi)存暴增,負(fù)優(yōu)化!初步探索失敗告終!

        4、直接把字符串改數(shù)組來(lái)存放臨時(shí)變量,雖然失敗了,但是會(huì)發(fā)現(xiàn),改成數(shù)組存放,時(shí)和內(nèi)存確實(shí)有所減少,只是大數(shù)組轉(zhuǎn)字符串這一步又大幅消耗了內(nèi)存。

        5、那么是不是可以嘗試分片:限制一定數(shù)量的字符放入到數(shù)組中,然后把數(shù)組轉(zhuǎn)成字符串,再把這些片段字符串拼接成最終的結(jié)果,這樣可以減少字符串拼接過(guò)程產(chǎn)生的臨時(shí)變量,也會(huì)控制數(shù)組的大小和生命周期,避免內(nèi)存占用過(guò)高。


           2.5 方案優(yōu)化


        1、首先把string進(jìn)行分片,每片 string 遍歷進(jìn)行 encode,encode 后的字符放入到 array 中存儲(chǔ),當(dāng)一片 string encode 完成后,把 array 轉(zhuǎn)字符串拼接到最終結(jié)果中去,這樣這個(gè)臨時(shí)存儲(chǔ)的 array 就可以及時(shí)釋放掉。

        // 簡(jiǎn)化代碼示意var limit = 1024;var out = ''for (var i = 0; i < string.length; i += limit) {  var segment = string.slice(i, i + limit);  var arr = [];  for (var j = 0; j < segment.length; j++) {    var c = segment.charCodeAt(j);    arr.push(c)  }  out += arr.join('');}


        2、根據(jù)以上方案進(jìn)行多項(xiàng)測(cè)試,最終對(duì)比之后發(fā)現(xiàn)分片大小為1024時(shí)性能最好。30M 的數(shù)據(jù)測(cè)試,耗時(shí)1701ms,內(nèi)存459M,性能提升約5倍!

        3、為什么分片是1024呢?qs 的作者也問(wèn)了這個(gè)問(wèn)題。如果分片太小,那么字符串拼接的次數(shù)還是很多,效果不明顯。如果分片太大,臨時(shí)數(shù)組本身占用內(nèi)存不能及時(shí)釋放掉,并且大的數(shù)組轉(zhuǎn)字符串性能也不佳。1024是考慮到減少字符串拼接次數(shù)和能讓臨時(shí)數(shù)組及時(shí)釋放掉之間的平衡,綜合測(cè)試得到的最好結(jié)果。




        03



        開(kāi)源貢獻(xiàn)


           3.1 歷程(僅 34 hours)


        本以為給開(kāi)源庫(kù)提交代碼到進(jìn)入正式的版本會(huì)經(jīng)過(guò)較長(zhǎng)周期,但是本次貢獻(xiàn)在和作者15個(gè)小時(shí)時(shí)差的情況下,從 GitHub 上發(fā)起 pull request 到 npm 新版本發(fā)布全程僅34小時(shí)!尤其是代碼合入主線后一小時(shí)內(nèi)就發(fā)布了新版本!

        • 04.11 20:53 提交 pull request。

        • 04.11 22:49 作者第一次 review;

          review & fix 耗時(shí)15小時(shí)。

        • 04.12 13:39 作者 approved;

          自動(dòng)化測(cè)試耗時(shí)13小時(shí),approved 后需要321項(xiàng)測(cè)試 checks passed 才能合入主線。

        • 04.13 05:36 代碼合入主線。

        • 04.13 06:24 npm 上 qs 新版發(fā)布。


        看 qs 歷史發(fā)布記錄一個(gè)新版需要幾個(gè)月時(shí)間,如果是這樣那在業(yè)務(wù)中還需要自己先單獨(dú)維護(hù)一個(gè)包非常麻煩,好在作者的支持非常及時(shí)高效。


           3.2 結(jié)果


        • 【前無(wú)古人】截至目前唯一的性能優(yōu)化更新 :

          成為 qs 庫(kù) GitHub 上的 contributors 之一。

          通過(guò)變更日志 CHANGELOG.md 文件查詢可知截止目前是 qs 庫(kù)唯一的性能優(yōu)化更新。



        • 【數(shù)據(jù)對(duì)比】qs 庫(kù)處理大數(shù)據(jù)性能提升約5倍:

          qs 庫(kù)版本升級(jí)前后 30M 中文測(cè)試耗時(shí)和內(nèi)存對(duì)比。



        // 測(cè)試腳本const qs = require('qs');const string = '好'.repeat(30 * 1024 * 1204);const start = Date.now();qs.stringify({ string });console.log(`cost: ${ Date.now() - start }ms, ${JSON.stringify(process.memoryUsage())}`);
        // 6.12.0版本測(cè)試結(jié)果cost: 7855ms, {"rss":2544050176,"heapTotal":2508939264,"heapUsed":2447279864,"external":231929,"arrayBuffers":18614}
        // 6.12.1版本測(cè)試結(jié)果cost: 2090ms, {"rss":482816000,"heapTotal":461070336,"heapUsed":421214168,"external":231929,"arrayBuffers":18614}




        04



        總結(jié)


        • 本次性能優(yōu)化雖然修改量不大范圍可控,但是收獲頗豐,性能提升約5倍

        • qs 庫(kù)在十多年的歷史中已持續(xù)不斷更新了數(shù)百個(gè)版本,在社區(qū)有著廣泛的影響力,但直到今天依然可以從實(shí)際業(yè)務(wù)中出發(fā),發(fā)現(xiàn)性能瓶頸并優(yōu)化改進(jìn)。日常開(kāi)發(fā)中如果發(fā)現(xiàn)開(kāi)源庫(kù)有哪些有待改進(jìn)的地方,可以積極參與,不僅解決實(shí)際業(yè)務(wù)問(wèn)題還可以反哺開(kāi)源社區(qū)。

        • 本次優(yōu)化和版本發(fā)布與作者溝通過(guò)程中非常高效,并得到其積極支持,review 時(shí)其給出改進(jìn)意見(jiàn),深受啟發(fā)受益匪淺。


        -End-
        原創(chuàng)作者 | 李鑫


        往期推薦


        js如何控制一次只加載一張圖片,加載完成后再加載下一張
        面試官:假如有幾十個(gè)請(qǐng)求,如何去控制并發(fā)?
        前端權(quán)限開(kāi)發(fā)——設(shè)計(jì)到實(shí)踐(保姆級(jí))

        最后


        • 歡迎加我微信,拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...

        • 歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個(gè)專業(yè)的技術(shù)人...

        點(diǎn)個(gè)在看支持我吧

        瀏覽 55
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            欧美猛少妇色xxxx久久久久 | 操逼www | 色色123| 波多野结衣操逼 | 红桃视频国产在线 | 欧美黑人超粗男潮 | 成人性爱视频网站 | 日韩精品成人一区二区三区免费 | 把美女操得不要不要的视频 | 三级女的在洗澡三级 |