Categories
程式開發

騰訊基於 Proxy 的代碼執行監聽上報實踐


隨著移動設備的普及,兼容性和性能測試正在逐漸成為前端測試面臨的兩大挑戰,再加上前端代碼主要運行在客戶設備上的特性,各種安全性挑戰又層出不窮,我們該如何應對和解決?騰訊高級 Web 前端工程師樊東東在 GMTC 全球大前端技術大會(深圳站)2019 分享了 《基於 Proxy 的代碼執行監聽上報》,介紹了前端 SDK 基於 ES6 Proxy 的深度日誌收集以及騰訊小程序前端測試案例。

本文根據演講內容整理而成。

後端有全鏈路監控,能分析出模塊之間的調用情況,前端一般日誌打點、收集、上報,都是麵條式的日誌。在前端 SDK 的執行過程中能否收集更多更全面的信息?如 API 及回調函數的附屬關係,API 和 API 間的上下文關係等。

一、需求背景

1. 打日誌繁瑣

日誌是程序監控問題排查的重要手段。

如下是一段 SDK 實現和使用的例子:

騰訊基於 Proxy 的代碼執行監聽上報實踐 1

作為 SDK 提供方,想要監控 SDK 運行情況,需要在各個 API 實現中打日誌。

SDK 使用方使用 SDK 的時候需要在使用前後打日誌。

能否統一處理 SDK 調用日誌,讓代碼更少,日誌更規範?

2. 線性日誌可讀性差

如下是上一段代碼的調用日誌。

騰訊基於 Proxy 的代碼執行監聽上報實踐 2

這種日誌線性無關聯,只能體現時序。在排查問題時候分析這種麵條式日誌十分痛苦。

對於 API 的回調中有其他 API 調用這種 API 間依賴的關係,在日誌中很難體現。

能否將日誌進行可視化展示,且能體現日誌間的上下文。

騰訊基於 Proxy 的代碼執行監聽上報實踐 3

這種日誌能幫助我們在不知道源碼的情況下回溯 SDK 的調用過程,幫助重現和分析問題。

3. 屬性監聽

一般情況下,我們只對方法調用打日誌,但在 QQ 小程序中一些組件(如 Video,對應的客戶端組件)屬性變化對應著客戶端接口的調用。

這種對象屬性的變化也想要追踪。

4. 目標

總結下,想在不改變 SDK 代碼的情況下做到:

(1)統一上報入口;
(2)屬性 get、set 上報;
(3)日誌能體現上下文關係。

二、解決方案

defineProperty 和 Proxy 都能修改對象默認行為,進而進行監聽。

1.defineProperty

defineProperty 是修改對象屬性,如下是監聽對象方法調用的例子:

騰訊基於 Proxy 的代碼執行監聽上報實踐 4

defineProperty 有如下缺點:

(1)只能監聽已有屬性,無法追踪新增屬性;
(2)需要深層遍歷對象屬性監聽;
(3)原型的屬性需要單獨監聽。

實現起來複雜,開發者對 SDK 的擴展無法監聽到。

2.Proxy

(1)定義

Proxy 是 ES6 新增語法。在目標對象前架設一層攔截,可以對外界的所有訪問進行過濾和改寫。

(2) 功效

這是一段 Proxy 的例子:

騰訊基於 Proxy 的代碼執行監聽上報實踐 5

利用 Proxy 可以:

  • 監聽屬性 get、set;
  • 結合函數包裝監聽方法調用。

這樣能在不修改源碼的情況下做到:

  • 統一監聽入口;
  • SDK 使用者也能使用。

3. 問題

Proxy 是在原對象的基礎上創建新的 Proxy 對象。需要考慮下面兩個問題:

  • 能否替代原對象;
  • Proxy 性能。

(1) 原型比較
騰訊基於 Proxy 的代碼執行監聽上報實踐 6

這段代碼中有一個 Test 類,創建一個 Test 對象,對像上再創建 Proxy 對象。比較他們的原型和類型。

