Skip to content
This repository has been archived by the owner on Feb 10, 2023. It is now read-only.

Commit

Permalink
Initial import of keas.profile: a WSGI profiler middleware.
Browse files Browse the repository at this point in the history
  • Loading branch information
mgedmin committed Dec 12, 2008
0 parents commit 63d8de5
Show file tree
Hide file tree
Showing 10 changed files with 360 additions and 0 deletions.
9 changes: 9 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
=======
CHANGES
=======


0.1.0 (2008-12-??)
------------------

- Initial release
26 changes: 26 additions & 0 deletions README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
===================
Profiler middleware
===================

This package provides middleware for profiling of the application. It's based
on `paste.debug.profile <http://pythonpaste.org/modules/debug.profile.html>`_,
but uses cProfile instead of hotshot.

If you use PasteScript, enabling the profiler is as simple as adding ::

[filter-app:profile]
use = egg:keas.profile#profiler
next = main

to your paster configuration file and passing ``--app-name=profile`` to
``paster``. When you access your web application, every page will have the
profiler output appended to the end of the document body.


Viewing profiles with KCacheGrind
---------------------------------

KCacheGrind is a GUI application for digging through the profile data and
visualizing call trees. keas.profile uses pyprof2calltree to convert the
profiler data into KCacheGrind format for your convenience. To view it,
open the log file (``profile.log.tmp.kgrind`` by default) in KCacheGrind.
52 changes: 52 additions & 0 deletions bootstrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
##############################################################################
#
# Copyright (c) 2008 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Bootstrap a buildout-based project
Simply run this script in a directory containing a buildout.cfg.
The script accepts buildout command-line options, so you can
use the -c option to specify an alternate configuration file.
$Id$
"""

import os, shutil, sys, tempfile, urllib2

tmpeggs = tempfile.mkdtemp()

ez = {}
exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
).read() in ez
ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)

import pkg_resources

cmd = 'from setuptools.command.easy_install import main; main()'
if sys.platform == 'win32':
cmd = '"%s"' % cmd # work around spawn lamosity on windows

ws = pkg_resources.working_set
assert os.spawnle(
os.P_WAIT, sys.executable, sys.executable,
'-c', cmd, '-mqNxd', tmpeggs, 'zc.buildout',
dict(os.environ,
PYTHONPATH=
ws.find(pkg_resources.Requirement.parse('setuptools')).location
),
) == 0

ws.add_entry(tmpeggs)
ws.require('zc.buildout')
import zc.buildout.buildout
zc.buildout.buildout.main(sys.argv[1:] + ['bootstrap'])
shutil.rmtree(tmpeggs)
30 changes: 30 additions & 0 deletions buildout.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[buildout]
develop = .
parts = test coverage-test coverage-report python ctags


[test]
recipe = zc.recipe.testrunner
eggs = keas.profile [test]


[coverage-test]
recipe = zc.recipe.testrunner
eggs = keas.profile [test]
defaults = ['--coverage', '../../coverage']


[coverage-report]
recipe = zc.recipe.egg
eggs = z3c.coverage
scripts = coverage=coverage-report
arguments = ('coverage', 'coverage/report')

[python]
recipe = zc.recipe.egg
eggs = keas.profile
interpreter = python

[ctags]
recipe = z3c.recipe.tag:tags
eggs = keas.profile
57 changes: 57 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
###############################################################################
#
# Copyright 2008 by Keas, Inc., San Francisco, CA
#
###############################################################################
"""Package setup.
$Id$
"""
import os
from setuptools import setup, find_packages

def read(*rnames):
return open(os.path.join(os.path.dirname(__file__), *rnames)).read()

setup(
name='keas.profile',
version = '0.2.0dev',
author='Marius Gedminas and the Zope Community.',
author_email="zope-dev@zope.org",
description='WSGI Profiler for Python Paste',
long_description=(
read('README.txt')
+ '\n\n' +
read('CHANGES.txt')
),
license="ZPL 2.1",
keywords="zope3 profile paste wsgi",
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: Zope Public License',
'Programming Language :: Python',
'Natural Language :: English',
'Operating System :: OS Independent',
'Topic :: Internet :: WWW/HTTP',
'Framework :: Zope3'],
url = 'http://pypi.python.org/pypi/keas.profile',
packages=find_packages('src'),
package_dir={'': 'src'},
namespace_packages=['keas'],
extras_require=dict(
test=['zope.testing',],
),
install_requires=[
'setuptools',
'paste',
'pyprof2calltree',
],
include_package_data=True,
zip_safe=False,
entry_points = """
[paste.filter_app_factory]
profiler = keas.profile.profiler:make_profiler
"""
)
7 changes: 7 additions & 0 deletions src/keas/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
try:
# Declare this a namespace package if pkg_resources is available.
import pkg_resources
pkg_resources.declare_namespace(__name__)
except ImportError:
pass

34 changes: 34 additions & 0 deletions src/keas/profile/README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
Keas.profile
------------

Middleware that profiles the request and displays profiling information at the
bottom of each page.

>>> from email import utils
>>> from keas.profile import profiler

Lets start with a simple "hello world" app to profile

>>> def simple_app(environ, start_response):
... start_response('200 OK', [('content-type', 'text/html')])
... return "hello world!"
...
>>> def start_response(status, headers, exc_info=None):
... pass

we can now generate a middleware profiler for the app

>>> profiled_app = profiler.make_profiler(simple_app, global_conf=None)

and call the app to profile it

>>> environ = {'HTTP_DATE': utils.formatdate(),
... 'PATH_INFO': '/' ,
... 'REQUEST_METHOD': 'GET'}

>>> profiled_app(environ, start_response)
['hello world!... function calls in ... CPU seconds...
...Ordered by: cumulative time, call count...]

The profiler output is appended to the end of the response body if it returns
HTML. (Yes, this violates the HTML standard, but seems to work in practice.)
1 change: 1 addition & 0 deletions src/keas/profile/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# make a package
117 changes: 117 additions & 0 deletions src/keas/profile/profiler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
##############################################################################
#
# Copyright (c) 2008 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
# Portions copyright (c) 2005 Ian Bicking and contributors; written for Paste
# (http://pythonpaste.org) and licensed under the MIT license:
# http://www.opensource.org/licenses/mit-license.php
#
###############################################################################
"""WSGI Profiler Middleware.
$Id$
"""

import cgi
import threading
import cProfile
import pstats
from cStringIO import StringIO

from paste import response


class ProfileMiddleware(object):
"""Middleware that profiles all requests.
This is a fork of paste.debug.profile.ProfileMiddleware. It uses
cProfile instead of hotshot (which is buggy). It doesn't cause the
truncate the profiler output to be truncated in the browser. It sorts
the stats by cumulative time rather than internal time.
If the following bugs were fixed upstream, we could switch to paste.debug
again:
http://trac.pythonpaste.org/pythonpaste/ticket/204
http://trac.pythonpaste.org/pythonpaste/ticket/311
http://trac.pythonpaste.org/pythonpaste/ticket/312
However upstream says at leas one of those won't be fixed, and suggests
we look into better-maintained WSGI profiler middleware products such as
repoze.profile or Dozer.
"""

style = ('clear: both; background-color: #ff9; color: #000; '
'border: 2px solid #000; padding: 5px;')

def __init__(self, app, global_conf=None,
log_filename='profile.log.tmp',
limit=40):
self.app = app
self.lock = threading.Lock()
self.log_filename = log_filename
self.limit = limit

def __call__(self, environ, start_response):
catch_response = []
body = []
def replace_start_response(status, headers, exc_info=None):
catch_response.extend([status, headers])
start_response(status, headers, exc_info)
return body.append
def run_app():
app_iter = self.app(environ, replace_start_response)
try:
body.extend(app_iter)
finally:
if hasattr(app_iter, 'close'):
app_iter.close()
self.lock.acquire()
try:
profiler = cProfile.Profile()
profiler.runctx("run_app()", globals(), locals())
body = ''.join(body)
headers = catch_response[1]
content_type = response.header_value(headers, 'content-type')
if content_type is None or not content_type.startswith('text/html'):
# We can't add info to non-HTML output
return [body]
stream = StringIO()
stats = pstats.Stats(profiler, stream=stream)
stats.strip_dirs()
stats.sort_stats('cumulative', 'calls')
stats.print_stats(self.limit)
output = stream.getvalue()
stream.reset()
stream.truncate()
stats.print_callers(self.limit)
output_callers = stream.getvalue()
stream.close()
body += '<pre style="%s">%s\n%s</pre>' % (
self.style, cgi.escape(output), cgi.escape(output_callers))
response.replace_header(headers, 'Content-Length', str(len(body)))
try:
import pyprof2calltree
except ImportError:
pass
else:
# Use kcachegrind to view the profile interactively.
pyprof2calltree.convert(profiler.getstats(),
self.log_filename + '.kgrind')
return [body]
finally:
self.lock.release()


def make_profiler(app, global_conf, **local_conf):
"""Create the Profiler app."""
return ProfileMiddleware(app, global_conf)

27 changes: 27 additions & 0 deletions src/keas/profile/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
##############################################################################
#
# Copyright (c) 2008 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
###############################################################################
"""Test Setup.
$Id$
"""
import unittest
from zope.testing import doctestunit, doctest

def test_suite():
return unittest.TestSuite((
doctestunit.DocFileSuite(
'README.txt',
optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
),
))

0 comments on commit 63d8de5

Please sign in to comment.