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

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

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

以下範例將逐步說明如何使用「Performance」面板。

設定及重建剖析情境

我們最近設定了目標,希望能提升成效面板的成效。特別是,我們希望能更快速地載入大量效能資料。舉例來說,當剖析長時間執行或複雜程序時,或擷取高精細程度資料時,就會發生這種情況。為達成這項目標,您必須先瞭解應用程式「如何」執行,以及「為何」以這種方式執行,這可以透過使用分析工具來達成。

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

設定完成後,您必須重新建立並記錄要剖析的情況。為避免混淆,我們將原始的 DevTools 視窗稱為「第一個」DevTools 例項,而檢查第一個例項的視窗則稱為「第二個」DevTools 例項。

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

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

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

初始狀態:找出改進空間

載入完成後,第二個 Perf 面板執行個體會出現以下內容:請專注於主要執行緒的活動,該活動會顯示在標示為「Main」的軌跡下方。從火焰圖中可以看到,活動分為五大類別。這些工作包括載入時間最長的任務。這些工作總共花費約 10 秒。在下列螢幕截圖中,我們使用成效面板聚焦於各個活動群組,看看能找到什麼。

螢幕截圖:開發人員工具中的效能面板,檢查另一個開發人員工具例項的效能追蹤畫面載入情形。設定檔載入時間大約需要 10 秒。這段時間主要分為五大活動群組。

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

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

第二個活動群組

在第二個活動群組中,解決方案不像第一個群組那麼簡單。buildProfileCalls 耗時約 0.5 秒,且無法避免這項工作。

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

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

開發人員工具中的記憶體分析器螢幕截圖,評估效能面板的記憶體用量。檢查器指出,buildProfileCalls 函式會造成記憶體耗損。

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

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

下圖顯示所收集的堆積積木快照。

記憶體分析器的螢幕截圖,已選取耗用大量記憶體的 Set 操作。

從這張堆積快照中,我們發現 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 毫秒。

變更前:

在對 appendEventAtLevel 函式進行最佳化前,成效面板的螢幕截圖。函式執行的總時間為 1,372.51 毫秒。

變更後:

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

第四個活動群組:延後非必要工作並快取資料,以免重複執行工作

放大這個視窗,您可以看到有兩個幾乎相同的函式呼叫區塊。查看呼叫的函式名稱,您可以推斷這些區塊包含建立樹狀結構的程式碼 (例如 refreshTreebuildChildren 等名稱)。事實上,相關程式碼就是在面板底部抽屜中建立樹狀檢視畫面的程式碼。有趣的是,這些樹狀檢視畫面並不會在載入後立即顯示。相反地,使用者必須選取樹狀檢視畫面 (抽屜中的「自下而上」、「呼叫樹狀圖」和「事件記錄」分頁),才能顯示樹狀圖。此外,如您從螢幕截圖中看到的,樹狀結構建構程序已執行兩次。

效能面板的螢幕截圖,顯示多項重複的工作,即使不需要執行,也仍會執行。這些工作可延後執行,以便視需要執行,而非提前執行。

我們發現這張相片有兩個問題:

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

現在我們要將樹狀結構計算時間延後到使用者手動開啟樹狀檢視的時間。只有在這種情況下,才值得花錢建立這些樹木。執行兩次的總時間約為 3.4 秒,因此延遲這會使得載入時間出現重大差異。我們也正在研究如何快取這類工作。

第五個活動群組:盡可能避免複雜的呼叫階層

仔細查看這個群組後,我們發現系統不斷呼叫特定呼叫鏈結。同樣的模式在火焰圖的不同位置出現了 6 次,這個時間窗口的總時間長度約為 2.4 秒!

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

多次呼叫的相關程式碼是處理要在「迷你地圖」(面板頂端的時間軸活動概覽) 上算繪的資料的部分。我們不清楚為何會發生多次,但肯定不是 6 次!事實上,如果沒有載入其他設定檔,程式碼的輸出內容應保持不變。理論上,程式碼應只執行一次。

