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