Profiling(性能调试)是我一直很感兴趣的一个话题,之前给大家介绍过Datadog这个工具,今天我们来看看Python语言中有哪些方法来做Profiling。

Poorman's Profiler

最基础的就是使用time.time()来计时,这个方法简单有效,也许所有写过Python代码的人都用过。我们可以创建一个decorator使它用起来更方便。

import time
import logging
import functools def simple_profiling(func):
@wraps.functools(func)
def wrapped(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
time_spent = time.time() - start_time
fullname = '{}.{}'.format(func.__module__, func.func_name)
logging.info('%s[args=%s kwargs=%s] completed in %.4fs',
fullname, args, kwargs, time_spent)
return result @simple_profiling
def foo(sec):
time.sleep(sec)

这个方法的优点是简单,额外开效非常低(大部分情况下可以忽略不计)。但缺点也很明显,除了总用时,没有任何其他信息。

cProfile

cProfile是Python标准库中的一个模块,它可以非常仔细地分析代码执行过程中所有函数调用的用时和次数。cProfile最简单的用法是用cProfile.run来执行一段代码,或是用python -m cProfile myscript.py来执行一个脚本。例如,

import cProfile
import requests cProfile.run('requests.get("http://tech.glowing.com")')

以上代码的执行结果如下

         5020 function calls (5006 primitive calls) in 1.751 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
1 0.000 0.000 1.751 1.751 <string>:1(<module>)
4 0.000 0.000 0.000 0.000 <string>:8(__new__)
13 0.000 0.000 0.000 0.000 Queue.py:107(put)
14 0.000 0.000 0.000 0.000 Queue.py:150(get)
1 0.000 0.000 0.000 0.000 Queue.py:234(_init)
27 0.000 0.000 0.000 0.000 Queue.py:237(_qsize)
13 0.000 0.000 0.000 0.000 Queue.py:240(_put)
...
1 1.027 1.027 1.027 1.027 {_socket.getaddrinfo}
3 0.715 0.238 0.715 0.238 {method 'recv' of '_socket.socket' objects}
...

完整的结果很长,这里我只截取了一部分。结果中每一行包含一个函数的执行信息,每个字段的含义如下

  • ncalls: 该函数被调用的次数
  • tottime: 该函数的总耗时,子函数的调用时间不计算在内
  • percall: tottime / ncalls
  • cumtime: 该函数加上其所有子函数的总耗时
  • percall: cumtime / ncalls

这里我们看到requests.get的时间主要是花费在_socket.getaddrinfo_socket.socket.recv这两个子函数上,其中_socket.getaddrinfo被调用了一次,而_socket.socket.recv被调用了3次。

虽然cProfile.run用起来非常直观,但在实际调试时并不灵活,并且最后打印出来的信息过于冗长。我们可以通过写一个decorator让cProfile的使用更方便,但这里我想介绍Python中另一个非常好用的特性,那就是contextmanager。Decorator用来包裹一个函数,而contextmanager则用来包裹一个代码块。例如,我们希望用cProfile来调试一个代码块,可以创建如下的contextmanager

import cProfile
import pstats
from contextlib import contextmanager import requests @contextmanager
def profiling(sortby='cumulative', limit=20):
pr = cProfile.Profile()
pr.enable()
yield
pr.disable()
ps = pstats.Stats(pr).sort_stats(sortby)
ps.print_stats(limit) with profiling(sortby='tottime', limit=5):
requests.get('http://tech.glowing.com')

这样就可以很方便地用with语法对一段代码进行性能调试,以上代码的执行结果如下

   5992 function calls (5978 primitive calls) in 0.293 seconds

   Ordered by: internal time
List reduced from 383 to 5 due to restriction <5> ncalls tottime percall cumtime percall filename:lineno(function)
6 0.276 0.046 0.276 0.046 {method 'recv' of '_socket.socket' objects}
5 0.002 0.000 0.002 0.000 {_socket.gethostbyname}
1 0.001 0.001 0.001 0.001 {_socket.getaddrinfo}
5 0.001 0.000 0.001 0.000 {_scproxy._get_proxy_settings}
8 0.001 0.000 0.001 0.000 /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/urllib.py:1376(getproxies_environment)

因为这里的结果按tottime排序,并且只显示最费时的5个函数,所以看起来要比之前的结果清晰很多。

Line Profiler

与cProfile相比,Line Profiler的结果更加直观,它可以告诉你一个函数中每一行的耗时。Line Profiler并不在标准库中,需要用pip来安装

pip install line_profiler

Line Profiler的使用方法与cProfile类似,同样建议为其创建decorator或是contextmanager来方便使用。具体代码可以看这里

例如,我们用Line Profiler来调试urlparse.parse_qsl函数,其输出结果如下

Line #  % Time  Line Contents
=============================
390 def parse_qsl(qs, keep_blank_values=0, strict_parsing=0):
...
409 15.3 pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
410 0.0 r = []
411 3.2 for name_value in pairs:
412 2.8 if not name_value and not strict_parsing:
413 continue
414 8.2 nv = name_value.split('=', 1)
415 5.7 if len(nv) != 2:
416 if strict_parsing:
417 raise ValueError, "bad query field: %r" % (name_value,)
418 # Handle case of a control-name with no equal sign
419 if keep_blank_values:
420 nv.append('')
421 else:
422 continue
423 5.3 if len(nv[1]) or keep_blank_values:
424 32.0 name = unquote(nv[0].replace('+', ' '))
425 22.4 value = unquote(nv[1].replace('+', ' '))
426 4.6 r.append((name, value))
427
428 0.4 return r

Line Profiler的结果虽然直观,但由于它不能显示子函数运行的详细信息,所以实际可以使用的场景比较有限。

定制低开销的Profiler

cProfile虽然是非常不错的性能调试工具,应用场景也很广泛,但它最主要的缺点是有很大的额外开销(overhead)。这使cProfile不适合在生产环境中使用,而且过大的额外开销有时会导致Profiler打开与关闭状态下代码的性能瓶颈不一致。cProfile高开销来自于它对每一个函数调用进行计时,但很多时候,其实我们只关心某些类库的执行时间。以典型的网络移动应用的服务程序为例,它们的大部分时间花费在I/O上,我们在做性能分析时,关心的是在响应用户请求时,服务器在RPC, Cache和DB这些I/O操作上各花费了多少时间。如果更细致一些,我们也许会关心在一次用户请求中,DB的select/insert/update各被调用了多少次,花费了多少时间。

如果大家熟悉如New Relic之类的APM服务(Application Performance Monitoring),它们可以将每一个web请求的响应时间分解成RPC/Cache/DB的开销,同时它并不会对服务器造成太多额外的负担。我们也可以自己编写这类的Profiler,需要做的就是那一些特定的I/O函数进行monkey-patch,将原先的函数替换成带有计时器的函数。比如,我们想对所有的requests.get函数计时,但又不能更改requests模块的代码,我们可以这样做

import requests

setattr(requests, 'get', simple_profiling(requests.get))

如果你想对requests模块下所有的公有API计时,我们可以写一个patch_module辅助函数

import types
import requests def patch_module(module):
methods = [m for m in dir(module) if not m.startswith('_')
and isinstance(getattr(module, m), types.FunctionType)]
for name in methods:
func = getattr(module, name)
setattr(module, name, simple_profiling(func)) patch_module(requests)

那要是我们想对所有redis的访问计时呢?这时我们需要对redis.StrictRedis这个类下的所有方法进行monkey-patch,我们可以写一个patch_class辅助函数

import types
import redis def patch_class(cls):
methods = [m for m in dir(cls) if not m.startswith('_')
and isinstance(getattr(cls, m), types.MethodType)]
for name in methods:
method = getattr(cls, name)
patched_func = simple_profiling(method.im_func)
if isinstance(method, classmethod):
setattr(cls, name, classmethod(patched_func))
elif isinstance(method, staticmethod):
setattr(cls, name, staticmethod(patched_func))
else:
setattr(cls, name, patched_func) patch_class(redis.StrictRedis)

在Github上,我们给出了一个完整的自定义Profiler的例子,它可以patch一个模块或是一个类,对不同的方法归类,profiling结束时打印出各个方法的时间,以及各个类别的总耗时。同时这个Profiler也是线程安全的。由于篇幅有限,这里只给出使用这个Profiler的例子,实现部分请查阅源代码

patch_module(requests, category='http')
patch_class(redis.StrictRedis, category='redis')
with profiling('Custom profiler demo', verbose=True):
resp = requests.get('http://tech.glowing.com')
redis_conn = redis.StrictRedis()
redis_conn.set('foo', 'bar')
redis_conn.get('foo')

其执行结果如下

Performance stats for Custom profiler demo
CATEGROY TIME
---------- -----------
http 0.405049
redis 0.000648975 CALL TIME
---------------------------- -----------
requests.api.get 0.405049
redis.client.StrictRedis.set 0.000494003
redis.client.StrictRedis.get 0.000154972 CALL HISTORY www.90168.org
0.4050s requests.api.get[args=('http://tech.glowing.com',) kwargs={}]
0.0005s redis.client.StrictRedis.set[args=(<redis.client.StrictRedis object at 0x7ffddb574710>, 'foo', 'bar') kwargs={}]
0.0002s redis.client.StrictRedis.get[args=(<redis.client.StrictRedis object at 0x7ffddb574710>, 'foo') kwargs={}]

小结

性能调试是一个很大的题目,这里介绍的只是非常初级的知识。在最后一小节给出的例子也是希望大家不要局限于现有的工具,而是能根据自己的需求,打造最合适的工具。欢迎大家交流讨论!

Python profiling的更多相关文章

  1. Python的Profilers性能分析器

    关于Python Profilers性能分析器 关于性能分析,python有专门的文档,可查看:http://docs.python.org/library/profile.html?highligh ...

  2. python 文章集合

    Python profiling tools 我常用的 Python 调试工具

  3. Python Revisited Day 09 (调试、测试与Profiling)

    目录 9.1 调试 9.1.1 处理语法错误 9.1.2 处理运行时错误 9.1.3 科学的调试 9.2 单元测试 9.3 Profiling 9.1 调试 定期地进行备份是程序设计中地一个关键环节- ...

  4. python Pandas Profiling 一行代码EDA 探索性数据分析

    文章大纲 1. 探索性数据分析 代码样例 效果 解决pandas profile 中文显示的问题 1. 探索性数据分析 数据的筛选.重组.结构化.预处理等都属于探索性数据分析的范畴,探索性数据分析是帮 ...

  5. Python 资源大全中文版

    Python 资源大全中文版 我想很多程序员应该记得 GitHub 上有一个 Awesome - XXX 系列的资源整理.awesome-python 是 vinta 发起维护的 Python 资源列 ...

  6. python 模块基础介绍

    从逻辑上组织代码,将一些有联系,完成特定功能相关的代码组织在一起,这些自我包含并且有组织的代码片段就是模块,将其他模块中属性附加到你的模块的操作叫做导入. 那些一个或多个.py文件组成的代码集合就称为 ...

  7. [转载]Python 资源大全

    原文链接:Python 资源大全 环境管理 管理 Python 版本和环境的工具 p – 非常简单的交互式 python 版本管理工具. pyenv – 简单的 Python 版本管理工具. Vex  ...

  8. python常用库

    本文由 伯乐在线 - 艾凌风 翻译,Namco 校稿.未经许可,禁止转载!英文出处:vinta.欢迎加入翻译组. Awesome Python ,这又是一个 Awesome XXX 系列的资源整理,由 ...

  9. Python.Module.site

    site " This module is automatically imported during initialization. The automatic import can be ...

随机推荐

  1. IOS- 应用程序生命周期(前后台切换,应用的各种状态)详解

    iOS的应用程序的生命周期,还有程序是运行在前台还是后台,应用程序各个状态的变换,这些对于开发者来说都是很重要的. iOS系统的资源是有限的,应用程序在前台和在后台的状态是不一样的.在后台时,程序会受 ...

  2. Android Studio新建了一个项目提示Error:Unable to start the daemon process

    提示如下错误:

  3. Redis事件管理(三)

    Redis的事件管理和定时器的管理都是自己来实现的,Redis的事件管理分为两部分,一部分是封装了系统的异步事件API,还有一部分是在这基础上封装了一个通用的事件管理器,根据具体的系统来决定具体使用哪 ...

  4. 解决VS2010 C++ DLL不能断点调试的问题

    问题产生的过程是这样的,向exe项目(CSharp)中添加dll工程(c++开发)的引用,并将引用工程的属性“Link Library Dependencies”的值设为true,这样,在不加入lib ...

  5. 关于Linux环境变量

    查看全局变量:  printenv 查看单个环境变量的值可以用echo命令,必须在环境变量的名称前放一个$符号 如:

  6. DB2 for z: system catalog tables

    http://www.ibm.com/support/knowledgecenter/SSEPEK_10.0.0/com.ibm.db2z10.doc.sqlref/src/tpc/db2z_cata ...

  7. Mac使用入门

    mac常用快捷键 全屏/退出全屏 ctr+command+F 切换到桌面 fn+f11 输入法切换 ctr+空格 亮度 f1.f2 声音 f11.f12 复制.粘贴 command+c.command ...

  8. 多图上传 - Web Uploader

    http://fex.baidu.com/webuploader/   官方DEMO,我都不想说了,各种问题.参考ShuaiBi文章   http://www.cnblogs.com/ismars/p ...

  9. HTTPS的一些疑问解答

    PHP写的网站怎么用https访问,具体要怎样 这跟用什么语言写的网站没有关系,可以去申请个快速的SSL证书,一年也就几十块. 开启apache server的ssl,自己做个免费的ssl证书或者去申 ...

  10. 【JAVA集合框架之List与Set】

    一.概述 JAVA的集合框架中定义了一系列的类,这些类都是存储数据的容器.与数组.StringBuffer(StringBuilder)相比,它的特点是: 1.用于存储对象. 2.集合长度可变. 3. ...