构建FunctionTrace,一个图形化的Python分析器

Python部落(python.freelycode.com)组织翻译,禁止转载,欢迎转发。

Firefox Profiler用于性能分析

哈拉尔德的介绍

在Project Quantum时代,Firefox Profiler成为Firefox性能工作的基石。当你打开一个例子记录,你首先看到的界面特色调用树,堆栈图表,火焰图形和更强大的基于网络的性能分析。所有数据过滤,缩放,切片,转换操作均保留在可共享的URL中。您可以分享它的错误,记录发现的结果,将其与其他记录并排比较,或移交给其他研究。Firefox DevEdition偷窥了内置的分析流程,使记录和共享毫不费力。我们的目标是授权所有开发人员就性能进行协作,甚至超越Firefox。

早期,Firefox Profiler可以导入其他格式,从Linux性能和Chrome的配置文件开始。随着时间的推移,各个开发人员添加了更多格式。如今,出现了第一个采用Firefox作为分析工具的项目。FunctionTrace就是其中之一,在这里,Matt讲述了他如何构建它的故事。

认识FunctionTrace,这是Python代码的探查器

马特的专案

我最近构建了一个工具,以帮助开发人员更好地了解其Python代码的功能。FunctionTrace是用于Python的非采样探查器,可在未经修改的Python应用程序上以极低的开销(<5%)运行。重要的是,它已与Firefox Profiler集成在一起。这使您能够以图形方式与概要文件进行交互,从而更容易发现模式并改进代码库。

在本文中,我将讨论为什么我们构建FunctionTrace,并分享其实现的一些技术细节。我将展示这样的工具如何将Firefox Profiler作为强大的开源可视化工具。接下来,您还可以玩一个小样的演示!


1.png

在Firefox Profiler中打开的FunctionTrace配置文件的示例

以技术债务为动力

随着时间的推移,代码库往往会变得越来越大,尤其是在与许多人一起从事复杂项目时。某些语言对此提供了强大的支持,例如具有数十年构建的IDE功能的Java或具有强大的类型系统的Rust,使重构变得轻而易举。其他语言的代码库有时会变得越来越难以维护。在较旧的Python代码库中尤其如此(至少我们现在都在使用Python 3,对吗?)。

进行广泛的更改或重构您不熟悉的代码可能非常困难。相比之下,当我能够看到程序在做什么及其所有交互时,可以更轻松地进行正确的更改。通常,我什至发现自己对我从未打算接触的代码进行了改进,因为在屏幕上显示效率低下时,效率低下变得非常明显。

我希望能够理解我正在使用的Python代码库在做什么,而无需阅读数百个文件。我无法找到令人满意的适用于Python的现有工具,而且由于需要进行大量的UI工作,我自己对自己构建工具的兴趣大打折扣。但是,当我偶然发现Firefox Profiler时,重新点燃了快速理解程序执行的希望。

Profiler提供了所有“硬”部分–直观的开源UI,可以显示堆栈图,与时间相关的日志标记,火焰图以及与主要Web浏览器绑定的稳定性。任何能够发出格式正确的JSON概要文件的工具都将能够重用所有前面提到的图形分析功能。

FunctionTrace的设计

幸运的是,在发现Firefox Profiler之后,我已经计划了几天的假期。我认识另一个朋友,他有兴趣与我一起开发它,并在该周休假。

目标

开始构建FunctionTrace时,我们有几个目标:

1、使您能够查看程序中发生的一切。

2、处理多线程/多进程应用程序。

3、足够低的开销,我们可以在不牺牲性能的情况下使用它。

第一个目标对设计有重大影响,而后两个目标则增加了工程复杂性。根据过去使用此类工具的经验,我们俩都知道无法看到太短的函数调用而感到沮丧。当您以1ms采样但具有比该函数运行速度更快的重要功能时,您会错过程序内部正在发生的重要事情!

结果,我们知道我们需要能够跟踪所有函数调用,并且不能使用采样分析器。另外,我最近在一个代码库中度过了一段时光,在该代码库中,Python函数将与exec其他Python代码一起使用(通常通过中介shell脚本)。由此,我们知道我们也希望能够跟踪后代Python进程。

初步实施

为了支持多个进程和后代,我们选择了客户端-服务器模型。我们将检测Python客户端,该客户端会将跟踪数据发送到Rust服务器。在生成供Firefox Profiler使用的配置文件之前,服务器将聚合并压缩数据。我们选择Rust的原因有很多,包括强大的类型系统,对稳定性能和可预测内存使用的渴望,以及易于原型设计和重构的需求。

我们将客户端原型化为Python模块,称为via python -m functiontrace code.py。这使我们可以轻松地使用Python的内置跟踪钩子来记录执行的内容。最初的实现看起来非常类似于以下内容:

2.png

对于服务器,我们在Unix域套接字上侦听客户端连接。然后,我们从客户端读取数据并将其转换为Firefox Profiler的JSON格式。

Firefox Profiler支持各种配置文件类型,例如perf logs。但是,我们决定直接发出探查器的内部格式。与添加新的受支持格式相比,它需要的空间和维护更少。重要的是,Firefox Profiler保持了对配置文件版本的向后兼容性。这意味着我们将来发布的任何以当前格式版本为目标的配置文件都将自动转换为最新版本。此外,探查器格式通过整数ID引用字符串。这样可以通过重复数据删除节省大量空间(而使用indexmap可以轻松实现)。

