1. 程式人生 > >讓程式設計師少掉幾根頭髮的Facebook智慧bug修復神器

讓程式設計師少掉幾根頭髮的Facebook智慧bug修復神器

策劃編輯 | Natalie
作者 | JOHANNES BADER 等
譯者 | 核子可樂
編輯 | Vincent

AI 前線導讀:Facebook 開發了一款名為 Getafix 的工具,可以自動查找出 bug 的修復方案,並提供給工程師審批,這極大提高了工程師的工作效率和整體程式碼質量。Getafix 不僅能夠利用強大的聚類演算法,分析問題程式碼的上下文找到更合適的修復方案,而且給出的方案對於人類工程師來說很容易理解。Getafix 是第一款被大規模部署到 Facebook 生產環境中的自動修復工具,它進一步提升了 Facebook 擁有數十億使用者的應用程式的穩定性和效能。

更多幹貨內容請關注微信公眾號“AI 前線”,(ID:ai-front)

現代的生產環境程式碼庫非常複雜,並且一直持續不斷地更新。為了建立一個可以自動查詢 bug 修復方案的系統——在沒有工程師幫助的情況下——我們構建了一個工具,可以從工程師之前對程式碼庫的更改中學習如何修復 bug。它找到了一些隱藏的模式,並用這些模式來識別最有可能修復新 bug 的補救措施。

這個工具叫作 Getafix,已經被部署到 Facebook 的生產環境中,進一步提升數十億人使用的應用程式的穩定性。Getafix 一般與 Facebook 其他兩個工具結合使用,不過這項技術也可以用於其他地方。它目前能夠為 Infer 發現的 bug 提供修復建議,Infer 是我們的靜態分析工具,可識別 Android 和 Java 程式碼中的 null 指標異常等問題。它還通過 SapFix 提供修復建議——針對我們的智慧自動化測試系統 Sapienz 檢測到的 bug。現在,我們將深入瞭解 Getafix 是如何學習修復 bug(指 任意程式碼問題,而不僅僅是導致應用程式崩潰的問題)的。

Getafix 的目標是讓計算機處理日常工作,不過是在人類的監督之下,因為一個 bug 是否需要複雜的修復仍然需要由人類做出決定。這個工具將一種新的層次聚類方法應用於之前的數千個程式碼變更上,同時檢查程式碼變更本身及其上下文。它可以檢測 bug 的基礎模式,並提供之前的自動修復工具無法檢測到的修復方案。

Getafix 還能夠在 bug 修復過程當中,顯著縮小程式當中可能需要更改的具體空間,從而更快地選擇適當的修復手段 ; 此外,其不再像以往暴力破解及基於邏輯型技術那樣對計算時間提出極高的要求。這種更為高效的方法使得 Getafix 被成功部署至生產環境當中。與此同時,由於 Getafix 能夠以以往程式碼變化為基礎進行學習,因此足以產生讓人類工程師更容易理解的修復結論。

Getafix 目前已經在 Facebook 生產環境中部署完成,負責自動對 Infer 報告提供 null 解引用 bug 進行修復,同時亦可為 Sapienz 標記的與 null 解引用相關的崩潰錯誤提供修復建議。此外,Getafix 還被用於解決在較新版本 Infer 重新訪問現有程式碼時所發現的程式碼質量問題。

Getafix 與傳統簡單自動修復工具有何不同

在目前的行業實踐當中,自動修復功能主要用於各類基礎性問題,而程式碼修復則更為簡單。舉例來說,分析器可能會提出“致命異常”警告,強調開發人員可能忘記在新的 Exception(…) 之前新增一個 throw。自動修復工具能夠直接完成調整,而具體調整方式則可通過 lint 規則進行定義——換言之,其並不需要了解操作應用的特定情景。

Getafix 則完全不同,它提供更多通用性功能,並可結合上下文相關因素來解決問題。在以下程式碼示例當中,對應第 22 行中的 Infer 錯誤,Getafix 給出了下列修復結論:

