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