Commits

Mikhail Korobov committed d1b9970

Initial import

Comments (0)

Files changed (16)

+syntax: glob
+
+#IDE files
+.settings/*
+.project
+.pydevproject
+.cache/*
+
+#temp files
+*.pyc
+*.pyo
+*.orig
+*~
+
+#misc files
+pip-log.txt
+
+#os files
+.DS_Store
+Thumbs.db
+
+#setup files
+build/
+dist/
+.build/
+MANIFEST
+django_profiling_dashboard.egg-info
+Authors
+-------
+
+* Mikhail Korobov
+
+Copyright (c) 2012, Mikhail Korobov
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+    * Neither the name of the tastypie nor the
+      names of its contributors may be used to endorse or promote products
+      derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL MATT CROYDON BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+include *.txt
+include *.rst
+recursive-include profiling_dashboard/templates/profiling_dashboard/ *.html
+==========================
+django-profiling-dashboard
+==========================
+
+django-profiling-dashboard provides a dashboard with various profiling tools suitable
+for use in live servers.
+
+Requirements
+============
+
+* `yappi<http://code.google.com/p/yappi/>`_ for thread-aware live server profiling
+  that can be enabled and disabled at run time;
+* `Pympler <http://code.google.com/p/pympler/>`_ for memory debugging;
+* `psutil<http://code.google.com/p/psutil/>`_ for system resource usage investigation.
+
+Dashboard remplates are based on `Bootstrap<http://twitter.github.com/bootstrap/>`_ toolkit.
+
+django-profiling-dashboard requires django >= 1.3 and python >= 2.6.
+
+Installation
+============
+
+Make sure the requirements are installed::
+
+    pip install yappi pympler psutil
+
+and install django-profiling-dashboard using pip::
+
+    pip install django-profiling-dashboard
+
+Usage
+=====
+
+1. Add ``'profiling_dashboard'`` to ``INSTALLED_APPS``;
+2. include 'profiling_dashboard.urls' in your urls.py::
+
+      urlpatterns = patterns('',
+          # ...
+          url(r'^profiling-dashboard/', include('profiling_dashboard.urls')),
+          # ...
+      )
+
+3. visit /profiling-dashboard/
+
+Screenshots
+===========
+
+TODO
+
+
+Notes on CPU profiling in multi-process environment
+===================================================
+
+If there are several server processes then the profiler have to be started and stopped for each process,
+and the profiling stats will be different for different processes.
+
+In some deployment schemas (e.g. apache proxied by nginx) there is no way to make sure subsequent requests
+will be handled by the same server process so take this in account while using django-profiling-dashboard.

profiling_dashboard/__init__.py

+# -*- coding: utf-8 -*-
+from __future__ import absolute_import

profiling_dashboard/forms.py

+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+from pympler import muppy, summary
+from pympler.muppy import get_size
+from django import forms
+import yappi
+from profiling_dashboard.stats import get_top_info
+from .stats import get_full_yappi_stats, get_other_yappi_stats
+
+class YappiManageForm(forms.Form):
+    ACTIONS = (
+        ('start', 'start'),
+        ('start_with_builtins', 'start_with_builtins'),
+        ('stop', 'stop'),
+        ('reset', 'reset'),
+    )
+
+    action = forms.ChoiceField(ACTIONS)
+
+    def do_action(self):
+        action = self.cleaned_data['action']
+        if action == 'start':
+            yappi.start()
+        elif action == 'start_with_builtins':
+            yappi.start(builtins=True)
+        elif action == 'stop':
+            yappi.stop()
+        elif action == 'reset':
+            yappi.clear_stats()
+        return action
+
+
+class YappiFilterForm(forms.Form):
+    SORT_ORDER = (
+        (yappi.SORTORDER_ASCENDING, 'asc'),
+        (yappi.SORTORDER_DESCENDING, 'desc'),
+    )
+
+    SORT_TYPE = (
+        (yappi.SORTTYPE_NAME, 'name of the function being profiled'),
+        (yappi.SORTTYPE_NCALL, 'total call count of the function'),
+        (yappi.SORTTYPE_TAVG, 'average total time'),
+        (yappi.SORTTYPE_TSUB, 'total time spent in the function excluding sub-calls'),
+        (yappi.SORTTYPE_TTOTAL, 'total time spent in the function'),
+    )
+
+    sort_order = forms.TypedChoiceField(choices=SORT_ORDER, initial=yappi.SORTORDER_DESCENDING, coerce=int, widget=forms.HiddenInput())
+    sort_type = forms.TypedChoiceField(choices=SORT_TYPE, initial=yappi.SORTTYPE_TTOTAL, coerce=int, widget=forms.HiddenInput())
+    limit = forms.IntegerField(initial=20, help_text='-1 means no limit')
+
+    def get_stats(self):
+        try:
+            return get_full_yappi_stats(
+                sorttype=self.cleaned_data['sort_type'],
+                sortorder=self.cleaned_data['sort_order'],
+                limit=self.cleaned_data['limit'],
+            )
+        except Exception as e:
+            return ['Stats are not available.\n Reason: %s' % e]
+
+    def get_other_stats(self):
+        return get_other_yappi_stats()
+
+
+class MuppyFilterForm(forms.Form):
+    limit = forms.IntegerField(initial=20)
+    sort_by = forms.IntegerField(initial=2, widget=forms.HiddenInput())
+
+    def get_report(self):
+        all_objects = muppy.get_objects()
+        size = get_size(all_objects)
+        report = summary.summarize(all_objects)
+
+        sort_index = self.cleaned_data['sort_by']
+        limit = self.cleaned_data['limit']
+
+        report.sort(key=lambda item: item[sort_index], reverse=True)
+        if limit:
+            report = report[:limit]
+
+        return size, report
+
+class TopFilterForm(forms.Form):
+    limit = forms.IntegerField(initial=100)
+    sort_by = forms.CharField(initial='RSS', widget=forms.HiddenInput())
+    only_ready = forms.BooleanField(initial=True, required=False)
+
+    def get_processes(self):
+        processes = get_top_info()
+
+        sort_index = self.cleaned_data['sort_by']
+        limit = self.cleaned_data['limit']
+
+        if self.cleaned_data['only_ready']:
+            processes = filter(lambda proc: proc._READY, processes)
+
+        processes.sort(key = lambda proc: getattr(proc, sort_index, None), reverse=True)
+        if limit:
+            processes = processes[:limit]
+
+        return processes

profiling_dashboard/stats.py

+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+from collections import namedtuple
+from contextlib import contextmanager
+import datetime
+from time import strftime
+import psutil
+import yappi
+
+YappiStat = namedtuple('YappiStat', 'name ncall ttotal tsub tavg')
+
+def _get_all_yappi_stats():
+    res = []
+    def handle(data):
+        avg = data[2]/data[1]
+        stat = YappiStat(*(data+(avg,)))
+        res.append(stat)
+    yappi.enum_stats(handle)
+    return res
+
+def get_full_yappi_stats(sorttype=yappi.SORTTYPE_NCALL, sortorder=yappi.SORTORDER_DESCENDING, limit=yappi.SHOW_ALL):
+    """
+    Like yappi.get_stats, but returns a list of namedtuples with the full information for function profiling data.
+    """
+
+    stats = _get_all_yappi_stats()
+    stats.sort(key=lambda stat: stat[sorttype], reverse=sortorder)
+
+    if limit != yappi.SHOW_ALL:
+        stats = stats[:limit]
+
+    return stats
+
+def get_other_yappi_stats():
+    stats = yappi.get_stats(limit=0)
+    THREAD_HEADER = '\n\nname           tid    fname                                scnt     ttot'
+    thread_index = stats.index(THREAD_HEADER)
+    stats[thread_index] = stats[thread_index].strip()
+    return stats[thread_index:]
+
+def get_yappi_status():
+    try:
+        other_stats = get_other_yappi_stats()
+        last_line = other_stats[-1]
+        return last_line.split()[0]
+    except Exception as e:
+        return str(e)
+
+@contextmanager
+def _ignore_AccessDenied():
+    try:
+        yield
+    except psutil.AccessDenied:
+        pass
+    except AttributeError:
+        pass
+
+def proc_annotate_with_short_info(p):
+    p._READY = False
+    with _ignore_AccessDenied():
+        p.PID = p.pid
+        p.USERNAME = p.username
+        p.CMDLINE = ' '.join(p.cmdline)
+        p.NAME = p.name
+        p.RSS, p.VMS = p.get_memory_info()
+        p.MEMPERCENT = p.get_memory_percent()
+        p.CPUPERCENT = p.get_cpu_percent(interval=0)
+        p.CPU_USER, p.CPU_SYSTEM = p.get_cpu_times()
+        p._READY = True
+    return p
+
+def proc_annotate_with_full_info(p):
+
+    with _ignore_AccessDenied():
+        p.CREATE_TIMESTAMP = p.create_time
+        p.CREATE_TIME = datetime.datetime.fromtimestamp(p.CREATE_TIMESTAMP)
+        p.NOW = datetime.datetime.now()
+
+    with _ignore_AccessDenied():
+        p.CPUPERCENT = p.get_cpu_percent(interval=1)
+
+    with _ignore_AccessDenied():
+        p.IO_COUNTERS = p.get_io_counters()
+
+    with _ignore_AccessDenied():
+        p.THREADS = p.get_threads()
+
+    with _ignore_AccessDenied():
+        p.OPEN_FILES = p.get_open_files()
+
+    with _ignore_AccessDenied():
+        p.CONNECTIONS = p.get_connections()
+
+def get_top_info():
+    processes = psutil.get_process_list()
+
+    for p in processes:
+        try:
+            proc_annotate_with_short_info(p)
+        except psutil.NoSuchProcess:
+            processes.remove(p)
+
+    return processes

profiling_dashboard/templates/profiling_dashboard/base.html

+<!DOCTYPE html>
+<html>
+<head>
+    <link rel="stylesheet" href="http://twitter.github.com/bootstrap/1.4.0/bootstrap.min.css">
+    <style>
+        body{
+            padding-top: 40px;
+        }
+    </style>
+    <title>{% block title %}Django Profiling Dashboard{% endblock %}</title>
+    {% block extrahead %}{% endblock %}
+</head>
+<body>
+
+    <section id="navigation">
+        <div class="topbar-wrapper" style="z-index: 5;">
+            <div class="topbar">
+                <div class="topbar-inner">
+                    <div class="container">
+                        <h3><a href="#">Django Profiling Dashboard {% if pid %}(pid: {{ pid }}{% if tid %}, tid: {{ tid }}{% endif %}){% endif %}</a></h3>
+                        <ul class="nav">
+                            {% block nav-cpu %}<li><a href="{% url profiling_yappi_stats %}">CPU profiler</a></li>{% endblock %}
+                            {% block nav-memory %}<li><a href="{% url profiling_memory_usage %}">Python memory profiler</a></li>{% endblock %}
+                            {% block nav-top %}<li><a href="{% url profiling_web_top %}">top</a></li>{% endblock %}
+                        </ul>
+                    </div>
+                </div><!-- /topbar-inner -->
+            </div><!-- /topbar -->
+        </div><!-- /topbar-wrapper -->
+    </section>
+
+    {% if messages %}
+        {% for message in messages %}
+            <div class="alert-message {{ message.tags }}">
+                <p>{{ message }}</p>
+            </div>
+        {% endfor %}
+    {% endif %}
+
+    <section>
+        {% block content %}{% endblock %}
+    </section>
+
+    <footer class="footer">
+        <div class="container">
+            <p class="pull-right">
+                Built with <a href="http://code.google.com/p/yappi/">yappi</a>,
+                <a href="http://code.google.com/p/pympler/">Pympler</a>,
+                <a href="http://code.google.com/p/psutil/">psutil</a> and
+                <a href="http://twitter.github.com/bootstrap/">Bootstrap</a> by Mikhail Korobov.
+                Licensed under the MIT License.
+
+                Source code: <a href="https://github.com/kmike/django-profiling-dashboard">github</a>, <a href="https://bitbucket.org/kmike/django-profiling-dashboard">bitbucket</a>.
+                Bug tracker is at <a href="https://github.com/kmike/django-profiling-dashboard/issues">github</a>.
+            </p>
+        </div>
+    </footer>
+</body>
+</html>

profiling_dashboard/templates/profiling_dashboard/cpu.html

+{% extends "profiling_dashboard/base.html" %}
+{% load query_exchange_tags %}
+
+{% block nav-cpu %}
+    <li class="active"><a href="{% url profiling_yappi_stats %}">CPU profiler</a></li>
+{% endblock %}
+
+{% block content %}
+    <form action="{% url profiling_yappi_manage %}" method="POST">{% csrf_token %}
+        <div class="clearfix">
+            <div class="input">
+                <h4>{% if status %}Status: {{ status }}{% endif %}</h4>
+                <input type="submit" name="action" value="start" class="btn">
+                <input type="submit" name="action" value="start_with_builtins" class="btn">
+                <input type="submit" name="action" value="stop" class="btn">
+                <input type="submit" name="action" value="reset" class="btn">
+            </div>
+        </div>
+    </form>
+
+    <form action="{% url profiling_yappi_stats %}" method="GET">
+        {{ form.sort_type }}
+        {{ form.sort_order }}
+        <div class="clearfix">
+            {{ form.limit.label_tag }}
+            <div class="input">
+                {{ form.limit }}
+                {{ form.limit.errors }}
+                <input type="submit" value="Show" class="btn primary">
+            </div>
+        </div>
+    </form>
+
+    {% if other_stats %}
+    <pre>{% for stat in other_stats %}
+{{ stat }}{% endfor %}</pre>
+    {% endif %}
+
+    {% if stats %}
+    <table class="condensed-table zebra-striped">
+        <thead>
+            <tr>
+                <th title="Name of the function being profiled"><a href="{% url_with_query 'profiling_yappi_stats' keep 'limit','sort_order' add sort_type='0' %}">name</a></th>
+                <th title="Total call count"><a href="{% url_with_query 'profiling_yappi_stats' keep 'limit','sort_order' add sort_type='1' %}">ncall</a></th>
+                <th title="Total time spent in the function"><a href="{% url_with_query 'profiling_yappi_stats' keep 'limit','sort_order' add sort_type='2' %}">ttotal</a></th>
+                <th title="Total time spent in the function excluding sub-calls"><a href="{% url_with_query 'profiling_yappi_stats' keep 'limit','sort_order' add sort_type='3' %}">tsub</a></th>
+                <th title="Average total time"><a href="{% url_with_query 'profiling_yappi_stats' keep 'limit','sort_order' add sort_type='4' %}">tavg</a></th>
+            </tr>
+        </thead>
+        {% for stat in stats %}
+            <tr>
+                <td>{{ stat.name }}</td>
+                <td>{{ stat.ncall }}</td>
+                <td>{{ stat.ttotal }}</td>
+                <td>{{ stat.tsub }}</td>
+                <td>{{ stat.tavg }}</td>
+            </tr>
+        {% endfor %}
+    </table>
+    {% endif %}
+
+{% endblock %}

profiling_dashboard/templates/profiling_dashboard/memory.html

+{% extends "profiling_dashboard/base.html" %}
+{% load query_exchange_tags %}
+
+{% block nav-memory %}
+    <li class="active"><a href="{% url profiling_memory_usage %}">Python memory profiler</a></li>
+{% endblock %}
+
+{% block content %}
+    <br>
+    <form action="{% url profiling_memory_usage %}" method="GET">
+        {{ form.sort_by }}
+
+        <div class="clearfix">
+            {{ form.limit.label_tag }}
+            <div class="input">
+                {{ form.limit }}
+                {{ form.limit.errors }}
+                <input type="submit" value="Show" class="btn primary">
+            </div>
+        </div>
+    </form>
+
+    {% if form.errors %}
+        {{ form.errors }}
+    {% endif %}
+
+    {% if size %}<h4>Total size: {{ size|filesizeformat }}</h4>{% endif %}
+
+    {% if report %}
+    <table class="condensed-table">
+        <thead>
+            <tr>
+                <th>type</th>
+                <th><a href="{% url_with_query 'profiling_memory_usage' keep 'limit' add sort_by='1' %}">number</a></th>
+                <th><a href="{% url_with_query 'profiling_memory_usage' keep 'limit' add sort_by='2' %}">size</a></th>
+            </tr>
+        </thead>
+        {% for row in report %}
+            <tr>
+                <td>{{ row.0 }}</td>
+                <td>{{ row.1 }}</td>
+                <td>{{ row.2|filesizeformat }}</td>
+            </tr>
+        {% endfor %}
+    </table>
+    {% endif %}
+{% endblock %}

profiling_dashboard/templates/profiling_dashboard/process_info.html

+{% extends "profiling_dashboard/web_top.html" %}
+{% load query_exchange_tags %}
+
+{% block content %}
+    <div class="container">
+        <h2>{{ proc.NAME }} (pid: {{ proc.PID }})</h2>
+
+        <h3>General Info</h3>
+        <dl>
+            <dt>CMDLINE</dt>
+            <dd>{{ proc.CMDLINE }}</dd>
+
+            <dt>USERNAME</dt>
+            <dd>{{ proc.USERNAME }}</dd>
+
+            <dt>MEMORY: VMS / RSS / %MEM</dt>
+            <dd>{{ proc.VMS|filesizeformat }} / {{ proc.RSS|filesizeformat }} / {{ proc.MEMPERCENT }}</dd>
+
+            <dt>CPU: USER / SYSTEM / %CPU</dt>
+            <dd>{{ proc.CPU_USER }} / {{ proc.CPU_SYSTEM }} / {{ proc.CPUPERCENT }}</dd>
+
+            <dt>CREATE TIME (now is {{ proc.NOW }})</dt>
+            <dd>{{ proc.CREATE_TIME }}</dd>
+
+            {% if proc.IO_COUNTERS %}
+                <dt>IO: read / write (size)</dt>
+                <dd>{{ proc.IO_COUNTERS.read_bytes|filesizeformat }} / {{ proc.IO_COUNTERS.write_bytes|filesizeformat }}</dd>
+
+                <dt>IO: read / write (count)</dt>
+                <dd>{{ proc.IO_COUNTERS.read_count }} / {{ proc.IO_COUNTERS.write_count }}</dd>
+            {% endif %}
+        </dl>
+
+        {% if proc.THREADS %}
+            <h3>Threads</h3>
+            <table class="condensed-table">
+                <thead>
+                    <tr>
+                        <th>id</th>
+                        <th>user time</th>
+                        <th>system time</th>
+                    </tr>
+                </thead>
+                {% for thread in proc.THREADS %}
+                    <tr>
+                        <td>{{ thread.id }}</td>
+                        <td>{{ thread.user_time }}</td>
+                        <td>{{ thread.system_time }}</td>
+                    </tr>
+                {% endfor %}
+            </table>
+        {% endif %}
+
+        {% if proc.CONNECTIONS %}
+            <h3>Connections</h3>
+
+            <table class="condensed-table">
+                <thead>
+                    <tr>
+                        <th>fd</th>
+                        <th>family</th>
+                        <th>type</th>
+                        <th>local address</th>
+                        <th>remote address</th>
+                        <th>status</th>
+                    </tr>
+                </thead>
+                {% for conn in proc.CONNECTIONS %}
+                    <tr>
+                        <td>{{ conn.fd }}</td>
+                        <td>{{ conn.family }}</td>
+                        <td>{{ conn.type }}</td>
+
+                        <td>{{ conn.local_address.0 }}:{{ conn.local_address.1 }}</td>
+                        <td>{{ conn.remote_address.0 }}:{{ conn.remote_address.1 }}</td>
+                        <td>{{ conn.status }}</td>
+                    </tr>
+                {% endfor %}
+            </table>
+        {% endif %}
+
+        {% if proc.OPEN_FILES %}
+            <h3>Open Files</h3>
+
+            <table class="condensed-table">
+                <thead>
+                    <tr>
+                        <th>fd</th>
+                        <th>path</th>
+                    </tr>
+                </thead>
+                {% for file in proc.OPEN_FILES %}
+                    <tr>
+                        <td>{{ file.fd }}</td>
+                        <td>{{ file.path }}</td>
+                    </tr>
+                {% endfor %}
+            </table>
+
+        {% endif %}
+
+    </div>
+{% endblock %}

profiling_dashboard/templates/profiling_dashboard/web_top.html

+{% extends "profiling_dashboard/base.html" %}
+{% load query_exchange_tags %}
+
+{% block nav-top %}
+    <li class="active"><a href="{% url profiling_web_top %}">top</a></li>
+{% endblock %}
+
+{% block content %}
+    <br>
+    <form action="{% url profiling_web_top %}" method="GET">
+        {{ form.sort_by }}
+
+        <div class="clearfix">
+            <div class="input">
+                <label>
+                    No incomplete info
+                    {{ form.only_ready }}
+                    {{ form.only_ready.errors }}
+                </label>
+            </div>
+        </div>
+
+        <div class="clearfix">
+            {{ form.limit.label_tag }}
+            <div class="input">
+                {{ form.limit }}
+                {{ form.limit.errors }}
+                <input type="submit" value="Show" class="btn primary">
+            </div>
+        </div>
+    </form>
+
+    {% if form.errors %}
+        {{ form.errors }}
+    {% endif %}
+
+    {% if size %}<h4>Total size: {{ size|filesizeformat }}</h4>{% endif %}
+
+    {% if processes %}
+    <table>
+        <thead>
+            <tr>
+                <th><a href="{% url_with_query 'profiling_web_top' keep 'limit','only_ready' add sort_by='PID' %}">PID</a></th>
+                <th><a href="{% url_with_query 'profiling_web_top' keep 'limit','only_ready' add sort_by='USERNAME' %}">USER</a></th>
+                <th><a href="{% url_with_query 'profiling_web_top' keep 'limit','only_ready' add sort_by='VMS' %}">VIRT</a></th>
+                <th><a href="{% url_with_query 'profiling_web_top' keep 'limit','only_ready' add sort_by='RSS' %}">RES</a></th>
+                <th><a href="{% url_with_query 'profiling_web_top' keep 'limit','only_ready' add sort_by='CPUPERCENT' %}">%CPU</a></th>
+                <th><a href="{% url_with_query 'profiling_web_top' keep 'limit','only_ready' add sort_by='MEMPERCENT' %}">%MEM</a></th>
+                <th><a href="{% url_with_query 'profiling_web_top' keep 'limit','only_ready' add sort_by='CPU_USER' %}">CPU user</a></th>
+                <th><a href="{% url_with_query 'profiling_web_top' keep 'limit','only_ready' add sort_by='CPU_SYSTEM' %}">CPU system</a></th>
+                <th><a href="{% url_with_query 'profiling_web_top' keep 'limit','only_ready' add sort_by='NAME' %}">NAME</a></th>
+                <th><a href="{% url_with_query 'profiling_web_top' keep 'limit','only_ready' add sort_by='CMDLINE' %}">COMMAND</a></th>
+            </tr>
+        </thead>
+        {% for proc in processes %}
+            <tr style="white-space: nowrap">
+                <td>
+                    <a href="{% url profiling_process_info proc.PID %}">
+                    {% if proc.PID|lower == pid|lower %}<strong>&rarr; {{ proc.PID }}</strong>{% else %}{{ proc.PID }}{% endif %}
+                    </a>
+                </td>
+                <td>{{ proc.USERNAME }}</td>
+                <td>{{ proc.VMS|filesizeformat }}</td>
+                <td>{{ proc.RSS|filesizeformat }}</td>
+                <td>{{ proc.CPUPERCENT }}</td>
+                <td>{{ proc.MEMPERCENT }}</td>
+                <td>{{ proc.CPU_USER }}</td>
+                <td>{{ proc.CPU_SYSTEM }}</td>
+                <td>{{ proc.NAME }}</td>
+                <td>{{ proc.CMDLINE }}</td>
+            </tr>
+        {% endfor %}
+    </table>
+    {% endif %}
+{% endblock %}

profiling_dashboard/urls.py

+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('profiling_dashboard.views',
+    url(r'^do$', 'yappi_manage', name='profiling_yappi_manage'),
+    url(r'^memory-usage$', 'memory_usage', name='profiling_memory_usage'),
+    url(r'^top/$', 'web_top', name='profiling_web_top'),
+    url(r'^top/(?P<pid>\d+)$', 'process_info', name='profiling_process_info'),
+    url(r'^$', 'yappi_stats', name='profiling_yappi_stats')
+)

profiling_dashboard/views.py

+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+import os
+import threading
+import psutil
+from django.shortcuts import redirect
+from django.template.response import TemplateResponse
+from django.contrib.admin.views.decorators import staff_member_required
+from django.contrib import messages
+from .forms import YappiManageForm, YappiFilterForm, MuppyFilterForm, TopFilterForm
+from .stats import get_yappi_status, proc_annotate_with_short_info, proc_annotate_with_full_info
+
+def _tid_safe():
+    try:
+        return threading.currentThread().ident
+    except Exception as e:
+        return str(e)
+
+
+@staff_member_required
+def yappi_manage(request):
+    form = YappiManageForm(request.POST or None)
+    if form.is_valid():
+        pid = os.getpid()
+        try:
+            action = form.do_action()
+            messages.success(request, "yappi %s is successful for process #%s" % (action, pid))
+        except Exception as e:
+            messages.info(request, "process #%s" % pid)
+            messages.error(request, e)
+    return redirect('profiling_yappi_stats')
+
+
+@staff_member_required
+def yappi_stats(request):
+    form = YappiFilterForm(request.GET or None)
+    stats, other_stats = [], []
+    status = get_yappi_status()
+    pid = os.getpid()
+    if form.is_valid():
+        try:
+            stats = form.get_stats()
+            other_stats = form.get_other_stats()
+        except Exception as e:
+            messages.info(request, "process #%s" % pid)
+            messages.error(request, e)
+
+    return TemplateResponse(request, 'profiling_dashboard/cpu.html', {
+        'form': form,
+        'stats': stats,
+        'other_stats': other_stats,
+        'status': status,
+        'pid': pid,
+        'tid': _tid_safe(),
+    })
+
+
+@staff_member_required
+def memory_usage(request):
+    pid = os.getpid()
+
+    form = MuppyFilterForm(request.GET or None)
+    size, report = None, None
+    if form.is_valid():
+        size, report = form.get_report()
+
+    return TemplateResponse(request, 'profiling_dashboard/memory.html', {
+        'form': form,
+        'size': size,
+        'report': report,
+        'pid': pid,
+        'tid': _tid_safe(),
+    })
+
+@staff_member_required
+def web_top(request):
+    pid = os.getpid()
+    form = TopFilterForm(request.GET or None)
+
+    processes = []
+    if form.is_valid():
+        processes = form.get_processes()
+
+    return TemplateResponse(request, 'profiling_dashboard/web_top.html', {
+        'pid': pid,
+        'processes': processes,
+        'form': form,
+        'tid': _tid_safe(),
+    })
+
+@staff_member_required
+def process_info(request, pid):
+    try:
+        proc = psutil.Process(int(pid))
+    except Exception as e:
+        messages.error(request, "process %s: %s" % (pid, e))
+        return redirect('profiling_web_top')
+
+    proc_annotate_with_short_info(proc)
+    proc_annotate_with_full_info(proc)
+
+    return TemplateResponse(request, 'profiling_dashboard/process_info.html', {
+        'pid': os.getpid(),
+        'tid': _tid_safe(),
+        'proc': proc,
+    })
+#!/usr/bin/env python
+from distutils.core import setup
+
+for cmd in ('egg_info', 'develop'):
+    import sys
+    if cmd in sys.argv:
+        from setuptools import setup
+
+version='0.1'
+
+setup(
+    name='django-profiling-dashboard',
+    version=version,
+    author='Mikhail Korobov',
+    author_email='kmike84@gmail.com',
+
+    packages=['profiling_dashboard'],
+    package_data={
+        'profiling_dashboard': ['templates/profiling_dashboard/*.html',]
+    },
+
+    url='http://bitbucket.org/kmike/django-profiling-dashboard/',
+    download_url = 'http://bitbucket.org/kmike/django-profiling-dashboard/get/tip.zip',
+    license = 'MIT license',
+    description = """ Django profiling dashboard for debugging CPU, memory and other resources usage in live servers """,
+
+    long_description = open('README.rst').read(),
+    requires = ['django (>= 1.3)', 'yappi (>= 0.54)', 'psutil (>= 0.4.1)', 'pympler (>= 0.2.1)'],
+
+    classifiers=(
+        'Development Status :: 3 - Alpha',
+        'Environment :: Web Environment',
+        'Framework :: Django',
+        'Intended Audience :: Developers',
+        'Intended Audience :: System Administrators',
+        'License :: OSI Approved :: MIT License',
+        'Programming Language :: Python',
+        'Programming Language :: Python :: 2.6',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: Implementation :: CPython',
+        'Topic :: Software Development :: Libraries :: Python Modules',
+    ),
+)