運用績效感知功能,讓效能面板加快 400%

Andrés Olivares
Andrés Olivares
Nancy Li
Nancy Li

無論您開發的應用程式類型為何,最佳化效能並確保應用程式能快速載入,並提供流暢的互動,對使用者體驗和應用程式成功都很重要。達成上述目標的一種方法是使用剖析工具檢查應用程式活動,瞭解應用程式在某段時間執行期間的實際運作情形。開發人員工具中的「Performance」(效能) 面板是絕佳的分析工具,可用來分析網頁應用程式的效能,並進行最佳化調整。如果應用程式是在 Chrome 中執行,系統會以視覺化的方式,呈現瀏覽器在執行應用程式時正在執行的活動。瞭解這類活動有助於找出模式、瓶頸和效能熱點,並依此採取行動來改善效能。

下例示範如何使用「Performance」面板。

設定並重新建立剖析情境

最近我們的目標是提高「效能」面板的效能。具體來說,我們希望它能更快載入大量成效資料。舉例來說,當剖析長時間執行或複雜程序時,或擷取高精細程度資料時,就會發生這種情況。為了達到這個目的,您需要瞭解應用程式的效能,以及一開始需要使用該方法的原因,這會是剖析工具達成的目標。

如您所知,開發人員工具本身是網頁應用程式。因此,您可以使用「Performance」面板進行剖析。如要剖析這個面板本身,請開啟開發人員工具,然後開啟附加的另一個開發人員工具執行個體。在 Google,這項設定稱為「開發人員工具-on-DevTools」DevTools-on-DevTools

設定完成後,必須重新建立並記錄要剖析的情境。為避免混淆,原本的開發人員工具視窗會稱為「第一個」開發人員工具視窗,而檢查第一個執行個體的視窗會稱為「第二個」開發人員工具執行個體。

開發人員工具執行個體檢查開發人員工具中的元素的螢幕截圖。
開發人員工具:使用開發人員工具檢查開發人員工具。

在第二個開發人員工具執行個體中,從這裡開始稱為 perf 面板的「Performance」面板,將觀察第一個重建情境的開發人員工具執行個體,進而載入設定檔。

第二個開發人員工具執行個體開始錄製,在第一個例項中,設定檔則會從磁碟上的檔案載入。系統會載入大型檔案,以便準確分析大型輸入內容的處理效能。兩個執行個體完成載入後,效能分析資料 (通常稱為「追蹤記錄」) 會顯示在載入設定檔的第二個開發人員工具執行個體中。

初始狀態:找出改進空間

載入完成後,第二個 Perf 面板例項會出現以下內容,如下圖所示。聚焦在主執行緒的活動,會顯示在標示為「Main」的測試群組下方。可以看出火焰圖中有五個大型活動群組。包括載入所需時間最多的工作。這些工作的總時間為約 10 秒。在下方的螢幕截圖中,成效面板用來將焦點放在每個活動群組,以便找出可查看的內容。

開發人員工具中的效能面板螢幕截圖,在另一個開發人員工具執行個體的效能面板中檢查效能追蹤記錄的載入情況。設定檔載入時間大約需要 10 秒。此時間大多分散在五大活動群組中。

第一個活動群組:不必要的工作

第一組活動似乎是仍在執行,但其實並不需要的舊程式碼。基本上,綠色區塊下所有標示為 processThreadEvents 的項目都浪費了心力。這個成績真快!移除該函式呼叫可省下約 1.5 秒的時間。太棒了!

第二個活動群組

在第二個活動群組中,解決方案並不像第一個活動那麼簡單。buildProfileCalls 花費了 0.5 秒,但工作並非可避免的工作。

開發人員工具中的效能面板檢查另一個效能面板執行個體的畫面。與 buildProfileCalls 函式相關的工作大約需要 0.5 秒。

出於好奇,我們啟用了效能面板中的「Memory」選項以進一步調查,並且發現 buildProfileCalls 活動也使用大量記憶體。您可以在這裡看到在執行 buildProfileCalls 時,藍線圖形突然跳動的情形,這表示潛在的記憶體流失。

開發人員工具中的記憶體分析器螢幕截圖,評估效能面板的記憶體用量。檢查器建議 buildProfileCalls 函式負責發生記憶體流失。

為了追蹤這種懷疑,我們使用「Memory」面板 (開發人員工具中的另一個面板,與「效能」面板中的「記憶體導覽匣」不同) 進行調查。在「記憶體」面板中,查看「配置取樣」已選取剖析類型,並記錄載入 CPU 設定檔效能面板的堆積快照。

記憶體分析器的初始狀態的螢幕截圖。「分配取樣」這個選項會以紅色方塊醒目顯示,代表此選項最適合用於 JavaScript 記憶體剖析。

