作者
Björn Gustavsson <bjorn(at)erlang(dot)org>
狀態
最終/R15B 實作於 OTP 版本 R15B
類型
標準追蹤
建立日期
2011-03-01
Erlang 版本
OTP_R15B

EEP 36:例外中的行號 #

摘要 #

擴展從 erlang:get_stacktrace/0 BIF 和 catch 運算符返回的呼叫堆疊回溯 (以下簡稱 堆疊追蹤) 中的每個條目,使其包含檔案名稱和行號資訊。

規範 #

目前,從 erlang:get_stacktrace/0 (和 catch 運算符) 返回的堆疊追蹤是一個三元組列表,其中每個元組看起來像

{Module,Function,Arity}

(在某些情況下,第三個元素可能是一個參數列表,而不是函式元數。)

我們建議將每個元組更改為

{Module,Function,Arity,LocationInfo}

LocationInfo 是一個屬性列表 (一個二元組列表),其中包含檔案名稱和行號資訊。如果有行號資訊可用,則列表將如下所示

[{file,FilenameString},{line,LineNumber}]

應使用 proplists:get_value/3lists:keyfind/3 來存取列表,而不是直接匹配,因為未來版本可能會向列表添加更多項目或更改順序。

檔案名稱通常與模組相同,並加上擴展名“.erl”,但是如果函式定義已放置在標頭檔中,則檔案名稱將是標頭檔的名稱。如果 Erlang 原始檔是由諸如 yecc 之類的程式碼產生器產生,則檔案名稱也會有所不同。

行號永遠不會為零;相反,LocationInfo 將設定為空列表。

如果沒有可用的位置資訊,則列表將為空。以下是一些可能缺少位置資訊的原因

  • 模組已使用不支援產生行號資訊的較舊 BEAM 編譯器進行編譯。

  • 模組是透過使用不包含非零行號和/或檔案名稱的表單呼叫 compile:forms/1,2 建立的。

  • 剖析轉換建立的抽象表單的行號為零。

  • 模組是使用未提供檔案名稱和/或 (非零) 行號的替代編譯器建立的。

  • 行號資訊可能已從 BEAM 檔案中移除。

  • 例外情況發生在 BIF (在執行階段系統中以 C 實作)。

實作要求 #

此 EEP 並未明確指定應如何實作行號資訊,但確實對實作施加了一些要求

  • 如果沒有發生例外情況,則行號資訊的存在對程式的執行時間應 (實際上) 沒有影響。實際上,這意味著如果沒有發生例外情況,則允許實作新增額外的指令或 BIF 呼叫來執行。

  • 行號資訊不應取決於 BEAM 檔案中是否存在偵錯資訊。

  • 預設情況下,行號資訊應包含在 BEAM 檔案中。(可能會有選項可以關閉包含行號資訊。)

  • 載入行號資訊應為預設值。可能會有一個選項可以關閉載入行號資訊,以節省記憶體。

範例 #

在範例中,我們將使用以下模組

-module(example).
-export([m/1]).
-include("header.hrl").

m(L) ->
    {ok,lists:map(fun f/1, L)}.  %Line 6

以及標頭檔 header.hrl

f(X) ->
    abs(X) + 1.        %Line 2

使用 R14B01 呼叫我們的範例模組,我們得到以下結果

1> example:m([-1,0,1,2]).
{ok,[2,1,2,3]}
2> example:m([-1,0,1,2,not_a_number]).
** exception error: bad argument
     in function  abs/1
        called as abs(not_a_number)
     in call from example:f/1
     in call from lists:map/2
     in call from lists:map/2
     in call from example:m/1
3> catch example:m([-1,0,1,2,not_a_number]).
{'EXIT',{badarg,[{erlang,abs,[not_a_number]},
                 {example,f,1},
                 {lists,map,2},
                 {lists,map,2},
                 {example,m,1},
                 {erl_eval,do_apply,5},
                 {erl_eval,expr,5},
                 {shell,exprs,7}]}}

在啟用行號資訊的系統中,我們得到

1> example:m([-1,0,1,2]).
{ok,[2,1,2,3]}
2> example:m([-1,0,1,2,not_a_number]).
** exception error: bad argument
     in function  abs/1
        called as abs(not_a_number)
     in call from example:f/1 (header.hrl, line 2)
     in call from lists:map/2 (lists.erl, line 948)
     in call from lists:map/2 (lists.erl, line 948)
     in call from example:m/1 (example.erl, line 6)
3> catch example:m([-1,0,1,2,not_a_number]).
{'EXIT',{badarg,[{erlang,abs,[not_a_number],[]},
                 {example,f,1,[{file,"header.hrl"},{line,2}]},
                 {lists,map,2,[{file,"lists.erl"},{line,948}]},
                 {lists,map,2,[{file,"lists.erl"},{line,948}]},
                 {example,m,1,[{file,"example.erl"},{line,6}]},
                 {erl_eval,do_apply,5,[{file,"erl_eval.erl"},{line,482}]},
                 {erl_eval,expr,5,[{file,"erl_eval.erl"},{line,276}]},
                 {shell,exprs,7,[{file,"shell.erl"},{line,666}]}]}}

如果我們使用 R14B01 中的 BEAM 編譯器編譯 example 模組,則該模組將沒有任何行號資訊

1> example:m([-1,0,1,2,not_a_number]).
** exception error: bad argument
     in function  abs/1
        called as abs(not_a_number)
     in call from example:f/1
     in call from lists:map/2 (lists.erl, line 948)
     in call from lists:map/2 (lists.erl, line 948)
     in call from example:m/1
