src/eric7/DataViews/PyProfileDialog.py

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

eric ide

mercurial