原文 : 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;
}
}
}

[搬运] .NET Core 2.1中改进的堆栈信息的更多相关文章

  1. delphi中获取调用堆栈信息

    异常堆栈有利于分析程序的错误,Delphi的Exception有StackTrace属性,但是值为空,因为StackTrace的信息收集Delphi委托给了第三方组件来完成,真是脑子有毛病! 借助于m ...

  2. 【译】.NET Core 3.0 中的新变化

    .NET Core 3.0 是 .NET Core 平台的下一主要版本.本文回顾了 .Net Core 发展历史,并展示了它是如何从基本支持 Web 和数据工作负载的版本 1,发展成为能够运行 Web ...

  3. .NET Core 2.1中的分层编译(预览)

    如果您是.NET性能的粉丝,最近有很多好消息,例如.NET Core 2.1中的性能改进和宣布.NET Core 2.1,但我们还有更多的好消息.分层编译是一项重要的新特性功能,我们可以作为预览供任何 ...

  4. 你所不知道的库存超限做法 服务器一般达到多少qps比较好[转] JAVA格物致知基础篇:你所不知道的返回码 深入了解EntityFramework Core 2.1延迟加载(Lazy Loading) EntityFramework 6.x和EntityFramework Core关系映射中导航属性必须是public? 藏在正则表达式里的陷阱 两道面试题,带你解析Java类加载机制

    你所不知道的库存超限做法 在互联网企业中,限购的做法,多种多样,有的别出心裁,有的因循守旧,但是种种做法皆想达到的目的,无外乎几种,商品卖的完,系统抗的住,库存不超限.虽然短短数语,却有着说不完,道不 ...

  5. [转]【译】.NET Core 3.0 中的新变化

    .NET Core 3.0 是 .NET Core 平台的下一主要版本.本文回顾了 .Net Core 发展历史,并展示了它是如何从基本支持 Web 和数据工作负载的版本 1,发展成为能够运行 Web ...

  6. ASP.NET Core HTTP 管道中的那些事儿

    前言 马上2016年就要过去了,时间可是真快啊. 上次写完 Identity 系列之后,反响还不错,所以本来打算写一个 ASP.NET Core 中间件系列的,但是中间遇到了很多事情.首先是 NPOI ...

  7. 在.NET Core控制台程序中使用依赖注入

    之前都是在ASP.NET Core中使用依赖注入(Dependency Injection),昨天遇到一个场景需要在.NET Core控制台程序中使用依赖注入,由于对.NET Core中的依赖注入机制 ...

  8. .NET跨平台之旅:ASP.NET Core从传统ASP.NET的Cookie中读取用户登录信息

    在解决了asp.net core中访问memcached缓存的问题后,我们开始大踏步地向.net core进军——将更多站点向asp.net core迁移,在迁移涉及获取用户登录信息的站点时,我们遇到 ...

  9. ASP.NET Core 1.0 中的依赖项管理

    var appInsights=window.appInsights||function(config){ function r(config){t[config]=function(){var i= ...

随机推荐

  1. COM学习(四)——COM中的数据类型

    上一次说到,COM为了跨语言,有一套完整的规则,只要COM组件按照规则编写,而不同的语言也按照对应的规则调用,那么就可以实现不同语言间相互调用.但是根据那套规则,只能识别接口,并调用没有参数和返回类型 ...

  2. Windows服务框架与服务的编写

    从NT内核开始,服务程序已经变为一种非常重要的系统进程,一般的驻守进程和普通的程序必须在桌面登录的情况下才能运行,而许多系统的基础程序必须在用户登录桌面之前就要运行起来,而利用服务,可以很方便的实现这 ...

  3. JavaScript(一)js简单介绍

    JavaScript JS历史简述: javascript 是 netscape 网景公司 的  布兰德·艾奇  研发的, 网景要求  布兰德·艾奇 10天开发出来一个与Java相似 但要比java简 ...

  4. keynote 代码高亮

    brew install highlight (同时会安装 Lua 和 Boost) highlight -K 18 -s Vampire -u 'utf-8' -O rtf test.html | ...

  5. Keepalived概述和安装(1)

    一.Keepalived概述 本文主要了解开源高可用负载均衡集群利器keepalived,掌握keepalived的安装,运用keepalived配置高可用集群,并能够实现keepalived与负载均 ...

  6. 应用服务器GC回收常见问题总结

    近一段时间多次发现因GC问题造成系统性能问题(应用服务间歇性响应缓慢.应用服务器CPU占用较高等),在此总结一下: 1.代码中直接调用GC.Collect() 2.字符串等操作频繁的内存申请 3.频繁 ...

  7. React-Native 学习笔记-Android开发平台-开发环境搭建

    详细步骤请查看官网对应文档,BUT,有些注意事项请注意! 1,优先安装Node.js,因为后面可以使用npm安装软件, 2,注意不要使用CNPM!!!!!!!!! 3,Android Studio 请 ...

  8. bootstrap-table操作之“删除”

    最近在做一个新的后台管理系统,在对数据进行操作时需要写一个"删除"功能,如图所示: 下面我来描述一下实现过程中出现的bug以及解决方法: 1.href值为空(href=" ...

  9. CTF---密码学入门第三题 奇怪的短信

    奇怪的短信分值:10 来源: Ayn 难度:易 参与人数:5117人 Get Flag:2623人 答题人数:2858人 解题通过率:92% 收到一条奇怪的短信: 335321414374744361 ...

  10. bzoj:1661 [Usaco2006 Nov]Big Square 巨大正方形

    Description 农民 John 的牛参加了一次和农民 Bob 的牛的竞赛.他们在区域中画了一个N*N 的正方形点阵,两个农场的牛各自占据了一些点.当然不能有两头牛处于同一个点.农场的目标是用自 ...