<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          騰訊文檔表格內(nèi)存優(yōu)化總結(jié)

          共 4830字,需瀏覽 10分鐘

           ·

          2021-06-14 14:50

          在騰訊文檔表格中,如果用戶打開表格的內(nèi)容非常多,比如有幾萬(wàn)行或者幾十萬(wàn)個(gè)單元格。內(nèi)存占用會(huì)居高不下,在使用的過(guò)程中非常容易出現(xiàn)崩潰,卡頓等問(wèn)題,在進(jìn)行一系列的優(yōu)化之后,寫下這篇文章,總結(jié)下期間用到的一些優(yōu)化方案,希望可以或多或少幫助或者啟發(fā)一下他人。

          怎么找出內(nèi)存問(wèn)題

          在內(nèi)存這里,我們一般是借助 Google 瀏覽器的開發(fā)者工具,然后獲取下頁(yè)面的內(nèi)存快照,然后分享具體占用過(guò)大內(nèi)存的對(duì)象。

          除了查看內(nèi)存占用過(guò)大的對(duì)象之外,也可以切換到 Containment 這里查看一些占用內(nèi)存過(guò)大的全局對(duì)象。 

          常見(jiàn)的內(nèi)存優(yōu)化方案

          按需加載

          按需加載可能是能夠想到的最簡(jiǎn)單的內(nèi)存優(yōu)化的方式,比如當(dāng)某一個(gè)功能沒(méi)有使用的時(shí)候,不加載這個(gè)功能模塊,也不進(jìn)行初始化。舉個(gè)例子,比如帳號(hào)管理模塊,在沒(méi)有點(diǎn)擊右上角頭像部分的時(shí)候,是不需要加載的,這個(gè)時(shí)候就可以改成按需加載。 

          在騰訊文檔表格的優(yōu)化中,部分改造為按需加載的模塊使用率:

          1. Account 組件改成按需加載 用戶使用率:0.8%

          2. QuickFill 組件改成按需加載 用戶使用率:1.2%

          優(yōu)化了這些模塊之后,內(nèi)存占用大概降低了 10M 左右,優(yōu)化效果并不明顯。老板說(shuō): 

          對(duì)象池

          對(duì)象池模式在初始化的時(shí)候會(huì)創(chuàng)建固定數(shù)量的緩存,并且每個(gè)對(duì)象都可以復(fù)用,對(duì)象沒(méi)有更新的需求。 

          對(duì)象池這種模式主要用于游戲或者數(shù)據(jù)庫(kù)連接池等場(chǎng)景。因?yàn)轵v訊文檔表格優(yōu)化的時(shí)候沒(méi)有使用到對(duì)象池的方式,這里就不詳細(xì)描述了,有需要的可以自行 Google。

          享元

          享元模式的特點(diǎn):

          • 屬性全部只讀或者私有化

          • 提供創(chuàng)建對(duì)象實(shí)例的工廠方法

          • 修改時(shí)重新創(chuàng)建對(duì)象實(shí)例

          • 使用 key 緩存對(duì)象,key 相同直接返回緩存對(duì)象實(shí)例

          舉個(gè)例子:假設(shè)目前有三個(gè)變量 ABC,引用的都是同一個(gè)內(nèi)存中的對(duì)象。 

          假設(shè)這個(gè)時(shí)候需要修改對(duì)象 A 的屬性,則需要使用新的屬性參數(shù),創(chuàng)建一個(gè)新的對(duì)象,講 A 指向新創(chuàng)建的對(duì)象,此時(shí)內(nèi)存當(dāng)中的對(duì)象應(yīng)該是這樣的: 

           這個(gè)時(shí)候,內(nèi)存當(dāng)中就有兩個(gè)對(duì)象了,BC 還是指向的之前的對(duì)象,但是這個(gè)時(shí)候的 A 已經(jīng)指向新創(chuàng)建的對(duì)象了。

          下圖是在騰訊文檔表格中,部分使用享元模式優(yōu)化之后的內(nèi)存對(duì)比,可以發(fā)現(xiàn)優(yōu)化效果還是比較明顯的。 

          享元遇到的問(wèn)題

          以為用享元就能解決問(wèn)題了? 

          直到看到這個(gè)圖: 

          用戶在操作設(shè)置數(shù)據(jù)格式之后,前端提交到數(shù)據(jù)后臺(tái),落盤失敗!

          在做享元優(yōu)化的時(shí)候,遇到一個(gè)特殊的問(wèn)題,這里也記錄下,方便大家后面使用享元的時(shí)候注意。我們有一個(gè)對(duì)象,用來(lái)描述單元格的值,以及值的類型。如下所示: 

          type 表示類型,value 就是存儲(chǔ)的值。

          type 的類型有如下幾種 

           這個(gè)對(duì)象的 key=type+,+value

           假設(shè)此時(shí)實(shí)用如下參數(shù)創(chuàng)建一個(gè)對(duì)象:

          {  type: 1,  value: 123,}

          此時(shí)生成的 key='1,123'

          很明顯,這個(gè)時(shí)候的 value 和 type 的類型對(duì)應(yīng)不上,但是由于我們沒(méi)有任何校驗(yàn),導(dǎo)致錯(cuò)誤的創(chuàng)建了一個(gè)對(duì)象,并且以 key='1,123' 被緩存起來(lái)了。

          當(dāng)下次創(chuàng)建的一個(gè)對(duì)象的時(shí)候,傳入如下的參數(shù):

          {  type: 1,  value: '123',}

          這里的參數(shù),跟上面的區(qū)別是 value 和 type 是對(duì)應(yīng)上的,但是注意,這個(gè)時(shí)候生存的 key='1,123'。嗯?發(fā)現(xiàn)這個(gè) key 在之前已經(jīng)創(chuàng)建過(guò)了,并且緩存了一個(gè)對(duì)象。這個(gè)時(shí)候,不會(huì)再去創(chuàng)建一個(gè)新的對(duì)象,而是將上面的對(duì)象直接返回給到使用方,然后報(bào)錯(cuò)了。

          這個(gè)情況在騰訊文檔表格中就出現(xiàn)過(guò),有的業(yè)務(wù)模塊創(chuàng)建了臨時(shí)的 ExtenedValue 對(duì)象,在傳參數(shù)的時(shí)候,類型和值對(duì)應(yīng)不上,導(dǎo)致數(shù)據(jù)層在提交數(shù)據(jù)的時(shí)候報(bào)錯(cuò)。

          那如何解決這個(gè)問(wèn)題呢? 在創(chuàng)建 key 的時(shí)候,獲取到 value 的真實(shí)類型,將真實(shí)類型作為 key 的一部分,這樣,上述的兩個(gè)參數(shù)創(chuàng)建的就會(huì)是兩個(gè)不同的對(duì)象了。

          進(jìn)階版內(nèi)存優(yōu)化方案

          對(duì)象存儲(chǔ)優(yōu)化

          一個(gè) demo

           上圖中的代碼,占用內(nèi)存 4.8M。 

           上圖中的代碼,占用內(nèi)存 3.6M。

           對(duì)比兩者的內(nèi)存快照發(fā)現(xiàn)相差的就是 1.2M。這是為什么呢?

          對(duì)象在 v8 中的存儲(chǔ)方式

           首先來(lái)看下對(duì)象在 v8 中的存儲(chǔ)方式,在 v8 中,js 的對(duì)象使用 JSObject 對(duì)象來(lái)描述。這個(gè)對(duì)象有下面幾個(gè)主要屬性:

          • HiddenClass 用來(lái)存儲(chǔ)對(duì)象屬性存儲(chǔ)的位置信息

          • properties 用來(lái)存儲(chǔ)具名屬性

          • elements 用來(lái)存儲(chǔ)有順序的屬性,類似數(shù)組 在這種情況下,存儲(chǔ)在 properties 中的屬性,都是快屬性模式(使用數(shù)組的形式存儲(chǔ))。

          那針對(duì)上面的問(wèn)題,猜測(cè)可能是因?yàn)?/p>

          this.a = null

          初始化屬性 a 的時(shí)候,HiddenClass 的結(jié)構(gòu)會(huì)變化,從而增加內(nèi)存。接著使用 v8 的調(diào)試工具 d8 來(lái)驗(yàn)證下: 

           從上面的 debug 信息中可以看出,初始化的時(shí)候,如果賦值為 null,會(huì)在 HiddenClass 的 DescriptorArray 中添加記錄,用來(lái)描述屬性 a 的初始值。 所以,在初始化屬性的時(shí)候,如果參數(shù)沒(méi)有傳遞該屬性的值,那就不要初始化為 null,因?yàn)檫@樣會(huì)占用內(nèi)存。

          優(yōu)化驗(yàn)證

          針對(duì)上述的情況,我們優(yōu)化了騰訊文檔表格的單元格對(duì)象 CellData 的初始化過(guò)程。 

           可以發(fā)現(xiàn),優(yōu)化的效果非常明顯。30w 單元格的表格,CellData 的內(nèi)存占用從 24M 減少到了 12M。

          優(yōu)化到到這里的時(shí)候,老板再問(wèn)我能達(dá)到預(yù)期目標(biāo)的時(shí)候: 

          清除 delete 操作

          一個(gè) demo

          有 delete 操作

           

          沒(méi)有 delete 操作

          從上面的圖中可以看出,沒(méi)有 delete 操作會(huì)比有 delete 操作的內(nèi)存占用少非常多。這又是為什么呢?

          v8 HiddenClass 的轉(zhuǎn)化過(guò)程

          v8 在初始化對(duì)象,給對(duì)象添加屬性的時(shí)候,會(huì)變更 hiddenClass 指針的指向,將其指向最新的節(jié)點(diǎn)。先看一個(gè) v8 官方網(wǎng)站的圖: 

           在這個(gè)圖中,當(dāng)我們給對(duì)象 o 添加屬性的時(shí)候,hiddenClass 的指針一直在變化,也就是說(shuō)每次給對(duì)象添加新的屬性,都會(huì)生成一個(gè)新的 HiddenClass 節(jié)點(diǎn),所有的 HiddenClass 節(jié)點(diǎn)組成了一個(gè)的鏈表(其實(shí)準(zhǔn)確的說(shuō)應(yīng)該是 tree ),然后對(duì)象中的 hiddenClass 指針指向最新的節(jié)點(diǎn)。

          上面說(shuō)的所有的 HiddenClass 節(jié)點(diǎn)其實(shí)組成了一棵樹,在 v8 里面叫做 transition tree。例如:假設(shè)這個(gè)時(shí)候,新創(chuàng)建一個(gè)對(duì)象,并且添加了一個(gè)新的屬性: 

           這樣看是不是就知道為什么是一棵樹了。

          那這些跟 delete 操作有什么關(guān)系呢?

          delete 操作導(dǎo)致 v8 的對(duì)象存儲(chǔ)模式退化為字典模式

          當(dāng)對(duì)屬性進(jìn)行 delete 操作的時(shí)候,會(huì)將上面所說(shuō)的鏈表結(jié)構(gòu)打破,并且對(duì)象在 v8 內(nèi)的存儲(chǔ)方式也會(huì)發(fā)生變化。變成如下所示的結(jié)構(gòu): 

           或者直接看 v8 官方網(wǎng)站提供的圖: 

          可以看到,如果你對(duì)一個(gè)對(duì)象的屬性進(jìn)行 delete 操作,就會(huì)導(dǎo)致對(duì)象的存儲(chǔ)方式退化到字典模式(慢屬性模式)。相對(duì)于之前的快屬性模式,這種存儲(chǔ)方式更加消耗內(nèi)存。所以這也是為什么 delete 操作會(huì)導(dǎo)致對(duì)象內(nèi)存占用增加的根本原因。

          驗(yàn)證:

          const obj = {    a:1,    b: 2,}%DebugPrint(obj);delete obj.a;%DebugPrint(obj);

           刪除屬性 a 之后,退化到字典模式: 

          再看一下這個(gè)圖,我們發(fā)現(xiàn)在 HiddenClass 節(jié)點(diǎn)也有一個(gè) back pointer ,指向上一個(gè)節(jié)點(diǎn)。 

          所以,如果你是按照對(duì)象添加屬性的反方向刪除屬性的話,對(duì)象并不會(huì)退化到慢屬性模式,或者對(duì)象的內(nèi)存占用并不會(huì)增加。這里更多詳細(xì)的描述可以參考 v8 的官方文檔或者 superzheng 大佬在知乎的專欄。

          如何替換掉 delete 操作?

          賦值給 undefined

          假設(shè)有如下代碼:

          const data = {  a: '1',  b: 'c',}// do something...delete data.c;

          如果沒(méi)有特殊的要求,建議直接修改為:

          const data = {  a: '1',  b: 'c',}// do something...data.c = undefined;

          使用如下代碼測(cè)試:

          const startTime = +new Date();  for (let i = 0; i < 1000000; i++) {    const a = {      a: '1',      b: '2',      c: '3',    };    // a.b = undefined;    delete a.b;  }  const endTime = +new Date();  console.log(endTime - startTime);

          賦值給 undefined 比直接 delete 刪除,要快幾十倍,甚至上百倍。

          使用 Map 替代

          假設(shè)你的對(duì)象必須就是要?jiǎng)h除,那么還可以使用 Map 來(lái)替代對(duì)象。

          const data = {  a: '1',  b: 'c',}// do something...delete data.c;

          可以修改為如下的形式:

          const data = new Map<string, string>();data.set('a', '1');data.set('b', 'c');// do something...// 使用 Map 對(duì)象提供的 delete 方法刪除元素data.delete('a');

          性能監(jiān)控(保護(hù)優(yōu)化成果)

          內(nèi)存監(jiān)控

          內(nèi)存優(yōu)化的手段固然重要,但是如果在優(yōu)化之后一段時(shí)間,隨著需求的增加,由于一些新代碼的引入,或者寫代碼時(shí)候不注意,就有可能導(dǎo)致內(nèi)存暴增。這個(gè)時(shí)候,就在想是否能夠在發(fā)布之前,就可以自動(dòng)化的檢測(cè)到騰訊文檔的特征表格的內(nèi)存占用,然后跟現(xiàn)網(wǎng)環(huán)境的內(nèi)存占用對(duì)比。如果發(fā)現(xiàn)內(nèi)存占用比現(xiàn)網(wǎng)的要高很多,則可以在發(fā)布階段終止發(fā)布,接著進(jìn)入代碼 review 階段,分析哪里導(dǎo)致的內(nèi)存占用增加。解決了問(wèn)題之后,再重新進(jìn)行發(fā)布流程。

          針對(duì)這個(gè)自動(dòng)化測(cè)試,騰訊文檔這邊目前也實(shí)現(xiàn)了一個(gè)可以自動(dòng)化獲取頁(yè)面內(nèi)存快照和大小的工具,可以接入到流水線當(dāng)中,作為一個(gè)性能檢測(cè)的方式集成到發(fā)布流水線當(dāng)中,保護(hù)內(nèi)存優(yōu)化成功。

          參考資料

          • https://flaviocopes.com/how-to-remove-object-property-javascript/
          • https://v8.dev/blog/fast-properties
          • https://zhuanlan.zhihu.com/p/28872382 https://v8.dev/docs/build

          內(nèi)推社群


          我組建了一個(gè)氛圍特別好的騰訊內(nèi)推社群,如果你對(duì)加入騰訊感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行面試相關(guān)的答疑、聊聊面試的故事、并且在你準(zhǔn)備好的時(shí)候隨時(shí)幫你內(nèi)推。下方加 winty 好友回復(fù)「面試」即可。


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

          手機(jī)掃一掃分享

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

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久婷婷网 | 欧美日韩国产VA在线观看免费 | 操逼在线视频 | 性感小骚逼 | 欧美日韩国产电影 |