[Repoze-checkins] r704 - in repoze.profile/trunk: . repoze/profile

Chris McDonough chrism at agendaless.com
Wed Feb 20 14:12:05 UTC 2008


Author: Chris McDonough <chrism at agendaless.com>
Date: Wed Feb 20 09:12:04 2008
New Revision: 704

Log:
Add a UI.


Added:
   repoze.profile/trunk/repoze/profile/profiler.html
   repoze.profile/trunk/repoze/profile/tests.py   (contents, props changed)
Modified:
   repoze.profile/trunk/repoze/profile/__init__.py
   repoze.profile/trunk/repoze/profile/profiler.py
   repoze.profile/trunk/setup.py

Modified: repoze.profile/trunk/repoze/profile/__init__.py
==============================================================================
--- repoze.profile/trunk/repoze/profile/__init__.py	(original)
+++ repoze.profile/trunk/repoze/profile/__init__.py	Wed Feb 20 09:12:04 2008
@@ -1,9 +1 @@
-from paste.debug.profile import profile_decorator
-
-class OKView:
-    def __init__(self, context, request):
-        self.context = context
-        self.request = request
-
-    def __call__(self):
-        return 'OK'
+# a module

Added: repoze.profile/trunk/repoze/profile/profiler.html
==============================================================================
--- (empty file)
+++ repoze.profile/trunk/repoze/profile/profiler.html	Wed Feb 20 09:12:04 2008
@@ -0,0 +1,67 @@
+<html xmlns:meld="http://www.plope.com/software/meld3">
+  <head>
+  </head>
+  <body>
+    
+    <div meld:id="description" class="form-text">
+      There is not yet any profiling data to report.
+    </div>
+    
+    <div meld:id="formelements">
+      <form action="." meld:id="form" method="POST">
+        <table>
+          <tr>
+            <td>
+              <strong>Sort</strong>: 
+              <select name="sort" meld:id="sort">
+                <option value="time">time</option>
+                <option value="cumulative">cumulative</option>
+                <option value="calls">calls</option>
+                <option value="pcalls">pcalls</option>
+                <option value="name">name</option>
+                <option value="file">file</option>
+                <option value="module">module</option>
+                <option value="line">line</option>
+                <option value="nfl">nfl</option>
+                <option value="stdname">stdname</option>
+              </select>
+            </td>
+            <td>
+              <strong>Limit</strong>: 
+              <select name="limit" meld:id="limit">
+                <option value="100">100</option>
+                <option value="200">200</option>
+                <option value="300">300</option>
+                <option value="400">400</option>
+                <option value="500">500</option>
+              </select>
+            </td>
+            <td>
+              <strong>Full Dirs</strong>:
+              <input type="checkbox" name="full_dirs" value="1"
+                     meld:id="full_dirs"/>
+            </td>
+            <td>
+              <strong>Mode</strong>: 
+              <select name="mode" meld:id="mode">
+                <option value="stats">stats</option>
+                <option value="callees">callees</option>
+                <option value="callers">callers</option>
+              </select>
+            </td>
+            <td>
+              <input type="submit" name="submit" value="Update"/>
+            </td>
+            <td>
+              <input type="submit" name="clear" value="Clear"/>
+            </td>
+          </tr>
+        </table>
+      </form>
+    </div>
+    <pre meld:id="profiledata">
+    </pre>
+  </body>
+</html>
+
+

