eric6/DataViews/PyProfileDialog.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7229
53054eb5b15a
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2003 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a dialog to display profile data.
8 """
9
10 from __future__ import unicode_literals
11
12 import os
13 import pickle
14
15 from PyQt5.QtCore import Qt
16 from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QMenu, QHeaderView, \
17 QTreeWidgetItem, QApplication
18
19 from E5Gui import E5MessageBox
20
21 from .Ui_PyProfileDialog import Ui_PyProfileDialog
22 import Utilities
23
24
25 class ProfileTreeWidgetItem(QTreeWidgetItem):
26 """
27 Class implementing a custom QTreeWidgetItem to allow sorting on numeric
28 values.
29 """
30 def __getNC(self, itm):
31 """
32 Private method to get the value to compare on for the first column.
33
34 @param itm item to operate on (ProfileTreeWidgetItem)
35 @return comparison value for the first column (integer)
36 """
37 s = itm.text(0)
38 return int(s.split('/')[0])
39
40 def __lt__(self, other):
41 """
42 Special method to check, if the item is less than the other one.
43
44 @param other reference to item to compare against
45 (ProfileTreeWidgetItem)
46 @return true, if this item is less than other (boolean)
47 """
48 column = self.treeWidget().sortColumn()
49 if column == 0:
50 return self.__getNC(self) < self.__getNC(other)
51 if column == 6:
52 return int(self.text(column)) < int(other.text(column))
53 return self.text(column) < other.text(column)
54
55
56 class PyProfileDialog(QDialog, Ui_PyProfileDialog):
57 """
58 Class implementing a dialog to display the results of a profiling run.
59 """
60 def __init__(self, parent=None):
61 """
62 Constructor
63
64 @param parent parent widget (QWidget)
65 """
66 super(PyProfileDialog, self).__init__(parent)
67 self.setupUi(self)
68 self.setWindowFlags(Qt.Window)
69
70 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
71 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
72
73 self.cancelled = False
74 self.exclude = True
75 self.ericpath = os.path.dirname(
76 os.path.dirname(os.path.abspath(__file__)))
77 self.pyLibPath = Utilities.getPythonLibPath()
78
79 self.summaryList.headerItem().setText(
80 self.summaryList.columnCount(), "")
81 self.resultList.headerItem().setText(self.resultList.columnCount(), "")
82 self.resultList.header().setSortIndicator(0, Qt.DescendingOrder)
83
84 self.__menu = QMenu(self)
85 self.filterItm = self.__menu.addAction(
86 self.tr('Exclude Python Library'),
87 self.__filter)
88 self.__menu.addSeparator()
89 self.__menu.addAction(
90 self.tr('Erase Profiling Info'), self.__eraseProfile)
91 self.__menu.addAction(
92 self.tr('Erase Timing Info'), self.__eraseTiming)
93 self.__menu.addSeparator()
94 self.__menu.addAction(self.tr('Erase All Infos'), self.__eraseAll)
95 self.resultList.setContextMenuPolicy(Qt.CustomContextMenu)
96 self.resultList.customContextMenuRequested.connect(
97 self.__showContextMenu)
98 self.summaryList.setContextMenuPolicy(Qt.CustomContextMenu)
99 self.summaryList.customContextMenuRequested.connect(
100 self.__showContextMenu)
101
102 def __createResultItem(self, calls, totalTime, totalTimePerCall,
103 cumulativeTime, cumulativeTimePerCall, file, line,
104 functionName):
105 """
106 Private method to create an entry in the result list.
107
108 @param calls number of calls (integer)
109 @param totalTime total time (double)
110 @param totalTimePerCall total time per call (double)
111 @param cumulativeTime cumulative time (double)
112 @param cumulativeTimePerCall cumulative time per call (double)
113 @param file filename of file (string)
114 @param line linenumber (integer)
115 @param functionName function name (string)
116 """
117 itm = ProfileTreeWidgetItem(self.resultList, [
118 calls,
119 "{0: 8.3f}".format(totalTime),
120 totalTimePerCall,
121 "{0: 8.3f}".format(cumulativeTime),
122 cumulativeTimePerCall,
123 file,
124 str(line),
125 functionName
126 ])
127 for col in [0, 1, 2, 3, 4, 6]:
128 itm.setTextAlignment(col, Qt.AlignRight)
129
130 def __createSummaryItem(self, label, contents):
131 """
132 Private method to create an entry in the summary list.
133
134 @param label text of the first column (string)
135 @param contents text of the second column (string)
136 """
137 itm = QTreeWidgetItem(self.summaryList, [label, contents])
138 itm.setTextAlignment(1, Qt.AlignRight)
139
140 def __resortResultList(self):
141 """
142 Private method to resort the tree.
143 """
144 self.resultList.sortItems(self.resultList.sortColumn(),
145 self.resultList.header()
146 .sortIndicatorOrder())
147
148 def __populateLists(self, exclude=False):
149 """
150 Private method used to populate the listviews.
151
152 @param exclude flag indicating whether files residing in the
153 Python library should be excluded
154 """
155 self.resultList.clear()
156 self.summaryList.clear()
157
158 self.checkProgress.setMaximum(len(self.stats))
159 QApplication.processEvents()
160
161 progress = 0
162 total_calls = 0
163 prim_calls = 0
164 total_tt = 0
165
166 try:
167 # disable updates of the list for speed
168 self.resultList.setUpdatesEnabled(False)
169 self.resultList.setSortingEnabled(False)
170
171 # now go through all the files
172 for func, (cc, nc, tt, ct, _callers) in list(self.stats.items()):
173 if self.cancelled:
174 return
175
176 if not (self.ericpath and
177 func[0].startswith(self.ericpath)) and \
178 not func[0].startswith("DebugClients") and \
179 func[0] != "profile" and \
180 not (exclude and (
181 func[0].startswith(self.pyLibPath) or func[0] == "")):
182 if self.file is None or func[0].startswith(self.file) or \
183 func[0].startswith(self.pyLibPath):
184 # calculate the totals
185 total_calls += nc
186 prim_calls += cc
187 total_tt += tt
188
189 if nc != cc:
190 c = "{0:d}/{1:d}".format(nc, cc)
191 else:
192 c = str(nc)
193 if nc == 0:
194 tpc = "{0: 8.3f}".format(0.0)
195 else:
196 tpc = "{0: 8.3f}".format(tt / nc)
197 if cc == 0:
198 cpc = "{0: 8.3f}".format(0.0)
199 else:
200 cpc = "{0: 8.3f}".format(ct / cc)
201 self.__createResultItem(c, tt, tpc, ct, cpc, func[0],
202 func[1], func[2])
203
204 progress += 1
205 self.checkProgress.setValue(progress)
206 QApplication.processEvents()
207 finally:
208 # reenable updates of the list
209 self.resultList.setSortingEnabled(True)
210 self.resultList.setUpdatesEnabled(True)
211 self.__resortResultList()
212
213 # now do the summary stuff
214 self.__createSummaryItem(self.tr("function calls"),
215 str(total_calls))
216 if total_calls != prim_calls:
217 self.__createSummaryItem(self.tr("primitive calls"),
218 str(prim_calls))
219 self.__createSummaryItem(self.tr("CPU seconds"),
220 "{0:.3f}".format(total_tt))
221
222 def start(self, pfn, fn=None):
223 """
224 Public slot to start the calculation of the profile data.
225
226 @param pfn basename of the profiling file (string)
227 @param fn file to display the profiling data for (string)
228 """
229 self.basename = os.path.splitext(pfn)[0]
230
231 fname = "{0}.profile".format(self.basename)
232 if not os.path.exists(fname):
233 E5MessageBox.warning(
234 self,
235 self.tr("Profile Results"),
236 self.tr("""<p>There is no profiling data"""
237 """ available for <b>{0}</b>.</p>""")
238 .format(pfn))
239 self.close()
240 return
241 try:
242 f = open(fname, 'rb')
243 self.stats = pickle.load(f)
244 f.close()
245 except (EnvironmentError, pickle.PickleError, EOFError):
246 E5MessageBox.critical(
247 self,
248 self.tr("Loading Profiling Data"),
249 self.tr("""<p>The profiling data could not be"""
250 """ read from file <b>{0}</b>.</p>""")
251 .format(fname))
252 self.close()
253 return
254
255 self.file = fn
256 self.__populateLists()
257 self.__finish()
258
259 def __finish(self):
260 """
261 Private slot called when the action finished or the user pressed the
262 button.
263 """
264 self.cancelled = True
265 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
266 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
267 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
268 QApplication.processEvents()
269 self.resultList.header().resizeSections(QHeaderView.ResizeToContents)
270 self.resultList.header().setStretchLastSection(True)
271 self.summaryList.header().resizeSections(QHeaderView.ResizeToContents)
272 self.summaryList.header().setStretchLastSection(True)
273
274 def __unfinish(self):
275 """
276 Private slot called to revert the effects of the __finish slot.
277 """
278 self.cancelled = False
279 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
280 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
281 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
282
283 def on_buttonBox_clicked(self, button):
284 """
285 Private slot called by a button of the button box clicked.
286
287 @param button button that was clicked (QAbstractButton)
288 """
289 if button == self.buttonBox.button(QDialogButtonBox.Close):
290 self.close()
291 elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
292 self.__finish()
293
294 def __showContextMenu(self, coord):
295 """
296 Private slot to show the context menu of the listview.
297
298 @param coord the position of the mouse pointer (QPoint)
299 """
300 self.__menu.popup(self.mapToGlobal(coord))
301
302 def __eraseProfile(self):
303 """
304 Private slot to handle the Erase Profile context menu action.
305 """
306 fname = "{0}.profile".format(self.basename)
307 if os.path.exists(fname):
308 os.remove(fname)
309
310 def __eraseTiming(self):
311 """
312 Private slot to handle the Erase Timing context menu action.
313 """
314 fname = "{0}.timings".format(self.basename)
315 if os.path.exists(fname):
316 os.remove(fname)
317
318 def __eraseAll(self):
319 """
320 Private slot to handle the Erase All context menu action.
321 """
322 self.__eraseProfile()
323 self.__eraseTiming()
324
325 def __filter(self):
326 """
327 Private slot to handle the Exclude/Include Python Library context menu
328 action.
329 """
330 self.__unfinish()
331 if self.exclude:
332 self.exclude = False
333 self.filterItm.setText(self.tr('Include Python Library'))
334 self.__populateLists(True)
335 else:
336 self.exclude = True
337 self.filterItm.setText(self.tr('Exclude Python Library'))
338 self.__populateLists(False)
339 self.__finish()

eric ide

mercurial