需要注意的是,此修復方法不僅取決於變數 ctx,同時也與方法的返回型別相關。與簡單的 lint 修復方法不同,此類修復程式無法被納入 Infer 本身。

下圖所示為 Getafix 為 Infer bug 提供的修復方法 ; 儘管來自 Infer 的 bug 總是相同的(null 方法呼叫,有可能引發 NullPointerException 風險),但每一項具體修復操作仍然獨一無二。另外需要強調一點,Getafix 的修復方法與人類開發者的常見操作完全一致。

深入瞭解 Getafix 關鍵技術細節

Getafix 的組織形式如下圖內工具鏈所示。在本節中,我們將描述 Getafix 的三大主要元件及其各自的功能與挑戰。

Tree Differencer 標識樹級別的更改

基於抽象語法樹的 Differencer 首先負責在兩個原始檔之間識別實際的編輯痕跡,例如針對同一檔案的連續修訂。舉例來說,它會檢測以下粒度的編輯:使用 if 打包語句、新增的 @Nullableannotation 或者 import,以及將條件提前返回至某一現有方法之內等等。在以下示例中,插入條件判斷語句 if dog is null 並提前返回、將 public 重新命名為 private、方法的移動都會被檢測為實際編輯。而基於行的 diffing 工具只會將方法標記為完全移除與插入,Tree Differencer 則能夠檢測到這一移動並將移動方法之內的插入操作視為實際編輯。

Tree Differencer 的主要挑戰在於如何有效且精確地對樹級別中的“之前”與“之後”部分進行對齊,從而識別出正確的實際編輯及其對映關係。

新的修復模式挖掘方法

Getafix 通過利用新的層次聚類技術以及反合一方法(即一種能夠在不同符號表達式之間實現泛化的現有方法)進行模式挖掘。在此之後,它會建立可能相關的樹差異集合,進而選擇該集合中最為常見的程式並轉換為修復模式。這些模式可能是抽象的,且包含程式轉換所面向的不同“漏洞”。

以下示例影象展示了一組層次結構,即樹狀圖,其通過一組編輯生成。(在本示例中,我們直接採用上個示例中的編輯結果。)每一行皆展示出一種編輯模式——其中紫色代表“之前”,藍色代表“之後”——以及一些元資料。每個垂直黑條對應於層次結構中的具體層級,其中黑條頂部的編輯模式代表著通過對該結構中所有同一層級的其它編輯進行反合一所獲得的模式。其它編輯由較細的黑色線條連線。反合一將來自上一示例中的“如果 dog 為 null 則提前返回”條件與另一條編輯相結合——後者的唯一區別在於“dog 正在飲水”。結果是,其將生成一個代表共性的抽象修復模式。由反合一引入的符號 h0 代表著可以基於上下文實現例項化的“漏洞”。

接下來,該編輯模式可以與其它變數名稱更為多樣但仍然具有相同整體結構的編輯模式相結合。在根據梳理樹狀脈絡時,整個流程將產生越來越抽象的編輯模式。舉例來說,其能夠將此編輯與同貓相關的編輯組合在一起,從而獲得位於圖表上方位置的抽象編輯。

更值得強調的是,這種分層匹配流程為 Getafix 提供一套強大的框架,足以在程式碼變更中發現各類可複用模式。以下圖片所示,將總計 2288 項用於修復我們程式碼庫內 Infer 報告 null 指標錯誤的編輯彙總為一套樹狀圖(橫向佈局,小型化)。我們希望挖掘的修復模式,無疑正隱藏在這份樹狀圖內。

基於反合一方法的模式挖掘並非什麼新鮮事物,但要想以儘可能少的修復操作解決新 bug,我們還需要對挖掘得出的模式結果做進一步強化。

