探查器如何在Ruby和Python中工作?

本文的翻译是在预期高级课程“ Python Developer”的开始时准备的



原始文章可以在这里阅读










你好!作为Ruby Profiler的开胃酒,我想谈谈现有Ruby和Python Profiler的工作方式。它还将帮助回答许多人问我的问题:“如何编写分析器?”



在本文中,我们将重点介绍处理器分析器(而不是内存/堆分析器)。我将介绍一些用于编写探查器的基本方法,提供代码示例,并展示Ruby和Python中流行的探查器的许多示例,并向您展示它们是如何工作的。



可能是文章中有错误(在准备编写本文时,我部分浏览了14个库的代码以进行概要分析,并且到目前为止我还不熟悉它们),因此,如果找到它们,请告诉我。 ...



2种类型的探查器



处理器分析器主要有两种类型:采样分析器跟踪分析器。



跟踪分析器记录程序中的每个函数调用,最终提供报告。采样分析器采用统计方法,它们每隔几毫秒写入一次堆栈,并基于此数据生成报告。



使用采样探查器而不是跟踪探查器的主要原因是因为它是轻量级的。您每秒可以拍摄20或200张照片-不需要很多时间。如果您遇到严重的性能问题(花费80%的时间花费在调用一个缓慢的函数上),这些分析器将非常有效,因为每秒200张快照足以识别问题函数!



分析器



接下来,我将提供本文中讨论的探查器的一般摘要(从此处开始)。稍后我将解释本文中使用的术语(setitimerrb_add_event_hookptrace)。有趣的是,所有分析器都是使用一小组基本功能实现的。



Python分析器







“ Gdb hacks”实际上不是Python探查器,它链接到一个网站,该网站解释了如何将hacker探查器实现为gdb的外壳程序脚本包装这是专门针对Python的,因为更高版本的gbd实际上为您部署Python堆栈。穷人喜欢发烧



Ruby分析器







几乎所有这些探查器都存在于您的进程中,



在深入了解这些探查器之前,有一件非常重要的事情-除pyflame之外所有这些探查器都在Python / Ruby进程中运行。如果您位于Python / Ruby程序中,通常可以轻松访问堆栈。例如,下面是一个简单的Python程序,该程序打印每个正在运行的线程的堆栈内容:



import sys
import traceback

def bar():
    foo()

def foo():
    for _, frame in sys._current_frames().items():
        for line in traceback.extract_stack(frame):
            print line

bar()


这是控制台输出。您可以看到它具有来自堆栈的函数名称,行号,文件名-如果进行概要分析,则可能需要它。



('test2.py', 12, '<module>', 'bar()')
('test2.py', 5, 'bar', 'foo()')
('test2.py', 9, 'foo', 'for line in traceback.extract_stack(frame):')


在Ruby中甚至更简单:您可以使用puts调用程序来获取堆栈。



这些探查器大多数是C的性能扩展,因此它们略有不同,但是针对Ruby / Python程序的此类扩展也可以轻松访问调用堆栈。



跟踪探查器如何工作



在上表中列出了所有Ruby和Python跟踪配置文件:rblineprof,ruby-prof,line_profiler和cProfile。它们都以类似的方式工作。它们记录每个函数调用,并且是C扩展以减少开销。



它们如何工作?在Ruby和Python中,您都可以指定在发生各种解释器事件(例如“函数调用”或“代码执行行”)时触发的回调。调用回调时,它将写入堆栈以供以后分析。



准确查看这些回调在代码中的位置会很有帮助,因此我将链接到github上的相关代码行。



在Python中,您可以使用PyEval_SetTrace自定义回调PyEval_SetProfile。在文档部分中对此进行了描述在Python中进行性能分析和跟踪它说:“PyEval_SetTrace类似于PyEval_SetProfile跟踪功能接收行号事件。”



