[搬運] .NET Core 2.1中改進的堆棧信息

原文 : Stacktrace improvements in .NET Core 2.1
作者 : Ben Adams
譯者 : 張很水

. NET Core 2.1 現在具有可讀的異步堆棧信息!使得異步、迭代器和字典 ( key not found ) 中的堆棧更容易追蹤!

這個大膽的主張意味着什麼?

要知道,爲了確定調用 異步 和 迭代器方法的實際重載,(這在以前)從堆棧信息中跟蹤幾乎是不可能的:

System.Collections.Generic.KeyNotFoundException: The given key '0' was not present in the dictionary.
   at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
   at Program.Sequence(Int32 start)+MoveNext()
   at Program.Sequence(Int32 start, Int32 end)+MoveNext()
   at Program.MethodAsync()
   at Program.MethodAsync(Int32 v0)
   at Program.MethodAsync(Int32 v0, Int32 v1)
   at Program.MethodAsync(Int32 v0, Int32 v1, Int32 v2)
   at Program.MethodAsync(Int32 v0, Int32 v1, Int32 v2, Int32 v3)
   at Program.Main(String[] args)

問題: “使堆棧信息可讀”

David Kean(@davkean) 於 2017 年 10 月 13 日在 dotnet/corefx#24627 提出 使堆棧信息可讀 的問題:

如今在 任務 (Task)、異步 (async) 和 等待 (await) 中普遍存在堆棧難以閱讀的現象

對於在 .NET 中輸出異步的可閱讀堆棧信息已經夢魂縈繞了5年...

我直到 2017 年 10 月才意識到這個問題,好在 .NET Core 現在是完全開源的,所以我可以改變它。

作爲參考,請參閱文章底部的代碼,它將會輸出如下的異常堆棧:

System.Collections.Generic.KeyNotFoundException: The given key was not present in the dictionary.
   at System.ThrowHelper.ThrowKeyNotFoundException()
   at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
   at Program.<Sequence>d__8.MoveNext()
   at Program.<Sequence>d__7.MoveNext()
   at Program.<MethodAsync>d__6.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Program.<MethodAsync>d__5.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Program.<MethodAsync>d__4.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Program.<MethodAsync>d__3.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Program.<MethodAsync>d__2.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Program.<Main>d__1.MoveNext()

(爲簡潔起見,刪除了行號,如 in C:\Work\Exceptions\Program.cs:line 14

有時甚至可見更詳細的膠水信息:

   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() 
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) 
   at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task) 
   at System.Runtime.CompilerServices.TaskAwaiter.GetResult() 

跟蹤堆棧的一般用途是確定在源代碼中發生錯誤的位置以及對應的路徑。

然而,現如今我們無法避免異步堆棧,同時還要面對很多無用的噪聲(干擾)。

PR: “隱藏請求中的異常堆棧幀 ”

堆棧信息通常是從拋出異常的地方直接輸出的。

當異步函數拋出異常時,它會執行一些額外的步驟來確保響應,並且在延續執行(既定方法)之前會進行清理。

在異步函數拋出異常

當這些額外的步驟被添加到調用堆棧中時,它們不會對我們確定堆棧信息有任何幫助,因爲它們實際上是在出現異常 之後 執行。

所以它們是非常嘈雜和重複的,對於確定代碼在哪裏出現異常上並沒有任何額外的價值。

實際產生的調用堆棧和輸出的不一致:

輸出堆棧前

