使用 IntelliTrace 更快地诊断问题
思考一下您在调试时的典型工作流程。在成功确定问题的根本原因之前,您会陷入设置断点并重现问题的测试步骤的反复循环中。现在,您可以使用 IntelliTrace 在应用程序运行时记录历史调试信息。这将有助于您摆脱不断重复的工作。您可以只执行一次测试步骤以重现问题,然后使用历史调试来确定根本原因。
IntelliTrace 是一组历史调试技术,该组技术对 Visual Studio 2015 Enterprise 中的调试器进行了扩展。此外,还有一个可供您在 Visual Studio 之外使用的独立组件。IntelliTrace 记录您的应用程序执行以寻找有趣的事件。当发生有趣事件时,IntelliTrace 将自动记录调用堆栈和局部变量,同时应用程序将继续运行。您可以通过“工具 | 选项 | IntelliTrace | IntelliTrace 事件”控制 IntelliTrace 视为“相关”的事件。
IntelliTrace 使用时间线(事件的高级视图)和一个显示每个事件详细信息的表为您提供应用程序执行的历史。通过扩展并与 Visual Studio 调试器集成,您还可以通过其访问历史调试数据。这样,您可以按时返回并查看收集事件的调用堆栈与局部变量。
IntelliTrace 现位于 Visual Studio 2015 的诊断工具窗口中。该诊断工具窗口包含 CPU 使用率工具和内存使用率工具以及 IntelliTrace。如果您的项目类型和调试配置受支持(有关最新信息,请访问 aka.ms/diagtoolswindow),则在 Visual Studio 2015 中启动调试时,您会看到诊断工具窗口显示了(您可以按 F5 键或您始终可以使用“调试 | 显示诊断工具”手动将其打开)。在您激活“历史调试”后,您将看到类似图 1 所示的内容。
图 1 使用 IntelliTrace 进行的历史调试
浏览用户界面
以下是 IntelliTrace UI 各个组件的列表及其用途和功能:
调试器事件:“调试器事件”详细信息表(请参阅图 2)是 IntelliTrace 已收集事件的表格视图。从左到右,这些列是:
- 指向调试器当前显示信息所属事件的指针;只有一行将具有一个黄色箭头来指示当前指令指针的位置,而有一行可能包含一个粉色箭头来表明您已激活的历史事件。
- 用于表示调试器事件时间线上的事件的图标。
- 事件的简要说明。
- 从调试会话开始到收集该事件的那个时间所经过的秒数。
- 事件持续时间。(注意:并非所有事件都有一个持续时间。)
- 生成事件的线程 ID 和名称。(注意:并非所有事件都与线程关联)
图 2 调试器事件表
如果您单击列表中的某个事件将其展开,则可以选择“激活历史调试”并将调试器设置到 IntelliTrace 记录所选事件的点。
类别筛选器控件:此筛选控件可使您在事件仍处于收集状态时隐藏或显示事件类别。如果您想专注于特定类别或您完全不在意另一个类别,则可以使用此控件快速将这些类别引入和移出视图。当前的类别列表包括:ADO.NET、ASP.NET、控制台、数据绑定、调试器、环境变量、异常、文件、手势、延迟初始化、输出、注册表、服务模型、线程、跟踪,用户提示和 XAML。
线程筛选器控件:如果您只想诊断特定线程或您正执行的特定线程没有问题的情况下,此筛选控件可让您按生成事件的线程隐藏或显示事件。
从外部代码按钮显示事件:IntelliTrace 会考虑调试器的“仅我的代码”设置。这意味着,默认情况下,它会隐藏源自非用户代码的事件以减少干扰。单击此选项将绕过调试器的设置,并显示来自外部代码的事件。大多数情况下,这将导致输出内容过于详细。
调试器事件时间线:这是随着时间的推移 IntelliTrace 收集的事件的图形视图。这一不同视图所含信息与事件详细信息表中所示信息相同。使用时间线获取高级视图,通过事件详细信息表视图识别并选择您想要深入了解的区域。通过选择特定的时间范围,您可以筛选您在事件详细信息表视图中看到的内容。
标尺:在时间线上方有一个标尺,用于显示每个事件发生的时间点。它还允许您通过单击和拖动选择特定的时间范围。选择某个时间范围以筛选调试器事件详细信息表。
中断事件轨迹:每次发生与中断相关的事件时,它都将显示在此时间线轨迹上。中断事件包括命中断点、已完成的步骤、单击“全部中断”、调用 Debugger.Break 或未处理的异常中断执行。将其视为主时间线轨迹可帮助您确定其他轨迹中的事件在程序执行中出现的位置(因为使用断点和步骤是您控制应用程序执行的方式)。单击此跟踪中的事件将应用筛选“调试器事件”详细信息表中的事件的时间筛选器。这种方式可以轻松地只筛选单步执行代码行时或在您按 F5 键和命中断点之间出现的那些事件。
输出事件轨迹:此轨迹为事件显示“输出”窗口中所示的消息。此轨迹上的事件类别如下:引发的异常、程序输出(或 Console.WriteLine)、加载/卸载的模块、线程退出和进程退出。这允许您将标准调试输出消息与调试器历史信息的其余部分相关联。
IntelliTrace 事件轨迹:IntelliTrace 收集的每一个其他事件类别都将显示在此时间线轨迹上:ADO.NET、ASP.NET、控制台、数据绑定、环境变量、文件、手势、延迟初始化、注册表、服务模型、线程、跟踪、用户提示和 XAML。
诊断工具工具栏:该工具栏提供了放大和缩小按钮,以及一个用于将时间线重置回默认缩放级别的“重置视图”按钮,并清除任何现有的时间选择。它可对视图中收集的所有数据进行筛选。在“选择工具”下拉列表中,除了 IntelliTrace 工具,您还可以选择希望包括在“诊断工具”窗口中的其他工具。
您可以同时运行多个诊断工具。“诊断工具”窗口可以同时托管“内存使用率”、“CPU 使用率”工具和 IntelliTrace。这样,您可以获得应用程序行为的整体视图。例如,图 3 显示随着 ASP.NET 应用程序的启动,一系列“模块加载”事件使得内存和 CPU 使用率也随之升高。
图 3 诊断工具窗口显示了应用程序的行为和性能
使用 IntelliTrace 修复实际的 Bug
现在,我将向您介绍如何在 Visual Studio 2015 Enterprise 中使用 IntelliTrace 的“实时调试”功能修复一个实际的 Bug。我将调试的应用程序是来自 CodeProject 名为 SocialClub 的 Windows 窗体应用程序。
该应用程序维护着一个社交俱乐部的会员数据库。在注册会员之后出现搜索行为异常的 Bug。为了重现该 Bug,我启动该应用程序并注册了一个新会员。然后,我执行 Get All 搜索以期返回所有已注册会员。我只期望得到一个结果,但实际上我得到了两个(参见图 4)。第二个搜索结果是个意外,因此我需要进行修复。
图 4 Get All 搜索结果包含意外记录
若要修复此 Bug,接下来该怎么做?此时,我假设是 Get All 搜索函数或是新会员注册过程出现了问题。该应用程序还有一种需要采用特定搜索条件的搜索模式,因此我将用它来搜索 Get All 返回的意外记录(数据缺失或值未知)。
以下是可能出现的情况:如果我得不到任何结果,这可能意味着意外记录不存在于数据库中,问题可能出在 Get All 搜索函数上。如果我得到了与“职业和婚姻状况未知”匹配的记录,则问题可能是注册函数在数据库中输入了过多超出其预设的记录。
因此,现在我使用“职业未知”和“婚姻状况未知”进行搜索,而这恰好返回了一个结果:一条我已成功注册为工程师和已婚的记录。这可怪了,并且遗憾的是,这并未让我更加接近 Bug 的根本原因。与其花费时间一遍又一遍设置断点、注册新会员以及进行搜索,还不如看看 IntelliTrace 能够如何帮助加快调查。
我想要查看 IntelliTrace 收集的事件,但在调试器中断应用程序执行(即其击中断点)之前 IntelliTrace 事件没有更新。由于我没有感兴趣的特定断点,所以我只需单击 Visual Studio 工具栏上的“全部中断”。应用程序现处于中断状态,所有线程挂起。IntelliTrace 将在“诊断工具”窗口的时间线和表格详细信息视图中显示其收集的数据。
此时,自启动调试以来我已经与应用程序进行了大量交互。我登录、注册了新会员、使用 Get All 进行了搜索以及使用特定搜索条件进行了搜索。但是,我只对作为单击“注册”的直接结果出现的事件感兴趣。若要筛选我的视图以仅显示这些事件,我将鼠标悬停在时间线中的事件上,直到找到单击“注册”的位置。然后,我拖动并选择了一个事件集群。当选定时间后,我查看了我的表格详细视图,我可以看到列出的两个最新事件(除了“全部中断”)是两个 INSERT 语句(参见图 5)。
图 5 筛选表格详细信息视图以显示来自选定时间范围内的事件
单击列表中的事件可将其扩展到多个行,以显示整个执行的 SQL 语句。我可以看到发生的两个 INSERT 语句。第二个插入了具有 NULL 值的错误记录。下面是这两个 SQL 语句:
Execute Reader "insert [dbo].[ClubMembers]([Name], [DateOfBirth],
[Occupation], [Salary], [MaritalStatus], [HealthStatus], [NumberOfChildren],
[ExpirationDate])values (@0, @1, @2, @3, @4, @5, @6, @7)
select [Id] from [dbo].[ClubMembers] where @@ROWCOUNT > 0 and [Id] =
scope_identity()"
Execute Reader "insert [dbo].[ClubMembers]([Name], [DateOfBirth],
[Occupation], [Salary], [MaritalStatus], [HealthStatus], [NumberOfChildren],
[ExpirationDate])values (null, @0, @1, null, @2, @3, null, @4) select [Id] from
[dbo].[ClubMembers] where @@ROWCOUNT > 0 and [Id] = scope_identity()"
您可以忽略跟在 INSERT 语句后面的 SELECT 语句。这是检索其刚插入的记录的 ID 的实体框架。下一个问题是:为什么单击“注册”按钮一次会导致执行两个 SQL 语句?IntelliTrace 通过允许我为每个事件激活历史调试(参见图 6)以及在“调用堆栈”窗口中检查其相应的历史调用堆栈,帮我快速找到了问题答案。
图 6 针对两个 INSERT 语句中第一个激活历史调试
第一个 INSERT 的历史调用堆栈为:
John.SocialClub.Data.dll!John.SocialClub.Data.Service.ClubMemberService.Create(...)
John.SocialClub.Desktop.exe!John.SocialClub.Desktop.Forms.Membership.Manage.RegisterMember()
John.SocialClub.Desktop.exe!John.SocialClub.Desktop.Forms.Membership.Manage.Register_Click(...)
John.SocialClub.Desktop.exe!John.SocialClub.Desktop.Program.Main()
错误记录中的第二个 INSERT 的历史调用堆栈是:
John.SocialClub.Data.dll!John.SocialClub.Data.Service.ClubMemberService.Create(...)
John.SocialClub.Desktop.exe!John.SocialClub.Desktop.Forms.Membership.Manage.RegisterMember()
John.SocialClub.Desktop.exe!John.SocialClub.Desktop.Forms.Membership.Manage.btnRegister_MouseClick(...)
John.SocialClub.Desktop.exe!John.SocialClub.Desktop.Program.Main()
单击每个帧将我带入相应的代码行。检查了两个历史调用堆栈后,我已经确定我有两个不同的事件处理程序预定了相同的按钮单击:Register_Click(...) 和 btnRegister_MouseClick(...)。通过查看这两个函数中的代码,我迅速推断出是因为在每个新会员注册后,窗体的字段将重置,所以第一个事件处理程序会将记录正确插入数据库。然而,第二个事件处理程序会插入内容空白或为 NULL 的字段的记录。因此我使用“全部中断”快速找到了 Bug,然后使用 IntelliTrace 确定并导航到有问题的代码段。
如果 IntelliTrace 事件都不足以找到 Bug,该怎么办?
此时,就像 IntelliTrace 改善了您的调试方式那样让人兴奋,您可能很好奇地想知道,如果 IntelliTrace 不记录任何可帮您找到问题根本原因的相关事件,您该怎么办。您是不走运吗?不,您没有不走运。别忘了,您可以使用“工具 | 选项 | IntelliTrace | IntelliTrace 事件”控制 IntelliTrace 事件的启用。并非所有事件都默认为启用,但即使启用了所有事件,都可能始终无法解决一些麻烦的 Bug。
有关这些棘手的问题,您可以对 IntelliTrace 进行配置,以不仅仅记录事件,还会记录每个方法调用及其参数。只需转到“工具 | 选项 | IntelliTrace”并选择 IntelliTrace 事件和调用信息。这是一个强大的调试功能,但它比较耗费运行时。使用此设置,IntelliTrace 将截获每个方法调用并进行记录,这将影响应用程序的性能。这就是为什么默认情况下它不会收集方法调用。您需要通过 IntelliTrace 设置选择收集。
您可以通过两种不同的方式查看和导航此新信息。您可以在“调试器事件”详细信息表中使用子选项卡“调用”,这将列出所有记录的调用(有关“调用”视图的详细信息,请访问 aka.ms/itracecalls)。另一种方法是激活事件的“历史调试”并使用文本编辑器内的 IntelliTrace 控件在应用程序的执行中来回导航。这些控件显示在您的代码和指令指针之间。因此,IntelliTrace 可以让您了解所有实时调试情况。
如果您不能在开发计算机上重现 Bug,该怎么办?
这时,IntelliTrace 中的非实时调试功能就可以派上用场了。到目前为止,我假定您知道重现要调试的问题所必需的步骤。但事实并非总是如此。对于一些最艰巨且非常耗时的 Bug,您可能没有确切的重现步骤。IntelliTrace 通过允许您在生产或测试环境中记录应用程序的执行,消除了这一可怕的“无法重现”情况。然后,通过使用我在此用过的同一个“诊断工具”窗口浏览收集的信息,您可以在自己的开发计算机上对其进行调试。
IntelliTrace 提供了一个独立的收集器,您可以将其部署到 Visual Studio 不能连接到的其他环境。您应当不会遇到来自您的管理员的任何阻力,因为这不涉及到任何安装。只需将收集器复制到目标环境。该收集器会将应用程序的执行记录到您可以传输至开发计算机并使用 Visual Studio 打开的 .itrace 文件。这种情况被称为“非实时调试”,因为您无法在调试时控制应用程序执行。有关如何使用 IntelliTrace 独立收集器的最新信息,请访问 aka.ms/itracecollector。
总结
新的 IntelliTrace 体验和“诊断工具”窗口的集成可能会令人激动。您可以通过访问 aka.ms/DiagnosticsBlog 随时了解有关上述内容以及其他与诊断相关的功能的最新信息。
Angelos Petropoulos 是 Visual Studio 团队的高级项目经理。在获得面向对象的软件工程硕士学位后,他曾在英国担任过 IT 顾问。移居美国后,他加入了 Visual Studio 的诊断工具团队,现在他已是 IntelliTrace 的项目经理。
衷心感谢以下 Microsoft 技术专家对本文的审阅:Andrew Hall、Daniel Moth、Dan Taylor、Charles Willis