#!/usr/bin/env python
# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
from collections import defaultdict
import os
import pstats
from traits.api import (
Any, Bool, Constant, Dict, Event, Float, HasTraits, Instance, Int, List,
Property, Str, observe
)
from traitsui.api import (
CodeEditor, Group, HGroup, Item, Label, TabularAdapter, TabularEditor,
UItem, VGroup, View,
)
[docs]class SuperTuple(tuple):
""" Generic super-tuple using pre-defined attribute names.
"""
__names__ = []
def __new__(cls, *args, **kwds):
self = tuple.__new__(cls, *args, **kwds)
for i, attr in enumerate(cls.__names__):
setattr(self, attr, self[i])
return self
[docs]class Subrecord(SuperTuple):
""" The records referring to the calls a function makes.
"""
__names__ = [
"file_line_name",
"ncalls",
"nonrec_calls",
"inline_time",
"cum_time",
]
@property
def file(self):
return self[0][0]
@property
def line(self):
return self[0][1]
@property
def func_name(self):
return self[0][2]
[docs]class Record(Subrecord):
""" The top-level profiling record of a function.
"""
__names__ = [
"file_line_name",
"ncalls",
"nonrec_calls",
"inline_time",
"cum_time",
"callers",
]
profile_columns = [
("# Calls", "ncalls"),
("# Nonrec", "nonrec_calls"),
("Self Time", "inline_time"),
("Cum. Time", "cum_time"),
("Name", "func_name"),
("Line", "line"),
("File", "file"),
]
[docs]class ProfileAdapter(TabularAdapter):
""" Display profiling records in a TabularEditor.
"""
columns = profile_columns
# Whether filenames should only be displayed as basenames or not.
basenames = Bool(True, update=True)
# Whether times should be shown as percentages or not.
percentages = Bool(True, update=True)
# The total time to use for calculating percentages.
total_time = Float(1.0, update=True)
ncalls_width = Constant(55)
nonrec_calls_width = Constant(55)
inline_time_width = Constant(75)
cum_time_width = Constant(75)
func_name_width = Constant(200)
line_width = Constant(50)
ncalls_alignment = Constant("right")
nonrec_calls_alignment = Constant("right")
inline_time_alignment = Constant("right")
cum_time_alignment = Constant("right")
line_alignment = Constant("right")
file_text = Property(Str)
inline_time_text = Property(Str)
cum_time_text = Property(Str)
def _get_file_text(self):
fn = self.item.file_line_name[0]
if self.basenames and fn != "~":
fn = os.path.basename(fn)
return fn
def _get_inline_time_text(self):
if self.percentages:
return "%2.3f" % (self.item.inline_time * 100.0 / self.total_time)
else:
return str(self.item.inline_time)
def _get_cum_time_text(self):
if self.percentages:
return "%2.3f" % (self.item.cum_time * 100.0 / self.total_time)
else:
return str(self.item.cum_time)
[docs]def get_profile_editor(adapter):
return TabularEditor(
adapter=adapter,
editable=False,
operations=[],
selected="selected_record",
column_clicked="column_clicked",
dclicked="dclicked",
)
[docs]class ProfileResults(HasTraits):
""" Display profiling results.
"""
# The sorted list of Records that mirrors this dictionary.
records = List()
selected_record = Any()
dclicked = Event()
column_clicked = Event()
# The total time in seconds for the set of records.
total_time = Float(1.0)
# The column name to sort on.
sort_key = Str("inline_time")
sort_ascending = Bool(False)
adapter = Instance(ProfileAdapter)
basenames = Bool(True)
percentages = Bool(True)
[docs] def trait_view(self, name=None, view_element=None):
if name or view_element is not None:
return super().trait_view(name=name, view_element=view_element)
view = View(
Group(Item("total_time", style="readonly")),
Item(
"records",
editor=get_profile_editor(self.adapter),
show_label=False,
),
width=1024,
height=768,
resizable=True,
)
return view
[docs] def sorter(self, record):
""" Return the appropriate sort key for sorting the records.
"""
return getattr(record, self.sort_key)
[docs] def sort_records(self, records):
""" Resort the records according to the current settings.
"""
records = sorted(records, key=self.sorter)
if not self.sort_ascending:
records = records[::-1]
return records
def _adapter_default(self):
return ProfileAdapter(
basenames=self.basenames,
percentages=self.percentages,
total_time=self.total_time,
)
@observe("total_time,percentages,basenames")
def _adapter_traits_changed(self, event):
setattr(self.adapter, event.name, event.new)
@observe("sort_key,sort_ascending")
def _resort(self, event=None):
self.records = self.sort_records(self.records)
def _column_clicked_changed(self, new):
if new is None:
return
if isinstance(new.column, int):
key = profile_columns[new.column][1]
else:
key = new.column
if key == self.sort_key:
# Just flip the order.
self.sort_ascending = not self.sort_ascending
else:
self.trait_set(sort_ascending=False, sort_key=key)
[docs]class SillyStatsWrapper(object):
""" Wrap any object with a .stats attribute or a .stats dictionary such
that it can be passed to a Stats() constructor.
"""
def __init__(self, obj=None):
if obj is None:
self.stats = {}
elif isinstance(obj, dict):
self.stats = obj
elif isinstance(obj, str):
# Load from a file.
self.stats = pstats.Stats(obj)
elif hasattr(obj, "stats"):
self.stats = obj.stats
elif hasattr(obj, "create_stats"):
obj.create_stats()
self.stats = obj.stats
else:
raise TypeError("don't know how to fake a Stats with %r" % (obj,))
[docs] def create_stats(self):
pass
[docs] @classmethod
def getstats(cls, obj=None):
self = cls(obj)
return pstats.Stats(self)
[docs]class Sike(HasTraits):
""" Tie several profile-related widgets together.
Sike is like Gotcha, only less mature.
"""
# The main pstats.Stats() object providing the data.
stats = Any()
# The main results and the subcalls.
main_results = Instance(ProfileResults, args=())
caller_results = Instance(ProfileResults, args=())
callee_results = Instance(ProfileResults, args=())
# The records have list of callers. Invert this to give a map from function
# to callee.
callee_map = Dict()
# Map from the (file, lineno, name) tuple to the record.
record_map = Dict()
# GUI traits ############################################################
basenames = Bool(True)
percentages = Bool(True)
filename = Str()
line = Int(1)
code = Str()
traits_view = View(
VGroup(
HGroup(Item("basenames"), Item("percentages")),
HGroup(
UItem("main_results"),
VGroup(
Label("Callees"),
UItem("callee_results"),
Label("Callers"),
UItem("caller_results"),
UItem("filename", style="readonly"),
UItem("code", editor=CodeEditor(line="line")),
),
style="custom",
),
),
width=1024,
height=768,
resizable=True,
title="Profiling results",
)
[docs] @classmethod
def fromstats(cls, stats, **traits):
""" Instantiate an Sike from a Stats object, Stats.stats dictionary, or
Profile object, or a filename of the saved Stats data.
"""
stats = SillyStatsWrapper.getstats(stats)
self = cls(stats=stats, **traits)
self._refresh_stats()
return self
[docs] def add_stats(self, stats):
""" Add new statistics.
"""
stats = SillyStatsWrapper.getstats(stats)
self.stats.add(stats)
self._refresh_stats()
[docs] def records_from_stats(self, stats):
""" Create a list of records from a stats dictionary.
"""
records = []
for (
file_line_name,
(ncalls, nonrec_calls, inline_time, cum_time, calls),
) in stats.items():
newcalls = []
for sub_file_line_name, sub_call in calls.items():
newcalls.append(Subrecord((sub_file_line_name,) + sub_call))
records.append(
Record(
(
file_line_name,
ncalls,
nonrec_calls,
inline_time,
cum_time,
newcalls,
)
)
)
return records
[docs] def get_callee_map(self, records):
""" Create a callee map.
"""
callees = defaultdict(list)
for record in records:
for caller in record.callers:
callees[caller.file_line_name].append(
Subrecord((record.file_line_name,) + caller[1:])
)
return callees
@observe("percentages,basenames")
def _adapter_traits_changed(self, event):
for obj in [
self.main_results,
self.callee_results,
self.caller_results,
]:
setattr(obj, event.name, event.new)
[docs] @observe("main_results:selected_record")
def update_sub_results(self, event):
new = event.new
if new is None:
return
self.caller_results.total_time = new.cum_time
self.caller_results.records = new.callers
self.callee_results._resort()
self.caller_results.selected_record = (
self.caller_results.activated_record
) = None
self.callee_results.total_time = new.cum_time
self.callee_results.records = self.callee_map.get(
new.file_line_name, []
)
self.callee_results._resort()
self.callee_results.selected_record = (
self.callee_results.activated_record
) = None
filename, line, name = new.file_line_name
if os.path.exists(filename):
with open(filename, "ru") as f:
code = f.read()
self.code = code
self.filename = filename
self.line = line
else:
self.trait_set(code="", filename="", line=1)
[docs] @observe("caller_results:dclicked," "callee_results:dclicked")
def goto_record(self, event):
new = event.new
if new is None:
return
if new.item.file_line_name in self.record_map:
record = self.record_map[new.item.file_line_name]
self.main_results.selected_record = record
@observe("stats")
def _refresh_stats(self, event=None):
""" Refresh the records from the stored Stats object.
"""
self.main_results.records = self.main_results.sort_records(
self.records_from_stats(self.stats.stats)
)
self.callee_map = self.get_callee_map(self.main_results.records)
self.record_map = {}
total_time = 0.0
for record in self.main_results.records:
self.record_map[record.file_line_name] = record
total_time += record.inline_time
self.main_results.total_time = total_time
[docs]def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("file")
args = parser.parse_args()
stats = pstats.Stats(args.file)
app = Sike.fromstats(stats)
app.configure_traits()
if __name__ == "__main__":
main()