2> catch example:m([-1,0,1,2,not_a_number]).
{'EXIT',{badarg,[{erlang,abs,[not_a_number],[]},
                 {example,f,1,[]},
                 {lists,map,2,[{file,"lists.erl"},{line,948}]},
                 {lists,map,2,[{file,"lists.erl"},{line,948}]},
                 {example,m,1,[]},
                 {erl_eval,do_apply,5,[{file,"erl_eval.erl"},{line,482}]},
                 {erl_eval,expr,5,[{file,"erl_eval.erl"},{line,276}]},
                 {shell,exprs,7,[{file,"shell.erl"},{line,666}]}]}}

動機 #

例外情況中缺少行號資訊是許多初學者的主要障礙,並且對於經驗豐富的 Erlang 程式設計師來說也是浪費時間的事情。

為了減輕缺少行號資訊的問題,經常重複的建議是編寫較小的函式。在某種程度上,這是很好的建議,但是某些函式最自然地編寫為具有許多子句的單個函式。一個範例是 gen_server 流程的 handle_call/3 回呼。另一個範例是測試套件。在典型的測試套件中,每一行都會測試一個條件,並且可能會失敗。將可能失敗的每一行放在單獨的函式中是不切實際的。

基於 common_test 的測試套件會自動透過剖析轉換來執行,該轉換在發生例外情況時提供行號資訊。剖析轉換會在每一行程式碼之前插入程式碼,將目前的函式名稱和行號儲存在流程字典中。發生例外情況時,可以擷取並顯示行號。

這種方法的一個問題是測試套件的執行速度會變慢,如果正在測試的系統中逾時過期,則可能會導致測試案例失敗。另一個問題是,預設情況下,剖析轉換僅在測試模組本身上執行,因此程式碼的其他部分 (測試的支援程式庫或產品本身) 中發生的例外情況沒有任何行資訊。

基本原理 #

我們選擇讓 erlang:get_stacktrace/0catch 運算符返回具有檔案名稱和行號資訊的堆疊追蹤 (而不是引入一個新的函式,例如名為 erlang:get_full_stacktrace/3)。這表示只是傳遞堆疊追蹤 (到 erlang:raise/3) 的程式碼不需要更新。例如,以下捕捉例外情況、記錄例外情況並傳遞例外的程式碼不需要更新

try
    some_call_that_may_fail()
catch
    Class:Reason ->
        Stk = erlang:get_stacktrace(),
        log(Class, Reason, Stk),
        erlang:raise(Class, Reason, Stk)
end

另一方面,這表示假設堆疊追蹤只能包含三元組的程式碼將不再起作用,並且需要更新。

預設情況下應載入行號資訊 (而不是透過給定選項排序) 的原因有幾個。

  • 在實際系統中,程式碼大小通常不是問題,因為它被用於流程堆積、堆積外二進位檔和 ETS 表格的記憶體所掩蓋。因此,程式碼大小增加 10% (如參考實作中所衡量) 對大多數使用者來說不是問題,但是擁有行號資訊的好處可能是巨大的。

  • Erlang 的新手最需要行號資訊,他們應該在不給出任何特殊選項的情況下獲得它。如果需要一個選項,則關於如何找出哪個原始碼行導致了例外情況的郵件列表的問題將繼續浪費時間。

  • 如果必須給出選項,即使知道該選項的開發人員也可能忘記給出該選項,因此最終可能不得不在沒有行號資訊的情況下調查例外情況。(如果問題不容易重現,則可能會浪費大量時間。)

因此,最好是那些無法負擔載入程式碼大小有任何增加的開發人員是必須給出選項才能關閉載入行號資訊的開發人員。

回溯相容性 #

檢查堆疊追蹤並假設它包含三元組的應用程式必須更新。erlang:raise/3 BIF 仍然接受三元組 (它會將這些三元組轉換為第四個元素為空列表的四元組);因此,不必強制更新對 erlang:raise/3 的呼叫。

實作 #

可以從 Github 取得參考實作,如下所示

git fetch git://github.com/bjorng/otp.git bjorn/line-numbers-in-exceptions

以下是實作的概述

BEAM 編譯器會在每個可能產生例外情況的結構之前以及每個將包含在堆疊追蹤中的呼叫之前插入 line 指令。(本機尾端遞迴呼叫不需要 line 指令,但是外部尾端遞迴呼叫需要 line 指令,因為它們可能是對 BIF 的呼叫。)

line 指令具有單個運算元,即行號表的索引。行號表儲存在 BEAM 檔案中的“Line”區塊中。“Line”區塊和行指令會使 BEAM 檔案的檔案大小增加約 5%。

載入器會從將要執行的程式碼中移除 line 指令,但是會記住它們的位置,並建立一個以位址順序排序的表,將程式計數器對應到行號資訊。當需要建立堆疊追蹤時,執行階段系統將對造成例外情況的指令和每個接續指標的程式計數器進行二元搜尋。

為了受益於在非常受限的記憶體空間中執行的嵌入式系統,可以使用 '+L' 選項啟動執行階段系統,以停用載入行號資訊。由於編譯器無法對造成例外情況的指令 (例如 badmatch 指令) 進行程式碼共用最佳化,因此程式碼仍然比沒有行號資訊編譯的程式碼大約 1%。

在目前的實作中,行號資訊會使載入的程式碼大小增加約 10%。

著作權 #

本文檔已置於公有領域。