11. def trace_calls(self, frame, event, arg):
co = frame.f_code
filename = co.co_filename
if filename in (__file__,):
# Ignore ourself
return
self._send_notice(frame, event, arg)
return self.trace_calls
12. def _send_notice(self, frame, event, arg):
co = frame.f_code
func_name = co.co_name
line_no = frame.f_lineno
filename = os.path.abspath(co.co_filename)
for d in IGNORE_DIRS:
if filename.startswith(d):
return
# …
13. # …
interesting_locals = {
n: v
for n, v in frame.f_locals.items()
if (not inspect.ismodule(v)
and not inspect.isfunction(v)
and not inspect.ismethod(v)
and (n[:2] != '__' and n[-2:] != '__'))
}
# …
15. $ smiley help
usage: smiley [--version] [-v] [--log-file LOG_FILE]
[-q] [-h] [--debug]
smiley spies on your apps as they run
optional arguments:
--version show program's version number
and exit
-v, --verbose Increase verbosity of output.
--log-file LOG_FILE Specify a file to log output.
-q, --quiet suppress output except warnings
-h, --help show this help message and exit
--debug show tracebacks on errors
Commands:
complete print bash completion command
help print detailed help for another command
monitor Listen for running programs and show
their progress.
run Run another program with monitoring
enabled.
16. $ smiley help run
usage: smiley run [-h] [--socket SOCKET] command
[command ...]
Run another program with monitoring enabled.
positional arguments:
command the command to spy on
optional arguments:
-h, --help show this help message and exit
--socket SOCKET URL for the socket where the listener
will be (tcp://127.0.0.1:5556)
17. $ smiley help monitor
usage: smiley monitor [-h] [--socket SOCKET]
Listen for running programs and show their progress.
optional arguments:
-h, --help show this help message and exit
--socket SOCKET URL for the socket where to monitor on
(tcp://127.0.0.1:5556)
18. def _process_message(self, msg):
print 'MESSAGE:', msg
msg_type, msg_payload = msg
if msg_type == 'start_run':
print (‘Starting new run:',
msg_payload.get(‘command_line'))
elif msg_type == 'end_run':
print 'Finished run'
else:
line = linecache.getline(msg_payload['filename'],
msg_payload['line_no']).rstrip()
if msg_type == 'return':
print '%s:%4s: return>>> %s' % (
msg_payload['filename'],
msg_payload[‘line_no'], msg_payload['arg'])
else:
print '%s:%4s: %s' % (
msg_payload['filename'],
msg_payload[‘line_no'], line)
if msg_payload.get('locals'):
for n, v in sorted(msg_payload['locals'].items()):
print '%s %s = %s' % (
' ' * len(msg_payload['filename']),
n,
v,
)
print
19. def gen(m):
for i in xrange(m):
yield i
def c(input):
print 'input =', input
data = list(gen(input))
print 'Leaving c()'
def b(arg):
val = arg * 5
c(val)
print 'Leaving b()'
return val
def a():
print 'args:', sys.argv
b(2)
print 'Leaving a()'
a()
33. class EventProcessor(object):
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def start_run(self, run_id, cwd, description,
start_time):
"""Called when a 'start_run' event is seen.
"""
@abc.abstractmethod
def end_run(self, run_id, end_time, message,
traceback):
"""Called when an 'end_run' event is seen.
"""
@abc.abstractmethod
def trace(self, run_id, event,
func_name, line_no, filename,
trace_arg, local_vars,
timestamp):
"""Called when any other event type is seen.
"""
34. def get_runs(self):
"Return the runs available to browse."
with transaction(self.conn) as c:
c.execute(
"""
SELECT
id, cwd, description, start_time,
end_time, error_message
FROM run
"""
)
return c.fetchall()
37. class DBLineCache(object):
def __init__(self, db, run_id):
self._db = db
self._run_id = run_id
self._files = {}
def getline(self, filename, line_no):
if filename not in self._files:
body = self._db.get_cached_file(
self._run_id, filename)
self._files[filename] = body.splitlines()
try:
return self._files[filename][line_no]
except IndexError:
# Line number is out of range
return ''
38. def take_action(self, parsed_args):
# Fix import path
cwd = os.getcwd()
if (cwd not in sys.path and
os.curdir not in sys.path):
sys.path.insert(0, cwd)
# Fix command line args
sys.argv = parsed_args.command
# Run the app
p = publisher.Publisher(parsed_args.socket)
t = tracer.Tracer(p)
t.run(parsed_args.command)
39. def take_action(self, parsed_args):
# Fix import path
cwd = os.getcwd()
if (cwd not in sys.path and
os.curdir not in sys.path):
sys.path.insert(0, cwd)
# Fix command line args
sys.argv = parsed_args.command
# Run the app
if parsed_args.mode == 'remote':
p = publisher.Publisher(parsed_args.socket)
else:
p = db.DB(parsed_args.database)
t = tracer.Tracer(p)
t.run(parsed_args.command)
48. class StyledLineCache(object):
def __init__(self, db, run_id):
self._db = db
self._run_id = run_id
self._files = {}
EXPECTED_PREFIX = '<div class="highlight"><pre>'
EXPECTED_SUFFIX = '</pre></div>'
def getline(self, filename, line_no):
if filename not in self._files:
body = self._db.get_cached_file(self._run_id,
filename)
styled_body = apply_style(filename, body,
linenos=False)
start = len(self.EXPECTED_PREFIX)
end = -1 * (len(self.EXPECTED_SUFFIX) + 1)
middle_body = styled_body[start:end].rstrip('n')
self._files[filename] = middle_body.splitlines()
try:
return self._files[filename][line_no-1]
except IndexError:
# Line number is out of range
return ''
49.
50.
51.
52.
53. ✓ Web UI
✓ Profiling Data
✓ Call Graph
✓ Syntax Highlighting
• Only Changed Variables
• Comments
54. def _mk_seq(d):
return sorted(
(k, pformat(v, width=20))
for k, v in d.iteritems()
)
def get_variable_changes(older, newer):
s_a = _mk_seq(older)
s_b = _mk_seq(newer)
matcher = difflib.SequenceMatcher(None, s_a, s_b)
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
if tag in {'insert', 'replace'}:
for i in s_b[j1:j2]:
yield i
Good morning, and thank you all for coming.
I am Doug Hellmann, and I am currently working as an OpenStack contributor for HP.
Today I am going to talk a bit about how I glued together modules from the standard library and a few other packages to make a python power debugger.
The project I will be talking about runs along side your program, and watches not just how the code is executed but also all of the values of all variables, recording them to produce a complete trace of the project’s execution history for you to examine at your leisure.
But before we go too far into detail…
…I want to make sure that I’ve set your expectations clearly.
Most project-based presentations are set up to cover the project and its features, often with demos. Those are great talks, and they’re a big part of why we all come to conferences. If I was giving a talk like that it would have a shorter title.
But that’s not what I want to do today,…
…instead I want to talk about HOW I built it.
It’s easy to forget, when you watch a bunch of conference talks announcing new projects, that these things don’t spring fully formed into the world. They take continual iterative development over time.
I’ve found that it’s important to talk about that evolutionary process for projects with newer developers, as examples of how to start with something simple and build something more complex. How to add features carefully, adjusting existing code as you go.
So what I want to focus on, more than the implementation details of the program, are the thought processes I followed as I was working and…
…the story of how I created the project I call SMILEY.
I will show code and talk about the tools as I tell the story, but there’s too much code to really walk through all of it, so if you’re the sort of person who reads the last page of a book before starting it, you may want to go ahead and jump into the github repo and look around.
[PAUSE]
First things first, what’s up with the name? When I started building this tool, I thought of it as “spying” on what the program was doing.
So of course, …
… I started thinking of spy names.
This guy was way too obvious.
So I opted to go with…
…George Smiley, the John le Carré character.
He’s more intellectual, less flashy, and is focuses on the spying rather than shooting things up.
That’s more my style.
With the naming question…
…resolved, I could get down to business and start working on the features I wanted smiley to have.
I wanted to record the same data you might expect to see in a live debugger like pdb, but without tediously stepping through calls. I also wanted the best of “print” based debugging, without having to decide in advance which values to print out.
RECORD CALLS & DATA
REMOTE MONITORING
BROWSE HISTORY & ANALYZE LOCALLY
I was also motivated to build something a little audacious, to see how far I could take the idea, and learn some new tools along the way.
To start out…
…I knew Python’s trace API because I had used it before, so I knew enough to believe I could make that work for me. And I’ve done network programming before, so I knew if it came down to it I could build the remote execution part.
But I also wanted to learn some new tools as part of the project, so I started by figuring out how to make the networking library ZeroMQ work for me. That involved reading a lot of documentation and poking around with some different implementations that I didn’t save in the history. I mostly had no idea what I was doing, so I just keep trying to get the 2 programs to talk until they did.
Initially I used PUB/SUB…
…sockets because I was communicating one way, but quickly realized that was a mistake and switched to a PUSH/PULL socket pair. PUB/SUB doesn’t ensure message delivery and I didn’t want to miss the beginning of the trace.
So I created a pair of classes that I could use to send arbitrary messages one way between two processes, a publisher and a listener.
After I had the publisher and listener classes…
…I created a tracer class that uses the communication classes to publish data about what is happening inside a program as it runs.
The tracer works…
…by installing its trace_calls() method using sys.settrace() so that every time the python interpreter does something it calls this method.
On each call, the trace function is given the stack frame and a string with the name of the event type (e.g., CALL, LINE, RETURN, EXCEPTION). Some of those event types include an extra argument, which is passed as the final parameter.
The work for sending the event is in…
…the send_notice() method which does three things.
First, it figures out if the event SHOULD be recorded, based on the NAME OF THE FILE where it happened. I started out thinking I would just want to ignore the standard library, but over time the facility for ignoring directories changed a bit.
If a file should be included in the trace, then…
…it builds a list of interesting local variables by looking at the stack frame.
The FRAME includes EVERYTHING, but I ignore any modules, functions, and methods because those aren’t as likely to change value over time as other types of variables. And I get the source code being executed by reading those files directly.
Finally, it builds…
…a standard data structure to hold the trace event data.
At this point I was not worried about the database schema or anything like that, so it’s a simple flat dictionary describing the event. The event type from the trace call is used as the message type, and I added a few other message types for starting a trace and ending a trace.
The next step was to…
…build some sort of a UI for running programs and recording the trace.
I started with some simple demo scripts built on argparse, but as I thought about it I realized I would need several different commands for different tasks.
I switched to cliff, a framework I had created for use in OpenStack. It relies on argparse for parsing options and setuptools entry points to discover available sub-commands defined as plugins. That means applications built on cliff can be extended by packages written by someone else. As a developer tool, I wanted to make smiley easy to extend.
I needed a RUN command…
…to set up the trace code and publish events to a ZMQ socket.
And a MONITOR command…
…to listen for those messages and print them to the console.
Both commands default to a localhost socket, but it’s possible to specify another using a URL argument so remote monitoring is possible.
The monitor command…
…sets up a listener to call this process_message() method to actually print the output.
The PATTERN of looking at the EVENT TYPE and doing something DIFFERENT repeats a few times in smiley. I considered making each event type its OWN CLASS, but since the messages are fairly low level and DON’T HAVE ANY BEHAVIOR themselves that seemed like overkill.
I would still need to do the work of turning a transmitted message into the right class anyway, and I wanted to keep the display and database LOGIC ISOLATED in their own classes. So I chose to KEEP the basic data object as SIMPLE as possible.
To test all of this out I created a little script…
…with a few functions that call each other with different arguments and return values.
It was simple but still interesting enough to be useful for some of the edge cases like loops and generators.
In one terminal I can use “smiley run simple.py” to run the test script and then…
…in another terminal “smiley monitor” collects the output.
You can see that it isn’t terribly readable, but it was good enough to keep experimenting.
It includes the event types…
…file names, and line numbers.
And later in the output, when the program is inside a function,…
…MONITOR shows the function name for each line as well.
At this point the RUN command traces a user’s program and sends the results to the MONITOR command, which prints everything out on the console.
That’s interesting, and a good early step, but not necessarily a “minimum viable product.”
The next step was to figure out…
…where else to send the output.
I decided I was going to want several commands for writing to different outputs so I needed to do some cleanup work and to refactor the existing monitor command to make a reusable base class.
A side note here: To be HISTORICALLY accurate, I’m showing the EARLY, UGLY, versions of most of the code & output today, so it may not match what you find if you check out the git repository. I won’t go through the refactoring line-by-line, you can read the git logs if you want that. I’m going to focus on the CONCEPTUAL changes.
Now I needed decide how to record runs for browsing. I knew…
…I wanted some sort of database.
I’ve used both ZODB and MongoDB before with good results, but neither quite had the query semantics that I wanted, in terms of being able to fetch a subset of the run easily. After thinking about how I might implement replay, I convinced myself that the query patterns I anticipated meant I should use a relational database.
I didn’t want to depend on having a separate server running, so I went with SQLite, since it is built into the standard library and available in most environments.
Smiley doesn’t use an ORM,…
…but I did create a class to hold the query API that I would support.
I assume that if the database doesn’t exist it should be initialized, so there’s no separate “make me a database” step like with some projects. This is a DESKTOP APP not a server tool, so creating the file automatically felt more friendly.
There’s no database SCHEMA VERSIONING facility or migrations. Right now, I just delete the database if I need to add a column, but I’ll probably have to do something better than that at some point.
The schema definition…
…is kept in a separate file distributed as package data.
To start I had these TWO TABLES, though a few more were added later, along with some indexes.
For now, though:
RUN holds the start/end time and the result of running the script.
TRACE holds all of the trace event data.
The database API…
…defines the logical operations, such as start_run(). This lets me separate the data representation from the way I manipulate it.
The arguments to start a run don’t completely fill in a row, since some of the values are only known when the run is completed.
The first version of the API only knew how to START a run, but using that one operation I could prove to myself that a message coming from the RUN command process would make it through the ZMQ socket to the process recording the data then into the database where I could retrieve it later. That would ensure I had all of the communication parts working, and then I could expand the API for more features.
The RECORD command class has a…
…method for processing the messages that looks at the type to decide which DB API method to call, similar to how the MONITOR command checks the type before formatting the output.
This is the only place that has to know about both the line-format for the messages coming in from the RUN command and the arguments needed for the database.
After I had enough of the pieces working to record the start of the run…
…I could update both the MONITOR command and DB API to support ending a run and recording the intermediate trace items.
Iterating like this let me focus on one piece at a time:
First set up ZMQ communication, then hook into the TRACE to observe the program, then send that data to the CONSOLE to make sure I had it right, then build up the DATABASE handling piece by piece.
Each step along the way I only needed to learn one tool at a time or think about one API at a time.
Now that I had a basic way to record program runs,…
… I had to decide what to tackle next.
I could start building the database query API to let me REPLAY old runs, OR I could look at some of the more COMPLEX DATA TYPES.
I had reserved a column for recording the final exception from a program that dies with an error, but at this point I was only recording the error message and not the full traceback. I needed to do more to produce a useful representation of the traceback, so I decided to look into how to transmit more complex data types so I could have that working before I wrote output or replay code that I might have to rewrite for those more complex types.
So, I started writing some…
…custom JSON encoding logic to handle complex types.
The traceback module has an EXTRACT_TB function tailor-made for getting the traceback data into a structure that is easier to format, so I started with that.
Classes and other type objects are sent using their REPR value.
INSTANCES of other complex types are represented using a dictionary containing the CLASS NAME, the MODULE where the class can be found (in case the name is not unique) and then the ATTRIBUTES of the object.
So now I could send complex data…
…but I still had no way to replay old runs from the database.
I knew I had the formatting code I needed in the monitor command, so I started by refactoring that implementation out into its own module so I could reuse it from the new command I was planning.
And that’s where I had a sort of epiphany…
… It seems like a small thing in retrospect, but at the time it made a big difference to realize that the output formatter, database API, and the ZMQ publisher all shared a common pattern, they all need to perform the same 3 operations.
I realized with a little work they could all share a common API, so I defined a new EventProcessor base class and fixed up the APIs for the existing classes to match it, including renaming some methods and arguments.
I used the abc module to make an abstract base class, since there’s not really a lot of implementation to be shared.
Then I went on to work on the REPLAY command, except…
…I realized there was no way for me to TEST that command until I had a way to get a list of the runs from the database.
Up until this point I had been looking at the database directly or reading monitor output, so I hadn’t needed a list command.
Now I did and, as you can see, at this stage adding a new command still meant adding to the db API.
So, first a LIST command, then the REPLAY command…
…which, with the new EventProcessor API turned out to be pretty straightforward.
I added new methods to the DB API to return the RUN record and related TRACE records, and then loop over the results calling the appropriate methods on the output formatter.
One of the features…
…of both the MONITOR and REPLAY commands is showing the line of source code being executed with the variables as context.
This was implemented originally using the LINECACHE module from the standard library, which has an API for finding line N of a given file by name.
However, as I continued to run tests and experiment, I realized that replaying a very old run was showing the WRONG source lines because I was changing the input program. The feature also wouldn’t work properly for remote monitoring, since the source files might not be on the MONITOR and REPLAY machine at all. I was going to need to save the source code of the program as it ran, too, in order for the history to be accurate.
So, I added…
…some calls to the database API to let me store the source code there in a new table.
And then I created a DBLineCache class with the same getline() API as the linecache module, but that reads the files out of the database instead of from the filesystem.
It’s not a complicated class, but it let me substitute an instance of the new class where I was using linecache before, and not change the output formatter implementation.
At this point…
…I was running MONITOR and RUN commands over and over, and getting a little tired of using them as two commands.
I wanted to keep MONITOR for remote debugging, but when working locally I knew I could just use the RUN command if I taught it how to write to a database instead of a socket.
Since my DB API was in part derived from the EventProcessor base class, I knew…
…I could just instantiate a DB and pass it to the tracer.
I added a flag to RUN to tell me whether to use a remote connection or a local database, and updated RUN to check that flag.
That’s two cases where by identifying an abstract API, I could provide separate concrete implementations for maximum code reuse.
Now I had something…
… that was usable for very simple python programs and an iterative pattern was emerging in my development process.
I learned to use a NEW TOOL, used it to build a FEATURE, evolve the feature by ENHANCING it and FIXING bugs, and then moved on to the next cycle.
LEARN, BUILD, EVOLVE
It’s not a perfect description of how the work happened, because sometimes I skipped steps, but it’s generally how I approached it.
So far I’ve talked about…
…NINE of these iterations, starting with the basics of ZMQ and SQLite and moving on to various smaller usability enhancements.
As I worked, I built up quite a database of test runs, and I wanted a better UI for BROWSING them instead of typing lots of JOB ID values on the command line. I transitioned into the next major feature addition cycle by starting the web interface or “server” mode.
As with the command line tools…
…I didn’t want to build everything from scratch, and this was definitely an area where I needed to go outside of the standard library.
I’m no web designer, so I knew I was going to need a framework to do most of the heavy lifting. I started out with PureCSS, which I had used for my blog recently, but ended up switching to BOOTSTRAP because it has better widget support.
I chose PECAN, since I had used it recently for some other work and I know the maintainers through the Atlanta meetup. Pecan uses MAKO templates by default, and I hadn’t really used those much, so between figuring out Mako and Bootstrap it took me a while to put together a basic page.
As with REPLAY…
…I had to start with showing a list of the existing runs.
The table showing run IDs, times, and outcome, is a little easier to read here than the LIST command output was on the console.
Next I turned those run values in the left column into links and…
…wrote a view for the trace data.
The trace shows each line of the script being run, just like the console app.
For the web app…
… I wanted to be able to show the complete contents of source files, too.
So I needed a new view for that.
Of course, plain text in a browser window is boring…
…so I hooked up the PYGMENTS library so I could have syntax highlighting.
That involved reading the body of the file from the database, and passing it to pygments to render as HTML, and adding the appropriate CSS block to the stylesheet for the app.
The results…
…could probably use some help from a more skilled designer, but they’re an improvement over the plain text.
It was a little more challenging to add syntax highlighting to the trace view, which is showing one line at a time.
Pygments doesn’t always know how to parse a partial statement or snippet of source code…
…so I wanted to feed the whole file to it to ensure the parser would understand it.
I noticed that the pygments output preserved the original lines, with a little bit of prefix and suffix text for the whole block of output.
Using that information, I went back and created another line cache implementation. This one takes the full pygments output for a file, trims the header and footer, saves the part that represented the actual source code, and return lines from that instead of the raw plain text in the database.
Again, I was able to drop this class in to replace the old version, and then the web UI…
…had syntax highlighting in the trace output, seen here in the statement column.
Adding syntax highlighting to the local variable definitions and return values in the far right column…
…was as simple as adding a view filter to the Mako templates.
It’s mostly useful for strings, but there are a few special values like None that are treated as keywords, too.
Now I could see what my program was doing, and easily browse through the trace output. The next thing to do was make it easier to understand where it was spending lots of time. I had originally thought I would keep up with this sort of data myself using unique IDs for each call and using the time values to build the necessary statistics.
Then I looked again at Python’s profiler and trace APIs, and realized they used different hooks, so they could both be used together.
Instead of building my own profiler, I just turned on the built-in profiler and collected the data, sending it across the socket at the end of a run.
I added a new web view to show the profile stats data as a table.
Then Ryan Petrello showed me…
… how to generate a visualization with gprof2dot so I added another view that shows that diagram.
The diagram uses color to indicate where in the call tree the program spends most of its time.
Unfortunately the implementation that I have generates the image on each invocation, so it’s not exactly efficient, but for the current class of applications smiley supports it was fine.
[PAUSE]
After adding the profiler support, I had reached another plateau in my feature work…
… and was ready to move to the next step in that LEARN, BUILD, EVOLVE cycle.
I decided to shift from ADDING major features to REFINING some of the features I already had.
I noticed that the web UI was showing a lot of the same data over and over, so I decided to make it only show local variables as their values CHANGED from line to line.
I also thought about how important COMMENTS are in understanding a program, so I decided to try to show comments near code that had been executed.
I started with…
…detecting changes in variables.
The first version of this used difflib on a sequence of sorted variable names and their representations. This was pretty convenient, since difflib works on any 2 sequences and would tell me what had been added or changed (to be displayed).
That let me…
… remove this duplicated DATA, by comparing the local variables at each line with the ones from the previous line.
Any time execution enters or leaves a function,…
…it automatically triggers re-dumping the local values for the new context.
Within a given function scope, only the variables that actually changed are shown.
The result was output with lots of lines without any variables, because they didn’t change very much.
That gave me the idea to collapse those rows together…
… and show them as a block of source instead of one line at a time.
I wrapped the existing iterator of trace data in another generator function that produced the aggregate values.
That wrapper also replaces the single line number value with a tuple containing the start and stop points of the range of lines to show. I added a getlines() method to the line cache to retrieve multiple lines with one call so the template didn’t have to make a bunch of calls to get each line separately.
Next, I extended getlines()…
…to include comment lines immediately preceding the start of the requested range.
It scans backwards, allowing a single blank line between the code and a comment block. If it hits anything that isn’t a comment, or a second blank line, it stops and uses the range of lines it has identified.
At this point I was doing quite a bit of processing of the data in the UI, and it was starting to seem slow. I knew that the problem would only become worse as the length of program runs grew so,
I decided to add pagination…
…to the UI to address that.
After pagination was working, I added a few other features.
Multi-threaded programs record the thread id of each step of the program, and the UI lets you filter the output by thread.
There is a command for generating a static HTML report,…
…which can then be published on a website and shared.
It works much like the interactive browser, except it is for a single run and all of the pages are static.
And there are also commands for exporting run data from one database and importing it into another.
Each of these feature development cycles followed the same LEARN, BUILD, EVOLVE process.
[PAUSE]
All of this work was done over the course…
…of a couple of years, though not full time since this was a fun side project.
This chart shows the days on which I had commits, and the lines of code at the end of each day. There were almost always more than one commit on any given day, but I collapsed those to make the graph easier to follow.
If we accept lines of code…
…as a rough analog for project feature and complexity growth, you can see a relatively steady progression resulting from that iterative process. The one notable exception is that big jump around the end of 2013, which is when I committed a large batch of CSS and template code for the static report generator.
And there were plenty of days where I read or experimented or otherwise worked without writing any code.
But slowly, over time, smiley GREW from a very RUDIMENTARY trace monitoring tool, to have a full browser-based GUI and some fairly advanced collaboration features.
There are still a lot of things I would like to add…
…to make smiley even more useful.
Performance work is probably the biggest need. Transmitting or recording all of that data is expensive, so offloading it to another process or figuring out how to send less data would be valuable.
Pre-calculating and storing the collapsed trace views would also be an improvement.
There are several UI enhancements on the list, too.
If you are interested in getting involved,…
…please check out the code and drop me an email.
I would love to have some other folks helping to build smiley into a more usable tool.
…resolved, I could get down to business and start working on the features I wanted smiley to have.
I wanted to record the same data you might expect to see in a live debugger like pdb, but without stepping through calls individually. Combining the best of “print” based debugging with an interactive debugger that has access to variables you didn’t think to print out.
I was also motivated to build something a little audacious, to see how far I could take the idea, and learn some new tools along the way.
So, where did I start?
…with Gene Hackman
But it’s not really a spy movie, per se.
…where to send the output.
I decided I was going to want several commands for listening in different ways (console output, database, etc.) so the next phase was some cleanup work and to refactor the existing monitor command to make a base class.
A side note here: To be historically accurate, I’m showing the early, ugly version of most of the code here today, so it may not match what you find if you check out the git repository. I won’t go through the refactoring line-by-line, you can read the git logs if you want that. I’m going to focus on the conceptual changes, instead.
The next step was to decide how to record runs for browsing. I knew…
… I had to decide what to tackle next.
I could start building the database query API to let me replay old runs, or I could look at some of the more complex data types.
I had reserved a column for recording the final exception from a program that dies with an error, but at this point I was only recording the error message and not the full traceback. I needed to do more to produce a useful representation of the traceback, so I decided to look into how to transmit more complex data types so I could have that working before I wrote output or replay code that I might have to rewrite for those types.
So, I started writing some…
…but I still had no way to replay old runs from the database.
I knew I had the formatting code I needed in the monitor command, so I started by refactoring that implementation out into its own module so I could reuse it from the new command I was planning.
And that’s where I had a sort of epiphany…
…I had something that was usable for very simple python programs.
The iterative pattern in the development process should be clear. I have been learning to use a new tool, using it to build a feature, making the feature a little better by enhancing it and fixing bugs, and then moving on to the next cycle. It’s not a perfect description of how the work happens, because sometimes you skip steps, but it’s generally how I approached it.
So far I’ve talked about…
…I had something that was usable for very simple python programs.
The iterative pattern in the development process should be clear. I have been learning to use a new tool, using it to build a feature, evolving the feature to make it a little better by enhancing it and fixing bugs, and then moving on to the next cycle. It’s not a perfect description of how the work happens, because sometimes you skip steps, but it’s generally how I approached it.
So far I’ve talked about…
…NINE of these iterations, starting with the basics of ZMQ and SQLite and moving on to various smaller usability enhancements.
By this time, I was building up quite a database of test runs, so I wanted to think about a better UI for browsing them instead of typing lots of UUID values on the command line. I transitioned into the next major feature addition cycle by starting the web interface or “server” mode.
As with the command line tools…
…I didn’t want to build everything from scratch, and this was definitely an area where I needed to go outside of the standard library.
I used the pecan framework, since I had used it recently for some other work and I know the maintainers through the Atlanta meetup and working at Dreamhost. I used python’s built-in WSGI server, and although it is single-threaded, performed adequately for my needs in this case. I’m not building a massive front-end being hit by a bunch of users at once, just a dev tool, and this was easy to embed for deployment.
I’m no web designer, so I knew I was going to want to use some sort of framework to do most of the heavy lifting. I started out with the PureCSS framework, which I had used for my blog recently, but ended up dumping it and switching to Bootstrap for better widget support.
As with REPLAY…
…I went on to implement a few other features.
Multi-threaded programs record the thread id of each step of the program, and the UI lets you filter the output by thread.
There is a command for generating a static HTML report, which can then be published on a website and shared. It works much like the interactive browser, except it is for a single run and all of the pages are static.
And there are commands for exporting run data from one database and importing it into another.
There are still plenty of things I would like to add…
…to make smiley even more useful.
Performance work is probably the biggest need. Transmitting or recording all of that data is expensive, so offloading it to another process or figuring out how to send less data would be valuable.
Pre-calculating and storing the collapsed trace views would also be an improvement.
There are several UI enhancements on the list, too.
If you are interested in getting involved,…
…to make smiley even more useful.
Performance work is probably the biggest need. Transmitting or recording all of that data is expensive, so offloading it to another process or figuring out how to send less data would be valuable.
Pre-calculating and storing the collapsed trace views would also be an improvement.
There are several UI enhancements on the list, too.
If you are interested in getting involved,…
…now I needed to know how many pages of output it would produce.
So I still had to calculate the entire collapsed trace, but for small runs I could cache the results in memory if I assumed the user would be moving through it page by page — or even jumping around.
I decided to hard-code the number of items to show per page, while I debugged my pagination viewer code. The results looked like..
…this.
But showing each page number in the list of pages was quickly going to cause layout issues, so in addition to letting the user control how many items to show on a page I changed how those buttons were displayed so there are only a few on the screen at a time.
The pagination code evolved a few times, …
… because as I added support for “many” pages, the UI for “few” pages broke once or twice.
Smiley’s test coverage isn’t what I would like it to be. I already mentioned that I was not really doing test driven development because it is a fun exploratory project. I do have some tests, but definitely wasn’t writing them first and this is one place where that hurt a little.
After pagination was working…
…I wrapped the existing iterator of trace data in another generator function that produced the aggregate values.
It also replaces the single line number value with a tuple containing the start and stop points of the range of lines to show. I added a getlines() method to the line cache to retrieve multiple lines with one call.
Next, I extended getlines()…