eric7/DataViews/PyProfileDialog.py

branch
eric7
changeset 8312
800c432b34c8
parent 8222
5994b80b8760
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2003 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a dialog to display profile data.
8 """
9
10 import os
11 import pickle # secok
12
13 from PyQt5.QtCore import Qt
14 from PyQt5.QtWidgets import (
15 QDialog, QDialogButtonBox, QMenu, QHeaderView, QTreeWidgetItem,
16 QApplication
17 )
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().__init__(parent)
67 self.setupUi(self)
68 self.setWindowFlags(Qt.WindowType.Window)
69
70 self.buttonBox.button(
71 QDialogButtonBox.StandardButton.Close).setEnabled(False)
72 self.buttonBox.button(
73 QDialogButtonBox.StandardButton.Cancel).setDefault(True)
74
75 self.cancelled = False
76 self.exclude = True
77 self.ericpath = os.path.dirname(
78 os.path.dirname(os.path.abspath(__file__)))
79 self.pyLibPath = Utilities.getPythonLibPath()
80
81 self.summaryList.headerItem().setText(
82 self.summaryList.columnCount(), "")
83 self.resultList.headerItem().setText(self.resultList.columnCount(), "")
84 self.resultList.header().setSortIndicator(
85 0, Qt.SortOrder.DescendingOrder)
86
87 self.__menu = QMenu(self)
88 self.filterItm = self.__menu.addAction(
89 self.tr('Exclude Python Library'),
90 self.__filter)
91 self.__menu.addSeparator()
92 self.__menu.addAction(
93 self.tr('Erase Profiling Info'), self.__eraseProfile)
94 self.__menu.addAction(
95 self.tr('Erase Timing Info'), self.__eraseTiming)
96 self.__menu.addSeparator()
97 self.__menu.addAction(self.tr('Erase All Infos'), self.__eraseAll)
98 self.resultList.setContextMenuPolicy(
99 Qt.ContextMenuPolicy.CustomContextMenu)
100 self.resultList.customContextMenuRequested.connect(
101 self.__showContextMenu)
102 self.summaryList.setContextMenuPolicy(
103 Qt.ContextMenuPolicy.CustomContextMenu)
104 self.summaryList.customContextMenuRequested.connect(
105 self.__showContextMenu)
106
107 def __createResultItem(self, calls, totalTime, totalTimePerCall,
108 cumulativeTime, cumulativeTimePerCall, file, line,
109 functionName):
110 """
111 Private method to create an entry in the result list.
112
113 @param calls number of calls (integer)
114 @param totalTime total time (double)
115 @param totalTimePerCall total time per call (double)
116 @param cumulativeTime cumulative time (double)
117 @param cumulativeTimePerCall cumulative time per call (double)
118 @param file filename of file (string)
119 @param line linenumber (integer)
120 @param functionName function name (string)
121 """
122 itm = ProfileTreeWidgetItem(self.resultList, [
123 calls,
124 "{0: 8.3f}".format(totalTime),
125 totalTimePerCall,
126 "{0: 8.3f}".format(cumulativeTime),
127 cumulativeTimePerCall,
128 file,
129 str(line),
130 functionName
131 ])
132 for col in [0, 1, 2, 3, 4, 6]:
133 itm.setTextAlignment(col, Qt.AlignmentFlag.AlignRight)
134
135 def __createSummaryItem(self, label, contents):
136 """
137 Private method to create an entry in the summary list.
138
139 @param label text of the first column (string)
140 @param contents text of the second column (string)
141 """
142 itm = QTreeWidgetItem(self.summaryList, [label, contents])
143 itm.setTextAlignment(1, Qt.AlignmentFlag.AlignRight)
144
145 def __resortResultList(self):
146 """
147 Private method to resort the tree.
148 """
149 self.resultList.sortItems(self.resultList.sortColumn(),
150 self.resultList.header()
151 .sortIndicatorOrder())
152
153 def __populateLists(self, exclude=False):
154 """
155 Private method used to populate the listviews.
156
157 @param exclude flag indicating whether files residing in the
158 Python library should be excluded
159 """
160 self.resultList.clear()
161 self.summaryList.clear()
162
163 self.checkProgress.setMaximum(len(self.stats))
164 QApplication.processEvents()
165
166 total_calls = 0
167 prim_calls = 0
168 total_tt = 0
169
170 try:
171 # disable updates of the list for speed
172 self.resultList.setUpdatesEnabled(False)
173 self.resultList.setSortingEnabled(False)
174
175 # now go through all the files
176 for progress, (func, (cc, nc, tt, ct, _callers)) in enumerate(
177 list(self.stats.items()), start=1
178 ):
179 if self.cancelled:
180 return
181
182 if (
183 not (self.ericpath and
184 func[0].startswith(self.ericpath)) and
185 not func[0].startswith("DebugClients") and
186 func[0] != "profile" and
187 not (exclude and (func[0].startswith(self.pyLibPath) or
188 func[0] == "")
189 ) and
190 (self.file is None or
191 func[0].startswith(self.file) or
192 func[0].startswith(self.pyLibPath))
193 ):
194 # calculate the totals
195 total_calls += nc
196 prim_calls += cc
197 total_tt += tt
198
199 if nc != cc:
200 c = "{0:d}/{1:d}".format(nc, cc)
201 else:
202 c = str(nc)
203 if nc == 0:
204 tpc = "{0: 8.3f}".format(0.0)
205 else:
206 tpc = "{0: 8.3f}".format(tt / nc)
207 if cc == 0:
208 cpc = "{0: 8.3f}".format(0.0)
209 else:
210 cpc = "{0: 8.3f}".format(ct / cc)
211 self.__createResultItem(c, tt, tpc, ct, cpc, func[0],
212 func[1], func[2])
213
214 self.checkProgress.setValue(progress)
215 QApplication.processEvents()
216 finally:
217 # reenable updates of the list
218 self.resultList.setSortingEnabled(True)
219 self.resultList.setUpdatesEnabled(True)
220 self.__resortResultList()
221
222 # now do the summary stuff
223 self.__createSummaryItem(self.tr("function calls"),
224 str(total_calls))
225 if total_calls != prim_calls:
226 self.__createSummaryItem(self.tr("primitive calls"),
227 str(prim_calls))
228 self.__createSummaryItem(self.tr("CPU seconds"),
229 "{0:.3f}".format(total_tt))
230
231 def start(self, pfn, fn=None):
232 """
233 Public slot to start the calculation of the profile data.
234
235 @param pfn basename of the profiling file (string)
236 @param fn file to display the profiling data for (string)
237 """
238 self.basename = os.path.splitext(pfn)[0]
239
240 fname = "{0}.profile".format(self.basename)
241 if not os.path.exists(fname):
242 E5MessageBox.warning(
243 self,
244 self.tr("Profile Results"),
245 self.tr("""<p>There is no profiling data"""
246 """ available for <b>{0}</b>.</p>""")
247 .format(pfn))
248 self.close()
249 return
250 try:
251 with open(fname, 'rb') as f:
252 self.stats = pickle.load(f) # secok
253 except (OSError, pickle.PickleError, EOFError):
254 E5MessageBox.critical(
255 self,
256 self.tr("Loading Profiling Data"),
257 self.tr("""<p>The profiling data could not be"""
258 """ read from file <b>{0}</b>.</p>""")
259 .format(fname))
260 self.close()
261 return
262
263 self.file = fn
264 self.__populateLists()
265 self.__finish()
266
267 def __finish(self):
268 """
269 Private slot called when the action finished or the user pressed the
270 button.
271 """
272 self.cancelled = True
273 self.buttonBox.button(
274 QDialogButtonBox.StandardButton.Close).setEnabled(True)
275 self.buttonBox.button(
276 QDialogButtonBox.StandardButton.Cancel).setEnabled(False)
277 self.buttonBox.button(
278 QDialogButtonBox.StandardButton.Close).setDefault(True)
279 QApplication.processEvents()
280 self.resultList.header().resizeSections(
281 QHeaderView.ResizeMode.ResizeToContents)
282 self.resultList.header().setStretchLastSection(True)
283 self.summaryList.header().resizeSections(
284 QHeaderView.ResizeMode.ResizeToContents)
285 self.summaryList.header().setStretchLastSection(True)
286
287 def __unfinish(self):
288 """
289 Private slot called to revert the effects of the __finish slot.
290 """
291 self.cancelled = False
292 self.buttonBox.button(
293 QDialogButtonBox.StandardButton.Close).setEnabled(False)
294 self.buttonBox.button(
295 QDialogButtonBox.StandardButton.Cancel).setEnabled(True)
296 self.buttonBox.button(
297 QDialogButtonBox.StandardButton.Cancel).setDefault(True)
298
299 def on_buttonBox_clicked(self, button):
300 """
301 Private slot called by a button of the button box clicked.
302
303 @param button button that was clicked (QAbstractButton)
304 """
305 if button == self.buttonBox.button(
306 QDialogButtonBox.StandardButton.Close
307 ):
308 self.close()
309 elif button == self.buttonBox.button(
310 QDialogButtonBox.StandardButton.Cancel
311 ):
312 self.__finish()
313
314 def __showContextMenu(self, coord):
315 """
316 Private slot to show the context menu of the listview.
317
318 @param coord the position of the mouse pointer (QPoint)
319 """
320 self.__menu.popup(self.mapToGlobal(coord))
321
322 def __eraseProfile(self):
323 """
324 Private slot to handle the Erase Profile context menu action.
325 """
326 fname = "{0}.profile".format(self.basename)
327 if os.path.exists(fname):
328 os.remove(fname)
329
330 def __eraseTiming(self):
331 """
332 Private slot to handle the Erase Timing context menu action.
333 """
334 fname = "{0}.timings".format(self.basename)
335 if os.path.exists(fname):
336 os.remove(fname)
337
338 def __eraseAll(self):
339 """
340 Private slot to handle the Erase All context menu action.
341 """
342 self.__eraseProfile()
343 self.__eraseTiming()
344
345 def __filter(self):
346 """
347 Private slot to handle the Exclude/Include Python Library context menu
348 action.
349 """
350 self.__unfinish()
351 if self.exclude:
352 self.exclude = False
353 self.filterItm.setText(self.tr('Include Python Library'))
354 self.__populateLists(True)
355 else:
356 self.exclude = True
357 self.filterItm.setText(self.tr('Exclude Python Library'))
358 self.__populateLists(False)
359 self.__finish()

eric ide

mercurial