以下螢幕截圖顯示收集的堆積快照。

記憶體分析器的螢幕截圖,已選取,其中含有大量記憶體的集合作業。

從這張堆積快照中,我們發現 Set 類別耗用了大量記憶體。透過檢查呼叫點,我們發現我們對 Set 類型的屬性進行了不必要的指派,也就是大量建立的物件。成本增加了,且使用了大量的記憶體,因此應用程式在大量輸入時異常終止。

組合有助於儲存不重複項目,且可提供具有內容獨特性的作業,例如複製資料集和提高查詢效率。但由於儲存的資料保證不會重複,所以不需要這些功能。因此一開始並不需要準備組合。為了改善記憶體配置,屬性類型已從 Set 變更為純陣列。套用這項變更後,系統又拍攝了一個堆積快照,並觀察到記憶體配置減少。進行這項變革後,速度並沒有大幅提升,其好處在於應用程式當機的頻率降低。

記憶體分析器的螢幕截圖。已將先前耗用大量記憶體的 Set 作業變更為使用純陣列,這大幅降低了記憶體成本。

第三個活動群組:權衡資料結構的取捨

第三個部分是關鍵:在火焰圖中,您可以看到它由狹長又高的欄組成,代表深層函式呼叫,在本例中為深度遞迴。這個區塊總共持續 1.4 秒。查看本節的底部後,我們發現這些資料欄的寬度是由單一函式的時間長度決定:appendEventAtLevel,因此這可能導致資料出現瓶頸

appendEventAtLevel 函式的實作中,有一件事浮現。對於輸入中的每個資料項目 (在程式碼中稱為「事件」),系統會將項目加入追蹤時間軸項目垂直位置的對應圖。這會造成問題,因為儲存的項目數量非常龐大。Google 地圖能夠提供快速的鍵查詢功能,但這類優勢並不容易。隨著地圖不斷擴增,在地圖中加入資料也會因為重置而變得昂貴。當您連續新增大量項目至地圖時,可察覺此成本。

/**
 * Adds an event to the flame chart data at a defined vertical level.
 */
function appendEventAtLevel (event, level) {
  // ...

  const index = data.length;
  data.push(event);
  this.indexForEventMap.set(event, index);

  // ...
}

我們實驗了另一種方法,可以不需要在火焰圖中為每個項目新增一個項目。這些改善措施的顯著性,證明該瓶頸確實與將所有資料新增至地圖時產生的負荷相關。活動群組耗費約 1.4 秒到 200 毫秒的時間。

變更前:

最佳化發生前的成效面板螢幕截圖,顯示最佳化至 addEventAtLevel 函式。函式執行的總時間為 1,372.51 毫秒。

變更後:

最佳化調整後的成效面板螢幕截圖。這個函式的總執行時間為 207.2 毫秒。

第四個活動群組:延遲非關鍵工作和快取資料,以防止重複作業

放大這個視窗,可以看到有兩個近似相同的函式呼叫區塊。透過查看呼叫的函式名稱,即可推論這些區塊是由建構樹狀結構的程式碼所構成 (例如名稱是 refreshTreebuildChildren)。事實上,相關程式碼是在面板底部導覽匣中建立樹狀檢視。值得一提的是,這些樹狀檢視在載入後並不會立即顯示。相反地,使用者需要選取樹狀檢視 (導覽匣中的「Bottom Up」、「Call Tree」和「Event Log」分頁),即可顯示樹狀圖。此外,從螢幕截圖中可看出,樹狀建構程序已執行兩次。

效能面板的螢幕截圖,顯示多個重複執行的工作,即使不需要的情況也是如此。您可以把這些工作延後到依需求執行,而不是事先。

下圖有兩個問題:

  1. 非關鍵工作會拖慢載入時間效能。使用者不一定需要輸出內容。因此,此工作在載入設定檔時並非必要。
  2. 系統未快取這些工作的結果。這就是為什麼樹分計算了兩次,但資料並未改變。

現在我們要將樹狀結構計算時間延後到使用者手動開啟樹狀檢視的時間。只有製作這些樹木需要付費。執行兩次的總時間約為 3.4 秒,因此延遲這會使得載入時間出現重大差異。我們仍在考慮快取這類工作。

第五個活動群組:請盡量避免使用複雜的呼叫階層

仔細觀察這個群組後,顯然會重複叫用特定呼叫鏈。相同的模式在火焰圖中曾在不同位置出現 6 次,這段期間的總時間長度約為 2.4 秒!

效能面板的螢幕截圖,顯示六個獨立的函式呼叫,用於產生相同的追蹤記錄迷你地圖,每個呼叫都含有深度呼叫堆疊。

