diff options
Diffstat (limited to 'venv/lib/python3.9/site-packages/pympler/web.py')
-rw-r--r-- | venv/lib/python3.9/site-packages/pympler/web.py | 346 |
1 files changed, 346 insertions, 0 deletions
diff --git a/venv/lib/python3.9/site-packages/pympler/web.py b/venv/lib/python3.9/site-packages/pympler/web.py new file mode 100644 index 00000000..12888ab0 --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/web.py @@ -0,0 +1,346 @@ +""" +This module provides a web-based memory profiling interface. The Pympler web +frontend exposes process information, tracker statistics, and garbage graphs. +The web frontend uses `Bottle <http://bottlepy.org>`_, a lightweight Python +web framework. Bottle is packaged with Pympler. + +The web server can be invoked almost as easily as setting a breakpoint using +*pdb*:: + + from pympler.web import start_profiler + start_profiler() + +Calling ``start_profiler`` suspends the current thread and executes the Pympler +web server, exposing profiling data and various facilities of the Pympler +library via a graphic interface. +""" + +import sys +import os +import threading + +from inspect import getouterframes +from json import dumps +from shutil import rmtree +from tempfile import mkdtemp +from threading import Thread +from weakref import WeakValueDictionary +from wsgiref.simple_server import make_server + +from pympler import asizeof +from pympler.garbagegraph import GarbageGraph +from pympler.process import get_current_threads, ProcessMemoryInfo + +from pympler.util.stringutils import safe_repr + +# Prefer the installed version of bottle.py. If bottle.py is not installed +# fallback to the vendored version. +try: + import bottle +except ImportError: + from pympler.util import bottle + + +class ServerState(threading.local): + """ + Represents the state of a running server. Needs to be thread local so + multiple servers can be started in different threads without interfering + with each other. + + Cache internal structures (garbage graphs, tracker statistics). + """ + def __init__(self): + self.server = None + self.stats = None + self.garbage_graphs = None + self.id2ref = WeakValueDictionary() + self.id2obj = dict() + + def clear_cache(self): + self.garbage_graphs = None + + +server = ServerState() + + +def get_ref(obj): + """ + Get string reference to object. Stores a weak reference in a dictionary + using the object's id as the key. If the object cannot be weakly + referenced (e.g. dictionaries, frame objects), store a strong references + in a classic dictionary. + + Returns the object's id as a string. + """ + oid = id(obj) + try: + server.id2ref[oid] = obj + except TypeError: + server.id2obj[oid] = obj + return str(oid) + + +def get_obj(ref): + """Get object from string reference.""" + oid = int(ref) + return server.id2ref.get(oid) or server.id2obj[oid] + + +pympler_path = os.path.dirname(os.path.abspath(__file__)) +static_files = os.path.join(pympler_path, 'templates') + +bottle.TEMPLATE_PATH.append(static_files) + + +@bottle.route('/') +@bottle.view('index') +def root(): + """Get overview.""" + pmi = ProcessMemoryInfo() + return dict(processinfo=pmi) + + +@bottle.route('/process') +@bottle.view('process') +def process(): + """Get process overview.""" + pmi = ProcessMemoryInfo() + threads = get_current_threads() + return dict(info=pmi, threads=threads) + + +@bottle.route('/tracker') +@bottle.view('tracker') +def tracker_index(): + """Get tracker overview.""" + stats = server.stats + if stats and stats.snapshots: + stats.annotate() + timeseries = [] + for cls in stats.tracked_classes: + series = [] + for snapshot in stats.snapshots: + series.append(snapshot.classes.get(cls, {}).get('sum', 0)) + timeseries.append((cls, series)) + + series = [s.overhead for s in stats.snapshots] + timeseries.append(("Profiling overhead", series)) + + if stats.snapshots[0].system_total.data_segment: + # Assume tracked data resides in the data segment + series = [s.system_total.data_segment - s.tracked_total - s.overhead + for s in stats.snapshots] + timeseries.append(("Data segment", series)) + series = [s.system_total.code_segment for s in stats.snapshots] + timeseries.append(("Code segment", series)) + series = [s.system_total.stack_segment for s in stats.snapshots] + timeseries.append(("Stack segment", series)) + series = [s.system_total.shared_segment for s in stats.snapshots] + timeseries.append(("Shared memory", series)) + else: + series = [s.total - s.tracked_total - s.overhead + for s in stats.snapshots] + timeseries.append(("Other", series)) + timeseries = [dict(label=label, data=list(enumerate(data))) + for label, data in timeseries] + return dict(snapshots=stats.snapshots, timeseries=dumps(timeseries)) + else: + return dict(snapshots=[]) + + +@bottle.route('/tracker/class/<clsname>') +@bottle.view('tracker_class') +def tracker_class(clsname): + """Get class instance details.""" + stats = server.stats + if not stats: + bottle.redirect('/tracker') + stats.annotate() + return dict(stats=stats, clsname=clsname) + + +@bottle.route('/refresh') +def refresh(): + """Clear all cached information.""" + server.clear_cache() + bottle.redirect('/') + + +@bottle.route('/traceback/<threadid>') +@bottle.view('stacktrace') +def get_traceback(threadid): + threadid = int(threadid) + frames = sys._current_frames() + if threadid in frames: + frame = frames[threadid] + stack = getouterframes(frame, 5) + stack.reverse() + stack = [(get_ref(f[0].f_locals),) + f[1:] for f in stack] + else: + stack = [] + return dict(stack=stack, threadid=threadid) + + +@bottle.route('/objects/<oid>') +@bottle.view('referents') +def get_obj_referents(oid): + referents = {} + obj = get_obj(oid) + if type(obj) is dict: + named_objects = asizeof.named_refs(obj) + else: + refs = asizeof._getreferents(obj) + named_objects = [(repr(type(x)), x) for x in refs] + for name, o in named_objects: + referents[name] = (get_ref(o), type(o).__name__, + safe_repr(o, clip=48), asizeof.asizeof(o)) + return dict(referents=referents) + + +@bottle.route('/static/<filename>') +def static_file(filename): + """Get static files (CSS-files).""" + return bottle.static_file(filename, root=static_files) + + +def _compute_garbage_graphs(): + """ + Retrieve garbage graph objects from cache, compute if cache is cold. + """ + if server.garbage_graphs is None: + server.garbage_graphs = GarbageGraph().split_and_sort() + return server.garbage_graphs + + +@bottle.route('/garbage') +@bottle.view('garbage_index') +def garbage_index(): + """Get garbage overview.""" + garbage_graphs = _compute_garbage_graphs() + return dict(graphs=garbage_graphs) + + +@bottle.route('/garbage/<index:int>') +@bottle.view('garbage') +def garbage_cycle(index): + """Get reference cycle details.""" + graph = _compute_garbage_graphs()[int(index)] + graph.reduce_to_cycles() + objects = graph.metadata + objects.sort(key=lambda x: -x.size) + return dict(objects=objects, index=index) + + +def _get_graph(graph, filename): + """Retrieve or render a graph.""" + try: + rendered = graph.rendered_file + except AttributeError: + try: + graph.render(os.path.join(server.tmpdir, filename), format='png') + rendered = filename + except OSError: + rendered = None + graph.rendered_file = rendered + return rendered + + +@bottle.route('/garbage/graph/<index:int>') +def garbage_graph(index): + """Get graph representation of reference cycle.""" + graph = _compute_garbage_graphs()[int(index)] + reduce_graph = bottle.request.GET.get('reduce', '') + if reduce_graph: + graph = graph.reduce_to_cycles() + if not graph: + return None + filename = 'garbage%so%s.png' % (index, reduce_graph) + rendered_file = _get_graph(graph, filename) + if rendered_file: + return bottle.static_file(rendered_file, root=server.tmpdir) + else: + return None + + +@bottle.route('/help') +def show_documentation(): + """Redirect to online documentation.""" + bottle.redirect('https://pympler.readthedocs.io/en/latest/') + + +class PymplerServer(bottle.ServerAdapter): + """Simple WSGI server.""" + def run(self, handler): + self.server = make_server(self.host, self.port, handler) + self.server.serve_forever() + + +def start_profiler(host='localhost', port=8090, tracker=None, stats=None, + debug=False, **kwargs): + """ + Start the web server to show profiling data. The function suspends the + Python application (the current thread) until the web server is stopped. + + The only way to stop the server is to signal the running thread, e.g. press + Ctrl+C in the console. If this isn't feasible for your application use + `start_in_background` instead. + + During the execution of the web server, profiling data is (lazily) cached + to improve performance. For example, garbage graphs are rendered when the + garbage profiling data is requested and are simply retransmitted upon later + requests. + + The web server can display profiling data from previously taken snapshots + when `tracker` or `stats` is specified. The former is useful for profiling + a running application, the latter for off-line analysis. Requires existing + snapshots taken with + :py:meth:`~pympler.classtracker.ClassTracker.create_snapshot` or + :py:meth:`~pympler.classtracker.ClassTracker.start_periodic_snapshots`. + + :param host: the host where the server shall run, default is localhost + :param port: server listens on the specified port, default is 8090 to allow + coexistance with common web applications + :param tracker: `ClassTracker` instance, browse profiling data (on-line + analysis) + :param stats: `Stats` instance, analyze `ClassTracker` profiling dumps + (useful for off-line analysis) + """ + if tracker and not stats: + server.stats = tracker.stats + else: + server.stats = stats + try: + server.tmpdir = mkdtemp(prefix='pympler') + server.server = PymplerServer(host=host, port=port, **kwargs) + bottle.debug(debug) + bottle.run(server=server.server) + finally: + rmtree(server.tmpdir) + + +class ProfilerThread(Thread): + """Encapsulates a thread to run the web server.""" + def __init__(self, group=None, target=None, name='Pympler web frontend', + **kwargs): + super(ProfilerThread, self).__init__(group=group, + target=target, + name=name) + self.kwargs = kwargs + self.daemon = True + + def run(self): + start_profiler(**self.kwargs) + + +def start_in_background(**kwargs): + """ + Start the web server in the background. A new thread is created which + serves the profiling interface without suspending the current application. + + For the documentation of the parameters see `start_profiler`. + + Returns the created thread object. + """ + thread = ProfilerThread(**kwargs) + thread.start() + return thread |