[Top] [Contents] [Index] [ ? ]

Homura

Replay recorded text.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

1. Usage

Homura takes text logs and produces HTML files that allows interactive playback. Homura accepts up to two arguments, first argument is the input file, and second argument is the output file.

 
./homura input.log output.html

If second argument is omitted, output is written to stdout:

 
./homura input.log > output.html

If the first argument is omitted, or if first argument is "-", input is read from stdin:

 
./homura < input.log > output.html
cat input.log | ./homura - output.html

See section Tools for scripts to produce the input logs.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

2. Input

Input files are line-based text files. Two types of input lines are accepted (all other lines are ignored):


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

2.1 Snapshot cursor

 
YrowXcolumnFframeTtime

Set the cursor position to (row, column), and set timestamp for all subsequent events to frame at time.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

2.2 Snapshot line

 
LlineEsize=text

[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

3. Output

Homura outputs stand-alone HTML files with all the playback information packaged together.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

3.1 Controls

Regarding playback speed: most of the events match the original speed which they were originally recorded, except long periods of idleness are compressed. For example, if you were away from keyboard for an hour, Homura will inject about 3 seconds of non-activity into the output log as opposed to the full hour. If your system is fast, you can try decreasing realtime_step to a smaller value, which will make the playback smoother at the expense of more CPU.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

3.2 Compatibility

Output of Homura is officially supported on two browsers:

Homura requires JavaScript, HTML5, and CSS2.

Regarding HTML5: Output uses HTML5’s range inputs. On browsers that supports this (e.g. Chrome), you will see a slider bar for fast forward and rewind controls. On browsers that don’t support it (e.g. Firefox 5.0 and earlier), you will see a text box instead. Note that it’s still possible to fast forward or rewind using the text box, just enter the millisecond timestamp values into the box.

Regarding CSS2: Output uses position and z-index attributes for positioning. If your browser supports CSS Color Model Level 3, the controls will fade out to make the underlying text more visible. Moving the mouse cursor into the control area will bring the controls into foreground again.

Regarding JavaScript: Output uses mostly very simple JavaScript features, but the scripting engines must process quite a bit of data to be able to replay the text in real time (target refresh rate is ~12 frames per second).

On some systems, despite having a fast HTML renderer and layout engine, you may see some text being dropped when the text is being replayed at a high speed, or the timestamp might not be updated for a few seconds. Pausing the replay appears to force everything to render correctly again.

Output of Homura passes HTML validation. If the output doesn’t work with your browser, it’s time to get a different browser. In particular, the output is not compatible with Internet Explorer 8 and its atrocious CSS support.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

4. Tools

Two scripts are included in the package:


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

5. Implementation

Gory implementation details and design rationales.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

5.1 Input format

Initially, I had just wanted this simple VIM script for recording text editing sessions. And I had intended it to be really simple, like taking a full snapshot of the file every second or so. At this point, the input format would contain only two things: line number of the line that is currently being snapshotted, and the contents of that line. This would be half of the input format.

As it turns out, taking a snapshot every second would be expensive, and still doesn’t capture enough text. I look for autocommands that are built into VIM, and I found that the best one to use was to hook onto cursor movements: every time a CursorMoved or CursorMovedI event was fired, I would snapshot the file. Since the snapshots are hooked onto cursor movements, I figured I would capture the cursor position at the same time. This determined the other half of the input format.

Capturing the cursor position turns out to be a very good idea, since a significant portion of the events recorded will in fact be cursor movement events. Since the cursor events are fairly compact (one line per event, as opposed to multiple lines for text snapshot events), I have decided to store the timestamps with only the cursor events.

Two extra bits of detail that was added later to the input format to finalize it:

These were not immediately obvious, but as it turns out: VIM can not record sub-second timestamps, even though multiple editing events usually happen in the same second. To differentiate sub-second events, a frame counter is used. The sub-second events will be spaced evenly in replay, as opposed to having actual sub-second accuracy. This is good enough for most people.

The file size field for the line snapshots was the other non-obvious bit. Without which, it’s not possible to tell when lines are deleted from a file (since the last lines of the snapshot persists forever). There are other ways for representing this data as opposed to having to store the file size on every line, but this turns out to be the most robust encoding.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

5.2 Event list

Animation events are encoded in a long array of 6-tuples. Five formats are possible:

The first two entries of every event are always the start and end times. Start time is when the event should appear, and end time is when the event will be obsoleted by another event (e.g. a replace or append event on a particular slot will be obsoleted by a replace or delete event on the same slot).

End time is strictly an optimization to make fast forwards efficient: when user seeks to a particular point in time, all events with end times less than the target will be ignored. This worked so well that I didn’t have to define other event types for "keyframes". As a side benefit, Homura knows which lines will remain constant (or only appended to later) by looking at these end times, so it was possible to add the functionality to differentiate the transient/constant lines.

Third number indicates the event type. Types 1, 2, and 3 are text editing events. All of these operate on output slots (See section Output slots). For replace and append events, index and length refers to string index and prefix length in the string table (See section String table).

Cursor events (type 0) positions the cursor to some (row, column) position. Note that column position unit is in bytes and not characters, same as what’s in the input. Cursor can be outside the bounds of all currently visible text, and actually this is the common case since the text editor usually records cursor positions to be just past the end of the last character.

Slow cursor events (all remaining types) serves the same purpose as cursor events, but is needed to deal with Unicode characters and tabs. It also needs 4 pieces of data instead of 3, so the type fields is overloaded to encode cursor size. The fields are:

See section Cursor layer for more details on cursor rendering.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

5.3 Output slots

Since most text operations are line-based, it made sense that the output would also be line-based. One way to implement line-based output in HTML is to have lots of <SPAN>s, one per line. This is very straightforward if we only ever append lines to a file, or if inserting a single line means shifting all the lines after it. Neither is very efficient.

To make line-based operations efficient with JavaScript, Homura use the concept of output slots. That is:

For example, if we have a file with "line 1" and "line 3", and "line 2" is inserted in between at some later point in time, the output slots will be initialized to be the following:

 
<span id="slot0">Line 1
</span><span id="slot1"></span><span id="slot2">Line 3
</span>

When "Line 2" is inserted later:

 
<span id="slot0">Line 1
</span><span id="slot1">Line 2
</span><span id="slot2">Line 3
</span>

The slot numbers are assigned serially. Output slots are recycled where possible. For example, if "Line 2" above is deleted and a different line is inserted later, the new line will likely share the same output slot.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

5.4 Cursor layer

While editing text, it’s quite common for the cursor to be located outside of all currently visible text (e.g. just past the last character). It’s also more common to see cursor movements than actual text edits. For these reasons, we want to be able to handle the cursor independently from all text events, and be able to render the cursor without disturbing all existing text. Fortunately, this is quite easy with CSS2:

 
<div style="position:relative">
   <pre style="position:absolute; top:0px; left:0px; z-index:1" id="cursor"></pre>
   <pre style="position:absolute; top:0px; left:0px; z-index:2" id="text"></pre>
</div>

All the cursor drawing will be done inside the "cursor" block, and all the text drawing happens in the "text" block. Both blocks use absolute positioning relative to the parent div, so they are rendered on top of each other. Text will be drawn in front of the cursor because it has a greater z-index.

This works great for inputs containing only ASCII characters. For generic Unicode, it becomes a mess because it’s impossible to get the cursor to line up with the text. Try the following snippet:

 
<pre>
<span style="background-color:#f00">AA</span>
<span style="background-color:#0f0">&#xFF71;&#xFF71;</span>
<span style="background-color:#00f">&#x3042;</span>
</pre>

The first line contains two ASCII "A" characters, which are each half-width. The second line contains two half-width Katakana "A" characters, and the last line contains a single full-width Hiragana "A". All of these are inside a <pre>, so monospace font should be used. Per Unicode standards, all 3 lines should have widths that add up to exactly 1em of space, so all 3 lines should be equally wide. In practice, the first line is 16 pixels wide, the second line is 14 pixels, and the last line is 12 pixels. Same observation on both Chrome and Firefox, both Windows and Linux.

As it turns out, knowing exactly how wide non-ASCII characters are is a total mess, so we can’t use the naive cursor positioning code to draw the cursor block (plus, it probably wouldn’t be the right width anyways). This is why there is a slow cursor event, which basically renders the cursor by duplicating the foreground text in the cursor layer, but do it in different colors. This technique guarantees that the cursor will be at the right position with the right width.

Without any Unicode, using tabs alone would be enough to cause slow cursor events to be inserted. While we could make some effort to expand tabs to spaces and translate cursor positions accordingly, doing so loses information about the original cursor positions, so slow cursor events are used for tabs as well.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

5.5 String table

It’s likely the case that the same set of strings appear multiple times in the file. This is especially true for editing ASCII files, where there are only so many characters, and they are appended one at a time. To avoid redundant definitions of the same strings, Homura maintains a string table that keeps track of all strings, so that the event list references them by index instead having to embed the full string in each event.

To further reduce output size, strings that are prefixes of other strings are not stored separately. For example, given two strings "hogepiyo" and "hoge", only the "hogepiyo" string will be stored in the string table. This requires all string table references to include an extra "prefix length" field, but it’s a good tradeoff, and made encoding line truncations more efficient.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

5.6 Optimizations

Event list value encodings employs a few optimizations:

The primary goal is to reduce output size without obfuscating the output too much. The secondary goal is to decrease entropy so that output files compress better. And as we all know, reduced entropy helps the entire universe.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

5.7 Testing

Homura has been fairly well tested, using unit testing and fuzz testing techniques, passes valgrind, and has been tested using real world data.

record.vim introduces negligible overhead in terms of RAM and CPU, but takes up quite a bit of space. A full recording of the editing session for this manual (~2 hours of editing) takes up ~30MB of space, and full recording of my entry for ICFP 2011 (~1500 lines of C++ code, edited for ~8 hours) takes up ~330MB of space. Homura is able to process both in less than a minute, and produce HTML files less than ~4MB.

record.vim and Homura combined are ready for every day use. Homura is not meant to replace version control systems, but fills a gap that has very practical uses – you can now see versions of a file at very fine time granularities, all without having to explicitly commit changes to source control systems.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

5.8 Miscellaneous

I had intended to create just a simple script to capture some ASCII art editing sessions, the initial goal was just enough to produce animated GIFs or something. As usual, I have underestimated the scope of the project, and the end result was ~3K lines of C++. Oh well.

I also did not plan to spend more than a month working on this when I started, but I just got too busy with my full time job and couldn’t find enough free time. For real. The fact that I also re-watched Mahou Shoujo Madoka Magika about 3 times in the last month had nothing to do with it.

I am most proud of the fact that Homura works with UTF-8. I think most of the grief was spent on getting the cursor right with Unicode.

Homura is named after Akemi Homura, from "Mahou Shoujo Madoka Magika". So named for her ability to rewind time.


[Top] [Contents] [Index] [ ? ]

Table of Contents


[Top] [Contents] [Index] [ ? ]

About This Document

This document was generated by omoikane on February 25, 2012 using texi2html 1.82.

The buttons in the navigation panels have the following meaning:

Button Name Go to From 1.2.3 go to
[ < ] Back Previous section in reading order 1.2.2
[ > ] Forward Next section in reading order 1.2.4
[ << ] FastBack Beginning of this chapter or previous chapter 1
[ Up ] Up Up section 1.2
[ >> ] FastForward Next chapter 2
[Top] Top Cover (top) of document  
[Contents] Contents Table of contents  
[Index] Index Index  
[ ? ] About About (help)  

where the Example assumes that the current position is at Subsubsection One-Two-Three of a document of the following structure:


This document was generated by omoikane on February 25, 2012 using texi2html 1.82.