Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
catcombo committed Dec 11, 2018
2 parents f49d2ed + 80ab2d6 commit 4d6ed51
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 49 deletions.
39 changes: 29 additions & 10 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,6 @@ to control profiler state. Press ``Reset`` button to reset all data.
Configuration
=============

To exclude some urls from profiling add it to the ``SPEEDINFO_EXCLUDE_URLS`` list.
``SpeedInfo`` uses re.match internally to test requested url. Example::

SPEEDINFO_EXCLUDE_URLS = [
r'/admin/',
r'/news/$',
r'/movie/\d+/$',
]

``SpeedInfo`` automatically detects when using Django per-site caching via
``UpdateCacheMiddleware`` and ``FetchFromCacheMiddleware`` middlewares
or per-view caching via ``cache_page`` decorator and counts cache hit
Expand All @@ -84,7 +75,7 @@ Example::
setattr(response, SPEEDINFO_CACHED_RESPONSE_ATTR_NAME, True)
return response

Change ``SPEEDINFO_REPORT_COLUMNS`` settings to customize Django admin profiler columns.
Change ``SPEEDINFO_REPORT_COLUMNS`` setting to customize Django admin profiler columns.
Default value::

SPEEDINFO_REPORT_COLUMNS = (
Expand All @@ -93,6 +84,34 @@ Default value::
)


Profiling conditions
====================

``SPEEDINFO_PROFILING_CONDITIONS`` setting allows to declare a list of imported by path classes
to define the conditions for profiling the processed view. By default, the only condition is enabled::

SPEEDINFO_PROFILING_CONDITIONS = [
'speedinfo.conditions.exclude_urls.ExcludeURLCondition',
]


``ExcludeURLCondition`` allows you to exclude some urls from profiling by adding them to
the ``SPEEDINFO_EXCLUDE_URLS`` list. ``ExcludeURLCondition`` uses ``re.match`` internally to test
requested url. Example::

SPEEDINFO_EXCLUDE_URLS = [
r'/admin/',
r'/news/$',
r'/movie/\d+/$',
]


To define your own condition class, you must inherit from the base class ``speedinfo.conditions.base.Condition``
and implement all abstract methods. See ``ExcludeURLCondition`` source code for implementation example. Then add
full path to your class to ``SPEEDINFO_PROFILING_CONDITIONS`` list as shown above. Conditions in mentioned list
are executed in a top-down order. The first condition returning ``False`` interrupts the further check.


Notice
======

Expand Down
5 changes: 2 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@