可以看到 Proxy 對象同原對象原型一致、類型一致。

(2) 屬性比較
騰訊基於 Proxy 的代碼執行監聽上報實踐 7

這段代碼 Test 類中有屬性 name,創建的對像新增屬性 age 和方法 api2,對像上再創建 Proxy 對象。比較他們的屬性。

可以看到 Proxy 對象和原對象屬性相同

(3)代理 DOM 對象
騰訊基於 Proxy 的代碼執行監聽上報實踐 8

在 Chrome 瀏覽器中,在 DOM 對像上創建 Proxy 對象,修改屬性、調用方法會報錯。

對於這種問題,可以用一個空對像做中轉,如下所示:

騰訊基於 Proxy 的代碼執行監聽上報實踐 9

  • 在空對像上創建 Proxy 對象;
  • 屬性 get 取原 DOM 對象屬性;
  • 屬性 set 設置原 DOM 對象屬性;
  • Proxy 對象的原型設為原 DOM 對象原型。

避免直接對 DOM 對象進行代理造成的使用報錯的問題。

(4)總結

Proxy 對象和原對像有同樣的原型、同樣的屬性,可以認為在使用上等同於原對象。

4. 性能

(1)運行性能

如下是原對象、Proxy 對象和 defineProperty 修改對象的屬性 get、set 和方法調用的耗時對比。

騰訊基於 Proxy 的代碼執行監聽上報實踐 10

從結果中看到,Proxy 對象的性能最低。算下來一次耗時不到 1 微秒。

不是高頻調用,性能影響可以忽略。

起先大家也認為 Promise 性能低,但因為好用,Promise 還是流行起來了。

(2)上報性能

在實踐過程中,對性能影響最大的是上報。比如小遊戲中繪圖 API 的高頻調用,極大影響上報性能。

需要控制上報頻率,方案如下:

  • 緩存一定數目再上報;
  • 黑名單內 API 不做上報;
  • 同一個 API 限制一定時間內上報次數;
  • 同一屬性 get、set,同值只報一次。

三、API 上下文

如下是一段 API 回調中調其他 API 的例子:

騰訊基於 Proxy 的代碼執行監聽上報實踐 11

想要在日誌中體現代碼上下文關係,需要關聯如下關係:

  • API 調用;
  • API 回調函數;
  • 回調參數(API 結果)使用;
  • 回調中其他 API 調用。

然後通過根 API 將整個上下文關聯起來。

下面我們來看下如何一步步對他們進行關聯。

1. 關聯 API

和其回調函數API 和回調函數都是函數,可以在 Proxy 的屬性訪問時對他們進行函數包裝。

對 API 進行包裝:

騰訊基於 Proxy 的代碼執行監聽上報實踐 12

API 的包裝函數運行時可獲得回調函數,對回調函數進行包裝,將 API 對應的 ID 傳遞過去。

騰訊基於 Proxy 的代碼執行監聽上報實踐 13

回調函數的包裝函數運行時就能知道其屬於哪個 API。

(1)踩到的坑

包裝函數碰到下面兩個問題:

  • 構造函數;
  • 事件監聽。

構造函數

對象的方法可能會被當做構造函數使用。要能在方法的包裝函數運行時知道其是被當做方法使用還是被當做構造函數使用。

如下圖:

騰訊基於 Proxy 的代碼執行監聽上報實踐 14

可以在包裝函數中判斷 this 的原型是否等同於包裝函數的原型。

事件監聽

取消事件監聽的回調函數必須和事件監聽的回調函數是同一個,才能取消成功。對函數進行包裝會返回新的函數,導致取消事件監聽無效。

可以對函數的包裝函數進行緩存。避免事件監聽與取消監聽函數不一致的情況。

騰訊基於 Proxy 的代碼執行監聽上報實踐 15

2. 關聯回調函數和參數

回調函數的包裝函數運行時生成對應 id,其參數如果是對象,則繼續進行代理。參數的 Proxy 對像在使用時就能知道其從屬於哪個回調函數。