其中的變化之一就是引入一部分周邊程式碼,即編輯結果當中沒有變更的部分。如此一來,我們不僅能夠發現人們在變更中採取的模式,同時也能發現應用變更時上下文中存在的某些模式。舉例來說,在上面的第一份樹狀圖中,我們注意到有兩項不同的編輯會在 dog.drink(…); 之前新增 if(dog==null)return。儘管 dog.drink(…); 沒有變更,但其應被作為模式“之前”與“之後”部分的上下文資訊進行考量,從而幫助我們理解這項修復的應用情景。從更高的編輯層級上考慮,dog.drink() 這一上下文與其它上下文合併成為了抽象的上下文 h0.h1(),用以限制模式的適用位置。在下一節中,我們將介紹另一個更具現實意義的示例。

根據以往的自動修復工具文獻所述,貪婪聚類演算法往往不太可能學習到上述情況。這是因為貪婪聚類演算法傾向於維持各個聚類的單一表示,因此如果上下文不存在於訓練資料的全部編輯當中,則該演算法將不會引入該上下文。例如,如果某項編輯會在 do(list.get()); 與以上示例中提到的 dog.drink() 合併時插入 if (list != null) return,那麼貪婪聚類演算法會丟棄全部關於提前返回具體插入位置的上下文。與此相反,Getafix 的分層聚類方法則儘可能在各層級上保留上下文,從而確保整體結構的通用性水平。在某種程度上講,雖然我們希望學習的某些常規上下文可能丟失,但其仍將存在於結構當中的某些底層位置。

除了周邊程式碼之外,我們還將編輯與提示這些編輯的 Infer bug 報告關聯起來,從而瞭解編輯模式與對應的 bug 報告之間的對映關係。在前文第一份樹狀圖中,可以看到 Infer 在 bug 報告中將“errorVar”視為 bug 來源變數,並在進行反合一之後給出漏洞 h0。以此為基礎,我們接下來即可在釋出新的 Infer bug 報告時將需要關注的變數修改為 h0,從而使得整個修復模式更為具體。

Getafix 如何建立補丁

最後一步,我們需要考慮如何獲取存在 bug 的原始碼並從挖掘到的結論中生成修復模式,從而針對原始碼生成修復補丁。在這方面,我們往往擁有多種修復模式可以選擇(如前文樹狀圖所示)。因此,接下來的挑戰就是如何選擇正確的模式以修復特定 bug。如果該模式適用於多個位置,Getafix 還需要選擇出正確的匹配專案。以下示例說明了我們採用的常規方法以及如何在 Getafix 當中切實解決這項挑戰。

示例 1:考慮我們之前挖掘到的模式: h0.h1(); → if (h0 == null) return; h0.h1();

下面,我們將簡要介紹如何為完全陌生的程式碼生成以下補丁。

Getafix 通過以下步驟建立補丁:

找到與“之前”部分匹配的 sub-AST: mListView.clearListeners();

對漏洞 h0 與 h1 進行例項化

利用例項化之後的部分替換 sub-AST

請注意,之後部分中的 h0 是繫結的,因為其中包含了未修改的上下文 h0.h1();,這將有助於限制模式適用的位置數量。如果不修改上下文,則該模式將為→ if (h0 == null) return;。很明顯,這種模式將適用於眾多與預期無關的位置,例如 mListView.clearListeners(); 之後、甚至是 mListView = null; 之後。

實際上,僅插入模式也有可能出現在樹狀圖中的某些較高位置,其中具有 h0.h1(); 這一上下文的模式已經通過負責向另一不同語句之前插入 return 的模式完成了反合一。以下示例說明了 Getafix 如何處理這類模式適用範圍過廣的情況。

示例 2:請考慮以下模式: h0.h1() → h0!=null && h0.h1()

通常情況下,此補丁應該來自對 if 條件或者 return 表示式的修復模式,因此我們當然希望其適用於這類上下文。但其同時也適用於其它一些情況,例如以上示例當中提到的呼叫語句:mListView.clearListeners();。Getafix 的排名策略會嘗試對模式的修復效能做出估算,併為其分配最可能實現修復效果的上下文。這項策略使得該系統能夠在之後的運行當中不再依賴於驗證步驟,從而顯著降低計算時間。