多次呼叫相關程式碼,是處理資料並顯示在「小圖」上的部分(面板上方的時間軸活動總覽)。我們不清楚為何會屢次發生這個問題,但其實不必發生 6 次。事實上,如未載入其他設定檔,程式碼的輸出內容應會保持在最新狀態。理論上,程式碼只應執行一次。

經過調查後,我們發現載入管道中有多個部分直接或間接呼叫計算迷你地圖的函式,而呼叫了相關程式碼。這是因為程式的呼叫圖會隨時間不斷演變,而在不知情的情況下添加了更多對該程式碼的依附性。這個問題無法快速修正,解決方法取決於相關程式碼集的架構。在本範例中,我們必須稍微降低呼叫階層的複雜度,並新增檢查機制,避免在輸入資料維持不變的情況下執行程式碼。完成實作後,我們來看看時間軸的發展方向:

效能面板的螢幕截圖,顯示產生相同追蹤記錄小導覽的六個不同函式呼叫,僅減少為兩次。

請注意,小導覽圖算繪作業會執行兩次,而不是一次。這是因為每個設定檔都會繪製兩張迷你地圖:一個用於面板頂端的總覽,另一個則用於從記錄中選取目前可見的個人資料 (此選單中的每個項目都包含其選取之個人資料總覽)。不過,這兩種格式的內容都完全相同,因此應該可以用於另一個。

由於這些迷你地圖都是繪製在畫布上的圖片,因此使用 drawImage 畫佈公用程式,之後僅執行程式碼一次,可省下更多時間。因此,我們將群組持續時間從 2.4 秒縮短為 140 毫秒。

結論

套用所有修正項目 (以及這裡幾個其他小問題後),設定檔載入時間軸的變更如下:

變更前:

效能面板的螢幕截圖,顯示最佳化前的追蹤記錄載入作業。整個過程大約需要十秒鐘。

變更後:

效能面板的螢幕截圖,顯示最佳化後正在載入追蹤記錄。現在整個過程大約需要兩秒鐘。

改善措施後的載入時間為 2 秒,意味著提升約 80% 的成果相對輕鬆,因為大多數的事情都是由快速修正措施完成。當然,正確辨別一開始「應該做」的是關鍵,而效能樣本就是絕佳的利器。

另外也請務必強調,這些數據會因研究科目中的個人資料而異。這筆個人資料龐大,因此獲益良多。不過,由於每個設定檔的處理管道都是相同的,因此能大幅改善的改善項目將套用至效能面板內載入的每個設定檔。

重點整理

以下的課程有助於您從這些結果中解決應用程式的效能最佳化:

1. 使用剖析工具找出執行階段的效能模式

如要瞭解應用程式運作期間的情況,就特別適合使用剖析工具,尤其是找出能改善效能的機會。Chrome 開發人員工具的「效能」面板是網頁應用程式的理想選擇,因為這是瀏覽器的原生網頁剖析工具,而且我們會主動維護該面板,提供最新的網路平台功能。另外,速度也明顯變快了!😉

使用可當做代表性工作負載的範例,看看會發現什麼!

2. 避免複雜的呼叫階層

請盡量避免讓呼叫圖表過於複雜。使用複雜的呼叫階層時,很容易引入效能迴歸,且難以瞭解程式碼執行方式的原因,就更難以收回改善成效。

3. 找出不必要的工作

老舊程式碼集包含不再需要的程式碼。在我們案例中,舊和不必要的程式碼佔了總載入時間的大量時間。只是移除它,卻是最糟糕的水果。

4. 適當使用資料結構

運用資料結構來最佳化效能,但在決定要採用哪種資料結構時,也請一併瞭解每種資料結構帶來的成本與優缺點。這不僅僅是資料結構本身的空間複雜性,只要適用作業的時間也相當複雜,

5. 快取結果,避免複雜或重複的作業重複執行

如果作業的執行成本很高,就應該儲存結果,供下次需要時使用。另外,如果作業會多次執行,您也可以考慮這麼做,即使個別時間的設計成本不會特別高也無妨。

6. 延後非重要工作

如果工作執行時不需要立即用到輸出內容,而且工作執行作業正在擴充關鍵路徑,請考慮在實際需要輸出內容時,延後呼叫工作來延後工作。

7. 針對大型輸入內容使用高效率演算法

對大量輸入內容而言,最佳時間複雜度演算法至關重要。在這個示例中,我們並未研究這個類別,但可能不太重視這些類別的重要性。

8. 額外步驟:為管道執行基準測試

為了確保不斷快速演進的程式碼,您最好監控行為,並與標準比較。如此一來,您就能主動找出迴歸問題並改善整體可靠性,奠定長期成功基礎。