Ver Fonte

Added tracebd.py, a script for rendering block device operations

Based on a handful of local hacky variations, this sort of trace
rendering is surprisingly useful for getting an understanding of how
different filesystem operations interact with the underlying
block-device.

At some point it would probably be good to reimplement this in a
compiled language. Parsing and tracking the trace output quickly
becomes a bottleneck with the amount of trace output the tests
generate.

Note also that since tracebd.py run on trace output, it can also be
used to debug logged block-device operations post-run.
Christopher Haster há 3 anos atrás
pai
commit
91200e6678
7 ficheiros alterados com 878 adições e 80 exclusões
  1. 9 9
      bd/lfs_testbd.c
  2. 5 5
      bd/lfs_testbd.h
  3. 44 39
      runners/test_runner.c
  4. 1 1
      scripts/coverage.py
  5. 20 9
      scripts/tailpipe.py
  6. 22 17
      scripts/test.py
  7. 777 0
      scripts/tracebd.py

+ 9 - 9
bd/lfs_testbd.c

@@ -245,10 +245,10 @@ int lfs_testbd_read(const struct lfs_config *cfg, lfs_block_t block,
                 size);
     }
 
-    if (bd->cfg->read_delay) {
+    if (bd->cfg->read_sleep) {
         int err = nanosleep(&(struct timespec){
-                .tv_sec=bd->cfg->read_delay/1000000000,
-                .tv_nsec=bd->cfg->read_delay%1000000000},
+                .tv_sec=bd->cfg->read_sleep/1000000000,
+                .tv_nsec=bd->cfg->read_sleep%1000000000},
             NULL);
         if (err) {
             err = -errno;
@@ -325,10 +325,10 @@ int lfs_testbd_prog(const struct lfs_config *cfg, lfs_block_t block,
         }
     }
 
-    if (bd->cfg->prog_delay) {
+    if (bd->cfg->prog_sleep) {
         int err = nanosleep(&(struct timespec){
-                .tv_sec=bd->cfg->prog_delay/1000000000,
-                .tv_nsec=bd->cfg->prog_delay%1000000000},
+                .tv_sec=bd->cfg->prog_sleep/1000000000,
+                .tv_nsec=bd->cfg->prog_sleep%1000000000},
             NULL);
         if (err) {
             err = -errno;
@@ -408,10 +408,10 @@ int lfs_testbd_erase(const struct lfs_config *cfg, lfs_block_t block) {
         }
     }
 
-    if (bd->cfg->erase_delay) {
+    if (bd->cfg->erase_sleep) {
         int err = nanosleep(&(struct timespec){
-                .tv_sec=bd->cfg->erase_delay/1000000000,
-                .tv_nsec=bd->cfg->erase_delay%1000000000},
+                .tv_sec=bd->cfg->erase_sleep/1000000000,
+                .tv_nsec=bd->cfg->erase_sleep%1000000000},
             NULL);
         if (err) {
             err = -errno;

+ 5 - 5
bd/lfs_testbd.h

@@ -58,8 +58,8 @@ typedef uint32_t lfs_testbd_powercycles_t;
 typedef int32_t lfs_testbd_spowercycles_t;
 
 // Type for delays in nanoseconds
-typedef uint64_t lfs_testbd_delay_t;
-typedef int64_t lfs_testbd_sdelay_t;
+typedef uint64_t lfs_testbd_sleep_t;
+typedef int64_t lfs_testbd_ssleep_t;
 
 // testbd config, this is required for testing
 struct lfs_testbd_config {
@@ -100,15 +100,15 @@ struct lfs_testbd_config {
 
     // Artificial delay in nanoseconds, there is no purpose for this other
     // than slowing down the simulation.
-    lfs_testbd_delay_t read_delay;
+    lfs_testbd_sleep_t read_sleep;
 
     // Artificial delay in nanoseconds, there is no purpose for this other
     // than slowing down the simulation.
-    lfs_testbd_delay_t prog_delay;
+    lfs_testbd_sleep_t prog_sleep;
 
     // Artificial delay in nanoseconds, there is no purpose for this other
     // than slowing down the simulation.
-    lfs_testbd_delay_t erase_delay;
+    lfs_testbd_sleep_t erase_sleep;
 };
 
 // A reference counted block

+ 44 - 39
runners/test_runner.c

@@ -257,9 +257,9 @@ const char *test_disk_path = NULL;
 const char *test_trace_path = NULL;
 FILE *test_trace_file = NULL;
 uint32_t test_trace_cycles = 0;
-lfs_testbd_delay_t test_read_delay = 0.0;
-lfs_testbd_delay_t test_prog_delay = 0.0;
-lfs_testbd_delay_t test_erase_delay = 0.0;
+lfs_testbd_sleep_t test_read_sleep = 0.0;
+lfs_testbd_sleep_t test_prog_sleep = 0.0;
+lfs_testbd_sleep_t test_erase_sleep = 0.0;
 
 
 // trace printing
@@ -278,14 +278,19 @@ void test_trace(const char *fmt, ...) {
             int fd;
             if (strcmp(test_trace_path, "-") == 0) {
                 fd = dup(1);
+                if (fd < 0) {
+                    return;
+                }
             } else {
                 fd = open(
                         test_trace_path,
                         O_WRONLY | O_CREAT | O_APPEND | O_NONBLOCK,
                         0666);
-            }
-            if (fd < 0) {
-                return;
+                if (fd < 0) {
+                    return;
+                }
+                int err = fcntl(fd, F_SETFL, O_WRONLY | O_CREAT | O_APPEND);
+                assert(!err);
             }
 
             FILE *f = fdopen(fd, "a");
@@ -669,9 +674,9 @@ static void run_powerloss_none(
         .erase_cycles       = ERASE_CYCLES,
         .badblock_behavior  = BADBLOCK_BEHAVIOR,
         .disk_path          = test_disk_path,
-        .read_delay         = test_read_delay,
-        .prog_delay         = test_prog_delay,
-        .erase_delay        = test_erase_delay,
+        .read_sleep         = test_read_sleep,
+        .prog_sleep         = test_prog_sleep,
+        .erase_sleep        = test_erase_sleep,
     };
 
     int err = lfs_testbd_createcfg(&cfg, test_disk_path, &bdcfg);
@@ -735,9 +740,9 @@ static void run_powerloss_linear(
         .erase_cycles       = ERASE_CYCLES,
         .badblock_behavior  = BADBLOCK_BEHAVIOR,
         .disk_path          = test_disk_path,
-        .read_delay         = test_read_delay,
-        .prog_delay         = test_prog_delay,
-        .erase_delay        = test_erase_delay,
+        .read_sleep         = test_read_sleep,
+        .prog_sleep         = test_prog_sleep,
+        .erase_sleep        = test_erase_sleep,
         .power_cycles       = i,
         .powerloss_behavior = POWERLOSS_BEHAVIOR,
         .powerloss_cb       = powerloss_longjmp,
@@ -816,9 +821,9 @@ static void run_powerloss_exponential(
         .erase_cycles       = ERASE_CYCLES,
         .badblock_behavior  = BADBLOCK_BEHAVIOR,
         .disk_path          = test_disk_path,
-        .read_delay         = test_read_delay,
-        .prog_delay         = test_prog_delay,
-        .erase_delay        = test_erase_delay,
+        .read_sleep         = test_read_sleep,
+        .prog_sleep         = test_prog_sleep,
+        .erase_sleep        = test_erase_sleep,
         .power_cycles       = i,
         .powerloss_behavior = POWERLOSS_BEHAVIOR,
         .powerloss_cb       = powerloss_longjmp,
@@ -895,9 +900,9 @@ static void run_powerloss_cycles(
         .erase_cycles       = ERASE_CYCLES,
         .badblock_behavior  = BADBLOCK_BEHAVIOR,
         .disk_path          = test_disk_path,
-        .read_delay         = test_read_delay,
-        .prog_delay         = test_prog_delay,
-        .erase_delay        = test_erase_delay,
+        .read_sleep         = test_read_sleep,
+        .prog_sleep         = test_prog_sleep,
+        .erase_sleep        = test_erase_sleep,
         .power_cycles       = (i < cycle_count) ? cycles[i] : 0,
         .powerloss_behavior = POWERLOSS_BEHAVIOR,
         .powerloss_cb       = powerloss_longjmp,
@@ -1081,9 +1086,9 @@ static void run_powerloss_exhaustive(
         .erase_cycles       = ERASE_CYCLES,
         .badblock_behavior  = BADBLOCK_BEHAVIOR,
         .disk_path          = test_disk_path,
-        .read_delay         = test_read_delay,
-        .prog_delay         = test_prog_delay,
-        .erase_delay        = test_erase_delay,
+        .read_sleep         = test_read_sleep,
+        .prog_sleep         = test_prog_sleep,
+        .erase_sleep        = test_erase_sleep,
         .powerloss_behavior = POWERLOSS_BEHAVIOR,
         .powerloss_cb       = powerloss_exhaustive_branch,
         .powerloss_data     = NULL,
@@ -1256,9 +1261,9 @@ enum opt_flags {
     OPT_STOP             = 8,
     OPT_DISK             = 'd',
     OPT_TRACE            = 't',
-    OPT_READ_DELAY       = 9,
-    OPT_PROG_DELAY       = 10,
-    OPT_ERASE_DELAY      = 11,
+    OPT_READ_SLEEP       = 9,
+    OPT_PROG_SLEEP       = 10,
+    OPT_ERASE_SLEEP      = 11,
 };
 
 const char *short_opts = "hYlLD:G:p:nrVd:t:";
@@ -1281,9 +1286,9 @@ const struct option long_opts[] = {
     {"step",             required_argument, NULL, OPT_STEP},
     {"disk",             required_argument, NULL, OPT_DISK},
     {"trace",            required_argument, NULL, OPT_TRACE},
-    {"read-delay",       required_argument, NULL, OPT_READ_DELAY},
-    {"prog-delay",       required_argument, NULL, OPT_PROG_DELAY},
-    {"erase-delay",      required_argument, NULL, OPT_ERASE_DELAY},
+    {"read-sleep",       required_argument, NULL, OPT_READ_SLEEP},
+    {"prog-sleep",       required_argument, NULL, OPT_PROG_SLEEP},
+    {"erase-sleep",      required_argument, NULL, OPT_ERASE_SLEEP},
     {NULL, 0, NULL, 0},
 };
 
@@ -1626,34 +1631,34 @@ powerloss_next:
             case OPT_TRACE:
                 test_trace_path = optarg;
                 break;
-            case OPT_READ_DELAY: {
+            case OPT_READ_SLEEP: {
                 char *parsed = NULL;
-                double read_delay = strtod(optarg, &parsed);
+                double read_sleep = strtod(optarg, &parsed);
                 if (parsed == optarg) {
-                    fprintf(stderr, "error: invalid read-delay: %s\n", optarg);
+                    fprintf(stderr, "error: invalid read-sleep: %s\n", optarg);
                     exit(-1);
                 }
-                test_read_delay = read_delay*1.0e9;
+                test_read_sleep = read_sleep*1.0e9;
                 break;
             }
-            case OPT_PROG_DELAY: {
+            case OPT_PROG_SLEEP: {
                 char *parsed = NULL;
-                double prog_delay = strtod(optarg, &parsed);
+                double prog_sleep = strtod(optarg, &parsed);
                 if (parsed == optarg) {
-                    fprintf(stderr, "error: invalid prog-delay: %s\n", optarg);
+                    fprintf(stderr, "error: invalid prog-sleep: %s\n", optarg);
                     exit(-1);
                 }
-                test_prog_delay = prog_delay*1.0e9;
+                test_prog_sleep = prog_sleep*1.0e9;
                 break;
             }
-            case OPT_ERASE_DELAY: {
+            case OPT_ERASE_SLEEP: {
                 char *parsed = NULL;
-                double erase_delay = strtod(optarg, &parsed);
+                double erase_sleep = strtod(optarg, &parsed);
                 if (parsed == optarg) {
-                    fprintf(stderr, "error: invalid erase-delay: %s\n", optarg);
+                    fprintf(stderr, "error: invalid erase-sleep: %s\n", optarg);
                     exit(-1);
                 }
-                test_erase_delay = erase_delay*1.0e9;
+                test_erase_sleep = erase_sleep*1.0e9;
                 break;
             }
             // done parsing

+ 1 - 1
scripts/coverage.py

@@ -501,7 +501,7 @@ if __name__ == "__main__":
         help="Show uncovered branches.")
     parser.add_argument('-c', '--context', type=lambda x: int(x, 0), default=3,
         help="Show a additional lines of context. Defaults to 3.")
-    parser.add_argument('-w', '--width', type=lambda x: int(x, 0), default=80,
+    parser.add_argument('-W', '--width', type=lambda x: int(x, 0), default=80,
         help="Assume source is styled with this many columns. Defaults to 80.")
     parser.add_argument('--color',
         choices=['never', 'always', 'auto'], default='auto',

+ 20 - 9
scripts/tailpipe.py

@@ -1,4 +1,7 @@
 #!/usr/bin/env python3
+#
+# Efficiently displays the last n lines of a file/pipe.
+#
 
 import os
 import sys
@@ -15,7 +18,7 @@ def openio(path, mode='r'):
     else:
         return open(path, mode)
 
-def main(path, lines=1, keep_open=False):
+def main(path='-', *, lines=1, sleep=0.01, keep_open=False):
     ring = [None] * lines
     i = 0
     count = 0
@@ -29,7 +32,7 @@ def main(path, lines=1, keep_open=False):
         nonlocal count
         nonlocal done
         while True:
-            with openio(path, 'r') as f:
+            with openio(path) as f:
                 for line in f:
                     with lock:
                         ring[i] = line
@@ -45,7 +48,7 @@ def main(path, lines=1, keep_open=False):
     try:
         last_count = 1
         while not done:
-            time.sleep(0.01)
+            time.sleep(sleep)
             event.wait()
             event.clear()
 
@@ -62,10 +65,15 @@ def main(path, lines=1, keep_open=False):
 
             for j in range(count_):
                 # move cursor, clear line, disable/reenable line wrapping
-                sys.stdout.write('\r%s\x1b[K\x1b[?7l%s\x1b[?7h%s' % (
-                    '\x1b[%dA' % (count_-1-j) if count_-1-j > 0 else '',
-                    ring_[(i_-count+j) % lines][:-1],
-                    '\x1b[%dB' % (count_-1-j) if count_-1-j > 0 else ''))
+                sys.stdout.write('\r')
+                if count_-1-j > 0:
+                    sys.stdout.write('\x1b[%dA' % (count_-1-j))
+                sys.stdout.write('\x1b[K')
+                sys.stdout.write('\x1b[?7l')
+                sys.stdout.write(ring_[(i_-count_+j) % lines][:-1])
+                sys.stdout.write('\x1b[?7h')
+                if count_-1-j > 0:
+                    sys.stdout.write('\x1b[%dB' % (count_-1-j))
 
             sys.stdout.flush()
 
@@ -83,14 +91,17 @@ if __name__ == "__main__":
     parser.add_argument(
         'path',
         nargs='?',
-        default='-',
         help="Path to read from.")
     parser.add_argument(
         '-n',
         '--lines',
         type=lambda x: int(x, 0),
-        default=1,
         help="Number of lines to show, defaults to 1.")
+    parser.add_argument(
+        '-s',
+        '--sleep',
+        type=float,
+        help="Seconds to sleep between reads, defaults to 0.01.")
     parser.add_argument(
         '-k',
         '--keep-open',

+ 22 - 17
scripts/test.py

@@ -489,7 +489,8 @@ def find_cases(runner_, **args):
         stdout=sp.PIPE,
         stderr=sp.PIPE if not args.get('verbose') else None,
         universal_newlines=True,
-        errors='replace')
+        errors='replace',
+        close_fds=False)
     expected_suite_perms = co.defaultdict(lambda: 0)
     expected_case_perms = co.defaultdict(lambda: 0)
     expected_perms = 0
@@ -528,7 +529,8 @@ def find_paths(runner_, **args):
         stdout=sp.PIPE,
         stderr=sp.PIPE if not args.get('verbose') else None,
         universal_newlines=True,
-        errors='replace')
+        errors='replace',
+        close_fds=False)
     paths = co.OrderedDict()
     pattern = re.compile(
         '^(?P<id>(?P<case>(?P<suite>[^#]+)#[^\s#]+)[^\s]*)\s+'
@@ -555,7 +557,8 @@ def find_defines(runner_, **args):
         stdout=sp.PIPE,
         stderr=sp.PIPE if not args.get('verbose') else None,
         universal_newlines=True,
-        errors='replace')
+        errors='replace',
+        close_fds=False)
     defines = co.OrderedDict()
     pattern = re.compile(
         '^(?P<id>(?P<case>(?P<suite>[^#]+)#[^\s#]+)[^\s]*)\s+'
@@ -616,17 +619,17 @@ def run_stage(name, runner_, **args):
             cmd.append('--disk=%s' % args['disk'])
         if args.get('trace'):
             cmd.append('--trace=%s' % args['trace'])
-        if args.get('read_delay'):
-            cmd.append('--read-delay=%s' % args['read_delay'])
-        if args.get('prog_delay'):
-            cmd.append('--prog-delay=%s' % args['prog_delay'])
-        if args.get('erase_delay'):
-            cmd.append('--erase-delay=%s' % args['erase_delay'])
+        if args.get('read_sleep'):
+            cmd.append('--read-sleep=%s' % args['read_sleep'])
+        if args.get('prog_sleep'):
+            cmd.append('--prog-sleep=%s' % args['prog_sleep'])
+        if args.get('erase_sleep'):
+            cmd.append('--erase-sleep=%s' % args['erase_sleep'])
         if args.get('verbose'):
             print(' '.join(shlex.quote(c) for c in cmd))
 
         mpty, spty = pty.openpty()
-        proc = sp.Popen(cmd, stdout=spty, stderr=spty)
+        proc = sp.Popen(cmd, stdout=spty, stderr=spty, close_fds=False)
         os.close(spty)
         children.add(proc)
         mpty = os.fdopen(mpty, 'r', 1)
@@ -815,9 +818,6 @@ def run_stage(name, runner_, **args):
     
 
 def run(**args):
-    # measure runtime
-    start = time.time()
-
     # query runner for tests
     runner_ = runner(**args)
     print('using runner: %s'
@@ -839,6 +839,9 @@ def run(**args):
     if args.get('trace'):
         trace = openio(args['trace'], 'w', 1)
 
+    # measure runtime
+    start = time.time()
+
     # spawn runners
     expected = 0
     passed = 0
@@ -863,6 +866,8 @@ def run(**args):
         if (failures and not args.get('keep_going')) or killed:
             break
 
+    stop = time.time()
+
     if output:
         output.close()
     if trace:
@@ -878,7 +883,7 @@ def run(**args):
             '%d/%d passed' % (passed, expected),
             '%d/%d failed' % (len(failures), expected),
             '%dpls!' % powerlosses if powerlosses else None,
-            'in %.2fs' % (time.time()-start)]))))
+            'in %.2fs' % (stop-start)]))))
     print()
 
     # print each failure
@@ -1023,11 +1028,11 @@ if __name__ == "__main__":
         help="Direct trace output to this file.")
     test_parser.add_argument('-o', '--output',
         help="Direct stdout and stderr to this file.")
-    test_parser.add_argument('--read-delay',
+    test_parser.add_argument('--read-sleep',
         help="Artificial read delay in seconds.")
-    test_parser.add_argument('--prog-delay',
+    test_parser.add_argument('--prog-sleep',
         help="Artificial prog delay in seconds.")
-    test_parser.add_argument('--erase-delay',
+    test_parser.add_argument('--erase-sleep',
         help="Artificial erase delay in seconds.")
     test_parser.add_argument('--runner', default=[RUNNER_PATH],
         type=lambda x: x.split(),

+ 777 - 0
scripts/tracebd.py

@@ -0,0 +1,777 @@
+#!/usr/bin/env python3
+#
+# Display operations on block devices based on trace output
+#
+
+import collections as co
+import itertools as it
+import math as m
+import re
+import shutil
+import threading as th
+import time
+
+
+def openio(path, mode='r'):
+    if path == '-':
+        if 'r' in mode:
+            return os.fdopen(os.dup(sys.stdin.fileno()), 'r')
+        else:
+            return os.fdopen(os.dup(sys.stdout.fileno()), 'w')
+    else:
+        return open(path, mode)
+
+# space filling Hilbert-curve
+def hilbert_curve(width, height):
+    # memoize the last curve
+    if getattr(hilbert_curve, 'last', (None,))[0] == (width, height):
+        return hilbert_curve.last[1]
+
+    # based on generalized Hilbert curves:
+    # https://github.com/jakubcerveny/gilbert
+    #
+    def hilbert_(x, y, a_x, a_y, b_x, b_y):
+        w = abs(a_x+a_y)
+        h = abs(b_x+b_y)
+        a_dx = -1 if a_x < 0 else +1 if a_x > 0 else 0
+        a_dy = -1 if a_y < 0 else +1 if a_y > 0 else 0
+        b_dx = -1 if b_x < 0 else +1 if b_x > 0 else 0
+        b_dy = -1 if b_y < 0 else +1 if b_y > 0 else 0
+
+        # trivial row
+        if h == 1:
+            for _ in range(w):
+                yield (x,y)
+                x, y = x+a_dx, y+a_dy
+            return
+
+        # trivial column
+        if w == 1:
+            for _ in range(h):
+                yield (x,y)
+                x, y = x+b_dx, y+b_dy
+            return
+
+        a_x_, a_y_ = a_x//2, a_y//2
+        b_x_, b_y_ = b_x//2, b_y//2
+        w_ = abs(a_x_+a_y_)
+        h_ = abs(b_x_+b_y_)
+
+        if 2*w > 3*h:
+            # prefer even steps
+            if w_ % 2 != 0 and w > 2:
+                a_x_, a_y_ = a_x_+a_dx, a_y_+a_dy
+
+            # split in two
+            yield from hilbert_(x, y, a_x_, a_y_, b_x, b_y)
+            yield from hilbert_(x+a_x_, y+a_y_, a_x-a_x_, a_y-a_y_, b_x, b_y)
+        else:
+            # prefer even steps
+            if h_ % 2 != 0 and h > 2:
+                b_x_, b_y_ = b_x_+b_dx, b_y_+b_dy
+
+            # split in three
+            yield from hilbert_(x, y, b_x_, b_y_, a_x_, a_y_)
+            yield from hilbert_(x+b_x_, y+b_y_, a_x, a_y, b_x-b_x_, b_y-b_y_)
+            yield from hilbert_(
+                x+(a_x-a_dx)+(b_x_-b_dx), y+(a_y-a_dy)+(b_y_-b_dy),
+                -b_x_, -b_y_, -(a_x-a_x_), -(a_y-a_y_))
+
+    if width >= height:
+        curve = hilbert_(0, 0, +width, 0, 0, +height)
+    else:
+        curve = hilbert_(0, 0, 0, +height, +width, 0)
+
+    curve = list(curve)
+    hilbert_curve.last = ((width, height), curve)
+    return curve
+
+# space filling Z-curve/Lebesgue-curve
+def lebesgue_curve(width, height):
+    # memoize the last curve
+    if getattr(lebesgue_curve, 'last', (None,))[0] == (width, height):
+        return lebesgue_curve.last[1]
+
+    # we create a truncated Z-curve by simply filtering out the points
+    # that are outside our region
+    curve = []
+    for i in range(2**(2*m.ceil(m.log2(max(width, height))))):
+        # we just operate on binary strings here because it's easier
+        b = '{:0{}b}'.format(i, 2*m.ceil(m.log2(i+1)/2))
+        x = int(b[1::2], 2) if b[1::2] else 0
+        y = int(b[0::2], 2) if b[0::2] else 0
+        if x < width and y < height:
+            curve.append((x, y))
+
+    lebesgue_curve.last = ((width, height), curve)
+    return curve
+
+
+class Block:
+    def __init__(self, wear=0, readed=False, proged=False, erased=False):
+        self._ = ((wear << 3)
+            | (1 if readed else 0)
+            | (2 if proged else 0)
+            | (4 if erased else False))
+
+    @property
+    def wear(self):
+        return self._ >> 3
+
+    @property
+    def readed(self):
+        return (self._ & 1) != 0
+
+    @property
+    def proged(self):
+        return (self._ & 2) != 0
+
+    @property
+    def erased(self):
+        return (self._ & 4) != 0
+
+    def read(self):
+        self._ |= 1
+
+    def prog(self):
+        self._ |= 2
+
+    def erase(self):
+        self._ = (self._ | 4) + 8
+
+    def clear(self):
+        self._ &= ~7
+
+    def reset(self):
+        self._ = 0
+
+    def copy(self):
+        return Block(self.wear, self.readed, self.proged, self.erased)
+
+    def __add__(self, other):
+        return Block(
+            max(self.wear, other.wear), 
+            self.readed | other.readed,
+            self.proged | other.proged,
+            self.erased | other.erased)
+
+    def draw(self,
+            ascii=False,
+            chars=None,
+            wear_chars=None,
+            color='always',
+            read=True,
+            prog=True,
+            erase=True,
+            wear=False,
+            max_wear=None,
+            block_cycles=None):
+        if not chars: chars = '.rpe'
+        c = chars[0]
+        f = []
+
+        if wear:
+            if not wear_chars and ascii: wear_chars = '0123456789'
+            elif not wear_chars:         wear_chars = '.₁₂₃₄₅₆789'
+
+            if block_cycles:
+                w = self.wear / block_cycles
+            else:
+                w = self.wear / max(max_wear, len(wear_chars)-1)
+
+            c = wear_chars[min(
+                int(w*(len(wear_chars)-1)),
+                len(wear_chars)-1)]
+            if color == 'wear' or (
+                    color == 'always' and not read and not prog and not erase):
+                if w*9 >= 9:   f.append('\x1b[1;31m')
+                elif w*9 >= 7: f.append('\x1b[35m')
+
+        if erase and self.erased:  c = chars[3]
+        elif prog and self.proged: c = chars[2]
+        elif read and self.readed: c = chars[1]
+
+        if color == 'ops' or color == 'always':
+            if erase and self.erased:  f.append('\x1b[44m')
+            elif prog and self.proged: f.append('\x1b[45m')
+            elif read and self.readed: f.append('\x1b[42m')
+
+        if color in ['always', 'wear', 'ops'] and f:
+            return '%s%c\x1b[m' % (''.join(f), c)
+        else:
+            return c
+
+class Bd:
+    def __init__(self, *, blocks=None, size=1, count=1, width=80):
+        if blocks is not None:
+            self.blocks = blocks
+            self.size = size
+            self.count = count
+            self.width = width
+        else:
+            self.blocks = []
+            self.size = None
+            self.count = None
+            self.width = None
+            self.smoosh(size=size, count=count, width=width)
+
+    def get(self, block=slice(None), off=slice(None)):
+        if not isinstance(block, slice):
+            block = slice(block, block+1)
+        if not isinstance(off, slice):
+            off = slice(off, off+1)
+
+        if (not self.blocks
+                or not self.width
+                or not self.size
+                or not self.count):
+            return
+
+        if self.count >= self.width:
+            scale = (self.count+self.width-1) // self.width
+            for i in range(
+                    (block.start if block.start is not None else 0)//scale,
+                    (min(block.stop if block.stop is not None else self.count,
+                        self.count)+scale-1)//scale):
+                yield self.blocks[i]
+        else:
+            scale = self.width // self.count
+            for i in range(
+                    block.start if block.start is not None else 0,
+                    min(block.stop if block.stop is not None else self.count,
+                        self.count)):
+                for j in range(
+                        ((off.start if off.start is not None else 0)
+                            *scale)//self.size,
+                        (min(off.stop if off.stop is not None else self.size,
+                            self.size)*scale+self.size-1)//self.size):
+                    yield self.blocks[i*scale+j]
+
+    def __getitem__(self, block=slice(None), off=slice(None)):
+        if isinstance(block, tuple):
+            block, off = block
+        if not isinstance(block, slice):
+            block = slice(block, block+1)
+        if not isinstance(off, slice):
+            off = slice(off, off+1)
+
+        # needs resize?
+        if ((block.stop is not None and block.stop > self.count)
+                or (off.stop is not None and off.stop > self.size)):
+            self.smoosh(
+                count=max(block.stop or self.count, self.count),
+                size=max(off.stop or self.size, self.size))
+
+        return self.get(block, off)
+
+    def smoosh(self, *, size=None, count=None, width=None):
+        size = size or self.size
+        count = count or self.count
+        width = width or self.width
+
+        if count >= width:
+            scale = (count+width-1) // width
+            self.blocks = [
+                sum(self.get(slice(i,i+scale)), start=Block())
+                for i in range(0, count, scale)]
+        else:
+            scale = width // count
+            self.blocks = [
+                sum(self.get(i, slice(j*(size//width),(j+1)*(size//width))),
+                    start=Block())
+                for i in range(0, count)
+                for j in range(scale)]
+
+        self.size = size
+        self.count = count
+        self.width = width
+
+    def read(self, block=slice(None), off=slice(None)):
+        for c in self[block, off]:
+            c.read()
+
+    def prog(self, block=slice(None), off=slice(None)):
+        for c in self[block, off]:
+            c.prog()
+
+    def erase(self, block=slice(None), off=slice(None)):
+        for c in self[block, off]:
+            c.erase()
+
+    def clear(self, block=slice(None), off=slice(None)):
+        for c in self[block, off]:
+            c.clear()
+
+    def reset(self, block=slice(None), off=slice(None)):
+        for c in self[block, off]:
+            c.reset()
+
+    def copy(self):
+        return Bd(
+            blocks=[b.copy() for b in self.blocks],
+            size=self.size, count=self.count, width=self.width)
+
+
+def main(path='-', *,
+        read=False,
+        prog=False,
+        erase=False,
+        wear=False,
+        reset=False,
+        ascii=False,
+        chars=None,
+        wear_chars=None,
+        color='auto',
+        block=None,
+        start=None,
+        stop=None,
+        start_off=None,
+        stop_off=None,
+        block_size=None,
+        block_count=None,
+        block_cycles=None,
+        width=None,
+        height=1,
+        scale=None,
+        lines=None,
+        coalesce=None,
+        sleep=None,
+        hilbert=False,
+        lebesgue=False,
+        keep_open=False):
+    if not read and not prog and not erase and not wear:
+        read = True
+        prog = True
+        erase = True
+    if color == 'auto':
+        color = 'always' if sys.stdout.isatty() else 'never'
+
+    start = (start if start is not None
+        else block if block is not None
+        else 0)
+    stop = (stop if stop is not None
+        else block+1 if block is not None
+        else block_count if block_count is not None
+        else None)
+    start_off = (start_off if start_off is not None
+        else 0)
+    stop_off = (stop_off if stop_off is not None
+        else block_size if block_size is not None
+        else None)
+
+    bd = Bd(
+        size=(block_size if block_size is not None
+            else stop_off-start_off if stop_off is not None
+            else 1),
+        count=(block_count if block_count is not None
+            else stop-start if stop is not None
+            else 1),
+        width=(width or 80)*height)
+    lock = th.Lock()
+    event = th.Event()
+    done = False
+
+    # adjust width?
+    def resmoosh():
+        if width is None:
+            w = shutil.get_terminal_size((80, 0))[0] * height
+        elif width == 0:
+            w = max(int(bd.count*(scale or 1)), 1)
+        else:
+            w = width * height
+
+        if scale and int(bd.count*scale) > w:
+            c = int(w/scale)
+        elif scale and int(bd.count*scale) < w:
+            w = max(int(bd.count*(scale or 1)), 1)
+            c = bd.count
+        else:
+            c = bd.count
+
+        if w != bd.width or c != bd.count:
+            bd.smoosh(width=w, count=c)
+    resmoosh()
+
+    # parse a line of trace output
+    pattern = re.compile(
+        'trace.*?bd_(?:'
+            '(?P<create>create\w*)\('
+                '(?:'
+                    'block_size=(?P<block_size>\w+)'
+                    '|' 'block_count=(?P<block_count>\w+)'
+                    '|' '.*?' ')*' '\)'
+            '|' '(?P<read>read)\('
+                '\s*(?P<read_ctx>\w+)\s*' ','
+                '\s*(?P<read_block>\w+)\s*' ','
+                '\s*(?P<read_off>\w+)\s*' ','
+                '\s*(?P<read_buffer>\w+)\s*' ','
+                '\s*(?P<read_size>\w+)\s*' '\)'
+            '|' '(?P<prog>prog)\('
+                '\s*(?P<prog_ctx>\w+)\s*' ','
+                '\s*(?P<prog_block>\w+)\s*' ','
+                '\s*(?P<prog_off>\w+)\s*' ','
+                '\s*(?P<prog_buffer>\w+)\s*' ','
+                '\s*(?P<prog_size>\w+)\s*' '\)'
+            '|' '(?P<erase>erase)\('
+                '\s*(?P<erase_ctx>\w+)\s*' ','
+                '\s*(?P<erase_block>\w+)\s*' '\)'
+            '|' '(?P<sync>sync)\('
+                '\s*(?P<sync_ctx>\w+)\s*' '\)' ')')
+    def parse_line(line):
+        # string searching is actually much faster than
+        # the regex here
+        if 'trace' not in line or 'bd' not in line:
+            return False
+        m = pattern.search(line)
+        if not m:
+            return False
+
+        if m.group('create'):
+            # update our block size/count
+            size = int(m.group('block_size'), 0)
+            count = int(m.group('block_count'), 0)
+
+            if stop_off is not None:
+                size = stop_off-start_off
+            if stop is not None:
+                count = stop-start
+
+            with lock:
+                if reset:
+                    bd.reset()
+                    
+                # ignore the new values is stop/stop_off is explicit
+                bd.smoosh(
+                    size=(size if stop_off is None
+                        else stop_off-start_off),
+                    count=(count if stop is None
+                        else stop-start))
+            return True
+
+        elif m.group('read') and read:
+            block = int(m.group('read_block'), 0)
+            off = int(m.group('read_off'), 0)
+            size = int(m.group('read_size'), 0)
+
+            if stop is not None and block >= stop:
+                return False
+            block -= start
+            if stop_off is not None:
+                if off >= stop_off:
+                    return False
+                size = min(size, stop_off-off)
+            off -= start_off
+
+            with lock:
+                bd.read(block, slice(off,off+size))
+            return True
+
+        elif m.group('prog') and prog:
+            block = int(m.group('prog_block'), 0)
+            off = int(m.group('prog_off'), 0)
+            size = int(m.group('prog_size'), 0)
+
+            if stop is not None and block >= stop:
+                return False
+            block -= start
+            if stop_off is not None:
+                if off >= stop_off:
+                    return False
+                size = min(size, stop_off-off)
+            off -= start_off
+
+            with lock:
+                bd.prog(block, slice(off,off+size))
+            return True
+
+        elif m.group('erase') and (erase or wear):
+            block = int(m.group('erase_block'), 0)
+
+            if stop is not None and block >= stop:
+                return False
+            block -= start
+
+            with lock:
+                bd.erase(block)
+            return True
+
+        else:
+            return False
+
+
+    # print a pretty line of trace output
+    history = []
+    def push_line():
+        # create copy to avoid corrupt output
+        with lock:
+            resmoosh()
+            bd_ = bd.copy()
+            bd.clear()
+
+        max_wear = None
+        if wear:
+            max_wear = max(b.wear for b in bd_.blocks)
+
+        def draw(b):
+            return b.draw(
+                ascii=ascii,
+                chars=chars,
+                wear_chars=wear_chars,
+                color=color,
+                read=read,
+                prog=prog,
+                erase=erase,
+                wear=wear,
+                max_wear=max_wear,
+                block_cycles=block_cycles)
+
+        # fold via a curve?
+        if height > 1:
+            w = (len(bd.blocks)+height-1) // height
+            if hilbert:
+                grid = {}
+                for (x,y),b in zip(hilbert_curve(w, height), bd_.blocks):
+                    grid[(x,y)] = draw(b)
+                line = [
+                    ''.join(grid.get((x,y), ' ') for x in range(w))
+                    for y in range(height)]
+            elif lebesgue:
+                grid = {}
+                for (x,y),b in zip(lebesgue_curve(w, height), bd_.blocks):
+                    grid[(x,y)] = draw(b)
+                line = [
+                    ''.join(grid.get((x,y), ' ') for x in range(w))
+                    for y in range(height)]
+            else:
+                line = [
+                    ''.join(draw(b) for b in bd_.blocks[y*w:y*w+w])
+                    for y in range(height)]
+        else:
+            line = [''.join(draw(b) for b in bd_.blocks)]
+
+        if not lines:
+            # just go ahead and print here
+            for row in line:
+                sys.stdout.write(row)
+                sys.stdout.write('\n')
+            sys.stdout.flush()
+        else:
+            history.append(line)
+            del history[:-lines]
+
+    last_rows = 1
+    def print_line():
+        nonlocal last_rows
+        if not lines:
+            return 
+
+        # give ourself a canvas
+        while last_rows < len(history)*height:
+            sys.stdout.write('\n')
+            last_rows += 1
+
+        for i, row in enumerate(it.chain.from_iterable(history)):
+            jump = len(history)*height-1-i
+            # move cursor, clear line, disable/reenable line wrapping
+            sys.stdout.write('\r')
+            if jump > 0:
+                sys.stdout.write('\x1b[%dA' % jump)
+            sys.stdout.write('\x1b[K')
+            sys.stdout.write('\x1b[?7l')
+            sys.stdout.write(row)
+            sys.stdout.write('\x1b[?7h')
+            if jump > 0:
+                sys.stdout.write('\x1b[%dB' % jump)
+
+
+    if sleep is None or (coalesce and not lines):
+        # read/parse coalesce number of operations
+        try:
+            while True:
+                with openio(path) as f:
+                    changes = 0
+                    for line in f:
+                        change = parse_line(line)
+                        changes += change
+                        if change and changes % (coalesce or 1) == 0:
+                            push_line()
+                            print_line()
+                            # sleep between coalesced lines?
+                            if sleep is not None:
+                                time.sleep(sleep)
+                if not keep_open:
+                    break
+        except KeyboardInterrupt:
+            pass
+    else:
+        # read/parse in a background thread
+        def parse():
+            nonlocal done
+            while True:
+                with openio(path) as f:
+                    changes = 0
+                    for line in f:
+                        change = parse_line(line)
+                        changes += change
+                        if change and changes % (coalesce or 1) == 0:
+                            if coalesce:
+                                push_line()
+                            event.set()
+                if not keep_open:
+                    break
+            done = True
+
+        th.Thread(target=parse, daemon=True).start()
+
+        try:
+            while not done:
+                time.sleep(sleep)
+                event.wait()
+                event.clear()
+                if not coalesce:
+                    push_line()
+                print_line()
+        except KeyboardInterrupt:
+            pass
+
+    if lines:
+        sys.stdout.write('\n')
+
+
+if __name__ == "__main__":
+    import sys
+    import argparse
+    parser = argparse.ArgumentParser(
+        description="Display operations on block devices based on "
+            "trace output.")
+    parser.add_argument(
+        'path',
+        nargs='?',
+        help="Path to read from.")
+    parser.add_argument(
+        '-r',
+        '--read',
+        action='store_true',
+        help="Render reads.")
+    parser.add_argument(
+        '-p',
+        '--prog',
+        action='store_true',
+        help="Render progs.")
+    parser.add_argument(
+        '-e',
+        '--erase',
+        action='store_true',
+        help="Render erases.")
+    parser.add_argument(
+        '-w',
+        '--wear',
+        action='store_true',
+        help="Render wear.")
+    parser.add_argument(
+        '-R',
+        '--reset',
+        action='store_true',
+        help="Reset wear on block device initialization.")
+    parser.add_argument(
+        '-A',
+        '--ascii',
+        action='store_true',
+        help="Don't use unicode characters.")
+    parser.add_argument(
+        '--chars',
+        help="Characters to use for noop, read, prog, erase operations.")
+    parser.add_argument(
+        '--wear-chars',
+        help="Characters to use to show wear.")
+    parser.add_argument(
+        '--color',
+        choices=['never', 'always', 'auto', 'ops', 'wear'],
+        help="When to use terminal colors, defaults to auto.")
+    parser.add_argument(
+        '-b',
+        '--block',
+        type=lambda x: int(x, 0),
+        help="Show a specific block.")
+    parser.add_argument(
+        '--start',
+        type=lambda x: int(x, 0),
+        help="Start at this block.")
+    parser.add_argument(
+        '--stop',
+        type=lambda x: int(x, 0),
+        help="Stop before this block.")
+    parser.add_argument(
+        '--start-off',
+        type=lambda x: int(x, 0),
+        help="Start at this offset.")
+    parser.add_argument(
+        '--stop-off',
+        type=lambda x: int(x, 0),
+        help="Stop before this offset.")
+    parser.add_argument(
+        '-B',
+        '--block-size',
+        type=lambda x: int(x, 0),
+        help="Assume a specific block size.")
+    parser.add_argument(
+        '--block-count',
+        type=lambda x: int(x, 0),
+        help="Assume a specific block count.")
+    parser.add_argument(
+        '-C',
+        '--block-cycles',
+        type=lambda x: int(x, 0),
+        help="Assumed maximum number of erase cycles when measuring wear.")
+    parser.add_argument(
+        '-W',
+        '--width',
+        type=lambda x: int(x, 0),
+        help="Width in columns. A width of 0 indicates no limit. Defaults "
+            "to terminal width or 80.")
+    parser.add_argument(
+        '-H',
+        '--height',
+        type=lambda x: int(x, 0),
+        help="Height in rows. Defaults to 1.")
+    parser.add_argument(
+        '-x',
+        '--scale',
+        type=float,
+        help="Number of characters per block, ignores --width if set.")
+    parser.add_argument(
+        '-n',
+        '--lines',
+        type=lambda x: int(x, 0),
+        help="Number of lines to show, with 0 indicating no limit. "
+            "Defaults to 0.")
+    parser.add_argument(
+        '-c',
+        '--coalesce',
+        type=lambda x: int(x, 0),
+        help="Number of operations to coalesce together. Defaults to 1.")
+    parser.add_argument(
+        '-s',
+        '--sleep',
+        type=float,
+        help="Time in seconds to sleep between reads, while coalescing "
+            "operations.")
+    parser.add_argument(
+        '-I',
+        '--hilbert',
+        action='store_true',
+        help="Render as a space-filling Hilbert curve.")
+    parser.add_argument(
+        '-Z',
+        '--lebesgue',
+        action='store_true',
+        help="Render as a space-filling Z-curve.")
+    parser.add_argument(
+        '-k',
+        '--keep-open',
+        action='store_true',
+        help="Reopen the pipe on EOF, useful when multiple "
+            "processes are writing.")
+    sys.exit(main(**{k: v
+        for k, v in vars(parser.parse_args()).items()
+        if v is not None}))