setup(
name='django-speedinfo',
version='1.3.7',
packages=['speedinfo', 'speedinfo.migrations'],
version='1.4.0',
packages=['speedinfo', 'speedinfo.migrations', 'speedinfo.conditions'],
include_package_data=True,
license='MIT',
description='Live profiling tool for Django framework to measure views performance',
Expand All @@ -19,7 +19,6 @@
author='Evgeniy Krysanov',
author_email='[email protected]',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'Environment :: Web Environment',
'Framework :: Django',
Expand Down
34 changes: 34 additions & 0 deletions speedinfo/conditions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# coding: utf-8

from importlib import import_module

from speedinfo.settings import speedinfo_settings


class ConditionsDispatcher:
def __init__(self):
self.conditions = None

def import_conditions(self):
self.conditions = []

for module_path in speedinfo_settings.SPEEDINFO_PROFILING_CONDITIONS:
path, class_name = module_path.rsplit('.', 1)

try:
module = import_module(path)
self.conditions.append(
getattr(module, class_name)()
)
except (AttributeError, ImportError) as e:
msg = 'Could not import "{}". {}: {}.'.format(module_path, e.__class__.__name__, e)
raise ImportError(msg)

def get_conditions(self):
if self.conditions is None:
self.import_conditions()

return self.conditions


conditions_dispatcher = ConditionsDispatcher()
22 changes: 22 additions & 0 deletions speedinfo/conditions/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# coding: utf-8


class Condition:
"""
Base class for user-defined profiling condition classes
"""
def process_request(self, request):
"""
:type request: :class:`django.http.HttpRequest`
:return: False if requested page should be excluded from profiling
:rtype: bool
"""
raise NotImplementedError

def process_response(self, response):
"""
:type response: :class:`django.http.HttpResponse`
:return: False if requested page should be excluded from profiling
:rtype: bool
"""
raise NotImplementedError
37 changes: 37 additions & 0 deletions speedinfo/conditions/exclude_urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# coding: utf-8

import re

from speedinfo.conditions.base import Condition
from speedinfo.settings import speedinfo_settings


class ExcludeURLCondition(Condition):
"""
Plugin for conditional profiling based on the list of
urls specified in SPEEDINFO_EXCLUDE_URLS settings
"""
def __init__(self):
self.patterns = None

def get_patterns(self):
if (self.patterns is None) or speedinfo_settings.SPEEDINFO_TESTS:
self.patterns = [re.compile(pattern) for pattern in speedinfo_settings.SPEEDINFO_EXCLUDE_URLS]

return self.patterns

def process_request(self, request):
"""Checks requested page url against the list of excluded urls.
:type request: :class:`django.http.HttpRequest`
:return: False if path matches to any of the exclude urls
:rtype: bool
"""
for pattern in self.get_patterns():
if pattern.match(request.path):
return False

return True

def process_response(self, response):
return True
84 changes: 51 additions & 33 deletions speedinfo/middleware.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
# coding: utf-8

import re

from itertools import islice
from timeit import default_timer

from django.db import connection
from speedinfo import profiler
from speedinfo.conditions import conditions_dispatcher
from speedinfo.settings import speedinfo_settings

try:
Expand All @@ -26,20 +25,6 @@ def __init__(self, get_response=None):
self.force_debug_cursor = False
self.start_time = 0
self.existing_sql_count = 0
self.exclude_urls_re = [re.compile(pattern) for pattern in speedinfo_settings.SPEEDINFO_EXCLUDE_URLS]

def match_exclude_urls(self, path):
"""Looks for a match requested page url to the exclude urls list.
:param str path: Path to the requested page, not including the scheme or domain
:return: True if path matches to any of the exclude urls
:rtype: bool
"""
for url_re in self.exclude_urls_re:
if url_re.match(path):
return True

return False

def get_view_name(self, request):
"""Returns full view name from request, eg. 'app.module.view_name'.
Expand All @@ -54,17 +39,47 @@ def get_view_name(self, request):
except Resolver404:
return None

def can_process_request(self, request):
"""Checks conditions to start profiling the request
:type request: :class:`django.http.HttpRequest`
:return: True if request can be processed
:rtype: bool
"""
result = profiler.is_on and hasattr(request, 'user') and self.get_view_name(request)

for condition in conditions_dispatcher.get_conditions():
result = result and condition.process_request(request)

if not result:
break

return result

def can_process_response(self, response):
"""Checks conditions to finish profiling the request
:type response: :class:`django.http.HttpResponse`
:return: True if request can be processed
:rtype: bool
"""
result = True

for condition in conditions_dispatcher.get_conditions():
result = result and condition.process_response(response)

if not result:
break

return result

def process_view(self, request, *args, **kwargs):
"""Initialize statistics variables and environment.
:return: Response object or None
:rtype: :class:`django.http.HttpResponse` or None
"""
# Checks conditions to start profiling the request
self.is_active = profiler.is_on and \
hasattr(request, 'user') and \
self.get_view_name(request) and \
not self.match_exclude_urls(request.path)
self.is_active = self.can_process_request(request)

if self.is_active:
# Force DB connection to debug mode to get sql time and number of queries
Expand All @@ -85,21 +100,24 @@ def process_response(self, request, response):
:rtype: :class:`django.http.HttpResponse` or :class:`django.http.StreamingHttpResponse`
"""
if self.is_active:
view_execution_time = default_timer() - self.start_time
if self.can_process_response(response):
view_execution_time = default_timer() - self.start_time

# Calculate the execution time and the number of queries.
# Exclude queries made before the call of our middleware (e.g. in SessionMiddleware).
sql_count = max(len(connection.queries) - self.existing_sql_count, 0)
sql_time = sum(float(q['time']) for q in islice(connection.queries, self.existing_sql_count, None))
connection.force_debug_cursor = self.force_debug_cursor
# Calculate the execution time and the number of queries.
# Exclude queries made before the call of our middleware (e.g. in SessionMiddleware).
sql_count = max(len(connection.queries) - self.existing_sql_count, 0)
sql_time = sum(float(q['time']) for q in islice(connection.queries, self.existing_sql_count, None))

# Collects request and response params
view_name = self.get_view_name(request)
is_anon_call = request.user.is_anonymous() if callable(request.user.is_anonymous) else request.user.is_anonymous
is_cache_hit = getattr(response, speedinfo_settings.SPEEDINFO_CACHED_RESPONSE_ATTR_NAME, False)

# Collects request and response params
view_name = self.get_view_name(request)
is_anon_call = request.user.is_anonymous() if callable(request.user.is_anonymous) else request.user.is_anonymous
is_cache_hit = getattr(response, speedinfo_settings.SPEEDINFO_CACHED_RESPONSE_ATTR_NAME, False)
# Saves profiler data
profiler.data.add(view_name, request.method, is_anon_call, is_cache_hit, sql_time, sql_count, view_execution_time)

# Saves profiler data
profiler.data.add(view_name, request.method, is_anon_call, is_cache_hit, sql_time, sql_count, view_execution_time)
# Rollback debug cursor value even if process response condition is disabled
connection.force_debug_cursor = self.force_debug_cursor

return response

Expand Down
4 changes: 4 additions & 0 deletions speedinfo/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
ReportColumnFormat = namedtuple('ReportColumnFormat', ['name', 'format', 'attr_name', 'order_field'])

DEFAULTS = {
'SPEEDINFO_TESTS': False,
'SPEEDINFO_CACHED_RESPONSE_ATTR_NAME': 'is_cached',
'SPEEDINFO_PROFILING_CONDITIONS': [
'speedinfo.conditions.exclude_urls.ExcludeURLCondition',
],
'SPEEDINFO_EXCLUDE_URLS': [],
'SPEEDINFO_REPORT_COLUMNS': (
'view_name', 'method', 'anon_calls_ratio', 'cache_hits_ratio',
Expand Down
6 changes: 3 additions & 3 deletions tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from django.core.urlresolvers import reverse


@override_settings(SPEEDINFO_EXCLUDE_URLS=[])
@override_settings(SPEEDINFO_EXCLUDE_URLS=[], SPEEDINFO_TESTS=True)
class ProfilerTest(TestCase):
def setUp(self):
cache.clear()
Expand Down Expand Up @@ -159,7 +159,7 @@ def test_reset(self):
self.assertFalse(ViewProfiler.objects.exists())


@override_settings(SPEEDINFO_EXCLUDE_URLS=[reverse('func-view')])
@override_settings(SPEEDINFO_EXCLUDE_URLS=[reverse('func-view')], SPEEDINFO_TESTS=True)
class ExcludeURLConditionTest(TestCase):
def setUp(self):
cache.clear()
Expand All @@ -180,7 +180,7 @@ def test_exclude_urls(self):
self.assertTrue(ViewProfiler.objects.exists())


@override_settings(SPEEDINFO_EXCLUDE_URLS=[reverse('admin:index')])
@override_settings(SPEEDINFO_EXCLUDE_URLS=[reverse('admin:index')], SPEEDINFO_TESTS=True)
class ProfilerAdminTest(TestCase):
def setUp(self):
cache.clear()
Expand Down

0 comments on commit 4d6ed51

Please sign in to comment.