forked from jazzband/django-silk
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Visualize profile result (jazzband#192)
* started working on showing a graph of the profile output which helps to quickly see the slow path through the profiled code * further work on profile graph visualization * finished backend and frontend work for displaying a graph of the profile and added some tests to verify dot generation * updated requirements for graph rendering * Fixed error in python 2 due to no mock library * update travis file to also install test requirements * removed double reference to mock in test-requirements * Fixed broken version number * Update profile_dot.py Fixed bug under python 2. StringIO is not a context manager * Open temp file in binary mode * fixed failing test on python 2 due to string encoding * added travis_retry to pip install steps since the pip install was causing the build to fail on python 3.4 while downloading pytz
- Loading branch information
1 parent
6ef5316
commit 6cf10ac
Showing
9 changed files
with
462 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,3 +11,5 @@ mock==1.0.1 | |
Pillow==2.5.1 | ||
factory-boy==2.8.1 | ||
freezegun==0.3.5 | ||
networkx==1.11 | ||
pydotplus==2.0.2 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
# std | ||
import os | ||
import cProfile | ||
import tempfile | ||
from contextlib import contextmanager | ||
from six import PY3 | ||
if PY3: | ||
from unittest.mock import MagicMock | ||
else: | ||
from mock import MagicMock | ||
# 3rd party | ||
from django.test import TestCase | ||
from networkx.drawing.nx_pydot import read_dot | ||
# silk | ||
from silk.views.profile_dot import _create_profile, _create_dot, _temp_file_from_file_field | ||
|
||
|
||
class ProfileDotViewTestCase(TestCase): | ||
|
||
@classmethod | ||
@contextmanager | ||
def _stats_file(cls): | ||
""" | ||
Context manager to create some arbitrary profiling stats in a temp file, returning the filename on enter, | ||
and removing the temp file on exit. | ||
""" | ||
try: | ||
with tempfile.NamedTemporaryFile(delete=False) as stats: | ||
pass | ||
cProfile.run('1+1', stats.name) | ||
yield stats.name | ||
finally: | ||
os.unlink(stats.name) | ||
|
||
@classmethod | ||
@contextmanager | ||
def _stats_data(cls): | ||
""" | ||
Context manager to create some arbitrary profiling stats in a temp file, returning the data on enter, | ||
and removing the temp file on exit. | ||
""" | ||
with cls._stats_file() as filename: | ||
with open(filename, 'rb') as f: | ||
yield f.read() | ||
|
||
@classmethod | ||
def _profile(cls): | ||
"""Create some arbitrary profiling stats.""" | ||
with cls._stats_file() as filename: | ||
# create profile - we don't need to convert a django file field to a temp file | ||
# just use the filename of the temp file already created | ||
@contextmanager | ||
def dummy(_): yield filename | ||
return _create_profile(filename, dummy) | ||
|
||
@classmethod | ||
def _mock_file(cls, data): | ||
""" | ||
Get a mock object that looks like a file but returns data when read is called. | ||
""" | ||
i = [0] | ||
def read(n): | ||
if not i[0]: | ||
i[0] += 1 | ||
return data | ||
|
||
stream = MagicMock() | ||
stream.open = lambda: None | ||
stream.read = read | ||
|
||
return stream | ||
|
||
def test_create_dot(self): | ||
""" | ||
Verify that a dot file is correctly created from pstats data stored in a file field. | ||
""" | ||
with self._stats_file() as filename: | ||
|
||
try: | ||
# create dot | ||
with tempfile.NamedTemporaryFile(delete=False) as dotfile: | ||
dot = _create_dot(self._profile(), 5) | ||
dot = dot.encode('utf-8') if PY3 else dot | ||
dotfile.write(dot) | ||
|
||
# verify generated dot is valid | ||
G = read_dot(dotfile.name) | ||
self.assertGreater(len(G.nodes()), 0) | ||
|
||
finally: | ||
os.unlink(dotfile.name) | ||
|
||
def test_temp_file_from_file_field(self): | ||
""" | ||
Verify that data held in a file like object is copied to a temp file. | ||
""" | ||
dummy_data = 'dummy data'.encode('utf-8') | ||
stream = self._mock_file(dummy_data) | ||
|
||
with _temp_file_from_file_field(stream) as filename: | ||
with open(filename, 'rb') as f: | ||
self.assertEqual(f.read(), dummy_data) | ||
|
||
# file should have been removed on exit | ||
self.assertFalse(os.path.exists(filename)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,3 +10,4 @@ Pillow>=3.2 | |
django>=1.8 | ||
freezegun>=0.3 | ||
factory-boy>=2.8.1 | ||
gprof2dot>=2016.10.13 |
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
# std | ||
import os | ||
import json | ||
import tempfile | ||
import shutil | ||
from contextlib import closing, contextmanager | ||
# 3rd party | ||
from six import StringIO | ||
from django.shortcuts import get_object_or_404 | ||
from django.utils.decorators import method_decorator | ||
from django.views.generic import View | ||
from django.http import HttpResponse | ||
from gprof2dot import DotWriter, PstatsParser, Profile, TEMPERATURE_COLORMAP | ||
# silk | ||
from silk.auth import login_possibly_required, permissions_possibly_required | ||
from silk.models import Request | ||
|
||
|
||
@contextmanager | ||
def _temp_file_from_file_field(source): | ||
""" | ||
Create a temp file containing data from a django file field. | ||
""" | ||
source.open() | ||
with closing(source): | ||
try: | ||
with tempfile.NamedTemporaryFile(delete=False) as destination: | ||
shutil.copyfileobj(source, destination) | ||
yield destination.name | ||
finally: | ||
os.unlink(destination.name) | ||
|
||
|
||
def _create_profile(source, get_filename=_temp_file_from_file_field): | ||
""" | ||
Parse a profile from a django file field source. | ||
""" | ||
with get_filename(source) as filename: | ||
return PstatsParser(filename).parse() | ||
|
||
|
||
def _create_dot(profile, cutoff): | ||
""" | ||
Create a dot file from pstats data stored in a django file field. | ||
""" | ||
node_cutoff = cutoff / 100.0 | ||
edge_cutoff = 0.1 / 100.0 | ||
profile.prune(node_cutoff, edge_cutoff, False) | ||
|
||
with closing(StringIO()) as fp: | ||
DotWriter(fp).graph(profile, TEMPERATURE_COLORMAP) | ||
return fp.getvalue() | ||
|
||
|
||
class ProfileDotView(View): | ||
|
||
@method_decorator(login_possibly_required) | ||
@method_decorator(permissions_possibly_required) | ||
def get(self, request, request_id): | ||
silk_request = get_object_or_404(Request, pk=request_id, prof_file__isnull=False) | ||
cutoff = float(request.GET.get('cutoff', '') or 5) | ||
profile = _create_profile(silk_request.prof_file) | ||
result = dict(dot=_create_dot(profile, cutoff)) | ||
return HttpResponse(json.dumps(result).encode('utf-8'), content_type='application/json') |