騰訊基於 Proxy 的代碼執行監聽上報實踐 16

3. 關聯回調函數和其中 API

回調函數和 API 都是函數,關聯回調函數和其中 API 本質上是要在函數運行時知道其外層函數。

(1)calleecaller 如下圖所示:

騰訊基於 Proxy 的代碼執行監聽上報實踐 17

  • 利用 callee 可以知道當前執行函數;
  • 利用 caller 可以知道當前執行函數外層函數;
  • 這樣就能關聯回調函數和其中的 API。

但在嚴格語法中 callee、caller 被禁止。

(2)棧

可以利用入棧出棧來模擬函數執行開始、執行完畢。

騰訊基於 Proxy 的代碼執行監聽上報實踐 18

  • 回調函數的包裝函數運行時生成 ID;
  • 回調函數 ID 執行前入棧,執行後出棧;
  • 內層函數運行時可通過棧尾 ID 知道外層函數。

如此可關聯回調函數和其中的其他 API。

(3)setTimeout

setTimeout 將回調和其中其他 API 斷開來了。 setTimeout 中其他 API 可以認為是新開一個 trace。

如果想把 API 回調函數、回調函數中 setTimeout、setTimeout 回調函數、setTimeout 回調函數中其他 API 關聯起來,可以把 setTimeout 當做 API 來處理。

四、可視化展示

zipkin 是一個開源的分佈式鏈路追踪系統。可以用來做日誌關聯和可視化展示。

如圖是 zipkin 所需要的日誌數據結構:

騰訊基於 Proxy 的代碼執行監聽上報實踐 19

只要有 traceId、ID 和 parentId 三個字段就能做日誌關聯並展示。

如圖是前後端調用鏈路示意圖:

騰訊基於 Proxy 的代碼執行監聽上報實踐 20

後端的全鏈路是一個模塊調另一個模塊,前面的請求發送是後面請求接收的 parentId

前端的上下文是一個 API 的回調中有其他 API。 API 調用可以看做回調的 parentId,回調可以看做其中其他 API 的 parentId。

根 API 的調用可以看做 traceId

這樣就與 zipkin 所需要的數據結構對應起來了。

如圖是 SDK 調用代碼和對應的可視化界面

騰訊基於 Proxy 的代碼執行監聽上報實踐 21

可以看到 API、回調和可視化界面中的日誌一一對應。

這樣在沒有源碼的情況下能通過上下文日誌回溯調用過程,幫助復現和分析問題。

五、其他場景

Proxy 監聽 SDK 的方法調用,能收集方法輸入、輸出。可用於單元測試,比對代碼修改前後的日誌,驗證代碼修改是否影響其他 API。

收集外網用戶真實使用信息,補充測試案例。

統計 API 的調用次數。

六、總結

  1. Proxy 能追踪屬性變化和方法調用;
  2. 日誌能集中上報,SDK 使用者也能使用;
  3. 通過函數包裝關聯 API 和回調函數;
  4. 利用棧能關聯回調函數和回調函數中其他 API;
  5. 日誌從線性變為有關聯關係的上下文;
  6. 借助全鏈路系統能可視化展示上下文,回溯調用過程。

作者介紹

樊東東,騰訊高級Web 前端工程師,2014 年進入騰訊,先後經歷過移動辦公、泛娛樂產品、QQ 小程序的Web 開發工作,有大量運營平台、H5、Node 等相關的開發經驗;現負責QQ 小程序開發者工具、小程序相關工具鏈、QQ 相關基礎運營平台等開發工作。他善於在平凡的代碼中找到不一樣的玩法。

活動推薦

技術永不止步,精彩永不落幕,GMTC 全球大前端技術大會(北京站)2020 將於 6 月 05-08 日再次與大家見面,聚焦前沿技術及實踐經驗,強勢輸出更多大前端 & 移動開發領域的技術趨勢與實踐案例!