+#! /usr/bin/python3
+
+import bisect as BS
+import curses as CU
+import optparse as OP
+import os as OS
+import random as RND
+import resource as RSC
+import subprocess as SUB
+import sys as SYS
+
+op = OP.OptionParser\
+ (usage = "%prog [-t] [-d FILE] [-j JOBS] [-n SAMPLES] [-r REF] VAR ...")
+op.disable_interspersed_args()
+for short, long, kw in \
+ [("-d", "--dictionary",
+ dict(type = "string", metavar = "DICT",
+ dest = "dict", default = "/usr/share/dict/words",
+ help = "word list to process")),
+ ("-j", "--jobs",
+ dict(type = "int", metavar = "JOBS",
+ dest = "njobs", default = 1,
+ help = "number of jobs to run concurrently")),
+ ("-n", "--samples",
+ dict(type = "int", metavar = "SAMPLES",
+ dest = "nsamples", default = 5,
+ help = "number of samples for each variant")),
+ ("-r", "--reference",
+ dict(type = "string", metavar = "REF",
+ dest = "ref", default = None,
+ help = "reference output data expected")),
+ ("-t", "--terminal",
+ dict(action = "store_true", dest = "terminal", default = None,
+ help = "show progress display"))]:
+ op.add_option(short, long, **kw)
+OPTS, ARGS = op.parse_args()
+
+REF = OPTS.ref
+if REF is None:
+ REF = "REF.%s" % OS.path.basename(OPTS.dict)
+ if not OS.path.exists(REF): REF = None
+
+IDLE = 0
+LIVE = 1
+DONE = 2
+
+class BasicVariant (object):
+
+ def __init__(me, name, row):
+ me.name = name
+ me.samples = []
+ me.row = row
+ me._prog = "./chain.%s" % name
+ me.state = IDLE
+
+ @property
+ def n(me):
+ return len(me.samples)
+
+ def start(me, out):
+ SYS.stdin.close()
+ fd = OS.open(OPTS.dict, OS.O_RDONLY)
+ if fd != 0: OS.dup2(fd, 0); OS.close(fd)
+ SYS.stdout.close()
+ fd = OS.open(out, OS.O_WRONLY | OS.O_CREAT | OS.O_TRUNC, 0o666)
+ if fd != 1: OS.dup2(fd, 1); OS.close(fd)
+ OS.execv(me._prog, [me._prog])
+
+ def end(me, time):
+ me.samples.append(time)
+ return time
+
+class LispVariant (BasicVariant):
+ def __init__(me, name, *args, **kw):
+ super(LispVariant, me).__init__(name, *args, **kw)
+ me._time = "time.%s" % name
+ me._lisp = name[5:]
+ me._prog = "chain.lisp"
+
+ def start(me, out):
+ SYS.stdout.close()
+ fd = OS.open(out, OS.O_WRONLY | OS.O_CREAT | OS.O_TRUNC, 0o666)
+ if fd != 1: OS.dup2(fd, 1); OS.close(fd)
+ OS.execvp("runlisp", ["runlisp", "-L%s" % me._lisp,
+ me._prog, "-T%s" % me._time, OPTS.dict])
+
+ def end(me, time):
+ #with open(me._time) as f: time = 1000*float(f.read())
+ OS.unlink(me._time)
+ me.samples.append(time)
+ return time
+
+def strwait(st):
+ if OS.WIFEXITED(st): return "rc = %d" % OS.WEXITSTATUS(st)
+ elif OS.WIFSIGNALED(st): return "signal = %d" % OS.WTERMSIG(st)
+ else: return "?st = %d" % st
+
+class State (object):
+
+ def __init__(me):
+ me.vars = []
+ me.idle = []
+ me.done = []
+ me.live = {}
+ me.bad = False
+
+ me._nextrow = 0
+ me._dpy = None
+
+ def _setdpy(me, dpy):
+ me._dpy = dpy
+
+ def addvar(me, var):
+ if arg.startswith("lisp-"): var = LispVariant(arg, me._nextrow)
+ else: var = BasicVariant(arg, me._nextrow)
+ me.vars.append(var); me.idle.append(var); me._nextrow += 1
+
+ def cycle(me):
+ if me.idle and not me.bad and len(me.live) < OPTS.njobs:
+ n = len(me.idle)
+ k = RND.randrange(n); pick = me.idle[k]; assert pick.state == IDLE
+ me.idle[k] = me.idle[n - 1]; me.idle.pop()
+ kid = OS.fork()
+ if not kid: pick.start("chain.%s.out" % pick.name)
+ #me._dpy.msg(";; start %s\n" % pick.name)
+ me.live[kid] = pick; pick.state = LIVE
+ me._dpy.paint_var(pick)
+ return True
+ elif me.live:
+ kid, st, ru = OS.wait3(0)
+ try: var = me.live.pop(kid)
+ except KeyError:
+ me._dpy.msg("ignoring unexpected child %d (%s)" %
+ (kid, strwait(st)))
+ return True
+ assert var.state == LIVE
+ if not (OS.WIFEXITED(st) and OS.WEXITSTATUS(st) == 0):
+ me._dpy.msg("chain.%s failed (%s)" % (var.name, strwait(st)))
+ me.bad = True
+ return True
+ out = "chain.%s.out" % var.name
+ if REF is not None:
+ if SUB.call(["./chkref", out, REF]):
+ me._dpy.msg("chain.%s produced incorrect output" % var.name)
+ me.bad = True
+ return True
+ OS.unlink(out)
+ time = var.end(1000*(ru.ru_utime + ru.ru_stime))
+ #me._dpy.msg(";; end %s, time = %.3f s" % (var.name, time))
+ if var.n < OPTS.nsamples:
+ me.idle.append(var); var.state = IDLE
+ else:
+ me.done.append(var); var.state = DONE
+ #me._dpy.msg(";; done %s\n" % var.name)
+ me._dpy.paint_var(var)
+ return True
+ else:
+ return False
+
+if OPTS.terminal or SYS.stdout.isatty():
+
+ class Display (object):
+
+ def __init__(me, state):
+ me._st = state
+ me._st._setdpy(me)
+ namewd = 16
+ for var in me._st.vars:
+ if len(var.name) >= namewd: namewd = len(var.name) + 1
+ me._a_idle = CU.A_NORMAL
+ me._a_live = CU.A_BOLD
+ me._namewd = namewd
+ me._msgs = []
+
+ def __enter__(me):
+
+ if not SYS.stdout.isatty():
+ ttyfd = OS.open("/dev/tty", OS.O_RDWR)
+ SYS.stdout = OS.fdopen(OS.dup(1), "w")
+ OS.dup2(ttyfd, 1)
+ OS.close(ttyfd)
+
+ me._scr = CU.initscr()
+ me._resize()
+ CU.start_color()
+ CU.use_default_colors()
+ if CU.COLORS and CU.COLOR_PAIRS >= 3:
+ CU.init_pair(1, CU.COLOR_GREEN, -1)
+ me._a_done = CU.color_pair(1)
+ CU.init_pair(2, CU.COLOR_BLACK, CU.COLOR_GREEN)
+ CU.init_pair(3, CU.COLOR_BLACK, CU.COLOR_YELLOW)
+ me._a_bar_left = CU.color_pair(2)
+ me._a_bar_right = CU.color_pair(3)
+ else:
+ me._a_live = CU.A_DIM
+ me._a_bar_left = CU.A_REVERSE
+ me._a_bar_right = None
+ me._msgrow = min(len(me._st.vars) + 1, me._ht - 1)
+ me._nmsgs = me._ht - me._msgrow
+ me._scr.setscrreg(me._msgrow, me._ht - 1)
+ me._scr.scrollok(True)
+ me._repaint()
+
+ def __exit__(me, exval, exty, tb):
+ CU.endwin()
+ for m in me._msgs: SYS.stderr.write(m + "\n")
+
+ def _resize(me):
+ me._ht, me._wd = me._scr.getmaxyx()
+ barwd = me._wd - me._namewd
+ if OPTS.nsamples <= barwd: me._barsc = 1
+ else: me._barsc = float(barwd)/OPTS.nsamples
+
+ def paint_var(me, var, refresh = True):
+ if var.row >= me._ht - 2: return
+ rbar = int(me._barsc*OPTS.nsamples + 0.5)
+ if var.state == LIVE: attr = me._a_live
+ elif var.state == DONE: attr = me._a_done
+ else: attr = me._a_idle
+ me._scr.addstr(var.row, 0, var.name, attr)
+ mbar = int(me._barsc*len(var.samples))
+ me._scr.attrset(me._a_bar_left)
+ me._scr.hline(var.row, me._namewd, " ", mbar)
+ if me._a_bar_right is not None:
+ me._scr.attrset(me._a_bar_right)
+ me._scr.hline(var.row, me._namewd + mbar, " ", rbar - mbar)
+ me._scr.move(var.row, me._namewd + rbar)
+ else:
+ me._scr.move(var.row, me._namewd + mbar)
+ me._scr.attrset(0)
+ if var.samples:
+ avg = "%.3f ms" % (sum(var.samples)/len(var.samples))
+ if me._wd - (me._namewd + rbar) > len(avg):
+ me._scr.addstr(var.row, me._namewd + rbar + 1, avg)
+ me._scr.clrtoeol()
+ if refresh: me._scr.refresh()
+
+ def msg(me, msg):
+ n = len(me._msgs)
+ if n >= me._nmsgs:
+ me._scr.scroll(1)
+ me._scr.addstr(min(me._msgrow + n, me._ht - 1), 0, msg)
+ me._msgs.append(msg)
+ me._scr.refresh()
+
+ def _repaint(me):
+ me._scr.erase()
+ for var in me._st.vars: me.paint_var(var, refresh = False)
+ n = len(me._msgs); y = me._msgrow
+ for i in range(max(0, n - me._nmsgs), n):
+ me._scr.addstr(y, 0, me._msg[i]); y += 1
+ me._scr.refresh()
+
+else:
+
+ class Display (object):
+
+ def __init__(me, state):
+ me._st = state
+ me._st._setdpy(me)
+
+ def __enter__(me):
+ pass
+ def __exit__(me, exval, exty, tb):
+ pass
+
+ def paint_var(me, var):
+ if var.state == IDLE:
+ SYS.stderr.write(";; end %s, time = %.3f ms\n" %
+ (var.name, var.samples[-1]))
+ elif var.state == LIVE:
+ SYS.stderr.write(";; start %s\n" % var.name)
+ elif var.state == DONE:
+ SYS.stderr.write(";; %s all done\n" % var.name)
+
+ def msg(me, msg):
+ SYS.stderr.write("%s\n" % msg)
+
+RND.seed()
+STATE = State()
+for arg in ARGS: STATE.addvar(arg)
+with Display(STATE):
+ while STATE.cycle(): pass
+if STATE.bad: SYS.exit(2)
+
+def quartile(i, x):
+ n = len(x)
+ k = i*n
+ j = k//4
+ if not k%4: return x[j]
+ else: return (x[j] + x[j + 1])/2
+
+def stats(x):
+ q1, mid, q3 = quartile(1, x), quartile(2, x), quartile(3, x)
+ iqr = q3 - q1
+ locut = BS.bisect_left(x, q1 - 1.5*iqr); lo = x[locut]
+ hicut = BS.bisect_right(x, q3 + 1.5*iqr); hi = x[hicut - 1]
+ avg = sum(x)/len(x)
+ return lo, q1, mid, avg, q3, hi
+
+done = STATE.vars
+done.sort(key = lambda var: var.name)
+n = OPTS.nsamples - 1
+for var in done:
+ x = [xi for xi in var.samples]; x.sort()
+ lo, q1, mid, avg, q3, hi = stats(x)
+ print("%-16s %s; %s" %
+ (var.name,
+ " ".join("%.3f" % xi for xi in var.samples),
+ " ".join("%.3f" % xi for xi in [lo, q1, mid, avg, q3, hi])))