开发图形探查器Python FunctionTrace





今天,我们将与您分享FunctionTrace的创建者的文章翻译,FunctionTrace是具有直观图形界面的Python分析器,可以对多处理器和多线程应用程序进行分析,并且比其他Python分析器使用的资源要少一个数量级。无论您只是学习使用Python进行Web开发还是已经使用了很长时间,都没关系-了解您的代码在做什么总是一件好事。关于这个项目的外观,关于其发展的细节-进一步削减。






介绍



Firefox ProfilerProject Quantum时代Firefox的基石。打开示例条目时,您会看到一个基于Web的强大性能分析界面,其中包括调用树,堆栈图,启动图等。所有数据过滤,缩放,切片和转换操作均保存在可共享的URL中。可以在错误报告中共享结果,可以将发现的结果记录下来,与其他记录进行比较,或者可以将信息传递进行进一步研究。Firefox DevEdition具有内置的分析线程。这种流程使交流变得容易。我们的目标是使所有开发人员(即使在Firefox之外)也能够高效地进行协作。



以前,Firefox Profiler导入了其他格式,从Linux perfChrome profile开始随着时间的推移,开发人员添加了更多的格式。如今,出现了第一个项目,以使Firefox适应分析工具。FunctionTrace就是这样的一个项目。马特(Matt)讲述了该乐器的制造过程。



功能跟踪



我最近创建了一个工具,以帮助开发人员更好地了解其Python代码中发生的事情。FunctionTrace是Python的无采样探查器,可在未经修改的应用程序上以极低的开销(不到5%)运行。重要的是要注意,它与Firefox Profiler集成在一起。这使您可以通过图形方式与配置文件进行交互,从而更容易发现模式并更改代码库。



我将介绍FunctionTrace的开发目标,并分享技术实现细节。最后,我们将进行一些演示





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



科技债务是动力



随着时间的推移,代码库往往会变得更大。特别是在有很多人的复杂项目上工作时。一些语言更好地解决了这个问题。例如,Java IDE的功能已经存在了数十年。或Rust及其强类型,这使得重构非常容易。有时随着其他语言的代码库的增长,它变得越来越难以维护。对于较旧的Python代码尤其如此。至少我们现在都是Python 3,对吗?



进行大规模更改或重构不熟悉的代码可能非常困难。当我看到程序的所有交互及其作用时,对我来说更容易正确地更改代码。通常,我什至发现自己在重写我从未打算接触的代码:当我在可视化中看到它时,效率很低。



我想了解代码中发生了什么,而不必读取数百个文件。但是找不到适合我需求的工具。另外,由于涉及大量的UI工作,我自己对构建这样的工具也失去了兴趣。并且界面是必需的。当我偶然发现Firefox分析器时,重新燃起了对程序执行快速了解的希望。



探查器提供了所有难以实现的元素-一个直观的开源用户界面,该界面显示堆栈图,限时日志标记,火警图表,并提供稳定性,其本质已与知名的Web浏览器绑定。任何可以编写格式正确的JSON概要文件的工具都可以重用前面提到的所有图形分析功能。



FunctionTrace设计



幸运的是,发现Firefox分析器后,我已经计划了一个星期的假期。我有一个朋友想和我一起开发乐器。那一周他也请了一天假。



目标



当我们开始开发FunctionTrace时,我们有几个目标:



  1. 能够查看程序发生的一切的能力
  2. .
  3. , .


第一个目标对设计产生了重大影响。最后两个增加了工程复杂性。我们都从过去使用类似工具的经验得知,令人沮丧的是我们不会看到太短的函数调用。当您记录一个1ms的跟踪记录时,但是您具有重要且快速的功能,则您会丢失程序内部发生的许多事情。



我们还知道我们需要跟踪所有函数调用。因此,我们不能使用采样分析器。另外,我最近花了一些时间编写代码,在这些代码中,Python函数通常通过外壳中间人脚本执行其他Python代码。基于此,我们希望能够跟踪子进程。



初步实施



为了支持多个进程和后代,我们选择了客户端-服务器模型。 Python客户端将跟踪数据发送到Rust服务器。服务器会在生成配置文件之前聚合并压缩数据,该配置文件可由Firefox探查器使用。我们选择Rust的原因有很多,包括强类型化,为获得一致的性能和可预测的内存使用而努力,以及易于原型设计和重构。



我们将客户端原型化为名为的Python模块python -m functiontrace code.py。这使得使用内置的跟踪钩子来记录执行变得容易。原始实现如下所示:



def profile_func(frame, event, arg):
    if event == "call" or event == "return" or event == "c_call" or event == "c_return":
        data = (event, time.time())
        server.sendall(json.dumps(data))

sys.setprofile(profile_func)




服务器正在Unix域套接字上侦听。然后,从客户端读取数据,然后由Firefox分析器将其转换为JSON



探查器支持各种类型的探查,例如perf日志。但是我们决定生成内部探查器格式的JSON。与添加新的受支持格式相比,它需要的空间和维护更少。重要的是要注意,探查器保持了两个探查版本之间的向后兼容性。这意味着,为将来的版本而设计的用于该格式当前版本的任何配置文件都将自动转换为最新版本。探查器还引用带有整数标识符的字符串。通过使用重复数据删除,可以节省大量空间(使用起来很简单)indexmap)。