Modified: repoze.profile/trunk/repoze/profile/profiler.py
==============================================================================
--- repoze.profile/trunk/repoze/profile/profiler.py	(original)
+++ repoze.profile/trunk/repoze/profile/profiler.py	Wed Feb 20 09:12:04 2008
@@ -2,9 +2,20 @@
 
 o Insprired by the paste.debug.profile version, which profiles single requests.
 """
+import os
 import profile
+import pstats
+import sys
+import StringIO
 import threading
 
+from paste.request import parse_formvars
+from paste.request import construct_url
+
+import meld3
+
+_HERE = os.path.abspath(os.path.dirname(__file__))
+
 DEFAULT_PROFILE_LOG = 'wsgi.prof'
 
 class AccumulatingProfileMiddleware(object):
@@ -13,17 +24,92 @@
                  global_conf=None,
                  log_filename=DEFAULT_PROFILE_LOG,
                  discard_first_request=True,
+                 path='/__profile__',
                 ):
         self.app = app
         self.profiler = profile.Profile()
         self.log_filename = log_filename
         self.first_request = discard_first_request
         self.lock = threading.Lock()
+        self.path = path
+        self.template = os.path.join(_HERE, 'profiler.html')
+        self.meldroot = meld3.parse_xml(self.template)
+
+    def index(self, environ):
+        root = self.meldroot.clone()
+        querydata = parse_formvars(environ)
+        full_dirs = int(querydata.get('full_dirs', 0))
+        sort = querydata.get('sort', 'time')
+        clear = querydata.get('clear', None)
+        limit = int(querydata.get('limit', 100))
+        mode = querydata.get('mode', 'stats')
+        output = StringIO.StringIO()
+        url = construct_url(environ)
+        log_exists = os.path.exists(self.log_filename)
+
+        if clear and log_exists:
+            os.remove(self.log_filename)
+            log_exists = False
+
+        if log_exists:
+            stats = pstats.Stats(self.log_filename)
+            if not full_dirs:
+                stats.strip_dirs()
+            stats.sort_stats(sort)
+
+            try:
+                orig_stdout = sys.stdout
+                sys.stdout = output
+                print_fn = getattr(stats, 'print_%s' % mode)
+                print_fn(limit)
+                sys.stdout.flush()
+            finally:
+                sys.stdout = orig_stdout
+
+        data = output.getvalue()
+
+        form = root.findmeld('form')
+        form.attributes(action=url)
+        if data:
+            description = root.findmeld('description')
+            description.content("""
+            Profiling information is generated using the standard Python 
+            profiler. To learn how to interpret the profiler statistics, 
+            see the <a
+            href="http://www.python.org/doc/current/lib/module-profile.html">
+            Python profiler documentation</a>.""", structure=True)
+            formelements = root.findmeld('formelements')
+
+            if full_dirs:
+                formelements.fillmeldhtmlform(sort=sort,
+                                              limit=str(limit),
+                                              full_dirs=True,
+                                              mode=mode)
+            else:
+                formelements.fillmeldhtmlform(sort=sort,
+                                              limit=str(limit),
+                                              mode=mode)
+
+            profiledata = root.findmeld('profiledata')
+            profiledata.content(data)
+        else:
+            formelements = root.findmeld('formelements')
+            formelements.content('')
+        return root.write_xhtmlstring()
 
     def __call__(self, environ, start_response):
         catch_response = []
         body = []
 
+        path_info = environ.get('PATH_INFO')
+
+        if path_info == self.path:
+            # we're being asked to render the profile view
+            body = self.index(environ)
+            start_response('200 OK', [('content-type', 'text/html'),
+                                      ('content-length', str(len(body)))])
+            return [body]
+
         def replace_start_response(status, headers, exc_info=None):
             catch_response.extend([status, headers])
             start_response(status, headers, exc_info)
@@ -52,11 +138,11 @@
         finally:
             self.lock.release()
 
-
 def make_profile_middleware(app,
                             global_conf,
                             log_filename=DEFAULT_PROFILE_LOG,
                             discard_first_request=True,
+                            path='/__profile__'
                            ):
     """Wrap the application in a component that will profile each
     request, appending data from each request to an aggregate
@@ -74,5 +160,6 @@
     return AccumulatingProfileMiddleware(
                 app,
                 log_filename=log_filename,
-                discard_first_request=discard_first_request
+                discard_first_request=discard_first_request,
+                path=path
                )

