Python profiler output in QT GUI

I wanted to sort my profiler result (using cProfile) but found usign the stats.Stats objects rather complicated.

Profiling is easy:

import cProfile
cProfile.run('''
MY CODE AS A STRING
''')

The profiler outputs itself to the console, so to instead catch it in a file we can change python’s console output.

import cProfile
import sys
import cStringIO

backup = sys.stdout
sys.stdout = cStringIO.StringIO()

cProfile.run('''
MY CODE AS A STRING
''')

profileLog = sys.stdout.getvalue()
sys.stdout.close()
sys.stdout = backup

The original sys.stdout is also stored as sys.__stdout__
but maybe at the point you are doing this the host application already has it’s own
stdout in use, so let’s just backup and restore explicitly so we’re certainly not breaking stuff.

Now the output is a huge ascii table of stats. By converting that to a QTableWidget we can
easily sort and analyse this data. So first let’s set up the table…

from PyQt4.QtCore import *
from PyQt4.QtGui import *

widget = QTableWidget()
widget.setColumnCount(6)
widget.setHorizontalHeaderLabels(['ncalls', 'tottime', 'percall', 'cumtime', 'percall', 'filename:lineno(function)'])

I manually copied the header names from the profile log, you may make the more sensible at your leisure… The widget needs to have it’s size set up before usage, so we can estimate the number of rows beforehand instead of resizing it in every iteration:

logLines = profileLog.split('\n')
widget.setRowCount(len(logLines))

Now this is a bit ugly, we essentially iterate all the lines and put their respective values into the widget. We’re splitting by whitespace with a regex.

enabled = False
y = 0
for i in range(len(logLines)):
    ln = logLines[i].strip()
    # skip empty lines
    if not ln:
        continue
    # start real iteration only after the header information
    if not enabled:
        if ln.lower() == r'ncalls  tottime  percall  cumtime  percall filename:lineno(function)'.lower():
            enabled = True
        continue
    segments = re.split('\s+', ln)
    c = len(segments)
    if c > 6:
        c = 6
        segments[5] = ' '.join(segments[5:])
    for x in range(c):
        item = QTableWidgetItem(segments[x])
        widget.setItem(y, x, item)
    y += 1

We manually increment the row count to account for the header lines and potential empty lines / otherwise ignored lines.
Last we strip off unused rows (remember we assumed line count as row count), enable sorting and show our widget.

widget.setRowCount(y)
widget.setSortingEnabled(True)
widget.show()

For convenience I wanted to make this a function that I could import and use instead of cProfile.run() at any given time. So this is my full code:

import re
import sys
import cProfile
import cStringIO
from PyQt4.QtCore import *
from PyQt4.QtGui import *


def profileToTable(code, globals=None, locals=None):
    backup = sys.stdout
    sys.stdout = cStringIO.StringIO()
    
    cProfile.run(code)
    
    profileLog = sys.stdout.getvalue()
    sys.stdout.close()
    sys.stdout = backup
    
    widget = QTableWidget()
    widget.show()
    widget.setColumnCount(6)
    widget.setHorizontalHeaderLabels(['ncalls', 'tottime', 'percall', 'cumtime', 'percall', 'filename:lineno(function)'])
    
    logLines = profileLog.split('\n')
    widget.setRowCount(len(logLines))
    
    enabled = False
    y = 0
    for i in range(len(logLines)):
        ln = logLines[i].strip()
        # skip empty lines
        if not ln:
            continue
        # start real iteration only after the header information
        if not enabled:
            if ln.lower() == r'ncalls  tottime  percall  cumtime  percall filename:lineno(function)'.lower():
                enabled = True
            continue
        segments = re.split('\s+', ln)
        c = len(segments)
        if c > 6:
            c = 6
            segments[5] = ' '.join(segments[5:])
        for x in range(c):
            item = QTableWidgetItem(segments[x])
            widget.setItem(y, x, item)
        y += 1
    return widget

We must cache the returned widget in memory for otherwise python’s garbage collection will try to delete it and then Qt will close it.

widget = profileToTable('re.compile("foo|bar")')

After that you may wish to add a search bar so you can look for specific functions that you wish to check for potential improvements or suspicious times. At least I did… simple Qt stuff! QTableWidget has a search by (partial) string utility as well as hide and show row functions, so a simple set of loops allows us to select and filter the table.

def filterTable(tableWidget):
    main = QWidget()
    layout = QVBoxLayout()
    main.setLayout(layout)
    
    search = QLineEdit()
    layout.addWidget(search)
    
    layout.addWidget(tableWidget)
    
    def filterTable(widget, text):
        # there seem to be many duplicate entries when we go from a string to an empty string
        rows = []
        if text:
            showItems = widget.findItems(text, Qt.MatchContains)
            for i in showItems:
                rows.append(i.row())
            rows.sort()
        allrows = range(widget.rowCount())
        for i in range(len(rows)-1, -1, -1):
            widget.showRow(rows[i])
            allrows.pop(rows[i])
        for i in allrows:
            widget.hideRow(i)
        
    search.textChanged.connect(functools.partial(filterTable, tableWidget))
    
    main.show()
    return main

This function takes as widget the result of the profile function so it completely appends to what’s already there. Again the returned widget must be cached. You may also make a utility function like so:

# regular usage example
widget = profileToTable('re.compile("foo|bar")')
wrapper = filterTable(widget)

def profileToFilterTable(code, globals=None, locals=None):
    return filterTable(profileToTable(code, globals, locals))

# with utlity
wrapper2 = profileToFilterTable('re.compile("foo|bar")')

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>