几种优化



大多数情况下,原始代码都能正常工作。在每次函数调用和返回时,Python都会调用该钩子。挂钩通过套接字将JSON消息发送到服务器,以将其转换为所需的格式。但是,速度非常慢。即使在批处理套接字调用之后,我们仍然看到某些测试程序的开销至少是其八倍。



在看到了这些成本之后,我们使用了PythonC API进入了C级别他们在相同程序上的开销系数为1.1。之后,我们可以进行另一项关键的优化,通过替换time.time()rdtsc操作的调用clock_gettime()... 我们减少了调用多个指令并生成64位数据的性能开销。它比在关键任务路径上链接Python调用和复杂算法要有效得多。



我提到支持跟踪多个线程和子进程。这是客户端最困难的部分之一,因此值得讨论一些较低级别的细节。



支持多种流



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



// This is installed as the setprofile() handler for new threads by
// threading.setprofile().  On its first execution, it initializes tracing for
// the thread, including creating the thread state, before replacing itself with
// the normal Fprofile_FunctionTrace handler.
static PyObject* Fprofile_ThreadFunctionTrace(..args..) {
    Fprofile_CreateThreadState();

    // Replace our setprofile() handler with the real one, then manually call
    // it to ensure this call is recorded.
    PyEval_SetProfile(Fprofile_FunctionTrace);
    Fprofile_FunctionTrace(..args..);
    Py_RETURN_NONE;
}




当钩子被调用时Fprofile_ThreadFunctionTrace(),它分配结构ThreadState该结构包含线程记录事件并与服务器通信所需的信息。然后,我们将初始化消息发送到配置文件服务器。在这里,我们通知服务器开始新的流,并提供一些初始信息:时间,PID等。初始化之后,我们将钩子替换为Fprofile_FunctionTrace()执行实际跟踪的钩子



对子进程的支持



当使用多个进程时,我们假设子进程是通过Python解释器启动的。不幸的是,没有使用调用子进程-m functiontrace,因此我们不知道如何跟踪它们。为了确保监视子进程,在启动时更改$ PATH环境变量。这可以确保Python指向知道以下内容的可执行文件functiontrace



# Generate a temp directory to store our wrappers in.  We'll temporarily
# add this directory to our path.
tempdir = tempfile.mkdtemp(prefix="py-functiontrace")
os.environ["PATH"] = tempdir + os.pathsep + os.environ["PATH"]

# Generate wrappers for the various Python versions we support to ensure
# they're included in our PATH.
wrap_pythons = ["python", "python3", "python3.6", "python3.7", "python3.8"]
for python in wrap_pythons:
    with open(os.path.join(tempdir, python), "w") as f:
        f.write(PYTHON_TEMPLATE.format(python=python))
        os.chmod(f.name, 0o755)




带参数的解释器-m functiontrace在包装器内部被调用。最后,在启动时添加环境变量。该变量指示使用哪个套接字与概要文件服务器进行通信。如果客户端初始化并看到已经设置的环境变量,则它将识别子进程。然后,它连接到现有服务器实例,从而使其跟踪与原始客户端的跟踪相关。



FunctionTrace现在



今天,FunctionTrace的实现与上述实现有很多共同点。在较高级别,通过FunctionTrace跟踪客户,如下所示:python -m functiontrace code.py。这行代码会加载Python模块进行一些自定义,然后调用C模块来设置各种跟踪挂钩。这些钩子包括上面提到的sys.setprofile内存分配钩子,以及具有有趣功能的自定义钩子,例如builtins.printbuiltins.__import__。此外,functiontrace-server还产生了一个实例,并设置了一个套接字与之通信,并确保以后的线程和子进程与同一服务器通信。



在每个跟踪事件上,Python客户端都会发送一个MessagePack条目... 它在流存储缓冲区中包含最少的事件信息和时间戳。当缓冲区已满(每128KB)时,它将通过共享套接字刷新到服务器,并且客户端继续执行其工作。服务器异步侦听每个客户端,快速将跟踪消耗到单独的缓冲区中,以避免阻塞它们。然后,与每个客户端相对应的线程可以解析每个跟踪事件,并将其转换为适当的最终格式。所有连接的客户端退出后,每个主题的日志将合并为完整的配置文件日志。最后,所有这些都发送到一个文件中,然后可以与Firefox的探查器一起使用。



得到教训



Python C模块可提供更大的功能和性能,但同时成本很高。需要更多的代码,很难找到好的文档,很少有可用的功能。 C模块似乎是编写高性能Python模块的未充分利用的工具。我说这是基于我见过的一些FunctionTrace配置文件。我们建议保持平衡。用Python编写大多数性能不佳的关键任务代码,并为Python不发光的程序部分调用内部循环或C设置代码。



当不需要可读性时,JSON编码和解码可能会非常慢。我们切换到MessagePack进行客户端与服务器之间的通信,发现它使用起来非常容易,同时将一些基准测试的时间缩短了一半!



在Python中支持多线程分析非常困难。可以理解为什么它不是以前的Python分析器中的关键功能。在我们对如何在保持高性能的同时使用GIL有了一个很好的了解之前,它采取了几种不同的方法以及很多分割错误。



图片


您可以通过在线SkillFactory课程从头开始获得所需的职业或技能和薪资水平的提升:





E







All Articles