Added: repoze.profile/trunk/repoze/profile/tests.py
==============================================================================
--- (empty file)
+++ repoze.profile/trunk/repoze/profile/tests.py	Wed Feb 20 09:12:04 2008
@@ -0,0 +1,118 @@
+import unittest
+
+class TestProfileMiddleware(unittest.TestCase):
+    def _makeOne(self, *arg, **kw):
+        from repoze.profile.profiler import AccumulatingProfileMiddleware
+        return AccumulatingProfileMiddleware(*arg, **kw)
+        
+
+    def _makeEnviron(self, kw):
+        environ = {}
+        environ['wsgi.url_scheme'] = 'http'
+        environ['CONTENT_TYPE'] = 'text/html'
+        environ['QUERY_STRING'] = ''
+        environ['SERVER_NAME'] = 'localhost'
+        environ['SERVER_PORT'] = '80'
+        environ['REQUEST_METHOD'] = 'POST'
+        environ.update(kw)
+        return environ
+
+    def test_index_post(self):
+        from StringIO import StringIO
+        fields = [
+            ('full_dirs', '1'),
+            ('sort', 'cumulative'),
+            ('limit', '500'),
+            ('mode', 'callers'),
+            ]
+        content_type, body = encode_multipart_formdata(fields)
+        environ = self._makeEnviron(
+            {'wsgi.input':StringIO(body),
+             'CONTENT_TYPE':content_type,
+             'CONTENT_LENGTH':len(body),
+             'REQUEST_METHOD':'POST',
+             })
+        middleware = self._makeOne(None)
+        html = middleware.index(environ)
+        self.failIf(html.find('There is not yet any profiling data') == -1)
+
+    def test_index_get(self):
+        environ = self._makeEnviron({
+             'REQUEST_METHOD':'GET',
+             'wsgi.input':'',
+             })
+        middleware = self._makeOne(None)
+        html = middleware.index(environ)
+        self.failIf(html.find('There is not yet any profiling data') == -1)
+
+    def test_index_clear(self):
+        from StringIO import StringIO
+        fields = [
+            ('full_dirs', '1'),
+            ('sort', 'cumulative'),
+            ('limit', '500'),
+            ('mode', 'callers'),
+            ('clear', 'submit'),
+            ]
+        content_type, body = encode_multipart_formdata(fields)
+        environ = self._makeEnviron(
+            {'wsgi.input':StringIO(body),
+            'CONTENT_TYPE':content_type,
+             'CONTENT_LENGTH':len(body),
+             'REQUEST_METHOD':'POST',
+             })
+
+        middleware = self._makeOne(None)
+        import tempfile
+        f = tempfile.mktemp()
+        open(f, 'w').write('x')
+        middleware.log_filename = f
+        html = middleware.index(environ)
+        self.failIf(html.find('There is not yet any profiling data') == -1)
+        import os
+        self.failIf(os.path.exists(f))
+
+    def test_call_withpath(self):
+        from StringIO import StringIO
+        fields = [
+            ('full_dirs', '1'),
+            ('sort', 'cumulative'),
+            ('limit', '500'),
+            ('mode', 'callers'),
+            ]
+        content_type, body = encode_multipart_formdata(fields)
+        environ = self._makeEnviron(
+            {'wsgi.input':StringIO(body),
+            'CONTENT_TYPE':content_type,
+             'CONTENT_LENGTH':len(body),
+             })
+
+        middleware = self._makeOne(None)
+        environ['PATH_INFO'] = middleware.path
+        statuses = []
+        headerses = []
+        def start_response(status, headers):
+            statuses.append(status)
+            headerses.append(headers)
+        iterable = middleware(environ, start_response)
+        html = iterable[0]
+        self.failIf(html.find('There is not yet any profiling data') == -1)
+        self.assertEqual(statuses[0], '200 OK')
+        self.assertEqual(headerses[0][0], ('content-type', 'text/html'))
+        self.assertEqual(headerses[0][1], ('content-length', str(len(html))))
+
+def encode_multipart_formdata(fields):
+    BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$'
+    CRLF = '\r\n'
+    L = []
+    for (key, value) in fields:
+        L.append('--' + BOUNDARY)
+        L.append('Content-Disposition: form-data; name="%s"' % key)
+        L.append('')
+        L.append(value)
+    L.append('--' + BOUNDARY + '--')
+    L.append('')
+    body = CRLF.join(L)
+    content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
+    return content_type, body
+

Modified: repoze.profile/trunk/setup.py
==============================================================================
--- repoze.profile/trunk/setup.py	(original)
+++ repoze.profile/trunk/setup.py	Wed Feb 20 09:12:04 2008
@@ -47,9 +47,9 @@
       include_package_data=True,
       namespace_packages=['repoze'],
       zip_safe=False,
-      tests_require = [],
-      install_requires=[],
-      #test_suite="repoze.",
+      tests_require = ['meld3', 'Paste'],
+      install_requires=['meld3', 'Paste'],
+      test_suite="repoze.profile.tests",
       entry_points = """\
       [paste.filter_app_factory]
       profile = repoze.profile.profiler:make_profile_middleware


More information about the Repoze-checkins mailing list