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

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

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

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

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

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) 中廣泛存在堆棧難以閱讀的現象spa

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

我直到 2017 年 10 月才意識到這個問題,好在 .NET Core 如今是徹底開源的,因此我能夠改變它。code

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

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 14ip

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

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;
        }
    }
}
相關文章
相關標籤/搜索