以上模式將與其它模式競爭,例如更為具體的 if (h0.h1()) { ... } → if (h0!=null && h0.h1()) { ... }或者示例 1 中僅適用於呼叫語句而非表示式的模式。由於具體程度更高的模式往往擁有更少的匹配位置數量,因此 Getafix 會將其視為更適合當前情況的解決方案併為其分配更高的排名。

Getafix 實際應用與表現

Getafix 現已部署在 Facebook 的生產環境中,負責為 Infer 報告的 null 解引用 bug 提供自動修復建議。順帶一提,Infer 是我們的一款統計分析工具,負責為 Sapienz 發現的、與 null 解引用相關的崩潰 bug 提供修復建議。此外,Getafix 還負責解決 Infer 以往提出的某些重要 bug。

在一次實驗當中,我們將 Getafix 計算出的修復建議與以往人工編寫的修復方法進行了比較,我們發現,在對大約包含 200 項小型編輯的資料集內各種 Infer null 方法呼叫 bug 進行修復時,需要修改的內容不足 5 行。此外,在大約四分之一的案例當中,Getafix 提出的排名最高修復補丁與人工建立的補丁完全匹配。

在另一項實驗中,我們著眼於 Instagram 程式碼庫中的一套子集,並嘗試批量修復其中存在的約 2000 個 null 方法呼叫問題。Getafix 能夠在大約六成 bug 中嘗試使用某個補丁,且其中 90% 的嘗試都通過了自動驗證——這意味著其可編譯且 Infer 將不再發出警告。總體來講,Getafix 成功以自動方式修復了 1077 條(佔比約 53%)的 null 方法呼叫錯誤。

除了針對新 Infer bug 提供修復建議之外,我們還利用相同的方式清理在原先程式碼審查中積壓的舊有 Infer bug。我們已經清理了數百個返回不可為空的 Infer bug 以及欄位不可為空的 Infer bug。有趣的是,在這項工作完成之後,Getafix 在自動修復建議中開始越來越擅長處理返回不可為空以及欄位不可為空類問題,二者的成功修復佔比分別由 56% 與 51% 增長至 62% 與 59%。總體而言,在過去三個月中,Getafix 提供的一系列建議幫助我們成功修復了數百項額外 bug。

Getafix 還為 SapFix 生成了修復建議,用以處理 Sapienz 檢測到的崩潰問題。過去幾個月以來,SapFix 所採用的修復方法中有約半數來自 Getafix 且實際有效(通過全部測試)。而在 Getafix 提供給 SapFix 的全部修復建議中,約 80% 通過了全部測試。

提升 Getafix 影響力

Getafix 幫助我們實現了讓計算機處理常規 bug 修復工作這一重大目標。隨著我們對自身測試及驗證工具的不斷完善,預計 Getafix 將能夠在未來更好地防止各類部署後故障問題。

我們還注意到,Getafix 所挖掘出的修復模式不僅僅是在響應 Infer 報告的 bug; 實際上,其同時也能夠針對手動程式碼檢查結果給出修復建議。這種額外的修復模式源將給自動重複程式碼審查帶來令人興奮的可能性。換句話說,未來我們有可能會將程式碼庫中曾被多次標記及修復的 bug 直接交給自動化工具處理,而不再需要任何人工篩查。

Getafix 是我們構建大型程式碼語料庫以及相關元資料統計分析智慧化工具這一整體性舉措中的組成部分。此類工具的出現,有望改善軟體開發生命週期中的各個層面,包括程式碼發現、程式碼質量與執行效率等等。我們從 Getafix 當中獲得的寶貴見解,也將幫助我們在這一領域構建並部署更多其它與之類似的重要工具。