經過調查後,我們發現系統會呼叫相關程式碼,因為載入管道中的多個部分會直接或間接呼叫用於計算迷你地圖的函式。這是因為程式的呼叫圖表複雜度會隨著時間演進,且使用者可能不知不覺地新增了更多此程式碼的依附元件。這個問題無法快速修正,解決方法取決於相關程式碼集的架構。在我們的案例中,我們必須稍微降低呼叫階層的複雜度,並新增檢查項目,以免在輸入資料未變更的情況下執行程式碼。實作這項功能後,我們得到了這個時間軸:

效能面板的螢幕截圖,顯示產生相同追蹤迷你地圖的六個個別函式呼叫,已減少至兩次。

請注意,小導覽圖算繪作業會執行兩次,而不是一次。這是因為每個設定檔都會繪製兩個迷你地圖:一個用於面板頂端的總覽,另一個用於下拉式選單,可從記錄中選取目前顯示的設定檔 (這個選單中的每個項目都包含所選設定檔的總覽)。不過,這兩個檔案的內容完全相同,因此可以重複使用。

由於這些迷你地圖都是在畫布上繪製的圖片,因此只要使用 drawImage canvas 公用程式,然後只執行一次程式碼即可,這樣就能節省一些時間。這項努力的結果是,群組的時間長度從 2.4 秒縮短為 140 毫秒。

結論

套用所有修正項目 (以及其他一些較小的修正項目) 後,設定檔載入時間軸的變化如下:

變更前:

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

變更後:

效能面板的螢幕截圖,顯示最佳化後的追蹤載入情形。這項程序現在大約需要兩秒鐘。

改善措施後的載入時間為 2 秒,意味著提升約 80% 的成果相對輕鬆,因為大多數的事情都是由快速修正措施完成。當然,正確辨識「要做什麼」是初期的重點,而效能面板就是這項工作的最佳工具。

請注意,這些數字僅適用於用於研究的個人資料。這個檔案格外龐大,因此我們對這項設定檔特別感興趣。不過,由於每個設定檔的處理管道都相同,因此這項重大改善會套用至效能面板中載入的每個設定檔。

重點整理

從這些結果中,我們可以學到一些關於應用程式效能最佳化的課題:

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

剖析工具非常適合用來瞭解應用程式執行期間發生的情形,尤其是找出可改善效能的機會。Chrome 開發人員工具中的「效能」面板是網路應用程式的絕佳選項,因為這是瀏覽器中的原生網路分析工具,且會積極維護,以便隨時更新最新的網路平台功能。此外,現在的速度也大幅提升!😉

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

2. 避免複雜的呼叫階層

盡量避免讓呼叫圖表過於複雜。使用複雜的呼叫階層很容易導致效能倒退,而且很難瞭解程式碼為何以這種方式執行,因此很難進行改善。

3. 找出不必要的工作

舊版程式碼庫通常會包含不再需要的程式碼。在我們的案例中,舊版和不必要的程式碼佔了總載入時間的大部分。移除這項功能是垂手可得的收益。

4. 妥善使用資料結構

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

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

如果作業的執行成本很高,就應該儲存結果,供下次需要時使用。如果作業執行多次,這麼做也相當合理,即使每次執行作業的成本不高也是如此。

6. 延後非必要工作

如果不需要立即取得工作輸出內容,且工作執行作業會延長關鍵路徑,建議您在實際需要輸出內容時,以延遲方式呼叫工作。

7. 在大量輸入內容上使用高效演算法

對於大量輸入內容,最佳的時間複雜度演算法就顯得至關重要。我們在這個範例中並未探討這個類別,但這類內容的重要性不容小覷。

8. 額外獎勵:管道基準

為確保不斷演進的程式碼仍能維持快速,建議您監控行為並與標準進行比較。這樣一來,您就能主動找出回歸情形並改善整體可靠性,為長期成功奠定基礎。