用户如何修复他们的代码中的 Bug?您设置一些断点、在调试器下运行程序、进行一点单步调试 – 并祈求能够轻而易举地发现问题,这样您就能继续处理其他事情。
几乎自 ENIAC 发明以来,我们就一直在进行着同样方式的调试。这种繁琐而耗时的调试方法为我们提供了很好的帮助,但是时候使调试更加轻松了。随着 Visual Studio 2010 Ultimate 的发布,新的 IntelliTrace 功能使开发人员能够更深入地了解应用程序的执行情况,从而使调试进入了 21 世纪。
与其他监视和跟踪工具(例如 Windows Sysinternals 中的 Process Monitor)非常类似,Visual Studio 2010 在应用程序执行时收集有关应用程序的数据,来帮助开发人员诊断错误。收集的数据称为 IntelliTrace 事件。这些事件将在默认调试过程中收集,此外,它们使开发人员能够进行回溯以查看应用程序中发生的情形,而不必重新启动调试器。
在本文中,我将向您介绍 IntelliTrace,并演示它如何在开发人员的日常开发活动中体现出价值。我将演示 IntelliTrace 如何提供在应用程序执行过程中所发生事件的时间线,以及开发人员如何能够使用这些事件来帮助调试。接着,我将论述一些设置,开发人员可以更改这些设置来收集有关应用程序的一组更深层的信息,从而获得完整的执行历史记录。最后,我将演示如何使用其他人(测试人员)创建的以前记录的 IntelliTrace 文件来调试应用程序,而不必运行应用程序来重现错误。
当 Visual Studio 诊断团队开始规划 Visual Studio 2010 时,我们花费了很多时间与客户讨论,了解客户如何诊断其应用程序中的问题。尽管每个人都有不同的方式和喜欢使用的工具集,但有一点是绝对清楚的:传统的应用程序问题诊断方法困难、耗时而且成本高昂。开发人员收到的 Bug 报告几乎从没有任何用于重现问题的步骤,并且大部分都是由像“我正在使用程序,突然间程序崩溃了”这样的语句组成。即使在极个别的情况下提供了有效的重现步骤,也可能会在特定的环境中出现 Bug,而这又会导致出现一组需要解决的全新问题。而且,Bug 通常是由于误解了框架或其他代码的运行方式而导致的。
考虑到这些难题,我们着手创建了一个新的调试器功能,用于在问题发生时收集到正确的信息。我们的目标是为开发人员提供准确的重现步骤和系统环境设置,以及公开他们所使用的框架和代码的行为,从而大幅提高可诊断性。随着 Visual Studio 2010 Ultimate 的发布,IntelliTrace 使开发人员能够更深入地了解应用程序和框架行为,并能够打开由测试人员收集的 IntelliTrace 文件来解决“无法重现”的情况,从而大大改善了调试体验。
当开发人员需要更深入地了解代码执行情况时,IntelliTrace 提供了一种“加速调试”的方式来收集应用程序的完整执行历史记录。
为了阐释这一点,我将使用 Tailspin Toys 演示应用程序来演示 IntelliTrace 可收集的信息类型。首先,我将在 Visual Studio 中打开解决方案并启动调试。当网站启动时,我将导航到“关于我们”页,并收到来自服务器的错误。如何才能诊断问题?如果您像我一样,则您首先想到的是配置 web.config 文件以不显示自定义错误,然后重新启动调试器。但如果此问题是间歇性的,又将如何呢?如果您可以在错误发生后就在此时进入进程,并从 Visual Studio 中获得应用程序中所发生情形的历史记录,是不是很好?
当您进行调试时,IntelliTrace 将在后台收集有关托管应用程序的数据,其中包括来自许多框架组件(例如 ADO.NET、ASP.NET 和 System.XML)的信息。这些 IntelliTrace 事件使开发人员能够查看先前在执行过程中发生的情况,并且最重要的是,能够进行“回溯”以查看应用程序的先前状态,而不必重新启动调试器。当我进入调试器时,我立即看到了按顺序列出的以前收集的 IntelliTrace 事件(请参见图 1)。
图 1 IntelliTrace 收集的诊断信息
正如您可从图 1 中看到的,IntelliTrace 事件的列表不仅仅局限于您在 Process Monitor 中看到的文件和注册表访问。我们为 Visual Studio 2010 定义了将近 150 个 IntelliTrace 事件,并计划随着时间的推移用其他事件扩充此列表。图 2 重点列出了一些 IntelliTrace 所收集事件的类别。
图 2 IntelliTrace 事件可跨 Microsoft .NET Framework 使用
类别 | 描述和收集的数据 |
ADO.NET | 与针对 SQL 执行查询、执行的命令以及连接字符串相关的事件。 |
MVC | 与 ASP.NET 管道以及请求处理和重定向相关的事件。 |
控制台 | 控制台输出。 |
数据绑定 | Windows 窗体数据绑定。 |
环境变量 | 对进程中的环境变量进行求值和检索。 |
文件 | 创建、删除和访问文件。 |
手势 | 用户对 Web 窗体、Windows 窗体和 WPF 中的常见控件执行的操作。除了收集有关与控件的交互的数据之外,单击其中一个事件还会自动将您重定向到相应的事件处理程序。 |
迟缓初始化 | 初始化延迟加载的变量。 |
注册表 | 创建、删除和查询注册表信息。 |
服务模型 | 从 WCF 中进行的 Web 服务调用。 |
线程处理 | 用户工作项和并行计算任务的排队。 |
跟踪 | 调试器跟踪输出和断言。 |
用户提示 | 显示 Windows 窗体和 WPF 消息框以及对话框的结果。 |
工作流 | 实例化和完成活动。 |
XML | XML 文件加载。 |
IntelliTrace 窗口允许我按类别(图 2 中显示的类别)或按线程对所收集事件的列表进行筛选。此外,我可以执行基于文本的搜索来查找可快速跳转到的重要事件。由于 IntelliTrace 还会收集异常,因此我可以搜索词条“异常”,列表将进行筛选以列出导致出现 ASP.NET 错误页的异常,既包括在其中引发的异常,也包括在其中捕获的异常。在本例中,错误是在分析位于第 10 行位置 53 的实体时由 XMLException 导致的(请参见图 3)。当我单击引发的异常事件时,其他调试器窗口(例如“调用堆栈”和“监视”窗口)将显示与事件本身相关的数据,因此就好像引发异常时您正在进行调试一样。此外,就像进入调用堆栈一样,编辑器将打开相应的源文件,并以橙色突出显示与事件对应的代码行以表示 IntelliTrace。
图 3 在分析位于第 10 行位置 53 上的实体时引发了 XMLException
IntelliTrace 为我提供了用于诊断问题的一段很有用的信息:加载了 XML 文件,而该 XML 文件中的特定字符为意外字符或不正确。但我仍然不知道访问了哪个文件。同样,IntelliTrace 收集了我所需的信息(即文件访问)。
再次查看图 3 中的 IntelliTrace 窗口,我可以看到紧靠异常之前的事件是“Content\Xml\Ads.xml”的 XML 文件加载事件。此文件肯定是导致错误的文件。我可以轻松地在 Visual Studio 中打开此文件。查看第 10 行的位置 53,我看到此文件中确实有一个错误,即“&b=1”对于 NavigateUrl XML 元素无效。通过删除这些无效字符,网站现在应可正常加载。
现在,我希望您考虑一下您用传统调试技术调试的上一个未处理异常。如果它是像这一样的异常,您将会看到异常的发生位置,但肯定看不到确切原因或无效字符。这就是 IntelliTrace 的关键所在 – 它为您提供了更好的信息来更快捷轻松地诊断问题。您有更重要的事情要做,而不是浪费时间来四处寻找信息。
我刚刚向您演示了 IntelliTrace 如何收集在应用程序的执行过程中发生的异常(已处理异常和未处理异常),以及如何通过跨框架的 IntelliTrace 事件来深入了解应用程序在后台执行的操作。但 IntelliTrace 的功能并非仅限于此。IntelliTrace 还可收集调试器所导致的事件,即断点、跟踪点和单步执行事件。
最常见的调试技术之一是在您认为存在问题的位置附近设置断点,然后单步执行代码并监视变量变化。在调试循环步骤、监视变量变为特定值时,这一点特别有用。遗憾的是,大多数开发人员都没有耐心,并连续按 F10 键来快速单步执行代码,结果却发现他们单步执行过了头。然后,他们需要重新启动其调试会话并重试。利用 IntelliTrace,将会记录所有断点和单步执行事件,以及这些事件的上下文数据,这样,您就能够快速导航到之前停止的位置。
如果我单击其中一个调试器事件,“监视”窗口将显示我之前查看的所有数据,其中包括我在“局部变量”、“监视”和“自动变量”窗口中计算的值,以及“快速监视”和“数据提示”。
通常,以前开发和部署的代码没有内置所需的跟踪来帮助调试可能出现的问题。利用断点,可以详细了解应用程序在后台所执行的操作。但大多数情况下,开发人员不需要在断点处停止;而是希望收集某些数据并继续执行。在您希望记录迭代器值而不必在每个迭代处停止的循环内,情况尤为如此。在这些情况下,跟踪点是绝佳的替代方案。利用跟踪点,开发人员能够让调试器执行自定义操作;也就是说,执行宏或输出跟踪消息,而不是中断执行。利用 IntelliTrace,将会收集跟踪点输出,并可在与其他 IntelliTrace 事件同样的界面中查看这些输出(请参见图 4)。
图 4 跟踪点可向代码中动态添加跟踪输出
默认情况下,IntelliTrace 配置为仅收集 IntelliTrace 事件。此解决方案开销很低,但不会提供应用程序的完整执行历史记录。如果您需要更深层的信息,则可以配置 IntelliTrace 来收集更多数据。像其他调试器设置一样,IntelliTrace 也可通过“选项”对话框(可从“工具”菜单中访问)进行配置(请参见图 5)。
图 5 可通过“选项”对话框更改 IntelliTrace 设置
通过选择“IntelliTrace 事件和调用信息”,可将 IntelliTrace 配置为不仅收集 IntelliTrace 事件,而且在每个方法进入、退出和调用站点时收集调用信息,例如位于这些位置的参数和返回值。遗憾的是,启用此模式会导致“编辑并继续”在调试会话过程被禁用。很明显,您选择收集更多信息意味着应用程序的开销更高。我们致力于找到有用的信息和性能之间的平衡点,但我们将完全控制权给予您。
每个调试会话都会创建一个存储在磁盘上的 IntelliTrace 文件,当 Visual Studio 关闭时,将会自动清理该文件。利用 IntelliTrace 的“高级”窗格(通过“选项”对话框访问),您可以配置要将在调试过程中创建的文件存储在何处,以及这些文件的最大文件大小。如果达到最大文件大小,则会使用一个循环缓冲区来帮助压缩和截断存储在 IntelliTrace 日志中的信息,从而减小日志文件在磁盘上占用的空间。利用“高级”窗格上的两项其他设置,您可以隐藏导航装订线,并禁用可用符号的 Team Foundation Server (TFS) 查找。
由于存在性能问题,因此默认情况下只会为集合启用定义的 IntelliTrace 事件的一个子集。您可能需要考虑启用的某些事件包括控制台输出、文件访问、迟缓初始化、注册表访问以及线程处理事件。您可以从“选项”对话框的“IntelliTrace 事件”窗格中启用或禁用整个事件类别或个别事件。
最后一个 IntelliTrace“选项”窗格允许您控制 IntelliTrace 从哪些模块中收集数据。默认情况下,将会收集除 Microsoft 作为 Microsoft .NET Framework 和 Visual Studio 一部分提供的模块之外的所有其他模块。采用这种方式收集到的数据可能非常多,因此您可以考虑将此列表更改为一个包含列表,并仅指定您关注的模块。相反,如果您使用某些常见的第三方库,则可能希望排除这些模块,因为它们超出了您的控制范围。
现在,我已验证了“关于我们”页在演示应用程序中可正常工作,让我们确保购物车同样也工作正常。当我将一架纸飞机添加到购物车进行购买时,我看到它确实已正常添加到购物车。但是,当我重复此操作将该物品的第二个实例添加到购物车时,数量仍然显示为 1,然而我预计数量会显示为 2。我希望使用调试器来解决问题而无需重新启动,但 IntelliTrace 事件的列表在这种情况下可能无助于事(所有工作都是在我的代码而不是 .NET Framework 中完成的)。在这里,IntelliTrace 的“IntelliTrace 事件和调用信息”模式可以帮助显示我的应用程序的执行历史记录。让我们进入调试器,看看我可以使用 IntelliTrace 的哪些其他功能来解决此问题。
我首先假设我的“将物品添加到购物车”逻辑出现了问题。由于此应用程序是一个模型-视图-控制器 (MVC) 应用程序,因此没有用于按钮单击的事件处理程序,而是会发送一个 POST 消息并由 MVC 处理。此 POST 消息已记录为一个 IntelliTrace 事件,并且,尽管该事件本身没有作用,但我使用它作为起点来进行调查。通过单击此 IntelliTrace 事件,我可以跳转到调试会话中“添加物品”逻辑的开始位置。通过在 IntelliTrace 窗口中搜索单词“POST”,我可以快速找到这些 POST 消息。用户进行了此操作两次,因此,不出所料,搜索返回了两个结果。选择第二个事件之后,如果清除搜索字段,则会在包含所有已收集事件的上下文中显示该事件(请参见图 6)。
图 6 “IntelliTrace 事件”视图可帮助您开始进行调查
既然我有了有关我处于应用程序时间线的何处的上下文,那么我希望进一步深入了解所进行的方法调用。从“事件”视图中,我可以切换视图以查看执行历史记录。通过单击窗口顶部的“切换到 IntelliTrace 调用视图”链接,我可以转换到“调用”视图以查看应用程序的完整执行历史记录(请参见图 7)。我可以随时通过单击“切换到 IntelliTrace 事件视图”链接返回到“事件”视图。
图 7 IntelliTrace 的“调用”视图显示应用程序的执行历史记录
用于浏览执行历史记录的一种机制是使用“调用”视图深入了解您在调查时感兴趣的调用。每次双击“调用”视图下半部分中的某个调用时,该调用将弹出到视图的上半部分,并且指令指针将在代码编辑器中与调用的方法入口点同步,就像您进入调用堆栈时的实时调试一样。您可以继续导航,并采用这种方式在 IntelliTrace 历史记录收集的数据中向后和向前浏览。通过浏览“调用”视图这种机制,可以快速了解执行历史记录的概况,并在代码库中进行大幅跳转。
使用“调用”视图是唯一的导航方法。我还可以通过单步执行代码来进行导航。在源代码窗口的装订线中,有一组新的 DVR 样式的控件,您可以利用这些控件来单步执行代码,就像在传统的调试会话中一样(请参见图 8)。由于我处于 IntelliTrace 调试模式中,因此单步执行由在每次调用站点、函数进入或函数退出时发生的事件记录。当然,如果您喜欢使用键盘控制,F10/F11 也可按预期方式工作。
图 8 导航栏提供了 DVR 样式的控件,使您能够单步执行应用程序
这两种导航方法非常适合于调查,但有时您确切知道将在何处设置断点。对于此应用程序,我知道将物品添加到购物车的函数的名称:Kona.Model.ShoppingCart::AddItem。我真正要做的是跳转到第二次调用此函数的位置,并检查传入函数和从函数返回的值。更具体地说,我希望搜寻进行“AdjustQuantity”函数调用的代码行。当然,IntelliTrace 也支持这种导航技术。
在编辑器中,我可以右键单击要搜寻的行,并从上下文菜单中选择“在 IntelliTrace 中搜索此行”(请参见图 9)。搜索将开始,并且结果将呈现在编辑器窗口顶部的搜索栏中,从而使我能够在搜索结果之间导航。与第二个搜索结果同步后,我的指令指针恰好位于我希望的位置,并且我可以使用其他调试器窗口来调查调用(请参见图 10)。
图 9 搜索功能使我能够恰好跳转到特定的函数调用
图 10 搜索结果显示在编辑器窗口顶部的搜索结果栏中
此时查看“局部变量”窗口,您可以看到购物车正在被调整,以使购物车中产品的新数量为 1。这是 Bug;我预期的调整后的数量为 2。查看代码行,新的 Quantity 参数不仅应考虑“item.Quantity”,而且应考虑传入函数调用的“quantity”变量。解决方法是将函数调用更改为:
AdjustQuantity(product, item.Quantity + quantity);
测试人员和开发人员经常会遇到“无法重现”情形,在这种情形中,测试人员会提交指出发生了某种错误的 Bug,最终又归结于一句注解“它没有在我的计算机上重现”。开发人员和测试人员都不想遇到这种情形,但他们没有适当的工具集来帮助他们传达出现故障时所发生的情况。我们之所以将系统设计为 IntelliTrace 在调试会话期间向开发人员提供的诊断信息与通过 Microsoft Test Manager (MTM) 执行手动测试时可收集的诊断信息相同,这就是原因所在。
让我们重新探讨我调试的第一个情况,即“关于我们”页未能正常呈现的情况。如果测试人员提交了此 Bug,它的标题将可能是诸如“查看关于页时发生应用程序错误”之类的标题,或许,如果开发人员幸运的话,还会附上错误的屏幕截图。如果您当前收到像这样的 Bug,可能会如何对其进行调试?您很有可能会从源代码管理中加载应用程序的解决方案,并在您的开发人员计算机上重现问题。在本例中,您将能够调试和解决问题,但想一想这种方法花费了多少时间。或者,如果问题是由您的计算机和测试人员的计算机之间的配置不同导致的,又该怎么办?就工作效率而言,开发人员在调试问题时的工作效率要比建立重现环境时高得多。
利用 Visual Studio 2010,MTM 使测试人员能够方便地创作、管理和执行手动测试和自动测试。但该工具的功能并非仅限于此。MTM 使测试人员能够让诊断数据适配器在测试执行的同时在后台收集信息。例如,您可以自动收集所执行测试的录像,以便开发人员能够看到与测试人员完全相同的情形。其他收集器能够从所测试的框架中收集系统信息和事件日志。
我们增加了 IntelliTrace 诊断数据适配器,以便自动在后台收集 IntelliTrace 文件。当测试人员的某个测试步骤失败并选择提交 Bug 时,收集的 IntelliTrace 文件将自动上载到 TFS 并链接到该 Bug。这样将可为开发人员提供一组更为丰富的用于调试的信息,而不仅仅是一组重现步骤。当开发人员打开链接到 Bug 的 IntelliTrace 文件时,他将体验到与调试小型转储类似的感觉,但却是有关整个应用程序时间线的转储,而不仅仅是应用程序崩溃时的转储。可以为本地或远程测试代理上的任何托管应用程序(包括运行于 IIS 下的 ASP.NET 应用程序)收集 IntelliTrace 数据。
当我打开 TFS Bug 时,MTM 已根据测试人员描述的手动测试步骤自动添加了重现步骤。更酷的是,如果我查看 Bug 的“所有链接”选项卡(请参见图 11),我可以看到同时收集的 IntelliTrace 文件。通过双击此文件,我将看到 IntelliTrace 文件的摘要页视图(请参见图 12)。
图 11 可通过“所有链接”选项卡访问收集的 IntelliTrace 文件
图 12 IntelliTrace 摘要提供了所收集数据的概览
摘要页为我提供了许多有关 IntelliTrace 文件所包含内容的信息。在该页的顶部,我可以看到应用程序中各个线程的时间线视图。如果我展开摘要页的“线程列表”部分,我可以看到以表格格式显示的相同数据。如果双击某个线程,将会从 IntelliTrace 文件中启动调试会话,并将我的指令指针置于该线程的开头。
我还将在 IntelliTrace 文件中看到收集的所有异常的列表。如果我选择某个异常,线程时间线上将出现一条橙色竖线,显示异常发生的时间点。如果双击某个异常,将会启动 IntelliTrace 调试会话,并且将在“IntelliTrace 事件”视图中选择异常事件。在摘要页上进一步向下看,我可以看到与手动重现步骤匹配的所有测试事件的列表。还会显示每个步骤的结果。单击其中某个事件也会向线程时间线上放置一条竖线,而双击事件将会启动 IntelliTrace 调试会话,同时选择尽可能最近的事件。摘要页的底部显示收集的有关计算机(测试的应用程序在该计算机上运行)的系统信息的列表,该列表下面列出了加载到进程中的所有模块及其文件路径。
您可能会问自己,它是否适合于发布版?当然可以!此外,在收集或查看 IntelliTrace 数据时,您无需访问符号。符号只有在将收集的数据链接到源文件时才是必要的,以便它们在您打开调试会话时有所帮助。
Visual Studio 2010 和 TFS 还增加了对符号和源服务器的支持,因此,如果您的应用程序是作为 TFS 生成系统的一部分生成的,则会按照 IntelliTrace 的需要自动从 TFS 下载正确版本的符号和源代码。您甚至不必知道符号服务器路径。
从摘要页中启动调试会话后,调试器的工作方式就好像实时调试会话一样(请参见图 13)。您可以使用大多数调试器窗口,并可以导航到 IntelliTrace 收集的任何数据。
图 13 当调试从摘要页中启动时,Visual Studio 的运行方式就像实时调试会话一样
在本文中,您了解了 IntelliTrace 如何能够大幅改善您的日常开发活动,并提升您快速轻松诊断问题的能力,而不必重新启动应用程序和使用传统的“中断-单步执行-检查”技术进行调试。我还向您介绍了组织如何能够通过在测试过程中收集 IntelliTrace 数据来减少“无法重现”的 Bug 数,从而使开发人员能够脱机调试问题,而无需访问实时重现。这只是功能的简要介绍,当您越来越熟悉 IntelliTrace 的强大功能时,它将开始改变您的调试方式。
Justin Marks 在麻省理工学院获得计算机科学与工程学士学位后,于 2002 年加入 Microsoft。他作为系统工程师参与过 MSN.com 的开发工作,作为测试软件设计工程师参与过 Windows 的开发工作,现在作为项目经理参与 Visual Studio 的开发工作。作为诊断团队的项目经理,Marks 一直参与开发 Visual Studio 2010 下一版本的 IntelliTrace 功能。