├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── django_dowser ├── __init__.py ├── reftree.py ├── templates │ └── django_dowser │ │ ├── base.html │ │ ├── graphs.html │ │ ├── main.css │ │ ├── trace.html │ │ └── tree.html ├── urls.py └── views.py ├── setup.py └── wiki └── screen0.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.egg 3 | *.pyc 4 | *.db 5 | *.bak 6 | .installed.cfg 7 | bin/ 8 | develop-eggs/ 9 | downloads/ 10 | eggs/ 11 | parts/ 12 | *~ 13 | fcgi.port 14 | .project 15 | .pydevproject 16 | download-cache/ 17 | 18 | .pydevproject 19 | 20 | /static/* 21 | /media/* 22 | .DS_Store 23 | 24 | .idea/ 25 | dist/ 26 | build/ 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mateusz Lapsa-Malawski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include LICENSE.dowser 3 | include README.rst 4 | recursive-include django_dowser/templates * 5 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | About 2 | ===== 3 | 4 | Based on: `Dowser `__ 5 | 6 | A Django specific Dowser port. 7 | 8 | Following enhancements have been implemented on top of the original Dowser: 9 | 10 | - long term historical analysis: 1m, 1h, 1d, 4w buffers 11 | - optimization by moving from lists to python deque 12 | - server load optimization by moving charts to google chart 13 | - only superuser can view the analysis (Django specific) 14 | 15 | .. figure:: https://github.com/munhitsu/django-dowser/raw/master/wiki/screen0.png 16 | :alt: Screen shot 17 | 18 | Screen shot 19 | 20 | Future 21 | ====== 22 | - move charts to javascript 23 | - move inline html to templates 24 | - drop Django 1.x and Python 2.x compatibility 25 | 26 | Installation 27 | ============ 28 | 29 | :: 30 | 31 | # latest release 32 | pip install django-dowser 33 | # or latest master 34 | pip install git+git://github.com/munhitsu/django-dowser.git 35 | 36 | Next, modify project configuration. 37 | 38 | settings.py 39 | ----------- 40 | 41 | :: 42 | 43 | INSTALLED_APPS += ['django_dowser'] 44 | 45 | urls.py 46 | ------- 47 | 48 | :: 49 | 50 | from django.urls import path, include 51 | urlpatterns += [url(r'^dowser/', include('django_dowser.urls'))] 52 | 53 | 54 | Note 55 | ---- 56 | 57 | Use django-dowser only on multithreaded/gevent servers. With forking, or multiple servers, each process 58 | will have it's own Dowser storage, so you will only get a glimpse into one process and further requests may be load 59 | balanced to the other servers. 60 | 61 | 62 | Usage 63 | ----- 64 | 65 | Start the project and open link: 66 | 67 | :: 68 | 69 | http://domain/dowser/ 70 | 71 | When running in the local development mode, it is usually: 72 | 73 | :: 74 | 75 | http://127.0.0.1:8000/dowser/ 76 | -------------------------------------------------------------------------------- /django_dowser/__init__.py: -------------------------------------------------------------------------------- 1 | import gc 2 | import threading 3 | import time 4 | from collections import deque 5 | import itertools 6 | 7 | # maximum from all old periods is being promoted to next one 8 | DOWSER_MAXENTRIES = [12, 120, 60, 60] 9 | DOWSER_TICKS = [5, 6, 48, 28] 10 | DOWSER_NAMES = ["1m", "1h", "1d", "4w"] 11 | 12 | 13 | class Dowser(object): 14 | """ 15 | A working thread to gather type usage 16 | """ 17 | history = {} 18 | samples = [] 19 | 20 | def __init__(self): 21 | # TODO: how to limit it only to server process not the monitor 22 | # TODO: cover multi-process configuration - maybe as separate daemon... 23 | self.running = False 24 | self.samples = [0] * len(DOWSER_MAXENTRIES) 25 | self.runthread = threading.Thread(target=self.start) 26 | self.runthread.daemon = True 27 | self.runthread.start() 28 | 29 | def start(self): 30 | self.running = True 31 | while self.running: 32 | self.tick() 33 | time.sleep(DOWSER_TICKS[0]) 34 | 35 | def tick(self): 36 | gc.collect() 37 | typecounts = {} 38 | 39 | for obj in gc.get_objects(): 40 | objtype = type(obj) 41 | typename = str(objtype.__module__) + "." + objtype.__name__ 42 | if typename in typecounts: 43 | typecounts[typename] += 1 44 | else: 45 | typecounts[typename] = 1 46 | 47 | for typename, count in typecounts.items(): 48 | if typename not in self.history: 49 | self.history[typename] = [deque([0] * x) for x in DOWSER_MAXENTRIES] 50 | self.history[typename][0].appendleft(count) 51 | 52 | self.samples[0] += 1 53 | promote = [False] * (len(DOWSER_MAXENTRIES)-1) 54 | 55 | # let's calculate what we promote 56 | for i in range(len(self.samples)-1): 57 | if self.samples[i] >= DOWSER_TICKS[i]: 58 | promote[i] = True 59 | self.samples[i+1] += 1 60 | self.samples[i] = 0 61 | 62 | for typename, hist in self.history.items(): 63 | history = self.history[typename] 64 | # let's promote max from (set of entries to lower granularity history) 65 | for i in range(len(self.samples)-1): 66 | if promote[i]: 67 | history[i+1].appendleft(max(itertools.islice(history[i], 0, DOWSER_TICKS[i]))) 68 | # let's limit history to DOWSER_MAXENTRIES 69 | for i in range(len(self.samples)): 70 | if len(history[i]) > DOWSER_MAXENTRIES[i]: 71 | history[i].pop() 72 | 73 | def stop(self): 74 | self.running = False 75 | 76 | 77 | dowser = Dowser() 78 | -------------------------------------------------------------------------------- /django_dowser/reftree.py: -------------------------------------------------------------------------------- 1 | import gc 2 | import sys 3 | import logging 4 | from types import FrameType 5 | 6 | log = logging.getLogger(__name__) 7 | 8 | 9 | class Tree(object): 10 | def __init__(self, obj): 11 | self.seen = {} 12 | self.maxdepth = None 13 | self.obj = obj 14 | self.filename = sys._getframe().f_code.co_filename 15 | self._ignore = {} 16 | 17 | def ignore(self, *objects): 18 | for obj in objects: 19 | self._ignore[id(obj)] = None 20 | 21 | def ignore_caller(self): 22 | f = sys._getframe() # = this function 23 | cur = f.f_back # = the function that called us (probably 'walk') 24 | self.ignore(cur, cur.f_builtins, cur.f_locals, cur.f_globals) 25 | caller = f.f_back # = the 'real' caller 26 | self.ignore(caller, caller.f_builtins, caller.f_locals, caller.f_globals) 27 | 28 | def walk(self, maxresults=100, maxdepth=None): 29 | """Walk the object tree, ignoring duplicates and circular refs.""" 30 | log.debug("step") 31 | self.seen = {} 32 | self.ignore(self, self.__dict__, self.obj, self.seen, self._ignore) 33 | 34 | # Ignore the calling frame, its builtins, globals and locals 35 | self.ignore_caller() 36 | self.maxdepth = maxdepth 37 | count = 0 38 | log.debug("will iterate results") 39 | for result in self._gen(self.obj): 40 | log.debug("will yeld") 41 | yield result 42 | count += 1 43 | if maxresults and count >= maxresults: 44 | yield 0, 0, "==== Max results reached ====" 45 | return 46 | 47 | def print_tree(self, maxresults=100, maxdepth=None): 48 | """Walk the object tree, pretty-printing each branch.""" 49 | self.ignore_caller() 50 | for depth, refid, rep in self.walk(maxresults, maxdepth): 51 | print(("%9d" % refid), (" " * depth * 2), rep) 52 | 53 | 54 | def _repr_container(obj): 55 | return "%s of len %s: %r" % (type(obj).__name__, len(obj), obj) 56 | repr_dict = _repr_container 57 | repr_set = _repr_container 58 | repr_list = _repr_container 59 | repr_tuple = _repr_container 60 | 61 | 62 | def repr_str(obj): 63 | return "%s of len %s: %r" % (type(obj).__name__, len(obj), obj) 64 | repr_unicode = repr_str 65 | 66 | 67 | def repr_frame(obj): 68 | return "frame from %s line %s" % (obj.f_code.co_filename, obj.f_lineno) 69 | 70 | 71 | def get_repr(obj, limit=250): 72 | typename = getattr(type(obj), "__name__", None) 73 | handler = globals().get("repr_%s" % typename, repr) 74 | 75 | try: 76 | result = handler(obj) 77 | except: 78 | result = "unrepresentable object: %r" % sys.exc_info()[1] 79 | 80 | if len(result) > limit: 81 | result = result[:limit] + "..." 82 | 83 | return result 84 | 85 | 86 | class ReferentTree(Tree): 87 | def _gen(self, obj, depth=0): 88 | if self.maxdepth and depth >= self.maxdepth: 89 | yield depth, 0, "---- Max depth reached ----" 90 | return 91 | 92 | for ref in gc.get_referents(obj): 93 | if id(ref) in self._ignore: 94 | continue 95 | elif id(ref) in self.seen: 96 | yield depth, id(ref), "!" + get_repr(ref) 97 | continue 98 | else: 99 | self.seen[id(ref)] = None 100 | yield depth, id(ref), get_repr(ref) 101 | for child in self._gen(ref, depth + 1): 102 | yield child 103 | 104 | 105 | class ReferrerTree(Tree): 106 | def _gen(self, obj, depth=0): 107 | if self.maxdepth and depth >= self.maxdepth: 108 | yield depth, 0, "---- Max depth reached ----" 109 | return 110 | 111 | refs = gc.get_referrers(obj) 112 | refiter = iter(refs) 113 | self.ignore(refs, refiter) 114 | for ref in refiter: 115 | # Exclude all frames that are from this module. 116 | if isinstance(ref, FrameType): 117 | if ref.f_code.co_filename == self.filename: 118 | continue 119 | if id(ref) in self._ignore: 120 | continue 121 | elif id(ref) in self.seen: 122 | yield depth, id(ref), "!" + get_repr(ref) 123 | continue 124 | else: 125 | self.seen[id(ref)] = None 126 | yield depth, id(ref), get_repr(ref) 127 | for parent in self._gen(ref, depth + 1): 128 | yield parent 129 | 130 | 131 | class CircularReferents(Tree): 132 | def walk(self, maxresults=100, maxdepth=None): 133 | """Walk the object tree, showing circular referents.""" 134 | self.stops = 0 135 | self.seen = {} 136 | self.ignore(self, self.__dict__, self.seen, self._ignore) 137 | 138 | # Ignore the calling frame, its builtins, globals and locals 139 | self.ignore_caller() 140 | 141 | self.maxdepth = maxdepth 142 | count = 0 143 | for result in self._gen(self.obj): 144 | yield result 145 | count += 1 146 | if maxresults and count >= maxresults: 147 | yield 0, 0, "==== Max results reached ====" 148 | return 149 | 150 | def _gen(self, obj, depth=0, trail=None): 151 | if self.maxdepth and depth >= self.maxdepth: 152 | self.stops += 1 153 | return 154 | 155 | if trail is None: 156 | trail = [] 157 | 158 | for ref in gc.get_referents(obj): 159 | if id(ref) in self._ignore: 160 | continue 161 | elif id(ref) in self.seen: 162 | continue 163 | else: 164 | self.seen[id(ref)] = None 165 | 166 | refrepr = get_repr(ref) 167 | if id(ref) == id(self.obj): 168 | yield trail + [refrepr] 169 | 170 | for child in self._gen(ref, depth + 1, trail + [refrepr]): 171 | yield child 172 | 173 | def print_tree(self, maxresults=100, maxdepth=None): 174 | """Walk the object tree, pretty-printing each branch.""" 175 | self.ignore_caller() 176 | for trail in self.walk(maxresults, maxdepth): 177 | print(trail) 178 | if self.stops: 179 | print("%s paths stopped because max depth reached" % self.stops) 180 | 181 | 182 | def count_objects(): 183 | d = {} 184 | for obj in gc.get_objects(): 185 | objtype = type(obj) 186 | d[objtype] = d.get(objtype, 0) + 1 187 | d = sorted((v, k) for k, v in d.items()) 188 | return d 189 | -------------------------------------------------------------------------------- /django_dowser/templates/django_dowser/base.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | {% block title %}Dowser{% endblock title %} 6 | 9 | 10 | 11 | 12 | {% block body %} 13 | 16 | {% endblock body %} 17 | 18 | 19 | -------------------------------------------------------------------------------- /django_dowser/templates/django_dowser/graphs.html: -------------------------------------------------------------------------------- 1 | {% extends "django_dowser/base.html" %} 2 | 3 | {% block title %}Dowser: Types{% endblock title %} 4 | 5 | {% block body %} 6 | 9 | 10 |
11 | {{output|safe}} 12 |
13 | 14 | {% endblock body %} 15 | -------------------------------------------------------------------------------- /django_dowser/templates/django_dowser/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | #header { 7 | text-align: center; 8 | background-color: #CCCCCC; 9 | margin: 0; 10 | padding: 0.25em; 11 | } 12 | 13 | #header p { 14 | margin: 0; 15 | padding: 0.25em; 16 | } 17 | 18 | #header h1 a { 19 | text-decoration: none; 20 | } 21 | 22 | #header h1 a:visited { 23 | color: black; 24 | } 25 | 26 | h1 { 27 | font: 900 20pt Verdana, sans-serif; 28 | text-align: center; 29 | margin: 0; 30 | padding: 0.25em; 31 | } 32 | 33 | h2 { 34 | text-align: center; 35 | } 36 | 37 | .obj { 38 | border: 1px dashed #CCCCCC; 39 | padding: 0.5em; 40 | margin: 2px; 41 | font: 10pt Arial, sans-serif; 42 | vertical-align: middle; 43 | } 44 | 45 | .typename { 46 | font-weight: bold; 47 | } 48 | 49 | .refs { 50 | border: 1px solid #CCCCCC; 51 | padding: 0.25em; 52 | margin: 0; 53 | } 54 | 55 | 56 | 57 | .typecount { 58 | border: 1px dashed #CCCCCC; 59 | padding: 0.5em; 60 | margin: 2px; 61 | font: 10pt Arial, sans-serif; 62 | vertical-align: middle; 63 | float: left; 64 | width: 30%; 65 | } 66 | 67 | .chart { 68 | border-bottom: 1px solid #CCCCCC; 69 | background-color: white; 70 | padding: 0.25em; 71 | margin: 0; 72 | } 73 | 74 | .gparent { 75 | border: 1px dashed #CCCCCC; 76 | padding: 0.5em; 77 | margin: 2px 2px 2px 2em; 78 | font: 10pt Arial, sans-serif; 79 | vertical-align: middle; 80 | } 81 | 82 | .attr { 83 | margin: 0.25em; 84 | padding: 0; 85 | font: 9pt Courier, monospace; 86 | } 87 | 88 | .branch, .seen { 89 | padding: 1px; 90 | margin: 0 0 0 1em; 91 | border: 1px solid #CCCCCC; 92 | } 93 | 94 | -------------------------------------------------------------------------------- /django_dowser/templates/django_dowser/trace.html: -------------------------------------------------------------------------------- 1 | {% extends "django_dowser/base.html" %} 2 | 3 | {% block title %}Dowser: Trace{% endblock title %} 4 | 5 | {% block body %} 6 | 9 | 10 |

{{typename}} {{objid}}

11 | 12 |
13 | {{output|safe}} 14 |
15 | {% endblock body %} 16 | -------------------------------------------------------------------------------- /django_dowser/templates/django_dowser/tree.html: -------------------------------------------------------------------------------- 1 | {% extends "django_dowser/base.html" %} 2 | 3 | {% block title %}Dowser: Tree{% endblock title %} 4 | 5 | {% block body %} 6 | 9 | 10 |

{{typename}} {{objid}}

11 | 12 |
13 | {{output|safe}} 14 |
15 | {% endblock body %} 16 | -------------------------------------------------------------------------------- /django_dowser/urls.py: -------------------------------------------------------------------------------- 1 | try: 2 | from django.conf.urls import url 3 | except ImportError: 4 | from django.conf.urls.defaults import url 5 | 6 | from . import views 7 | 8 | urlpatterns = [ 9 | url(r'^trace/(?P[\.\-\w]+)$', views.trace, name='dowser_trace_type'), 10 | url(r'^trace/(?P[\.\-\w]+)/(?P\d+)$', views.trace, name='dowser_trace_object'), 11 | url(r'^tree/(?P[\.\-\w]+)/(?P\d+)$', views.tree, name='dowser_tree'), 12 | url(r'^$', views.index, name='dowser_index'), 13 | ] 14 | -------------------------------------------------------------------------------- /django_dowser/views.py: -------------------------------------------------------------------------------- 1 | import gc 2 | import sys 3 | import threading 4 | 5 | from django.template import RequestContext 6 | from django.shortcuts import render 7 | from django.utils.html import escape 8 | from types import ModuleType, FrameType 9 | try: 10 | from django.urls import reverse 11 | except ImportError: 12 | from django.core.urlresolvers import reverse 13 | 14 | from django.contrib.auth.decorators import user_passes_test 15 | 16 | from . import dowser, DOWSER_NAMES 17 | from . import reftree 18 | 19 | 20 | @user_passes_test(lambda u: u.is_superuser) 21 | def index(request): 22 | floor = int(request.GET.get('floor', default=0)) 23 | rows = [] 24 | typenames = sorted(dowser.history.keys()) 25 | for typename in typenames: 26 | history = dowser.history[typename] 27 | maxhist = 0 28 | for hist in history: 29 | maxhist = max(maxhist, max(hist)) 30 | charts = " ".join(['%s' % 31 | (chart_url2(history[x]), DOWSER_NAMES[x]) 32 | for x in range(len(history))]) 33 | if maxhist > floor: 34 | row = ('
%s
' 35 | '%s
' 36 | 'Cur: %s Max: %s TRACE
' 37 | % (escape(typename), 38 | charts, 39 | history[0][0], maxhist, 40 | "trace/%s" % typename, 41 | ) 42 | ) 43 | rows.append(row) 44 | return render(request, "django_dowser/graphs.html", 45 | {'output': "\n".join(rows)}) 46 | 47 | 48 | def chart_url2(entries): 49 | url = ("http://chart.apis.google.com/chart?chs=%sx20&cht=ls&" 50 | "chco=0077CC&chd=t:%s") % ( 51 | len(entries), ",".join(str(x) for x in reversed(entries))) 52 | return url 53 | 54 | 55 | def chart_url(typename, history_slot=0): 56 | data = reversed(list(dowser.history[typename][history_slot])) # TODO 57 | url = chart_url2(data) 58 | return url 59 | 60 | 61 | @user_passes_test(lambda u: u.is_superuser) 62 | def trace(request, typename, objid=None): 63 | gc.collect() 64 | 65 | if objid is None: 66 | rows = trace_all(typename) 67 | else: 68 | rows = trace_one(typename, objid) 69 | 70 | return render(request, "django_dowser/trace.html", { 71 | 'output': "\n".join(rows), 72 | 'typename': typename, 73 | 'objid': objid or "", 74 | }) 75 | 76 | 77 | @user_passes_test(lambda u: u.is_superuser) 78 | def tree(request, objid, typename): 79 | gc.collect() 80 | 81 | rows = [] 82 | objid = int(objid) 83 | all_objs = gc.get_objects() 84 | for obj in all_objs: 85 | if id(obj) == objid: 86 | objtype = type(obj) 87 | if str(objtype.__module__) + "." + objtype.__name__ != typename: 88 | rows = ["

The object you requested is no longer " 89 | "of the correct type.

"] 90 | else: 91 | rows.append('
') 92 | 93 | my_tree = ReferrerTree(obj) 94 | my_tree.ignore(all_objs) 95 | for depth, parentid, parentrepr in my_tree.walk( 96 | maxresults=1000): 97 | rows.append(parentrepr) 98 | 99 | rows.append('
') 100 | break 101 | if not rows: 102 | rows = ["

The object you requested was not found.

"] 103 | 104 | return render(request, "django_dowser/tree.html", { 105 | 'output': "\n".join(rows), 'typename': typename, 'objid': objid}) 106 | 107 | 108 | method_types = [ 109 | type(tuple.__le__), # 'wrapper_descriptor' 110 | type([1].__le__), # 'method-wrapper' 111 | type(sys.getcheckinterval), # 'builtin_function_or_method' 112 | type(threading.Thread.isAlive), # 'instancemethod' 113 | ] 114 | 115 | 116 | def trace_all(typename): 117 | rows = [] 118 | for obj in gc.get_objects(): 119 | objtype = type(obj) 120 | if str(objtype.__module__) + "." + objtype.__name__ == typename: 121 | rows.append("

%s

" 122 | % ReferrerTree(obj).get_repr(obj)) 123 | if not rows: 124 | rows = ["

The type you requested was not found.

"] 125 | return rows 126 | 127 | 128 | def trace_one(typename, objid): 129 | rows = [] 130 | objid = int(objid) 131 | all_objs = gc.get_objects() 132 | for obj in all_objs: 133 | if id(obj) == objid: 134 | objtype = type(obj) 135 | if str(objtype.__module__) + "." + objtype.__name__ != typename: 136 | rows = ["

The object you requested is no longer " 137 | "of the correct type.

"] 138 | else: 139 | tree = ReferrerTree(obj) 140 | 141 | # repr 142 | rows.append("

%s

" % get_repr(obj, 5000)) 143 | 144 | # Attributes 145 | rows.append('

Attributes

') 146 | for k in dir(obj): 147 | v = getattr(obj, k) 148 | if type(v) not in method_types: 149 | rows.append('

%s: %s

' % 150 | (k, get_repr(v))) 151 | del v 152 | rows.append('
') 153 | 154 | # Referrers 155 | rows.append('

Referrers (Parents)

') 156 | rows.append('

Show the ' 157 | 'entire tree of reachable objects

' 158 | % ("../../tree/%s/%s" % (typename, objid))) 159 | tree.ignore(all_objs) 160 | for depth, parentid, parentrepr in tree.walk(maxdepth=1): 161 | if parentid: 162 | rows.append("

%s

" % parentrepr) 163 | rows.append('
') 164 | 165 | # Referents 166 | rows.append('

Referents (Children)

') 167 | for child in gc.get_referents(obj): 168 | rows.append("

%s

" % tree.get_repr(child)) 169 | rows.append('
') 170 | break 171 | if not rows: 172 | rows = ["

The object you requested was not found.

"] 173 | return rows 174 | 175 | 176 | def get_repr(obj, limit=250): 177 | return escape(reftree.get_repr(obj, limit)) 178 | 179 | 180 | class ReferrerTree(reftree.Tree): 181 | 182 | ignore_modules = True 183 | 184 | def _gen(self, obj, depth=0): 185 | if self.maxdepth and depth >= self.maxdepth: 186 | yield depth, 0, "---- Max depth reached ----" 187 | return 188 | if isinstance(obj, ModuleType) and self.ignore_modules: 189 | return 190 | 191 | refs = gc.get_referrers(obj) 192 | refiter = iter(refs) 193 | self.ignore(refs, refiter) 194 | thisfile = sys._getframe().f_code.co_filename 195 | for ref in refiter: 196 | # Exclude all frames that are from this module or reftree. 197 | if isinstance(ref, FrameType) and ref.f_code.co_filename in ( 198 | thisfile, self.filename): 199 | continue 200 | 201 | # Exclude all functions and classes from this module or reftree. 202 | mod = getattr(ref, "__module__", "") 203 | if "django_dowser" in mod or "reftree" in mod or mod == '__main__': 204 | continue 205 | 206 | # Exclude all parents in our ignore list. 207 | if id(ref) in self._ignore: 208 | continue 209 | 210 | # Yield the (depth, id, repr) of our object. 211 | yield depth, 0, '%s
' % (" " * depth) 212 | if id(ref) in self.seen: 213 | yield depth, id(ref), "see %s above" % id(ref) 214 | else: 215 | self.seen[id(ref)] = None 216 | yield depth, id(ref), self.get_repr(ref, obj) 217 | 218 | for parent in self._gen(ref, depth + 1): 219 | yield parent 220 | yield depth, 0, '%s
' % (" " * depth) 221 | 222 | def get_repr(self, obj, referent=None): 223 | """Return an HTML tree block describing the given object.""" 224 | objtype = type(obj) 225 | typename = str(objtype.__module__) + "." + objtype.__name__ 226 | prettytype = typename.replace("__builtin__.", "") 227 | 228 | name = getattr(obj, "__name__", "") 229 | if name: 230 | prettytype = "%s %r" % (prettytype, name) 231 | 232 | key = "" 233 | if referent: 234 | key = self.get_refkey(obj, referent) 235 | url = reverse('dowser_trace_object', args=( 236 | typename, 237 | id(obj) 238 | )) 239 | return ('%s ' 240 | '%s%s
' 241 | '%s' 242 | % (url, id(obj), prettytype, key, get_repr(obj, 100)) 243 | ) 244 | 245 | def get_refkey(self, obj, referent): 246 | """Return the dict key or attribute name of obj which refers to 247 | referent.""" 248 | if isinstance(obj, dict): 249 | for k, v in obj.items(): 250 | if v is referent: 251 | return " (via its %r key)" % k 252 | 253 | for k in dir(obj) + ['__dict__']: 254 | if getattr(obj, k, None) is referent: 255 | return " (via its %r attribute)" % k 256 | return "" 257 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | ROOT_DIR = os.path.dirname(__file__) 5 | SOURCE_DIR = os.path.join(ROOT_DIR) 6 | 7 | VERSION = '0.1.8' 8 | 9 | setup( 10 | name="django-dowser", 11 | version=VERSION, 12 | 13 | author="Mateusz Lapsa-Malawski", 14 | author_email="m@cr3.io", 15 | 16 | description="Django fork of amazing memory leaks tracking application for python wsgi - the Dowser", 17 | long_description=open("README.rst").read(), 18 | long_description_content_type='text/x-rst', 19 | 20 | url="http://github.com/munhitsu/django-dowser", 21 | 22 | keywords=["django", "dowser", "debug", "memory leak"], 23 | classifiers=[ 24 | "Programming Language :: Python", 25 | "Programming Language :: Python :: 2.7", 26 | "Programming Language :: Python :: 3", 27 | "Programming Language :: Python :: 3.7", 28 | "Intended Audience :: Developers", 29 | "Operating System :: OS Independent", 30 | "Topic :: Software Development :: Debuggers", 31 | "License :: OSI Approved :: MIT License", 32 | ], 33 | packages=['django_dowser'], 34 | include_package_data=True, 35 | zip_safe=False, # because we're including media that Django needs 36 | ) 37 | -------------------------------------------------------------------------------- /wiki/screen0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/munhitsu/django-dowser/3030be07cd3cf183adea634b066337bcd07074d6/wiki/screen0.png --------------------------------------------------------------------------------