├── LICENSE ├── MANIFEST.in ├── README.rst ├── example_project ├── __init__.py ├── dev.db ├── manage.py ├── settings.py ├── templates │ └── flatpages │ │ └── default.html └── urls.py ├── setup.py └── speedtracer ├── __init__.py └── middleware.py /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2010 Chris Adams. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are 4 | permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of 7 | conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list 10 | of conditions and the following disclaimer in the documentation and/or other materials 11 | provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY Chris Adams ``AS IS'' AND ANY EXPRESS OR IMPLIED 14 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 15 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Chris Adams OR 16 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 17 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 18 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 20 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 21 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 | 23 | The views and conclusions contained in the software and documentation are those of the 24 | authors and should not be interpreted as representing official policies, either expressed 25 | or implied, of Chris Adams. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django Speed Tracer 2 | =================== 3 | 4 | Simple performance monitoring for Django using Google Chrome's Speed Tracer 5 | 6 | Notes 7 | ----- 8 | 9 | Chrome Dev channel has not been stable with the Speed Tracer extension. If you 10 | don't see the server trace results or anything in the request/response headers 11 | you are probably running into this issue: 12 | 13 | http://code.google.com/p/speedtracer/issues/detail?id=28 14 | 15 | Installation 16 | ------------ 17 | 18 | #. Download and install Speed Tracer: 19 | 20 | http://code.google.com/webtoolkit/speedtracer/get-started.html 21 | 22 | #. Add ``"speedtracer"`` to your ``INSTALLED_APPS`` 23 | 24 | #. Add ``"speedtracer.middleware.SpeedTracerMiddleware"`` to the beginning of 25 | your ``MIDDLEWARE_CLASSES`` (this is important if you're also using projects like 26 | ``django-localeurl`` which alter normal URL routing) 27 | 28 | #. Load your page inside Chrome with SpeedTracer enabled 29 | 30 | #. Open SpeedTracer and expand the "Server Trace" in the page's detailed 31 | report which should look something like this: 32 | 33 | .. image:: http://farm5.static.flickr.com/4115/4815493734_4c20d6894f.jpg 34 | 35 | Example 36 | ------- 37 | 38 | There is a simple example project available in example_project which can 39 | be used to test the UI: 40 | 41 | #. Create a virtualenv 42 | #. Install django 43 | #. Change into example_project and run ``manage.py runserver`` 44 | -------------------------------------------------------------------------------- /example_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acdha/django-speedtracer/59a0f36d7b0add1326021e6e775d8f353c8438f3/example_project/__init__.py -------------------------------------------------------------------------------- /example_project/dev.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acdha/django-speedtracer/59a0f36d7b0add1326021e6e775d8f353c8438f3/example_project/dev.db -------------------------------------------------------------------------------- /example_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | try: 4 | import settings # Assumed to be in the same directory. 5 | except ImportError: 6 | import sys 7 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 8 | sys.exit(1) 9 | 10 | if __name__ == "__main__": 11 | execute_manager(settings) 12 | -------------------------------------------------------------------------------- /example_project/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for example_project project. 2 | import os 3 | 4 | DEBUG = True 5 | TEMPLATE_DEBUG = DEBUG 6 | 7 | ADMINS = ( 8 | # ('Your Name', 'your_email@domain.com'), 9 | ) 10 | 11 | MANAGERS = ADMINS 12 | 13 | DATABASES = { 14 | 'default': { 15 | 'ENGINE': 'django.db.backends.sqlite3', 16 | 'NAME': 'dev.db', 17 | 'USER': '', 18 | 'PASSWORD': '', 19 | 'HOST': '', 20 | 'PORT': '', 21 | } 22 | } 23 | 24 | # Local time zone for this installation. Choices can be found here: 25 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 26 | # although not all choices may be available on all operating systems. 27 | # On Unix systems, a value of None will cause Django to use the same 28 | # timezone as the operating system. 29 | # If running in a Windows environment this must be set to the same as your 30 | # system time zone. 31 | TIME_ZONE = 'America/Chicago' 32 | 33 | # Language code for this installation. All choices can be found here: 34 | # http://www.i18nguy.com/unicode/language-identifiers.html 35 | LANGUAGE_CODE = 'en-us' 36 | 37 | SITE_ID = 1 38 | 39 | # If you set this to False, Django will make some optimizations so as not 40 | # to load the internationalization machinery. 41 | USE_I18N = True 42 | 43 | # If you set this to False, Django will not format dates, numbers and 44 | # calendars according to the current locale 45 | USE_L10N = True 46 | 47 | # Absolute filesystem path to the directory that will hold user-uploaded files. 48 | # Example: "/home/media/media.lawrence.com/" 49 | MEDIA_ROOT = '' 50 | 51 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 52 | # trailing slash if there is a path component (optional in other cases). 53 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 54 | MEDIA_URL = '' 55 | 56 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 57 | # trailing slash. 58 | # Examples: "http://foo.com/media/", "/media/". 59 | ADMIN_MEDIA_PREFIX = '/media/' 60 | 61 | # Make this unique, and don't share it with anybody. 62 | SECRET_KEY = '##g-qj0@4-@spjqp!#w2#(h^oag^9#wr3kzdji8m(ychwplvea' 63 | 64 | # List of callables that know how to import templates from various sources. 65 | TEMPLATE_LOADERS = ( 66 | 'django.template.loaders.filesystem.Loader', 67 | 'django.template.loaders.app_directories.Loader', 68 | # 'django.template.loaders.eggs.Loader', 69 | ) 70 | 71 | MIDDLEWARE_CLASSES = ( 72 | 'speedtracer.middleware.SpeedTracerMiddleware', 73 | 'django.middleware.common.CommonMiddleware', 74 | 'django.contrib.sessions.middleware.SessionMiddleware', 75 | 'django.middleware.csrf.CsrfViewMiddleware', 76 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 77 | 'django.contrib.messages.middleware.MessageMiddleware', 78 | 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', 79 | ) 80 | 81 | ROOT_URLCONF = 'example_project.urls' 82 | 83 | TEMPLATE_DIRS = ( 84 | os.path.join(os.path.dirname(__file__), "templates"), 85 | ) 86 | 87 | INSTALLED_APPS = ( 88 | 'django.contrib.auth', 89 | 'django.contrib.contenttypes', 90 | 'django.contrib.sessions', 91 | 'django.contrib.sites', 92 | 'django.contrib.messages', 93 | 'django.contrib.admin', 94 | 'django.contrib.flatpages', 95 | 96 | 'speedtracer', 97 | ) 98 | -------------------------------------------------------------------------------- /example_project/templates/flatpages/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ flatpage.title }} 5 | 6 | 7 | {{ flatpage.content }} 8 | 9 | -------------------------------------------------------------------------------- /example_project/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | from django.contrib import admin 4 | admin.autodiscover() 5 | 6 | urlpatterns = patterns('', 7 | (r'^admin/', include(admin.site.urls)), 8 | ) 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | import os 3 | 4 | setup( 5 | name='django-speedtracer', 6 | version='0.1.1', 7 | license='BSD', 8 | description="Profile your Django site using Google Chrome's SpeedTracer", 9 | long_description=open(os.path.join(os.path.dirname(__file__), "README.rst")).read(), 10 | author='Chris Adams', 11 | author_email='chris@improbable.org', 12 | url='http://github.com/acdha/django-speedtracer', 13 | include_package_data=True, 14 | zip_safe=False, 15 | packages=[ 16 | 'speedtracer', 17 | ], 18 | classifiers=[ 19 | 'Development Status :: 4 - Beta', 20 | 'Environment :: Web Environment', 21 | 'Intended Audience :: Developers', 22 | 'License :: OSI Approved :: BSD License', 23 | 'Operating System :: OS Independent', 24 | 'Programming Language :: Python', 25 | 'Framework :: Django', 26 | ], 27 | ) 28 | 29 | -------------------------------------------------------------------------------- /speedtracer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acdha/django-speedtracer/59a0f36d7b0add1326021e6e775d8f353c8438f3/speedtracer/__init__.py -------------------------------------------------------------------------------- /speedtracer/middleware.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | import os 4 | import re 5 | import inspect 6 | import time 7 | import uuid 8 | import sys 9 | 10 | from django.conf import settings 11 | from django.core.cache import cache 12 | from django.http import Http404, HttpResponse 13 | from django.utils import simplejson 14 | 15 | 16 | class SpeedTracerMiddleware(object): 17 | """ 18 | Record server-side performance data for Google Chrome's SpeedTracer 19 | 20 | Getting started: 21 | 22 | 1. Download and install Speed Tracer: 23 | http://code.google.com/webtoolkit/speedtracer/get-started.html 24 | 2. Add this middleware to your MIDDLEWARE_CLASSES 25 | 3. Reload your page 26 | 4. Open SpeedTracer and expand the "Server Trace" in the page's detailed 27 | report which should look something like http://flic.kr/p/8kwEw3 28 | 29 | NOTE: Trace data is store in the Django cache. Yours must be functional. 30 | """ 31 | 32 | #: Traces will be stored in the cache with keys using this prefix: 33 | CACHE_PREFIX = getattr(settings, "SPEEDTRACER_CACHE_PREFIX", 'speedtracer-%s') 34 | 35 | #: Help debug SpeedTracerMiddleware: 36 | DEBUG = getattr(settings, 'SPEEDTRACER_DEBUG', False) 37 | 38 | #: Trace into Django code: 39 | TRACE_DJANGO = getattr(settings, 'SPEEDTRACER_TRACE_DJANGO', False) 40 | 41 | #: Trace data will be retrieved from here: 42 | TRACE_URL = getattr(settings, "SPEEDTRACER_API_URL", '/__speedtracer__/') 43 | 44 | def __init__(self): 45 | self.traces = [] 46 | self.call_stack = [] 47 | 48 | file_filter = getattr(settings, "SPEEDTRACER_FILE_FILTER_RE", None) 49 | if isinstance(file_filter, basestring): 50 | file_filter = re.compile(file_filter) 51 | elif file_filter is None: 52 | # We'll build a list of installed app modules from INSTALLED_APPS 53 | app_dirs = set() 54 | for app in settings.INSTALLED_APPS: 55 | try: 56 | if app.startswith("django.") and not self.TRACE_DJANGO: 57 | continue 58 | 59 | for k, v in sys.modules.items(): 60 | if k.startswith(app): 61 | app_dirs.add(*sys.modules[app].__path__) 62 | except KeyError: 63 | print >>sys.stderr, "Can't get path for app: %s" % app 64 | 65 | app_dir_re = "(%s)" % "|".join(map(re.escape, app_dirs)) 66 | 67 | print >> sys.stderr, "Autogenerated settings.SPEEDTRACER_FILE_FILTER_RE: %s" % app_dir_re 68 | 69 | file_filter = re.compile(app_dir_re) 70 | 71 | self.file_filter = file_filter 72 | 73 | def trace_callback(self, frame, event, arg): 74 | if not event in ('call', 'return'): 75 | return 76 | 77 | if not self.file_filter.match(frame.f_code.co_filename): 78 | return # No trace 79 | 80 | if self.DEBUG: 81 | print "%s: %s %s[%s]" % ( 82 | event, 83 | frame.f_code.co_name, 84 | frame.f_code.co_filename, 85 | frame.f_lineno, 86 | ) 87 | 88 | if event == 'call': 89 | code = frame.f_code 90 | 91 | class_name = module_name = "" 92 | 93 | module = inspect.getmodule(code) 94 | if module: 95 | module_name = module.__name__ 96 | 97 | try: 98 | class_name = frame.f_locals['self'].__class__.__name__ 99 | except (KeyError, AttributeError): 100 | pass 101 | 102 | new_record = { 103 | 'operation': { 104 | 'sourceCodeLocation': { 105 | 'className' : frame.f_code.co_filename, 106 | 'methodName' : frame.f_code.co_name, 107 | 'lineNumber' : frame.f_lineno, 108 | }, 109 | 'type': 'METHOD', 110 | 'label': '.'.join(filter(None, (module_name, class_name, frame.f_code.co_name))), 111 | }, 112 | 'children': [], 113 | 'range': {"start_time": time.time() }, 114 | } 115 | 116 | new_record['id'] = id(new_record) 117 | 118 | self.call_stack.append(new_record) 119 | 120 | return self.trace_callback 121 | 122 | elif event == 'return': 123 | end_time = time.time() 124 | 125 | if not self.call_stack: 126 | print >>sys.stderr, "Return without stack?" 127 | return 128 | 129 | current_frame = self.call_stack.pop() 130 | 131 | current_frame['range'] = self._build_range(current_frame['range']["start_time"], end_time) 132 | 133 | if not self.call_stack: 134 | self.traces.append(current_frame) 135 | else: 136 | self.call_stack[-1]['children'].append(current_frame) 137 | 138 | return 139 | 140 | def process_request(self, request): 141 | if request.path.endswith("symbolmanifest.json"): 142 | raise Http404 143 | 144 | if not request.path.startswith(self.TRACE_URL): 145 | request._speedtracer_start_time = time.time() 146 | sys.settrace(self.trace_callback) 147 | return 148 | 149 | trace_id = self.CACHE_PREFIX % request.path[len(self.TRACE_URL):] 150 | 151 | data = cache.get(trace_id, {}) 152 | 153 | return HttpResponse(simplejson.dumps(data), mimetype="application/json; charset=UTF-8") 154 | 155 | def process_response(self, request, response): 156 | sys.settrace(None) 157 | 158 | try: 159 | start_time = request._speedtracer_start_time 160 | except AttributeError: 161 | return response 162 | 163 | end_time = time.time() 164 | 165 | trace_id = uuid.uuid4() 166 | 167 | data = { 168 | 'trace': { 169 | 'id': str(trace_id), 170 | 'application': 'Django SpeedTracer', 171 | 'date': time.time(), 172 | 'range': self._build_range(start_time, end_time), 173 | 'frameStack': { 174 | 'id': 0, 175 | 'range': self._build_range(start_time, end_time), 176 | 'operation': { 177 | 'type': 'HTTP', 178 | 'label': "%s %s" % (request.method, request.path) 179 | }, 180 | 'children': self.traces, 181 | } 182 | } 183 | } 184 | 185 | cache.set(self.CACHE_PREFIX % trace_id, data, getattr(settings, "SPEEDTRACER_TRACE_TTL", 3600)) 186 | 187 | response['X-TraceUrl'] = "%s%s" % (self.TRACE_URL, trace_id) 188 | 189 | return response 190 | 191 | def _build_range(self, start_time, end_time): 192 | return { 193 | "start": start_time, 194 | "end": end_time, 195 | "duration": end_time - start_time, 196 | } 197 | --------------------------------------------------------------------------------