在刪除這些異常堆棧幀後(隱藏請求中的異常堆棧幀 dotnet/coreclr#14652 ),跟蹤堆棧開始變得平易近人:

System.Collections.Generic.KeyNotFoundException: The given key was not present in the dictionary.
   at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
   at Program.<Sequence>d__7.MoveNext()
   at Program.<Sequence>d__6.MoveNext()
   at Program.<MethodAsync>d__5.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at Program.<MethodAsync>d__4.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at Program.<MethodAsync>d__3.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at Program.<MethodAsync>d__2.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at Program.<MethodAsync>d__1.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at Program.<Main>d__0.MoveNext()

並且輸出的調用堆棧與實際的調用堆棧一致: 一致的堆棧信息

PR: “刪除異步的 Edi 邊界”

異步中的異常使用 ExceptionDispatchInfo 類傳播,這意味着着在每個連接點都會有這樣的邊界信息:

--- End of stack trace from previous location where exception was thrown ---

這只是讓你知道兩部分調用堆棧已經合併,並且有個過渡。

它如此頻繁地出現在異步中,增加了很多噪音,並沒有任何附加價值。

在 刪除異步的 Edi 邊界 dotnet/coreclr#15781 後 所有的 堆棧信息變得有價值:

System.Collections.Generic.KeyNotFoundException: The given key was not present in the dictionary.
   at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
   at Program.<Sequence>d__7.MoveNext()
   at Program.<Sequence>d__6.MoveNext()
   at Program.<MethodAsync>d__5.MoveNext()
   at Program.<MethodAsync>d__4.MoveNext()
   at Program.<MethodAsync>d__3.MoveNext()
   at Program.<MethodAsync>d__2.MoveNext()
   at Program.<MethodAsync>d__1.MoveNext()
   at Program.<Main>d__0.MoveNext()

PR: “處理迭代器和異步方法中的堆棧”

在上一節中,堆棧已經是乾淨了,但是要確定是什麼情況,還是很困難的一件事。

堆棧中包含着由 C# 編譯器創建的異步狀態機的基礎方法簽名,而不僅僅是(你的)源代碼產生的。

你可以確定方法的名稱,但是如果不深入挖掘,則無法確定所調用的實際重載。

在 處理迭代器和異步方法中的堆棧 dotnet/coreclr#14655 之後,堆棧更接近原始來源:

System.Collections.Generic.KeyNotFoundException: The given key was not present in the dictionary.
   at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
   at Program.Sequence(Int32 start)+MoveNext()
   at Program.Sequence(Int32 start, Int32 end)+MoveNext()
   at Program.MethodAsync()
   at Program.MethodAsync(Int32 v0)
   at Program.MethodAsync(Int32 v0, Int32 v1)
   at Program.MethodAsync(Int32 v0, Int32 v1, Int32 v2)
   at Program.MethodAsync(Int32 v0, Int32 v1, Int32 v2, Int32 v3)
   at Program.Main(String[] args)

PR: “實現 KeyNotFoundException 的堆棧追蹤”

因爲有額外的獎勵,我着手實現拋出 “ KeyNotFoundException ” 的堆棧追蹤。

Anirudh Agnihotry (@Anipik) 提出了 實現 KeyNotFoundException 的堆棧追蹤dotnet/coreclr#15201

這意味着這個異常現在要告訴你哪個 key 找不到的信息:

System.Collections.Generic.KeyNotFoundException: The given key '0' was not present in the dictionary.
   at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
   at Program.Sequence(Int32 start)+MoveNext()
   at Program.Sequence(Int32 start, Int32 end)+MoveNext()
   at Program.MethodAsync()
   at Program.MethodAsync(Int32 v0)
   at Program.MethodAsync(Int32 v0, Int32 v1)
   at Program.MethodAsync(Int32 v0, Int32 v1, Int32 v2)
   at Program.MethodAsync(Int32 v0, Int32 v1, Int32 v2, Int32 v3)
   at Program.Main(String[] args)

支持的運行時以及相關進展

這些改進將在稍晚的時間發佈到 Mono 上,並在下一個階段發佈。但是如果您使用的是較早的運行時版本 (.NET Core 1.0 - 2.0; .NET Framework 或 Mono) 想要獲得一樣的效果,需要使用 Ben.Demystifier 提供的Nuget 包,並且在你的異常中使用 .Demystify() 的方法:

catch (Exception e)
{
    Console.WriteLine(e.Demystify());
}

這些改進將會產生與 C#相得映彰的輸出信息,最令人高興的還是全都會被內置!

System.Collections.Generic.KeyNotFoundException: The given key was not present in the dictionary.
   at TValue System.Collections.Generic.Dictionary<TKey, TValue>.get_Item(TKey key)
   at IEnumerable<int> Program.Sequence(int start)+MoveNext()
   at IEnumerable<int> Program.Sequence(int start, int end)+MoveNext()
   at async Task<int> Program.MethodAsync()
   at async Task<int> Program.MethodAsync(int v0)
   at async Task<int> Program.MethodAsync(int v0, int v1)
   at async Task<int> Program.MethodAsync(int v0, int v1, int v2)
   at async Task<int> Program.MethodAsync(int v0, int v1, int v2, int v3)
   at async Task Program.Main(string[] args)

.NET Core 2.1 將成爲 .NET Core 的最佳版本,原因說不完,這只是變得更美好的一小步...

上面提到的觸發異常的代碼及對應的堆棧信息

class Program
{
    static Dictionary<int, int> _dict = new Dictionary<int, int>();

    static async Task Main(string[] args)
    {
        try
        {
            var value = await MethodAsync(1, 2, 3, 4);
            Console.WriteLine(value);
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }
    }

    static async Task<int> MethodAsync(int v0, int v1, int v2, int v3)
        => await MethodAsync(v0, v1, v2);

    static async Task<int> MethodAsync(int v0, int v1, int v2)
        => await MethodAsync(v0, v1);

    static async Task<int> MethodAsync(int v0, int v1)
        => await MethodAsync(v0);

    static async Task<int> MethodAsync(int v0)
        => await MethodAsync();

    static async Task<int> MethodAsync()
    {
        await Task.Delay(1000);

        int value = 0;
        foreach (var i in Sequence(0, 5))
        {
            value += i;
        }

        return value;
    }

    static IEnumerable<int> Sequence(int start, int end)
    {
        for (var i = start; i <= end; i++)
        {
            foreach (var item in Sequence(i))
            {
                yield return item;
            }
        }
    }

    static IEnumerable<int> Sequence(int start)
    {
        var end = start + 10;
        for (var i = start; i <= end; i++)
        {
            _dict[i] = _dict[i] + 1; // Throws exception
            yield return i;
        }
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章