profiler.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. # -*- coding: utf-8 -*-
  2. """
  3. werkzeug.contrib.profiler
  4. ~~~~~~~~~~~~~~~~~~~~~~~~~
  5. This module provides a simple WSGI profiler middleware for finding
  6. bottlenecks in web application. It uses the :mod:`profile` or
  7. :mod:`cProfile` module to do the profiling and writes the stats to the
  8. stream provided (defaults to stderr).
  9. Example usage::
  10. from werkzeug.contrib.profiler import ProfilerMiddleware
  11. app = ProfilerMiddleware(app)
  12. :copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
  13. :license: BSD, see LICENSE for more details.
  14. """
  15. import sys, time, os.path
  16. try:
  17. try:
  18. from cProfile import Profile
  19. except ImportError:
  20. from profile import Profile
  21. from pstats import Stats
  22. available = True
  23. except ImportError:
  24. available = False
  25. class MergeStream(object):
  26. """An object that redirects `write` calls to multiple streams.
  27. Use this to log to both `sys.stdout` and a file::
  28. f = open('profiler.log', 'w')
  29. stream = MergeStream(sys.stdout, f)
  30. profiler = ProfilerMiddleware(app, stream)
  31. """
  32. def __init__(self, *streams):
  33. if not streams:
  34. raise TypeError('at least one stream must be given')
  35. self.streams = streams
  36. def write(self, data):
  37. for stream in self.streams:
  38. stream.write(data)
  39. class ProfilerMiddleware(object):
  40. """Simple profiler middleware. Wraps a WSGI application and profiles
  41. a request. This intentionally buffers the response so that timings are
  42. more exact.
  43. By giving the `profile_dir` argument, pstat.Stats files are saved to that
  44. directory, one file per request. Without it, a summary is printed to
  45. `stream` instead.
  46. For the exact meaning of `sort_by` and `restrictions` consult the
  47. :mod:`profile` documentation.
  48. .. versionadded:: 0.9
  49. Added support for `restrictions` and `profile_dir`.
  50. :param app: the WSGI application to profile.
  51. :param stream: the stream for the profiled stats. defaults to stderr.
  52. :param sort_by: a tuple of columns to sort the result by.
  53. :param restrictions: a tuple of profiling strictions, not used if dumping
  54. to `profile_dir`.
  55. :param profile_dir: directory name to save pstat files
  56. """
  57. def __init__(self, app, stream=None,
  58. sort_by=('time', 'calls'), restrictions=(), profile_dir=None):
  59. if not available:
  60. raise RuntimeError('the profiler is not available because '
  61. 'profile or pstat is not installed.')
  62. self._app = app
  63. self._stream = stream or sys.stdout
  64. self._sort_by = sort_by
  65. self._restrictions = restrictions
  66. self._profile_dir = profile_dir
  67. def __call__(self, environ, start_response):
  68. response_body = []
  69. def catching_start_response(status, headers, exc_info=None):
  70. start_response(status, headers, exc_info)
  71. return response_body.append
  72. def runapp():
  73. appiter = self._app(environ, catching_start_response)
  74. response_body.extend(appiter)
  75. if hasattr(appiter, 'close'):
  76. appiter.close()
  77. p = Profile()
  78. start = time.time()
  79. p.runcall(runapp)
  80. body = b''.join(response_body)
  81. elapsed = time.time() - start
  82. if self._profile_dir is not None:
  83. prof_filename = os.path.join(self._profile_dir,
  84. '%s.%s.%06dms.%d.prof' % (
  85. environ['REQUEST_METHOD'],
  86. environ.get('PATH_INFO').strip('/').replace('/', '.') or 'root',
  87. elapsed * 1000.0,
  88. time.time()
  89. ))
  90. p.dump_stats(prof_filename)
  91. else:
  92. stats = Stats(p, stream=self._stream)
  93. stats.sort_stats(*self._sort_by)
  94. self._stream.write('-' * 80)
  95. self._stream.write('\nPATH: %r\n' % environ.get('PATH_INFO'))
  96. stats.print_stats(*self._restrictions)
  97. self._stream.write('-' * 80 + '\n\n')
  98. return [body]
  99. def make_action(app_factory, hostname='localhost', port=5000,
  100. threaded=False, processes=1, stream=None,
  101. sort_by=('time', 'calls'), restrictions=()):
  102. """Return a new callback for :mod:`werkzeug.script` that starts a local
  103. server with the profiler enabled.
  104. ::
  105. from werkzeug.contrib import profiler
  106. action_profile = profiler.make_action(make_app)
  107. """
  108. def action(hostname=('h', hostname), port=('p', port),
  109. threaded=threaded, processes=processes):
  110. """Start a new development server."""
  111. from werkzeug.serving import run_simple
  112. app = ProfilerMiddleware(app_factory(), stream, sort_by, restrictions)
  113. run_simple(hostname, port, app, False, None, threaded, processes)
  114. return action