src/eric7/DataViews/PyProfileDialog.py

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

eric ide

mercurial