profiler.py 5.0 KB

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