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() |