diff --git a/bd/lfs_testbd.c b/bd/lfs_testbd.c index 5a7e6b6b..61063af8 100644 --- a/bd/lfs_testbd.c +++ b/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; diff --git a/bd/lfs_testbd.h b/bd/lfs_testbd.h index 4d26a4be..b0c9d005 100644 --- a/bd/lfs_testbd.h +++ b/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 diff --git a/runners/test_runner.c b/runners/test_runner.c index 4ce22ab6..eea4b5f3 100644 --- a/runners/test_runner.c +++ b/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 diff --git a/scripts/coverage.py b/scripts/coverage.py index 91445c2b..d30b3ffd 100755 --- a/scripts/coverage.py +++ b/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', diff --git a/scripts/tailpipe.py b/scripts/tailpipe.py index 985f571c..101fd98c 100755 --- a/scripts/tailpipe.py +++ b/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', diff --git a/scripts/test.py b/scripts/test.py index a29423bb..c7c6b0f7 100755 --- a/scripts/test.py +++ b/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(?P(?P[^#]+)#[^\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(?P(?P[^#]+)#[^\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(), diff --git a/scripts/tracebd.py b/scripts/tracebd.py new file mode 100755 index 00000000..3638fef9 --- /dev/null +++ b/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_(?:' + '(?Pcreate\w*)\(' + '(?:' + 'block_size=(?P\w+)' + '|' 'block_count=(?P\w+)' + '|' '.*?' ')*' '\)' + '|' '(?Pread)\(' + '\s*(?P\w+)\s*' ',' + '\s*(?P\w+)\s*' ',' + '\s*(?P\w+)\s*' ',' + '\s*(?P\w+)\s*' ',' + '\s*(?P\w+)\s*' '\)' + '|' '(?Pprog)\(' + '\s*(?P\w+)\s*' ',' + '\s*(?P\w+)\s*' ',' + '\s*(?P\w+)\s*' ',' + '\s*(?P\w+)\s*' ',' + '\s*(?P\w+)\s*' '\)' + '|' '(?Perase)\(' + '\s*(?P\w+)\s*' ',' + '\s*(?P\w+)\s*' '\)' + '|' '(?Psync)\(' + '\s*(?P\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}))