1# -*- coding: utf-8 -*- 2# 3# Copyright © Spyder Project Contributors 4# based on pylintgui.py by Pierre Raybaut 5# 6# Licensed under the terms of the MIT License 7# (see spyder/__init__.py for details) 8 9""" 10Profiler widget 11 12See the official documentation on python profiling: 13http://docs.python.org/library/profile.html 14""" 15 16# Standard library imports 17from __future__ import with_statement 18import os 19import os.path as osp 20from itertools import islice 21import sys 22import time 23import re 24 25# Third party imports 26from qtpy.compat import getopenfilename, getsavefilename 27from qtpy.QtCore import (QByteArray, QProcess, QProcessEnvironment, QTextCodec, 28 Qt, Signal) 29from qtpy.QtGui import QColor 30from qtpy.QtWidgets import (QApplication, QHBoxLayout, QLabel, QMessageBox, 31 QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget) 32 33# Local imports 34from spyder.config.base import get_conf_path, get_translation, debug_print 35from spyder.py3compat import to_text_string 36from spyder.utils import icon_manager as ima 37from spyder.utils.qthelpers import (create_toolbutton, get_item_user_text, 38 set_item_user_text) 39from spyder.utils.programs import shell_split 40from spyder.widgets.comboboxes import PythonModulesComboBox 41from spyder.utils.misc import add_pathlist_to_PYTHONPATH, getcwd_or_home 42from spyder.widgets.variableexplorer.texteditor import TextEditor 43 44# This is needed for testing this module as a stand alone script 45try: 46 _ = get_translation("profiler", "spyder_profiler") 47except KeyError as error: 48 import gettext 49 _ = gettext.gettext 50 51 52locale_codec = QTextCodec.codecForLocale() 53 54 55def is_profiler_installed(): 56 from spyder.utils.programs import is_module_installed 57 return is_module_installed('cProfile') and is_module_installed('pstats') 58 59 60class ProfilerWidget(QWidget): 61 """ 62 Profiler widget 63 """ 64 DATAPATH = get_conf_path('profiler.results') 65 VERSION = '0.0.1' 66 redirect_stdio = Signal(bool) 67 68 def __init__(self, parent, max_entries=100): 69 QWidget.__init__(self, parent) 70 71 self.setWindowTitle("Profiler") 72 73 self.output = None 74 self.error_output = None 75 76 self._last_wdir = None 77 self._last_args = None 78 self._last_pythonpath = None 79 80 self.filecombo = PythonModulesComboBox(self) 81 82 self.start_button = create_toolbutton(self, icon=ima.icon('run'), 83 text=_("Profile"), 84 tip=_("Run profiler"), 85 triggered=lambda : self.start(), 86 text_beside_icon=True) 87 self.stop_button = create_toolbutton(self, 88 icon=ima.icon('stop'), 89 text=_("Stop"), 90 tip=_("Stop current profiling"), 91 text_beside_icon=True) 92 self.filecombo.valid.connect(self.start_button.setEnabled) 93 #self.connect(self.filecombo, SIGNAL('valid(bool)'), self.show_data) 94 # FIXME: The combobox emits this signal on almost any event 95 # triggering show_data() too early, too often. 96 97 browse_button = create_toolbutton(self, icon=ima.icon('fileopen'), 98 tip=_('Select Python script'), 99 triggered=self.select_file) 100 101 self.datelabel = QLabel() 102 103 self.log_button = create_toolbutton(self, icon=ima.icon('log'), 104 text=_("Output"), 105 text_beside_icon=True, 106 tip=_("Show program's output"), 107 triggered=self.show_log) 108 109 self.datatree = ProfilerDataTree(self) 110 111 self.collapse_button = create_toolbutton(self, 112 icon=ima.icon('collapse'), 113 triggered=lambda dD: 114 self.datatree.change_view(-1), 115 tip=_('Collapse one level up')) 116 self.expand_button = create_toolbutton(self, 117 icon=ima.icon('expand'), 118 triggered=lambda dD: 119 self.datatree.change_view(1), 120 tip=_('Expand one level down')) 121 122 self.save_button = create_toolbutton(self, text_beside_icon=True, 123 text=_("Save data"), 124 icon=ima.icon('filesave'), 125 triggered=self.save_data, 126 tip=_('Save profiling data')) 127 self.load_button = create_toolbutton(self, text_beside_icon=True, 128 text=_("Load data"), 129 icon=ima.icon('fileimport'), 130 triggered=self.compare, 131 tip=_('Load profiling data for comparison')) 132 self.clear_button = create_toolbutton(self, text_beside_icon=True, 133 text=_("Clear comparison"), 134 icon=ima.icon('editdelete'), 135 triggered=self.clear) 136 137 hlayout1 = QHBoxLayout() 138 hlayout1.addWidget(self.filecombo) 139 hlayout1.addWidget(browse_button) 140 hlayout1.addWidget(self.start_button) 141 hlayout1.addWidget(self.stop_button) 142 143 hlayout2 = QHBoxLayout() 144 hlayout2.addWidget(self.collapse_button) 145 hlayout2.addWidget(self.expand_button) 146 hlayout2.addStretch() 147 hlayout2.addWidget(self.datelabel) 148 hlayout2.addStretch() 149 hlayout2.addWidget(self.log_button) 150 hlayout2.addWidget(self.save_button) 151 hlayout2.addWidget(self.load_button) 152 hlayout2.addWidget(self.clear_button) 153 154 layout = QVBoxLayout() 155 layout.addLayout(hlayout1) 156 layout.addLayout(hlayout2) 157 layout.addWidget(self.datatree) 158 self.setLayout(layout) 159 160 self.process = None 161 self.set_running_state(False) 162 self.start_button.setEnabled(False) 163 self.clear_button.setEnabled(False) 164 165 if not is_profiler_installed(): 166 # This should happen only on certain GNU/Linux distributions 167 # or when this a home-made Python build because the Python 168 # profilers are included in the Python standard library 169 for widget in (self.datatree, self.filecombo, 170 self.start_button, self.stop_button): 171 widget.setDisabled(True) 172 url = 'http://docs.python.org/library/profile.html' 173 text = '%s <a href=%s>%s</a>' % (_('Please install'), url, 174 _("the Python profiler modules")) 175 self.datelabel.setText(text) 176 else: 177 pass # self.show_data() 178 179 def save_data(self): 180 """Save data""" 181 title = _( "Save profiler result") 182 filename, _selfilter = getsavefilename( 183 self, title, getcwd_or_home(), 184 _("Profiler result")+" (*.Result)") 185 if filename: 186 self.datatree.save_data(filename) 187 188 def compare(self): 189 filename, _selfilter = getopenfilename( 190 self, _("Select script to compare"), 191 getcwd_or_home(), _("Profiler result")+" (*.Result)") 192 if filename: 193 self.datatree.compare(filename) 194 self.show_data() 195 self.clear_button.setEnabled(True) 196 197 def clear(self): 198 self.datatree.compare(None) 199 self.datatree.hide_diff_cols(True) 200 self.show_data() 201 self.clear_button.setEnabled(False) 202 203 def analyze(self, filename, wdir=None, args=None, pythonpath=None): 204 if not is_profiler_installed(): 205 return 206 self.kill_if_running() 207 #index, _data = self.get_data(filename) 208 index = None # FIXME: storing data is not implemented yet 209 if index is None: 210 self.filecombo.addItem(filename) 211 self.filecombo.setCurrentIndex(self.filecombo.count()-1) 212 else: 213 self.filecombo.setCurrentIndex(self.filecombo.findText(filename)) 214 self.filecombo.selected() 215 if self.filecombo.is_valid(): 216 if wdir is None: 217 wdir = osp.dirname(filename) 218 self.start(wdir, args, pythonpath) 219 220 def select_file(self): 221 self.redirect_stdio.emit(False) 222 filename, _selfilter = getopenfilename( 223 self, _("Select Python script"), 224 getcwd_or_home(), _("Python scripts")+" (*.py ; *.pyw)") 225 self.redirect_stdio.emit(True) 226 if filename: 227 self.analyze(filename) 228 229 def show_log(self): 230 if self.output: 231 TextEditor(self.output, title=_("Profiler output"), 232 readonly=True, size=(700, 500)).exec_() 233 234 def show_errorlog(self): 235 if self.error_output: 236 TextEditor(self.error_output, title=_("Profiler output"), 237 readonly=True, size=(700, 500)).exec_() 238 239 def start(self, wdir=None, args=None, pythonpath=None): 240 filename = to_text_string(self.filecombo.currentText()) 241 if wdir is None: 242 wdir = self._last_wdir 243 if wdir is None: 244 wdir = osp.basename(filename) 245 if args is None: 246 args = self._last_args 247 if args is None: 248 args = [] 249 if pythonpath is None: 250 pythonpath = self._last_pythonpath 251 self._last_wdir = wdir 252 self._last_args = args 253 self._last_pythonpath = pythonpath 254 255 self.datelabel.setText(_('Profiling, please wait...')) 256 257 self.process = QProcess(self) 258 self.process.setProcessChannelMode(QProcess.SeparateChannels) 259 self.process.setWorkingDirectory(wdir) 260 self.process.readyReadStandardOutput.connect(self.read_output) 261 self.process.readyReadStandardError.connect( 262 lambda: self.read_output(error=True)) 263 self.process.finished.connect(lambda ec, es=QProcess.ExitStatus: 264 self.finished(ec, es)) 265 self.stop_button.clicked.connect(self.kill) 266 267 if pythonpath is not None: 268 env = [to_text_string(_pth) 269 for _pth in self.process.systemEnvironment()] 270 add_pathlist_to_PYTHONPATH(env, pythonpath) 271 processEnvironment = QProcessEnvironment() 272 for envItem in env: 273 envName, separator, envValue = envItem.partition('=') 274 processEnvironment.insert(envName, envValue) 275 self.process.setProcessEnvironment(processEnvironment) 276 277 self.output = '' 278 self.error_output = '' 279 self.stopped = False 280 281 p_args = ['-m', 'cProfile', '-o', self.DATAPATH] 282 if os.name == 'nt': 283 # On Windows, one has to replace backslashes by slashes to avoid 284 # confusion with escape characters (otherwise, for example, '\t' 285 # will be interpreted as a tabulation): 286 p_args.append(osp.normpath(filename).replace(os.sep, '/')) 287 else: 288 p_args.append(filename) 289 if args: 290 p_args.extend(shell_split(args)) 291 executable = sys.executable 292 if executable.endswith("spyder.exe"): 293 # py2exe distribution 294 executable = "python.exe" 295 self.process.start(executable, p_args) 296 297 running = self.process.waitForStarted() 298 self.set_running_state(running) 299 if not running: 300 QMessageBox.critical(self, _("Error"), 301 _("Process failed to start")) 302 303 def kill(self): 304 """Stop button pressed.""" 305 self.process.kill() 306 self.stopped = True 307 308 def set_running_state(self, state=True): 309 self.start_button.setEnabled(not state) 310 self.stop_button.setEnabled(state) 311 312 def read_output(self, error=False): 313 if error: 314 self.process.setReadChannel(QProcess.StandardError) 315 else: 316 self.process.setReadChannel(QProcess.StandardOutput) 317 qba = QByteArray() 318 while self.process.bytesAvailable(): 319 if error: 320 qba += self.process.readAllStandardError() 321 else: 322 qba += self.process.readAllStandardOutput() 323 text = to_text_string( locale_codec.toUnicode(qba.data()) ) 324 if error: 325 self.error_output += text 326 else: 327 self.output += text 328 329 def finished(self, exit_code, exit_status): 330 self.set_running_state(False) 331 self.show_errorlog() # If errors occurred, show them. 332 self.output = self.error_output + self.output 333 # FIXME: figure out if show_data should be called here or 334 # as a signal from the combobox 335 self.show_data(justanalyzed=True) 336 337 def kill_if_running(self): 338 if self.process is not None: 339 if self.process.state() == QProcess.Running: 340 self.process.kill() 341 self.process.waitForFinished() 342 343 def show_data(self, justanalyzed=False): 344 if not justanalyzed: 345 self.output = None 346 self.log_button.setEnabled(self.output is not None \ 347 and len(self.output) > 0) 348 self.kill_if_running() 349 filename = to_text_string(self.filecombo.currentText()) 350 if not filename: 351 return 352 353 if self.stopped: 354 self.datelabel.setText(_('Run stopped by user.')) 355 self.datatree.initialize_view() 356 return 357 358 self.datelabel.setText(_('Sorting data, please wait...')) 359 QApplication.processEvents() 360 361 self.datatree.load_data(self.DATAPATH) 362 self.datatree.show_tree() 363 364 text_style = "<span style=\'color: #444444\'><b>%s </b></span>" 365 date_text = text_style % time.strftime("%d %b %Y %H:%M", 366 time.localtime()) 367 self.datelabel.setText(date_text) 368 369def gettime_s(text): 370 """Parse text and returns a time in seconds 371 372 The text is of the format 0h : 0.min:0.0s:0 ms:0us:0 ns. 373 Spaces are not taken into account and any of the specifiers can be ignored""" 374 pattern = r'([+-]?\d+\.?\d*) ?([munsecinh]+)' 375 matches = re.findall(pattern, text) 376 if len(matches) == 0: 377 return None 378 time = 0. 379 for res in matches: 380 tmp = float(res[0]) 381 if res[1] == 'ns': 382 tmp *= 1e-9 383 elif res[1] == 'us': 384 tmp *= 1e-6 385 elif res[1] == 'ms': 386 tmp *= 1e-3 387 elif res[1] == 'min': 388 tmp *= 60 389 elif res[1] == 'h': 390 tmp *= 3600 391 time += tmp 392 return time 393 394class TreeWidgetItem( QTreeWidgetItem ): 395 def __init__(self, parent=None): 396 QTreeWidgetItem.__init__(self, parent) 397 398 def __lt__(self, otherItem): 399 column = self.treeWidget().sortColumn() 400 try: 401 if column == 1 or column == 3: #TODO: Hardcoded Column 402 t0 = gettime_s(self.text(column)) 403 t1 = gettime_s(otherItem.text(column)) 404 if t0 is not None and t1 is not None: 405 return t0 > t1 406 407 return float( self.text(column) ) > float( otherItem.text(column) ) 408 except ValueError: 409 return self.text(column) > otherItem.text(column) 410 411class ProfilerDataTree(QTreeWidget): 412 """ 413 Convenience tree widget (with built-in model) 414 to store and view profiler data. 415 416 The quantities calculated by the profiler are as follows 417 (from profile.Profile): 418 [0] = The number of times this function was called, not counting direct 419 or indirect recursion, 420 [1] = Number of times this function appears on the stack, minus one 421 [2] = Total time spent internal to this function 422 [3] = Cumulative time that this function was present on the stack. In 423 non-recursive functions, this is the total execution time from start 424 to finish of each invocation of a function, including time spent in 425 all subfunctions. 426 [4] = A dictionary indicating for each function name, the number of times 427 it was called by us. 428 """ 429 SEP = r"<[=]>" # separator between filename and linenumber 430 # (must be improbable as a filename to avoid splitting the filename itself) 431 def __init__(self, parent=None): 432 QTreeWidget.__init__(self, parent) 433 self.header_list = [_('Function/Module'), _('Total Time'), _('Diff'), 434 _('Local Time'), _('Diff'), _('Calls'), _('Diff'), 435 _('File:line')] 436 self.icon_list = {'module': ima.icon('python'), 437 'function': ima.icon('function'), 438 'builtin': ima.icon('python_t'), 439 'constructor': ima.icon('class')} 440 self.profdata = None # To be filled by self.load_data() 441 self.stats = None # To be filled by self.load_data() 442 self.item_depth = None 443 self.item_list = None 444 self.items_to_be_shown = None 445 self.current_view_depth = None 446 self.compare_file = None 447 self.setColumnCount(len(self.header_list)) 448 self.setHeaderLabels(self.header_list) 449 self.initialize_view() 450 self.itemActivated.connect(self.item_activated) 451 self.itemExpanded.connect(self.item_expanded) 452 453 def set_item_data(self, item, filename, line_number): 454 """Set tree item user data: filename (string) and line_number (int)""" 455 set_item_user_text(item, '%s%s%d' % (filename, self.SEP, line_number)) 456 457 def get_item_data(self, item): 458 """Get tree item user data: (filename, line_number)""" 459 filename, line_number_str = get_item_user_text(item).split(self.SEP) 460 return filename, int(line_number_str) 461 462 def initialize_view(self): 463 """Clean the tree and view parameters""" 464 self.clear() 465 self.item_depth = 0 # To be use for collapsing/expanding one level 466 self.item_list = [] # To be use for collapsing/expanding one level 467 self.items_to_be_shown = {} 468 self.current_view_depth = 0 469 470 def load_data(self, profdatafile): 471 """Load profiler data saved by profile/cProfile module""" 472 import pstats 473 try: 474 stats_indi = [pstats.Stats(profdatafile), ] 475 except (OSError, IOError): 476 return 477 self.profdata = stats_indi[0] 478 479 if self.compare_file is not None: 480 try: 481 stats_indi.append(pstats.Stats(self.compare_file)) 482 except (OSError, IOError) as e: 483 QMessageBox.critical( 484 self, _("Error"), 485 _("Error when trying to load profiler results")) 486 debug_print("Error when calling pstats, {}".format(e)) 487 self.compare_file = None 488 map(lambda x: x.calc_callees(), stats_indi) 489 self.profdata.calc_callees() 490 self.stats1 = stats_indi 491 self.stats = stats_indi[0].stats 492 493 def compare(self,filename): 494 self.hide_diff_cols(False) 495 self.compare_file = filename 496 497 def hide_diff_cols(self, hide): 498 for i in (2,4,6): 499 self.setColumnHidden(i, hide) 500 501 def save_data(self, filename): 502 """""" 503 self.stats1[0].dump_stats(filename) 504 505 def find_root(self): 506 """Find a function without a caller""" 507 self.profdata.sort_stats("cumulative") 508 for func in self.profdata.fcn_list: 509 if ('~', 0) != func[0:2] and not func[2].startswith( 510 '<built-in method exec>'): 511 # This skips the profiler function at the top of the list 512 # it does only occur in Python 3 513 return func 514 515 def find_callees(self, parent): 516 """Find all functions called by (parent) function.""" 517 # FIXME: This implementation is very inneficient, because it 518 # traverses all the data to find children nodes (callees) 519 return self.profdata.all_callees[parent] 520 521 def show_tree(self): 522 """Populate the tree with profiler data and display it.""" 523 self.initialize_view() # Clear before re-populating 524 self.setItemsExpandable(True) 525 self.setSortingEnabled(False) 526 rootkey = self.find_root() # This root contains profiler overhead 527 if rootkey: 528 self.populate_tree(self, self.find_callees(rootkey)) 529 self.resizeColumnToContents(0) 530 self.setSortingEnabled(True) 531 self.sortItems(1, Qt.AscendingOrder) # FIXME: hardcoded index 532 self.change_view(1) 533 534 def function_info(self, functionKey): 535 """Returns processed information about the function's name and file.""" 536 node_type = 'function' 537 filename, line_number, function_name = functionKey 538 if function_name == '<module>': 539 modulePath, moduleName = osp.split(filename) 540 node_type = 'module' 541 if moduleName == '__init__.py': 542 modulePath, moduleName = osp.split(modulePath) 543 function_name = '<' + moduleName + '>' 544 if not filename or filename == '~': 545 file_and_line = '(built-in)' 546 node_type = 'builtin' 547 else: 548 if function_name == '__init__': 549 node_type = 'constructor' 550 file_and_line = '%s : %d' % (filename, line_number) 551 return filename, line_number, function_name, file_and_line, node_type 552 553 @staticmethod 554 def format_measure(measure): 555 """Get format and units for data coming from profiler task.""" 556 # Convert to a positive value. 557 measure = abs(measure) 558 559 # For number of calls 560 if isinstance(measure, int): 561 return to_text_string(measure) 562 563 # For time measurements 564 if 1.e-9 < measure <= 1.e-6: 565 measure = u"{0:.2f} ns".format(measure / 1.e-9) 566 elif 1.e-6 < measure <= 1.e-3: 567 measure = u"{0:.2f} us".format(measure / 1.e-6) 568 elif 1.e-3 < measure <= 1: 569 measure = u"{0:.2f} ms".format(measure / 1.e-3) 570 elif 1 < measure <= 60: 571 measure = u"{0:.2f} sec".format(measure) 572 elif 60 < measure <= 3600: 573 m, s = divmod(measure, 3600) 574 if s > 60: 575 m, s = divmod(measure, 60) 576 s = to_text_string(s).split(".")[-1] 577 measure = u"{0:.0f}.{1:.2s} min".format(m, s) 578 else: 579 h, m = divmod(measure, 3600) 580 if m > 60: 581 m /= 60 582 measure = u"{0:.0f}h:{1:.0f}min".format(h, m) 583 return measure 584 585 def color_string(self, x): 586 """Return a string formatted delta for the values in x. 587 588 Args: 589 x: 2-item list of integers (representing number of calls) or 590 2-item list of floats (representing seconds of runtime). 591 592 Returns: 593 A list with [formatted x[0], [color, formatted delta]], where 594 color reflects whether x[1] is lower, greater, or the same as 595 x[0]. 596 """ 597 diff_str = "" 598 color = "black" 599 600 if len(x) == 2 and self.compare_file is not None: 601 difference = x[0] - x[1] 602 if difference: 603 color, sign = ('green', '-') if difference < 0 else ('red', '+') 604 diff_str = '{}{}'.format(sign, self.format_measure(difference)) 605 return [self.format_measure(x[0]), [diff_str, color]] 606 607 def format_output(self, child_key): 608 """ Formats the data. 609 610 self.stats1 contains a list of one or two pstat.Stats() instances, with 611 the first being the current run and the second, the saved run, if it 612 exists. Each Stats instance is a dictionary mapping a function to 613 5 data points - cumulative calls, number of calls, total time, 614 cumulative time, and callers. 615 616 format_output() converts the number of calls, total time, and 617 cumulative time to a string format for the child_key parameter. 618 """ 619 data = [x.stats.get(child_key, [0, 0, 0, 0, {}]) for x in self.stats1] 620 return (map(self.color_string, islice(zip(*data), 1, 4))) 621 622 def populate_tree(self, parentItem, children_list): 623 """Recursive method to create each item (and associated data) in the tree.""" 624 for child_key in children_list: 625 self.item_depth += 1 626 (filename, line_number, function_name, file_and_line, node_type 627 ) = self.function_info(child_key) 628 629 ((total_calls, total_calls_dif), (loc_time, loc_time_dif), (cum_time, 630 cum_time_dif)) = self.format_output(child_key) 631 632 child_item = TreeWidgetItem(parentItem) 633 self.item_list.append(child_item) 634 self.set_item_data(child_item, filename, line_number) 635 636 # FIXME: indexes to data should be defined by a dictionary on init 637 child_item.setToolTip(0, _('Function or module name')) 638 child_item.setData(0, Qt.DisplayRole, function_name) 639 child_item.setIcon(0, self.icon_list[node_type]) 640 641 child_item.setToolTip(1, _('Time in function '\ 642 '(including sub-functions)')) 643 child_item.setData(1, Qt.DisplayRole, cum_time) 644 child_item.setTextAlignment(1, Qt.AlignRight) 645 646 child_item.setData(2, Qt.DisplayRole, cum_time_dif[0]) 647 child_item.setForeground(2, QColor(cum_time_dif[1])) 648 child_item.setTextAlignment(2, Qt.AlignLeft) 649 650 child_item.setToolTip(3, _('Local time in function '\ 651 '(not in sub-functions)')) 652 653 child_item.setData(3, Qt.DisplayRole, loc_time) 654 child_item.setTextAlignment(3, Qt.AlignRight) 655 656 child_item.setData(4, Qt.DisplayRole, loc_time_dif[0]) 657 child_item.setForeground(4, QColor(loc_time_dif[1])) 658 child_item.setTextAlignment(4, Qt.AlignLeft) 659 660 child_item.setToolTip(5, _('Total number of calls '\ 661 '(including recursion)')) 662 663 child_item.setData(5, Qt.DisplayRole, total_calls) 664 child_item.setTextAlignment(5, Qt.AlignRight) 665 666 child_item.setData(6, Qt.DisplayRole, total_calls_dif[0]) 667 child_item.setForeground(6, QColor(total_calls_dif[1])) 668 child_item.setTextAlignment(6, Qt.AlignLeft) 669 670 child_item.setToolTip(7, _('File:line '\ 671 'where function is defined')) 672 child_item.setData(7, Qt.DisplayRole, file_and_line) 673 #child_item.setExpanded(True) 674 if self.is_recursive(child_item): 675 child_item.setData(7, Qt.DisplayRole, '(%s)' % _('recursion')) 676 child_item.setDisabled(True) 677 else: 678 callees = self.find_callees(child_key) 679 if self.item_depth < 3: 680 self.populate_tree(child_item, callees) 681 elif callees: 682 child_item.setChildIndicatorPolicy(child_item.ShowIndicator) 683 self.items_to_be_shown[id(child_item)] = callees 684 self.item_depth -= 1 685 686 def item_activated(self, item): 687 filename, line_number = self.get_item_data(item) 688 self.parent().edit_goto.emit(filename, line_number, '') 689 690 def item_expanded(self, item): 691 if item.childCount() == 0 and id(item) in self.items_to_be_shown: 692 callees = self.items_to_be_shown[id(item)] 693 self.populate_tree(item, callees) 694 695 def is_recursive(self, child_item): 696 """Returns True is a function is a descendant of itself.""" 697 ancestor = child_item.parent() 698 # FIXME: indexes to data should be defined by a dictionary on init 699 while ancestor: 700 if (child_item.data(0, Qt.DisplayRole 701 ) == ancestor.data(0, Qt.DisplayRole) and 702 child_item.data(7, Qt.DisplayRole 703 ) == ancestor.data(7, Qt.DisplayRole)): 704 return True 705 else: 706 ancestor = ancestor.parent() 707 return False 708 709 def get_top_level_items(self): 710 """Iterate over top level items""" 711 return [self.topLevelItem(_i) for _i in range(self.topLevelItemCount())] 712 713 def get_items(self, maxlevel): 714 """Return all items with a level <= `maxlevel`""" 715 itemlist = [] 716 def add_to_itemlist(item, maxlevel, level=1): 717 level += 1 718 for index in range(item.childCount()): 719 citem = item.child(index) 720 itemlist.append(citem) 721 if level <= maxlevel: 722 add_to_itemlist(citem, maxlevel, level) 723 for tlitem in self.get_top_level_items(): 724 itemlist.append(tlitem) 725 if maxlevel > 0: 726 add_to_itemlist(tlitem, maxlevel=maxlevel) 727 return itemlist 728 729 def change_view(self, change_in_depth): 730 """Change the view depth by expand or collapsing all same-level nodes""" 731 self.current_view_depth += change_in_depth 732 if self.current_view_depth < 0: 733 self.current_view_depth = 0 734 self.collapseAll() 735 if self.current_view_depth > 0: 736 for item in self.get_items(maxlevel=self.current_view_depth-1): 737 item.setExpanded(True) 738 739 740#============================================================================== 741# Tests 742#============================================================================== 743def primes(n): 744 """ 745 Simple test function 746 Taken from http://www.huyng.com/posts/python-performance-analysis/ 747 """ 748 if n==2: 749 return [2] 750 elif n<2: 751 return [] 752 s=list(range(3,n+1,2)) 753 mroot = n ** 0.5 754 half=(n+1)//2-1 755 i=0 756 m=3 757 while m <= mroot: 758 if s[i]: 759 j=(m*m-3)//2 760 s[j]=0 761 while j<half: 762 s[j]=0 763 j+=m 764 i=i+1 765 m=2*i+3 766 return [2]+[x for x in s if x] 767 768 769def test(): 770 """Run widget test""" 771 import inspect 772 import tempfile 773 from spyder.utils.qthelpers import qapplication 774 775 primes_sc = inspect.getsource(primes) 776 fd, script = tempfile.mkstemp(suffix='.py') 777 with os.fdopen(fd, 'w') as f: 778 f.write("# -*- coding: utf-8 -*-" + "\n\n") 779 f.write(primes_sc + "\n\n") 780 f.write("primes(100000)") 781 782 app = qapplication(test_time=5) 783 widget = ProfilerWidget(None) 784 widget.resize(800, 600) 785 widget.show() 786 widget.analyze(script) 787 sys.exit(app.exec_()) 788 789 790if __name__ == '__main__': 791 test() 792