一些优化

通常,最初的基础有效。在每个函数调用/返回上,Python都会调用我们的钩子。然后,该挂钩将通过套接字发送JSON消息,以使服务器转换为正确的格式。但是,它非常慢。即使在批处理套接字调用之后,我们在某些测试程序中也观察到至少8倍的开销!

在这一点上,我们下降到下使用Python的C API来代替。在相同程序上,我们的开销降至1.1倍。在那之后,我们可以通过更换呼叫做另一个关键的优化time.time()与rdtsc通过操作clock_gettime()。我们将函数调用的性能开销减少了几条指令,并释放了64位数据。这比在关键路径中使用一连串Python调用和复杂算术要有效得多。

我已经提到我们支持跟踪多个线程和后代进程。由于这是客户端中比较困难的部分之一,因此有必要讨论一些底层细节。

支持多线程

我们通过来在所有线程上安装处理程序threading.setprofile() :(注意:我们在设置线程状态时通过这样的处理程序进行注册,以确保Python正在运行并且当前已保存GIL。这使我们可以简化一些假设。)

3.png

当我们的Fprofile_ThreadFunctionTrace()钩子被调用时,它分配一个struct ThreadState,其中包含线程记录事件并与服务器通信所需的信息。然后,我们向配置文件服务器发送一条初始消息。在这里,我们通知它新线程已启动并提供一些初始信息(时间,PID等)。初始化之后,我们将钩子替换为Fprofile_FunctionTrace(),这将在将来进行实际跟踪。

支持后代进程

当处理多个进程时,我们假设子进程正在通过python解释器运行。不幸的是,不会用来调用这些子项-m functiontrace,因此我们不会跟踪它们。为了确保跟踪子进程,在启动时我们修改了$PATH环境变量。反过来,这确保python指向指向知道要加载的可执行文件functiontrace。

4.png

在包装器内部,我们只需要python使用的附加参数调用实际的解释器-m functiontrace。为了完善这种支持,我们在启动时添加了一个环境变量。该变量表示我们用于与概要文件服务器通信的套接字。如果客户端初始化并看到此环境变量已设置,则它将识别后代进程。然后,它连接到现有服务器实例,使我们能够将其跟踪与原始客户端的跟踪相关联。

当前实施

今天,FunctionTrace的总体实现与上述描述有许多相似之处。在较高级别上,当以调用时,将通过FunctionTrace跟踪客户端python -m functiontrace code.py。这将为某些设置加载一个Python模块,然后调用我们的C模块以安装各种跟踪挂钩。这些挂钩包括上述sys.setprofile挂钩,内存分配挂钩和各种“有趣”功能(如builtins.print或)上的自定义挂钩builtins.__import__。此外,我们产生一个functiontrace-server实例,设置一个与之对话的套接字,并确保将来的线程和后代进程将与同一服务器对话。

在每次跟踪事件中,Python客户端都会发出一条小的MessagePack记录。该记录包含最少的事件信息和线程本地内存缓冲区的时间戳。当缓冲区填满(每128KB)时,它将通过共享套接字转储到服务器,并且客户端继续执行。服务器异步侦听每个客户端,将它们的跟踪日志快速消耗到一个单独的缓冲区中,以避免阻塞它们。然后,与每个客户端相对应的线程便能够解析每个跟踪事件,并将其转换为正确的结束格式。一旦所有连接的客户端退出,每个线程的日志将被汇总为完整的配置文件日志。最后,它被发送到文件中,然后可以与Firefox Profiler一起使用。

得到教训

拥有Python C模块可以显着提高功能和性能,但会带来成本。它需要更多的代码,很难找到好的文档;而且很少有功能易于访问。尽管C模块似乎是编写高性能Python模块的未充分利用的工具(基于我见过的一些FunctionTrace配置文件),但我们还是建议您保持平衡。用Python编写大多数非性能关键代码,并用C调用内部循环或设置代码,以处理Python不发光的部分。

当不需要人类可读的方面时,JSON编码/解码可能会非常慢。我们切换到MessagePack进行客户端-服务器通信,发现它既易于使用,又将我们的基准测试时间缩短了一半!

Python中对多线程分析的支持非常繁琐,因此可以理解为什么它似乎并不是以前的主流Python分析器中的关键功能。在我们对如何在保持高性能的前提下在GIL周围进行操作有很好的了解之前,它采取了几种不同的方法和许多段错误。

请扩展探查器生态系统!

没有Firefox Profiler,该项目将不存在。为未经证实的性能工具创建复杂的前端可能太耗时了。我们希望看到其他针对Firefox Profiler的项目,既可以像FunctionTrace一样添加对Profiler格式的本机支持,也可以提供对自己格式的支持。尽管FunctionTrace尚未完全完成,但我希望在此博客上共享它可以使其他狡猾的开发人员意识到Firefox Profiler的潜力。Profiler为一些关键的开发工具提供了绝佳的机会,使其可以从命令行移到更适合于快速提取相关信息的GUI中。


英文原文:https://hacks.mozilla.org/2020/05/building-functiontrace-a-graphical-python-profiler/
译者:魂ф逝