├── speedtracer
├── __init__.py
└── middleware.py
├── example_project
├── __init__.py
├── dev.db
├── urls.py
├── templates
│ └── flatpages
│ │ └── default.html
├── manage.py
└── settings.py
├── MANIFEST.in
├── setup.py
├── README.rst
└── LICENSE
/speedtracer/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example_project/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.rst
2 |
--------------------------------------------------------------------------------
/example_project/dev.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/acdha/django-speedtracer/HEAD/example_project/dev.db
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/example_project/templates/flatpages/default.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ flatpage.title }}
5 |
6 |
7 | {{ flatpage.content }}
8 |
9 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------