about summary refs log tree commit diff stats
path: root/browse_trace
diff options
context:
space:
mode:
Diffstat (limited to 'browse_trace')
-rw-r--r--browse_trace/Readme.md78
-rwxr-xr-xbrowse_trace/browse_trace7
-rw-r--r--browse_trace/browse_trace.cc546
-rwxr-xr-xbrowse_trace/build83
-rwxr-xr-xbrowse_trace/clean6
5 files changed, 720 insertions, 0 deletions
diff --git a/browse_trace/Readme.md b/browse_trace/Readme.md
new file mode 100644
index 00000000..ccfbdca7
--- /dev/null
+++ b/browse_trace/Readme.md
@@ -0,0 +1,78 @@
+### A debugging helper that lets you zoom in/out on a trace.
+
+To try it out, first create an example trace (from the top-level `mu/`
+directory):
+
+  ```shell
+  ./mu --trace nqueens.mu
+  ```
+
+This command will save a trace of its execution in a file called `last_run`.
+The trace consists of a series of lines, each starting with an integer depth
+and a single-word 'label', followed by a colon and whitespace.
+
+Now browse this trace:
+
+  ```shell
+  ./browse_trace/browse_trace last_run
+  ```
+
+You should now find yourself in a UI showing a subsequence of lines from the
+trace, each line starting with a numeric depth, and ending with a parenthetical
+count of trace lines hidden after it with greater depths.
+
+For example, this line:
+
+  ```
+  2 app: line1 (30)
+  ```
+
+indicates that it was logged with depth 2, and that 30 following lines have
+been hidden at a depth greater than 2.
+
+(As an experiment, hidden counts of 1000 or more are highlighted in red.)
+
+The UI provides the following hotkeys:
+
+* `q` or `ctrl-c`: Quit.
+
+* `Enter`: 'Zoom into' this line. Expand lines hidden after it that were at
+  the next higher level.
+
+* `Backspace`: 'Zoom out' on a line after zooming in, collapsing lines below
+  expanded by some series of `Enter` commands.
+
+* `j` or `down-arrow`: Move/scroll cursor down one line.
+* `k` or `up-arrow`: Move/scroll cursor up one line.
+* `J` or `ctrl-f` or `page-down`: Scroll cursor down one page.
+* `K` or `ctrl-b` or `page-up`: Scroll cursor up one page.
+* `h` or `left-arrow`: Scroll cursor left one character.
+* `l` or `right-arrow`: Scroll cursor right one character.
+* `H`: Scroll cursor left one screen-width.
+* `L`: Scroll cursor right one screen-width.
+
+* `g` or `home`: Move cursor to start of trace.
+* `G` or `end`: Move cursor to end of trace.
+
+* `t`: Move cursor to top line on screen.
+* `c`: Move cursor to center line on screen.
+* `b`: Move cursor to bottom line on screen.
+* `T`: Scroll line at cursor to top of screen.
+
+* `/`: Search forward for a pattern.
+* '?': Search backward for a pattern.
+* `n`: Repeat the previous '/' or '?'.
+* `N`: Repeat the previous '/' or '?' in the opposite direction.
+
+After hitting `/`, the mini-editor on the bottom-most line supports the
+following hotkeys:
+* ascii characters: add the key to the pattern.
+* `Enter`: search for the pattern.
+* `Esc` or `ctrl-c`: cancel the current search, setting the screen back
+  to its state before the search.
+* `left-arrow`: move cursor left.
+* `right-arrow`: move cursor right.
+* `ctrl-a` or `home`: move cursor to start of search pattern.
+* `ctrl-e` or `end`: move cursor to end of search pattern.
+* `ctrl-u`: clear search pattern before cursor
+* `ctrl-k`: clear search pattern at and after cursor
diff --git a/browse_trace/browse_trace b/browse_trace/browse_trace
new file mode 100755
index 00000000..f80ea659
--- /dev/null
+++ b/browse_trace/browse_trace
@@ -0,0 +1,7 @@
+#!/bin/sh
+set -e
+
+cd `dirname $0`
+./build
+cd -
+`dirname $0`/browse_trace_bin $*
diff --git a/browse_trace/browse_trace.cc b/browse_trace/browse_trace.cc
new file mode 100644
index 00000000..cc03f5ce
--- /dev/null
+++ b/browse_trace/browse_trace.cc
@@ -0,0 +1,546 @@
+// Warning: zero automated tests
+
+// Includes
+#include "../termbox/termbox.h"
+
+#include <stdlib.h>
+#define SIZE(X) (assert((X).size() < (1LL<<(sizeof(int)*8-2))), static_cast<int>((X).size()))
+
+#include <assert.h>
+#include <iostream>
+using std::istream;
+using std::ostream;
+using std::iostream;
+using std::cin;
+using std::cout;
+using std::cerr;
+#include <iomanip>
+#include <string.h>
+#include <string>
+using std::string;
+#include <vector>
+using std::vector;
+#include <set>
+using std::set;
+#include <sstream>
+using std::ostringstream;
+#include <fstream>
+using std::ifstream;
+using std::ofstream;
+#include <map>
+using std::map;
+#include <utility>
+using std::pair;
+// End Includes
+
+// Types
+struct trace_line {
+  string contents;
+  string label;
+  int depth;  // 0 is 'sea level'; positive integers are progressively 'deeper' and lower level
+  trace_line(string c, string l, int d) {
+    contents = c;
+    label = l;
+    depth = d;
+  }
+};
+
+struct trace_stream {
+  vector<trace_line> past_lines;
+};
+
+enum search_direction { FORWARD, BACKWARD };
+// End Types
+
+// Function prototypes are auto-generated in the 'build*' scripts; define your
+// functions in any order. Just be sure to declare each function header all on
+// one line, ending with the '{'. Our auto-generation scripts are too minimal
+// and simple-minded to handle anything else.
+#include "function_list"  // by convention, files ending with '_list' are auto-generated
+
+// from http://stackoverflow.com/questions/152643/idiomatic-c-for-reading-from-a-const-map
+template<typename T> typename T::mapped_type& get(T& map, typename T::key_type const& key) {
+  typename T::iterator iter(map.find(key));
+  assert(iter != map.end());
+  return iter->second;
+}
+template<typename T> typename T::mapped_type const& get(const T& map, typename T::key_type const& key) {
+  typename T::const_iterator iter(map.find(key));
+  assert(iter != map.end());
+  return iter->second;
+}
+template<typename T> typename T::mapped_type const& put(T& map, typename T::key_type const& key, typename T::mapped_type const& value) {
+  // map[key] requires mapped_type to have a zero-arg (default) constructor
+  map.insert(std::make_pair(key, value)).first->second = value;
+  return value;
+}
+template<typename T> bool contains_key(T& map, typename T::key_type const& key) {
+  return map.find(key) != map.end();
+}
+template<typename T> typename T::mapped_type& get_or_insert(T& map, typename T::key_type const& key) {
+  return map[key];
+}
+
+// Globals
+trace_stream* Trace_stream = NULL;
+
+ofstream Trace_file;
+int Display_row = 0;
+int Display_column = 0;
+set<int> Visible;
+int Top_of_screen = 0;
+int Left_of_screen = 0;
+int Last_printed_row = 0;
+map<int, int> Trace_index;  // screen row -> trace index
+
+string Current_search_pattern = "";
+search_direction Current_search_direction = FORWARD;
+// End Globals
+
+bool has_data(istream& in) {
+  return in && !in.eof();
+}
+
+int main(int argc, char* argv[]) {
+  if (argc != 2) {
+    cerr << "Usage: browse_trace <trace file>\n";
+    return 1;
+  }
+  load_trace(argv[1]);
+  if (!Trace_stream) return 1;
+  cerr << "computing min depth to display\n";
+  int min_depth = 9999;
+  for (int i = 0;  i < SIZE(Trace_stream->past_lines);  ++i) {
+    trace_line& curr_line = Trace_stream->past_lines.at(i);
+    if (curr_line.depth < min_depth) min_depth = curr_line.depth;
+  }
+  cerr << "min depth is " << min_depth << '\n';
+  cerr << "computing lines to display\n";
+  for (int i = 0;  i < SIZE(Trace_stream->past_lines);  ++i) {
+    if (Trace_stream->past_lines.at(i).depth == min_depth)
+      Visible.insert(i);
+  }
+  tb_init();
+  tb_clear();
+  Display_row = Display_column = 0;
+  Top_of_screen = 0;
+  refresh_screen_rows();
+  while (true) {
+    render();
+    int key = read_key();
+    if (key == 'q' || key == 'Q' || key == TB_KEY_CTRL_C) break;
+    if (key == 'j' || key == TB_KEY_ARROW_DOWN) {
+      // move cursor one line down
+      if (Display_row < Last_printed_row) ++Display_row;
+    }
+    else if (key == 'k' || key == TB_KEY_ARROW_UP) {
+      // move cursor one line up
+      if (Display_row > 0) --Display_row;
+    }
+    else if (key == 't') {
+      // move cursor to top of screen
+      Display_row = 0;
+    }
+    else if (key == 'c') {
+      // move cursor to center of screen
+      Display_row = tb_height()/2;
+      while (!contains_key(Trace_index, Display_row))
+        --Display_row;
+    }
+    else if (key == 'b') {
+      // move cursor to bottom of screen
+      Display_row = tb_height()-1;
+      while (!contains_key(Trace_index, Display_row))
+        --Display_row;
+    }
+    else if (key == 'T') {
+      // scroll line at cursor to top of screen
+      Top_of_screen = get(Trace_index, Display_row);
+      Display_row = 0;
+      refresh_screen_rows();
+    }
+    else if (key == 'h' || key == TB_KEY_ARROW_LEFT) {
+      // pan screen one character left
+      if (Left_of_screen > 0) --Left_of_screen;
+    }
+    else if (key == 'l' || key == TB_KEY_ARROW_RIGHT) {
+      // pan screen one character right
+      ++Left_of_screen;
+    }
+    else if (key == 'H') {
+      // pan screen one screen-width left
+      Left_of_screen -= (tb_width() - 5);
+      if (Left_of_screen < 0) Left_of_screen = 0;
+    }
+    else if (key == 'L') {
+      // pan screen one screen-width right
+      Left_of_screen += (tb_width() - 5);
+    }
+    else if (key == 'J' || key == TB_KEY_PGDN || key == TB_KEY_CTRL_F) {
+      // page-down
+      if (Trace_index.find(tb_height()-1) != Trace_index.end()) {
+        Top_of_screen = get(Trace_index, tb_height()-1) + 1;
+        refresh_screen_rows();
+      }
+    }
+    else if (key == 'K' || key == TB_KEY_PGUP || key == TB_KEY_CTRL_B) {
+      // page-up is more convoluted
+      for (int screen_row = tb_height();  screen_row > 0 && Top_of_screen > 0;  --screen_row) {
+        --Top_of_screen;
+        if (Top_of_screen <= 0) break;
+        while (Top_of_screen > 0 && !contains_key(Visible, Top_of_screen))
+          --Top_of_screen;
+      }
+      if (Top_of_screen >= 0)
+        refresh_screen_rows();
+    }
+    else if (key == 'g' || key == TB_KEY_HOME) {
+        Top_of_screen = 0;
+        Last_printed_row = 0;
+        Display_row = 0;
+        refresh_screen_rows();
+    }
+    else if (key == 'G' || key == TB_KEY_END) {
+      // go to bottom of screen; largely like page-up, interestingly
+      Top_of_screen = SIZE(Trace_stream->past_lines)-1;
+      for (int screen_row = tb_height();  screen_row > 0 && Top_of_screen > 0;  --screen_row) {
+        --Top_of_screen;
+        if (Top_of_screen <= 0) break;
+        while (Top_of_screen > 0 && !contains_key(Visible, Top_of_screen))
+          --Top_of_screen;
+      }
+      refresh_screen_rows();
+      // move cursor to bottom
+      Display_row = Last_printed_row;
+      refresh_screen_rows();
+    }
+    else if (key == TB_KEY_CARRIAGE_RETURN) {
+      // expand lines under current by one level
+      assert(contains_key(Trace_index, Display_row));
+      int start_index = get(Trace_index, Display_row);
+      int index = 0;
+      // simultaneously compute end_index and min_depth
+      int min_depth = 9999;
+      for (index = start_index+1;  index < SIZE(Trace_stream->past_lines);  ++index) {
+        if (contains_key(Visible, index)) break;
+        trace_line& curr_line = Trace_stream->past_lines.at(index);
+        assert(curr_line.depth > Trace_stream->past_lines.at(start_index).depth);
+        if (curr_line.depth < min_depth) min_depth = curr_line.depth;
+      }
+      int end_index = index;
+      // mark as visible all intervening indices at min_depth
+      for (index = start_index;  index < end_index;  ++index) {
+        trace_line& curr_line = Trace_stream->past_lines.at(index);
+        if (curr_line.depth == min_depth) {
+          Visible.insert(index);
+        }
+      }
+      refresh_screen_rows();
+    }
+    else if (key == TB_KEY_BACKSPACE || key == TB_KEY_BACKSPACE2) {
+      // collapse all lines under current
+      assert(contains_key(Trace_index, Display_row));
+      int start_index = get(Trace_index, Display_row);
+      int index = 0;
+      // end_index is the next line at a depth same as or lower than start_index
+      int initial_depth = Trace_stream->past_lines.at(start_index).depth;
+      for (index = start_index+1;  index < SIZE(Trace_stream->past_lines);  ++index) {
+        if (!contains_key(Visible, index)) continue;
+        trace_line& curr_line = Trace_stream->past_lines.at(index);
+        if (curr_line.depth <= initial_depth) break;
+      }
+      int end_index = index;
+      // mark as visible all intervening indices at min_depth
+      for (index = start_index+1;  index < end_index;  ++index) {
+        Visible.erase(index);
+      }
+      refresh_screen_rows();
+    }
+    else if (key == '/') {
+      if (start_search_editor(FORWARD))
+        search(Current_search_pattern, Current_search_direction);
+    }
+    else if (key == '?') {
+      if (start_search_editor(BACKWARD))
+        search(Current_search_pattern, Current_search_direction);
+    }
+    else if (key == 'n') {
+      if (!Current_search_pattern.empty())
+        search(Current_search_pattern, Current_search_direction);
+    }
+    else if (key == 'N') {
+      if (!Current_search_pattern.empty())
+        search(Current_search_pattern, opposite(Current_search_direction));
+    }
+  }
+  tb_shutdown();
+  return 0;
+}
+
+bool start_search_editor(search_direction dir) {
+  const int bottom_screen_line = tb_height()-1;
+  // run a little editor just in the last line of the screen
+  clear_line(bottom_screen_line);
+  int col = 0;  // screen column of cursor on bottom line. also used to update pattern.
+  tb_set_cursor(col, bottom_screen_line);
+  tb_print('/', TB_WHITE, TB_BLACK);
+  ++col;
+  string pattern;
+  while (true) {
+    int key = read_key();
+    if (key == TB_KEY_ENTER) {
+      if (!pattern.empty()) {
+        Current_search_pattern = pattern;
+        Current_search_direction = dir;
+      }
+      return true;
+    }
+    else if (key == TB_KEY_ESC || key == TB_KEY_CTRL_C) {
+      return false;
+    }
+    else if (key == TB_KEY_ARROW_LEFT) {
+      if (col > /*slash*/1) {
+        --col;
+        tb_set_cursor(col, bottom_screen_line);
+      }
+    }
+    else if (key == TB_KEY_ARROW_RIGHT) {
+      if (col-/*slash*/1 < SIZE(pattern)) {
+        ++col;
+        tb_set_cursor(col, bottom_screen_line);
+      }
+    }
+    else if (key == TB_KEY_HOME || key == TB_KEY_CTRL_A) {
+      col = /*skip slash*/1;
+      tb_set_cursor(col, bottom_screen_line);
+    }
+    else if (key == TB_KEY_END || key == TB_KEY_CTRL_E) {
+      col = SIZE(pattern)+/*skip slash*/1;
+      tb_set_cursor(col, bottom_screen_line);
+    }
+    else if (key == TB_KEY_BACKSPACE || key == TB_KEY_BACKSPACE2) {
+      if (col > /*slash*/1) {
+        assert(col <= SIZE(pattern)+1);
+        --col;
+        // update pattern
+        pattern.erase(col-/*slash*/1, /*len*/1);
+        // update screen
+        tb_set_cursor(col, bottom_screen_line);
+        for (int x = col;  x < SIZE(pattern)+/*skip slash*/1;  ++x)
+          tb_print(pattern.at(x-/*slash*/1), TB_WHITE, TB_BLACK);
+        tb_print(' ', TB_WHITE, TB_BLACK);
+        tb_set_cursor(col, bottom_screen_line);
+      }
+    }
+    else if (key == TB_KEY_CTRL_K) {
+      int old_pattern_size = SIZE(pattern);
+      pattern.erase(col-/*slash*/1, SIZE(pattern) - (col-/*slash*/1));
+      tb_set_cursor(col, bottom_screen_line);
+      for (int x = col;  x < old_pattern_size+/*slash*/1;  ++x)
+        tb_print(' ', TB_WHITE, TB_BLACK);
+      tb_set_cursor(col, bottom_screen_line);
+    }
+    else if (key == TB_KEY_CTRL_U) {
+      int old_pattern_size = SIZE(pattern);
+      pattern.erase(0, col-/*slash*/1);
+      col = /*skip slash*/1;
+      tb_set_cursor(col, bottom_screen_line);
+      for (int x = /*slash*/1;  x < SIZE(pattern)+/*skip slash*/1;  ++x)
+        tb_print(pattern.at(x-/*slash*/1), TB_WHITE, TB_BLACK);
+      for (int x = SIZE(pattern)+/*slash*/1;  x < old_pattern_size+/*skip slash*/1;  ++x)
+        tb_print(' ', TB_WHITE, TB_BLACK);
+      tb_set_cursor(col, bottom_screen_line);
+    }
+    else if (key < 128) {  // ascii only
+      // update pattern
+      char c = static_cast<char>(key);
+      assert(col-1 >= 0);
+      assert(col-1 <= SIZE(pattern));
+      pattern.insert(col-/*slash*/1, /*num*/1, c);
+      // update screen
+      for (int x = col;  x < SIZE(pattern)+/*skip slash*/1;  ++x)
+        tb_print(pattern.at(x-/*slash*/1), TB_WHITE, TB_BLACK);
+      ++col;
+      tb_set_cursor(col, bottom_screen_line);
+    }
+  }
+}
+
+void search(const string& pat, search_direction dir) {
+  if (dir == FORWARD) search_next(pat);
+  else search_previous(pat);
+}
+
+search_direction opposite(search_direction dir) {
+  if (dir == FORWARD) return BACKWARD;
+  else return FORWARD;
+}
+
+void search_next(const string& pat) {
+  for (int trace_index = get(Trace_index, Display_row)+1;  trace_index < SIZE(Trace_stream->past_lines);  ++trace_index) {
+    if (!contains_key(Visible, trace_index)) continue;
+    const trace_line& line = Trace_stream->past_lines.at(trace_index);
+    if (line.label.find(pat) == string::npos && line.contents.find(pat) == string::npos) continue;
+    Top_of_screen = trace_index;
+    Display_row = 0;
+    refresh_screen_rows();
+    return;
+  }
+}
+
+void search_previous(const string& pat) {
+  for (int trace_index = get(Trace_index, Display_row)-1;  trace_index >= 0;  --trace_index) {
+    if (!contains_key(Visible, trace_index)) continue;
+    const trace_line& line = Trace_stream->past_lines.at(trace_index);
+    if (line.label.find(pat) == string::npos && line.contents.find(pat) == string::npos) continue;
+    Top_of_screen = trace_index;
+    Display_row = 0;
+    refresh_screen_rows();
+    return;
+  }
+}
+
+void clear_line(int screen_row) {
+  tb_set_cursor(0, screen_row);
+  for (int col = 0;  col < tb_width();  ++col)
+    tb_print(' ', TB_WHITE, TB_BLACK);
+  tb_set_cursor(0, screen_row);
+}
+
+// update Trace_indices for each screen_row on the basis of Top_of_screen and Visible
+void refresh_screen_rows() {
+  int screen_row = 0, index = 0;
+  Trace_index.clear();
+  for (screen_row = 0, index = Top_of_screen;  screen_row < tb_height() && index < SIZE(Trace_stream->past_lines);  ++screen_row, ++index) {
+    // skip lines without depth for now
+    while (!contains_key(Visible, index)) {
+      ++index;
+      if (index >= SIZE(Trace_stream->past_lines)) goto done;
+    }
+    assert(index < SIZE(Trace_stream->past_lines));
+    put(Trace_index, screen_row, index);
+  }
+done:;
+}
+
+void render() {
+  int screen_row = 0;
+  for (screen_row = 0;  screen_row < tb_height();  ++screen_row) {
+    if (!contains_key(Trace_index, screen_row)) break;
+    trace_line& curr_line = Trace_stream->past_lines.at(get(Trace_index, screen_row));
+    ostringstream out;
+    out << std::setw(4) << curr_line.depth << ' ' << curr_line.label << ": " << curr_line.contents;
+    if (screen_row < tb_height()-1) {
+      int delta = lines_hidden(screen_row);
+      // home-brew escape sequence for red
+      if (delta > 1) {
+        if (delta > 999) out << static_cast<char>(1);
+        out << " (" << delta << ")";
+        if (delta > 999) out << static_cast<char>(2);
+      }
+    }
+    render_line(screen_row, out.str(), screen_row == Display_row);
+  }
+  // clear rest of screen
+  Last_printed_row = screen_row-1;
+  for (;  screen_row < tb_height();  ++screen_row)
+    render_line(screen_row, "~", /*cursor_line?*/false);
+  // move cursor back to display row at the end
+  tb_set_cursor(0, Display_row);
+}
+
+int lines_hidden(int screen_row) {
+  assert(contains_key(Trace_index, screen_row));
+  if (!contains_key(Trace_index, screen_row+1))
+    return SIZE(Trace_stream->past_lines) - get(Trace_index, screen_row);
+  else
+    return get(Trace_index, screen_row+1) - get(Trace_index, screen_row);
+}
+
+void render_line(int screen_row, const string& s, bool cursor_line) {
+  int col = 0;
+  int color = TB_WHITE;
+  int background_color = cursor_line ? /*subtle grey*/240 : TB_BLACK;
+  vector<pair<size_t, size_t> > highlight_ranges = find_all_occurrences(s, Current_search_pattern);
+  tb_set_cursor(0, screen_row);
+  for (col = 0;  col < tb_width() && col+Left_of_screen < SIZE(s);  ++col) {
+    char c = s.at(col+Left_of_screen);  // todo: unicode
+    if (c == '\n') c = ';';  // replace newlines with semi-colons
+    // escapes. hack: can't start a line with them.
+    if (c == '\1') { color = /*red*/1;  c = ' '; }
+    if (c == '\2') { color = TB_WHITE;  c = ' '; }
+    if (in_range(highlight_ranges, col+Left_of_screen))
+      tb_print(c, TB_BLACK, /*yellow*/11);
+    else
+      tb_print(c, color, background_color);
+  }
+  for (;  col < tb_width();  ++col)
+    tb_print(' ', TB_WHITE, background_color);
+}
+
+vector<pair<size_t, size_t> > find_all_occurrences(const string& s, const string& pat) {
+  vector<pair<size_t, size_t> > result;
+  if (pat.empty()) return result;
+  size_t idx = 0;
+  while (true) {
+    size_t next_idx = s.find(pat, idx);
+    if (next_idx == string::npos) break;
+    result.push_back(pair<size_t, size_t>(next_idx, next_idx+SIZE(pat)));
+    idx = next_idx+SIZE(pat);
+  }
+  return result;
+}
+
+bool in_range(const vector<pair<size_t, size_t> >& highlight_ranges, size_t idx) {
+  for (int i = 0;  i < SIZE(highlight_ranges);  ++i) {
+    if (idx >= highlight_ranges.at(i).first && idx < highlight_ranges.at(i).second)
+      return true;
+    if (idx < highlight_ranges.at(i).second) break;
+  }
+  return false;
+}
+
+void load_trace(const char* filename) {
+  ifstream tin(filename);
+  if (!tin) {
+    cerr << "no such file: " << filename << '\n';
+    exit(1);
+  }
+  Trace_stream = new trace_stream;
+  while (has_data(tin)) {
+    tin >> std::noskipws;
+      skip_whitespace_but_not_newline(tin);
+      if (!isdigit(tin.peek())) {
+        string dummy;
+        getline(tin, dummy);
+        continue;
+      }
+    tin >> std::skipws;
+    int depth;
+    tin >> depth;
+    string label;
+    tin >> label;
+    if (*--label.end() == ':') label.erase(--label.end());
+    string line;
+    getline(tin, line);
+    Trace_stream->past_lines.push_back(trace_line(line, label, depth));
+  }
+  cerr << "lines read: " << Trace_stream->past_lines.size() << '\n';
+}
+
+int read_key() {
+  tb_event event;
+  do {
+    tb_poll_event(&event);
+  } while (event.type != TB_EVENT_KEY);
+  return event.key ? event.key : event.ch;
+}
+
+void skip_whitespace_but_not_newline(istream& in) {
+  while (true) {
+    if (!has_data(in)) break;
+    else if (in.peek() == '\n') break;
+    else if (isspace(in.peek())) in.get();
+    else break;
+  }
+}
diff --git a/browse_trace/build b/browse_trace/build
new file mode 100755
index 00000000..6047cc2b
--- /dev/null
+++ b/browse_trace/build
@@ -0,0 +1,83 @@
+#!/bin/sh
+# For details on the basic form of this script, see https://notabug.org/akkartik/basic-build.
+
+set -e  # stop immediately on error
+
+# Some environment variables that can be passed in. For example, to turn off
+# optimization:
+#   $ CFLAGS=-g ./build2
+test "$CXX" || export CXX=c++
+test "$CC" || export CC=cc
+test "$CFLAGS" || export CFLAGS="-g -O2"
+export CFLAGS="$CFLAGS -Wall -Wextra -ftrapv -fno-strict-aliasing"
+
+# there's two mechanisms for fast builds here:
+# - if a command is quick to run, always run it but update the result only on any change
+# - otherwise run it only if the output is 'older_than' the inputs
+#
+# avoid combining both mechanisms for a single file
+# otherwise you'll see spurious messages about files being updated
+# risk: a file may unnecessarily update without changes, causing unnecessary work downstream
+
+# return 1 if $1 is older than _any_ of the remaining args
+older_than() {
+  local target=$1
+  shift
+  if [ ! -e $target ]
+  then
+#?     echo "$target doesn't exist"
+    echo "updating $target" >&2
+    return 0  # success
+  fi
+  local f
+  for f in $*
+  do
+    if [ $f -nt $target ]
+    then
+      echo "updating $target" >&2
+      return 0  # success
+    fi
+  done
+  return 1  # failure
+}
+
+# redirect to $1, unless it's already identical
+update() {
+  if [ ! -e $1 ]
+  then
+    cat > $1
+  else
+    cat > $1.tmp
+    diff -q $1 $1.tmp >/dev/null  &&  rm $1.tmp  ||  mv $1.tmp $1
+  fi
+}
+
+noisy_cd() {
+  cd $1
+  echo "-- `pwd`" >&2
+}
+
+older_than browse_trace_bin browse_trace.cc *_list ../termbox/*  &&  {
+  # missing some deps here
+  noisy_cd ../termbox
+    older_than ../termbox/libtermbox.a ../termbox/*.c  &&  {
+      older_than utf8.o utf8.c && {
+        $CC $CFLAGS -c utf8.c
+      }
+      older_than termbox.o termbox.c termbox.h input.inl output.inl bytebuffer.inl && {
+        $CC $CFLAGS -c termbox.c
+      }
+      older_than libtermbox.a *.o && {
+        ar rcs libtermbox.a *.o
+      }
+    }
+  noisy_cd ../browse_trace
+
+  grep -h "^[^[:space:]#].*) {$" browse_trace.cc  |grep -v ":.*("  |sed 's/ {.*/;/'  |update function_list
+  # auto-generate list of tests to run
+  grep -h "^\s*void test_" browse_trace.cc  |sed 's/^\s*void \(.*\)() {.*/\1,/'  |update test_list
+  grep -h "^\s*void test_" browse_trace.cc  |sed 's/^\s*void \(.*\)() {.*/"\1",/'  |update test_name_list
+  $CXX $CFLAGS browse_trace.cc ../termbox/libtermbox.a -o browse_trace_bin
+}
+
+exit 0
diff --git a/browse_trace/clean b/browse_trace/clean
new file mode 100755
index 00000000..69b30478
--- /dev/null
+++ b/browse_trace/clean
@@ -0,0 +1,6 @@
+#!/bin/sh
+set -e
+
+set -v
+rm -rf browse_trace_bin* *_list
+rm -rf termbox/*.o termbox/libtermbox.a