代码:



  • line_profiler使用PyEval_SetTrace以下命令设置其回调:请参见line_profiler.pyx157
  • cProfile使用PyEval_SetProfile以下命令设置其回调:参见_lsprof.c 第693行(cProfile是使用lsprof实现的


在Ruby中,您可以使用来自定义回调rb_add_event_hook我找不到任何相关文档,但这就是它的外观



rb_add_event_hook(prof_event_hook,
      RUBY_EVENT_CALL | RUBY_EVENT_RETURN |
      RUBY_EVENT_C_CALL | RUBY_EVENT_C_RETURN |
      RUBY_EVENT_LINE, self);


签名prof_event_hook



static void
prof_event_hook(rb_event_flag_t event, VALUE data, VALUE self, ID mid, VALUE klass)




类似于PyEval_SetTracePython,但形式更灵活-您可以选择要通知哪些事件(例如“仅函数调用”)。



代码:



  • ruby-prof rb_add_event_hook : ruby-prof.c 329
  • rblineprof rb_add_event_hook : rblineprof.c 649


tracing-



跟踪以这种方式实现的探查器的主要缺点是,它们为执行的每个函数/行调用添加了固定数量的代码。它可能使您做出错误的决定!例如,如果您对某事进行两种实现-一种实现具有很多函数调用,而另一种实现则花费相同的时间,则性能分析时,第一种实现具有很多函数调用的速度会变慢。



为了说明这一点,我创建了一个名为test.py以下内容的小文件,并比较了执行时间python -mcProfile test.pypython test.pypython. test.py在大约0.6 s和python -mcProfile test.py大约1 s内完成。因此,对于此特定示例,我cProfile添加了额外的〜60%的开销。

该文档cProfile说:

Python的解释性质增加了太多的运行时开销,以至于确定性分析倾向于在普通应用程序中增加一点处理开销。


这似乎是一个很合理的声明-前面的示例(进行了350万个函数调用,仅此而已)显然不是常规的Python程序,几乎任何其他程序的开销都较小。

我还没有检查过ruby-profRuby跟踪分析器,但是它的自述文件指出:

大多数程序的运行速度大约慢一倍,而高度递归的程序(例如Fibonacci系列测试)的运行速度慢三倍


抽样探查器通常如何工作:setitimer



是时候谈论第二种分析器了:采样分析器!

Ruby和Python中的大多数采样分析器都是使用系统调用实现的setitimer这是什么?



假设您要每秒对程序堆栈进行快照50次。可以按照以下步骤进行:



  • 要求Linux内核每20毫秒发送一次信号(使用系统调用setitimer);
  • 当接收到信号时,为堆栈快照注册信号处理程序;
  • 分析完成后,请Linux停止向您发送信号并提供结果!


如果您想看到setitimer实现采样分析器的实际用例,我认为stacksampler.py最好的例子是一个有用的,可工作的分析器,在Python中大约有100行。这太酷了!在Python中只占用100行



的原因stacksampler.py是,当您将Python函数注册为信号处理程序时,该函数将传递到程序的当前堆栈中。因此,stacksampler.py注册信号处理程序非常容易:



def _sample(self, signum, frame):
   stack = []
   while frame is not None:
       stack.append(self._format_frame(frame))
       frame = frame.f_back

   stack = ';'.join(reversed(stack))
   self._stack_counts[stack] += 1


它只是从帧中弹出堆栈,并增加了查看特定堆栈的次数。很简单!非常酷!



让我们看看他们使用的所有其他探查器,setitimer并找出它们在代码中的位置setitimer



  • stackprof (Ruby): stackprof.c 118
  • perftools.rb (Ruby): , , , , gem (?)
  • stacksampler (Python): stacksampler.py 51
  • statprof (Python): statprof.py 239
  • vmprof (Python): vmprof_unix.c 294


重要的是setitimer-您需要决定如何计算时间。您要实时20ms吗? 20ms的用户cpu时间? 20ms用户+系统cpu时间?如果仔细查看上面的链接,您会注意到这些探查器实际上使用了不同的东西setitimer-有时行为是可自定义的,有时不是可定制的。手册页很setitimer短,非常值得阅读所有可能的配置。



@mgedminTwitter上指出了一个有趣的用例setitimer。此问题和此问题揭示了更多细节。



基于的探查器的一个有趣的缺点setitimer-什么计时器触发信号!信号有时会中断系统调用!系统调用有时需要几毫秒!如果拍摄快照太频繁,则可以使程序无限期地执行系统调用!



不使用setitimer的采样探查器



有几个不使用的采样分析器setitimer



  • pyinstrument使用PyEval_SetProfile(因此有点像跟踪分析器),但是在调用跟踪回调时,它并不总是收集堆栈快照。这是选择堆栈跟踪快照时间的代码此博客中了解有关此解决方案的更多信息(基本上:setitimer仅允许您在Py​​thon中剖析主线程)
  • pyflame使用系统调用在进程之外描述Python代码ptrace他使用一个循环来拍照,然后睡眠一段时间,然后再次执行相同的操作。这是电话等待。
  • python-flamegraph采取了类似的方法,它在Python进程中启动了一个新线程,并重新获得了堆栈跟踪,休眠和循环。这是一个等待电话


所有这三个事件探查器均拍摄实时快照。



Pyflame博客文章



除了之外,我几乎所有时间都花在了Profiler上pyflame,但实际上我最感兴趣的是,它从一个单独的进程中对Python程序进行了配置,这就是为什么我希望我的Ruby Profiler可以类似地工作。



当然,一切都比我描述的要复杂。我不会详细介绍,但是Evan Klitzke在他的博客上写了很多关于此的好文章:





可以在eklitzke.org上找到更多信息这些都是非常有趣的事情,我将更详细地进行阅读-也许它ptrace会比process_vm_readv实现Ruby分析器更好它的process_vm_readv开销较小,因为它不会停止该过程,但由于它不会停止该过程,因此也会给您带来不正确的快照:)。在我的实验中,获取有冲突的图片不是什么大问题,但是我认为在这里我将进行一系列实验。



今天就这些!



在这篇文章中,我没有涉及很多重要的细微之处-例如,我基本上说过-vmprofstacksampler-相似(它们不是-vmprof支持字符串分析和用C编写的Python函数的分析,我认为这会使探查器更加复杂。但是它们具有一些相同的基本原理,因此我认为今天的审查将是一个很好的起点。






有和没有pytest的TDD。免费网络研讨会






阅读更多:






All Articles