#!/usr/bin/env python3
"""Measure Home loading in the running Kodi (docs/plan/skin-optimization).

The numbers do not depend on how the rows get their cards: wcs/skin/perflog.py counts, per section, the rows whose
list has at least one card, and this script reads its lines from kodi.log. Every repetition starts with a fresh Kodi.

  avail    restart Kodi and touch nothing: per section the time from Kodi's start until its first row has cards and
           until all its rows have; the CPU time Kodi used by then
  jump     restart Kodi, and as soon as the first screen has cards go to the last section of the menu: time from the
           key until that section has cards (the worst case of "I go there before it is loaded")
  tour     with everything loaded walk the menu: time from the key until the section's rows are there
  change   on Filmy mark the movie rows changed: time until the rows on screen and all the others are rebuilt

  measure.py run LABEL [--reps N] [--all-sections] [--slow]     writes scripts/skin_harness/out/perf/LABEL.json
  measure.py compare BEFORE AFTER                               prints both side by side (medians)
  measure.py context LABEL [--reps N] [--slow]                  context menu: key -> menu on screen, for three kinds of
                                                                cards, and a menu action -> done; LABEL.json
  measure.py context-compare BEFORE AFTER

--all-sections switches every Home section on for the run (and restores the switches afterwards).
--slow starts Kodi with the lowest CPU priority (macOS `taskpolicy -b`: efficiency cores only), a stand-in for a weak box.
"""
import argparse
import json
import os
import re
import statistics
import subprocess
import sys
import time
from datetime import datetime

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, '..', 'dialog_harness'))
import kodi_live

OUT = os.path.join(HERE, 'out', 'perf')
PERF = '[DEBUG][Milionář Perf] '
KODI_BINARY = '/Applications/Kodi.app/Contents/MacOS/Kodi'
WATCH_FLAG = os.path.join(kodi_live.KODI_HOME, 'userdata', 'addon_data', 'plugin.video.milionar', 'perf_watch')
STAMP = re.compile(r'^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\.\d{3})')
CARDS = re.compile(r'^cards (\w+) (\d+)/(\d+)')
# menu order of Home.xml: (menu id, section, the skin setting that switches it off)
MENU = [('mil_foryou', 'foryou', 'HomeMenuNoMilForYouButton'), ('mil_continue', 'cont', 'HomeMenuNoMilContinueButton'),
        ('mil_mymovies', 'my_movies', 'HomeMenuNoMilMyMoviesButton'), ('mil_myseries', 'my_series', 'HomeMenuNoMilMySeriesButton'),
        ('mil_mytv', 'mytv', 'HomeMenuNoMilMyTVButton'), ('mil_vinyl', 'vinyl', 'HomeMenuNoMilVinylButton'),
        ('mil_mycinema', 'mycinema', 'HomeMenuNoMilMyCinemaButton'), ('mil_game', 'game', 'HomeMenuNoMilGameButton'),
        ('mil_ai', 'ai', 'HomeMenuNoMilAIButton'), ('mil_kraska', 'kraska', 'HomeMenuNoMilKraskaButton'),
        ('mil_tmdb', 'tmdb', 'HomeMenuNoMilTMDbButton')]
BY_MENU = {menu: section for menu, section, _flag in MENU}
# what watching a film marks changed (wcs/skin/refresh.media_rows('movie'))
MOVIE_CHANGE = ['cont_movies', 'my_movies_progress', 'my_movies_added', 'my_movies_unwatched', 'my_movies_watched',
                'my_movies_all', 'recent_played', 'search_my_movies', 'fy_continue', 'widget_game_topics',
                'widget_demo_movies']
QUIET = 8.0                                  # seconds without a perf line = the background loading is over
MIN_WAIT = 20.0


# --------------------------------------------------------------------------- Kodi

def stamp(line):
    found = STAMP.match(line)
    return datetime.strptime(found.group(1), '%Y-%m-%d %H:%M:%S.%f').timestamp() if found else None


def log_lines():
    return kodi_live.read_log_from(0)


def perf(lines=None):
    return [(stamp(line), line.split(PERF, 1)[1]) for line in (log_lines() if lines is None else lines) if PERF in line]


def ops(*items, timeout=60.0):
    return kodi_live.request(list(items), timeout=timeout)['results']


def builtin(*commands):
    ops(*[{'op': 'builtin', 'cmd': command} for command in commands])


def info(labels=(), conds=()):
    result = ops({'op': 'info', 'labels': list(labels), 'conds': list(conds)})[-1]['result']
    return result['labels'], result['conds']


def stop_kodi():
    if not kodi_live.kodi_running():
        return
    try:                                      # the quit event itself can block for minutes when Kodi hangs in its shutdown
        subprocess.run(['osascript', '-e', 'quit app "Kodi"'], capture_output=True, timeout=15)
    except subprocess.TimeoutExpired:
        pass
    deadline = time.time() + 10
    while kodi_live.kodi_running() and time.time() < deadline:
        time.sleep(0.5)
    for signal in ('-TERM', '-KILL'):         # Kodi sometimes hangs in its shutdown
        if kodi_live.kodi_running():
            subprocess.run(['pkill', signal, '-x', 'Kodi'])
            time.sleep(3)


def start_kodi(slow=False):
    """A fresh Kodi; returns when the harness listener answers."""
    stop_kodi()
    heartbeat = os.path.join(kodi_live.HARNESS_DIR, 'heartbeat')
    if os.path.exists(heartbeat):
        os.remove(heartbeat)
    kodi_live.enable()
    if slow:
        subprocess.Popen(['taskpolicy', '-b', KODI_BINARY], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                         start_new_session=True)
    else:
        subprocess.run(['open', '-a', 'Kodi'], check=True)
    if not kodi_live.wait_listener(180.0):
        raise SystemExit('Kodi started but the harness listener did not answer')
    # a key typed elsewhere while Kodi came to the front may have opened a dialog; it would block the loading
    _labels, conds = info(conds=['System.HasActiveModalDialog'])
    if conds['System.HasActiveModalDialog']:
        builtin('Dialog.Close(all,true)')
        return False
    return True


def cpu_seconds():
    """CPU time the Kodi process has used so far."""
    out = subprocess.run(['ps', '-o', 'cputime=,rss=', '-p', subprocess.run(
        ['pgrep', '-x', 'Kodi'], capture_output=True, text=True).stdout.split()[0]], capture_output=True, text=True).stdout.split()
    parts = [float(part) for part in out[0].replace('-', ':').split(':')]
    seconds = 0.0
    for part in parts:
        seconds = seconds * 60 + part
    return round(seconds, 1), round(int(out[1]) / 1024)


def wait_quiet(quiet=QUIET, least=0.0, cap=300.0):
    """Until no perf line was written for `quiet` seconds (and at least `least` seconds passed)."""
    started = time.time()
    while time.time() - started < cap:
        lines = perf()
        last = lines[-1][0] if lines else started
        if time.time() - max(last, started) >= quiet and time.time() - started >= least:
            return
        time.sleep(0.5)


def wait_for(start, pattern, timeout=45.0):
    """The first perf line at index >= start matching `pattern`: (index after it, time, match) or (start, None, None)."""
    deadline = time.time() + timeout
    while time.time() < deadline:
        lines = perf()
        for index in range(start, len(lines)):
            found = re.search(pattern, lines[index][1])
            if found:
                return index + 1, lines[index][0], found
        time.sleep(0.1)
    return start, None, None


def visible_sections():
    """The sections of the menu, in its order (AI and Kraska also need their provider)."""
    provider = {section: 'String.IsEmpty(Window(Home).Property(milionar.provider.{}))'.format(section) for section in ('ai', 'kraska')}
    conds = info(conds=['Skin.HasSetting({})'.format(flag) for _menu, _section, flag in MENU] + list(provider.values()))[1]
    return [section for _menu, section, flag in MENU
            if not conds['Skin.HasSetting({})'.format(flag)] and not conds.get(provider.get(section), False)]


def focus_menu(index):
    # "absolute": without it the position is relative to the list's view and lands elsewhere once the menu scrolls
    builtin('SetFocus(9000)', 'Control.SetFocus(9000,{},absolute)'.format(index))


# --------------------------------------------------------------------------- reading the log

def cards_timeline(lines, since=0.0):
    """{section: [(time, rows with cards, rows)]} from the probe's lines."""
    result = {}
    for when, text in lines:
        found = CARDS.match(text)
        if found and when >= since:
            result.setdefault(found.group(1), []).append((when, int(found.group(2)), int(found.group(3))))
    return result


def section_times(timeline, origin):
    """Per section: seconds from `origin` to the first row with cards and to the last increase of their number."""
    result = {}
    for section, entries in timeline.items():
        first = next((when for when, count, _rows in entries if count > 0), None)
        best = max(count for _when, count, _rows in entries)
        full = next((when for when, count, _rows in entries if count == best), None)
        rows = entries[-1][2]
        if rows:
            result[section] = {'first_s': None if first is None else round(first - origin, 2),
                               'all_s': None if not best else round(full - origin, 2), 'rows_with_cards': best, 'rows': rows}
    return result


def builds(lines):
    """How many rows were produced, by kind of line (plugin calls / rows bound by the service)."""
    return {'plugin_calls': sum(1 for _when, text in lines if text.startswith('row ')),
            'bound': sum(1 for _when, text in lines if text.startswith('bound '))}


# --------------------------------------------------------------------------- scenarios

def avail(slow):
    clean = start_kodi(slow)
    wait_quiet(least=MIN_WAIT)
    raw = log_lines()
    lines = perf(raw)
    started = next(stamp(line) for line in raw if stamp(line))
    skin = next((stamp(line) for line in raw if 'skin loaded...' in line), None)
    seconds, megabytes = cpu_seconds()
    times = section_times(cards_timeline(lines), started)
    return {'clean_start': clean, 'skin_loaded_s': round(skin - started, 2) if skin else None, 'sections': times,
            'everything_s': max([entry['all_s'] for entry in times.values() if entry['all_s'] is not None] or [None]),
            'cpu_s': seconds, 'rss_mb': megabytes, **builds(lines)}


def tour(sections):
    """Everything is loaded: time from the key until the section's rows are counted."""
    result = {}
    for index, section in enumerate(sections):
        other = (index + 1) % len(sections)
        focus_menu(other)
        time.sleep(1.2)
        start = len(perf())
        focus_menu(index)
        after, focused_at, _found = wait_for(start, r'^focus {}$'.format(section), timeout=5.0)
        if not focused_at:
            result[section] = None
            continue
        time.sleep(1.5)
        timeline = cards_timeline(perf()[after - 1:], focused_at)
        entry = section_times(timeline, focused_at).get(section)
        # no change of the count after the focus = the rows were already there; None = the rows were dropped
        result[section] = 0.0 if not entry else entry['all_s']
    return result


def change(sections):
    if 'my_movies' not in sections:
        return {'note': 'Filmy is switched off'}
    focus_menu(sections.index('my_movies'))
    wait_quiet(quiet=4.0)
    start = len(perf())
    ops({'op': 'call', 'module': 'wcs.skin.refresh', 'func': 'changed', 'args': [MOVIE_CHANGE]})
    _after, event_at, _found = wait_for(start, r'^change event', timeout=10.0)
    if not event_at:
        return {'note': 'no change event'}
    wait_quiet(quiet=5.0)
    lines = perf()[start:]
    made = [(when, text) for when, text in lines if text.startswith(('row ', 'bound '))]
    on_screen = [when for when, text in made if text.split()[1].startswith('my_movies.')]
    ends = [when for when, text in lines if text.startswith('warm ') and ' end after ' in text]
    return {'screen_s': round(on_screen[-1] - event_at, 2) if on_screen else None,
            'everything_s': round(max([when for when, _text in made] + ends) - event_at, 2) if made or ends else None,
            **builds(lines)}


def jump(slow):
    clean = start_kodi(slow)
    sections = visible_sections()
    _after, first_at, _found = wait_for(0, r'^cards {} [1-9]'.format(sections[0]), timeout=180.0)
    start = len(perf())
    focus_menu(len(sections) - 1)
    after, focused_at, _found = wait_for(start, r'^focus {}$'.format(sections[-1]), timeout=10.0)
    if not focused_at:
        return {'clean_start': clean, 'note': 'the focus did not reach {}'.format(sections[-1])}
    wait_quiet(quiet=5.0, least=6.0)
    lines = perf()
    entry = section_times(cards_timeline(lines), focused_at).get(sections[-1], {})
    before = [count for when, count, _rows in cards_timeline(lines).get(sections[-1], []) if when <= focused_at]
    return {'clean_start': clean, 'section': sections[-1], 'already_loaded_rows': before[-1] if before else 0,
            'first_s': max(entry.get('first_s') or 0.0, 0.0), 'all_s': max(entry.get('all_s') or 0.0, 0.0),
            'rows_with_cards': entry.get('rows_with_cards'), 'rows': entry.get('rows')}


# --------------------------------------------------------------------------- run / compare

def set_switches(values):
    """{flag: hidden}"""
    builtin(*['Skin.SetBool({})'.format(flag) if hidden else 'Skin.Reset({})'.format(flag) for flag, hidden in values.items()])
    time.sleep(2.5)                         # Kodi writes the skin settings with a delay; the next start reads them


def run(args):
    os.makedirs(OUT, exist_ok=True)
    open(WATCH_FLAG, 'w').close()
    result = {'label': args.label, 'when': datetime.now().isoformat(timespec='seconds'), 'slow': args.slow,
              'all_sections': args.all_sections, 'reps': []}
    saved = None
    try:
        start_kodi(args.slow)               # warm-up start: compiles the Python files, not measured
        wait_quiet(least=MIN_WAIT)
        if args.all_sections:
            flags = [flag for _menu, _section, flag in MENU]
            conds = info(conds=['Skin.HasSetting({})'.format(flag) for flag in flags])[1]
            saved = {flag: conds['Skin.HasSetting({})'.format(flag)] for flag in flags}
            set_switches({flag: False for flag in flags})
        for number in range(args.reps):
            rep = {'avail': avail(args.slow)}
            sections = visible_sections()
            rep['visible'] = sections
            rep['tour'] = tour(sections)
            rep['change'] = change(sections)
            rep['jump'] = jump(args.slow)
            result['reps'].append(rep)
            print('rep {}: everything after {} s, cpu {} s, jump to {} -> {} s'.format(
                number + 1, rep['avail']['everything_s'], rep['avail']['cpu_s'], rep['jump'].get('section'),
                rep['jump'].get('all_s')), flush=True)
            with open(os.path.join(OUT, args.label + '.json'), 'w', encoding='utf-8') as handle:
                json.dump(result, handle, ensure_ascii=False, indent=1)
    finally:
        if saved is not None:
            set_switches(saved)
        if os.path.exists(WATCH_FLAG):
            os.remove(WATCH_FLAG)
        if args.slow:
            start_kodi(False)               # leave a normal Kodi behind
        focus_menu(0)
    print('saved', os.path.relpath(os.path.join(OUT, args.label + '.json')))
    show(summary(result))


# --------------------------------------------------------------------------- context menu

KEYMAP = os.path.join(kodi_live.KODI_HOME, 'userdata', 'keymaps', 'milionar-home.xml')
BUSY_CHANGE = MOVIE_CHANGE + ['cont_series', 'my_series_next', 'my_series_new', 'my_series_added', 'my_series_unwatched',
                              'my_series_all', 'widget_demo_series']
MENU_CARDS = (('film card', 'my_movies', 29100), ('Pro tebe card', 'foryou', 32100), ('genre tile', 'my_movies', 29300))


def marked(*items):
    """Run harness ops right after a marker line in kodi.log; returns the marker's time."""
    token = 'perf-{}'.format(int(time.time() * 1000))
    ops({'op': 'mark', 'token': token}, *items)
    deadline = time.time() + 5
    while time.time() < deadline:
        for line in reversed(log_lines()):
            if 'MARK ' + token in line:
                return stamp(line)
        time.sleep(0.05)
    raise SystemExit('marker not found in kodi.log')


def after(when, pattern, timeout=40.0):
    """Seconds from `when` to the first perf line matching `pattern` written after it (None: it never came)."""
    deadline = time.time() + timeout
    while time.time() < deadline:
        for line_time, text in perf():
            if line_time >= when and re.search(pattern, text):
                return round(line_time - when, 3)
        time.sleep(0.05)
    return None


def context_trigger():
    """What the installed Home keymap runs for the context key."""
    with open(KEYMAP, encoding='utf-8') as handle:
        return re.search(r'<c>([^<]+)</c>', handle.read()).group(1)


def unwatched_movie():
    """TMDb id of a film that is neither watched nor started: marking it unwatched changes nothing."""
    from urllib.parse import parse_qsl, unquote
    for index in range(10):
        prefix = 'Container(29200).ListItemAbsolute({})'.format(index)
        labels, conds = info(labels=[prefix + '.Property(milionar.ctxdata)', prefix + '.Playcount'], conds=[prefix + '.IsResumable'])
        data = dict(parse_qsl(unquote(labels[prefix + '.Property(milionar.ctxdata)'])))
        if data.get('type') == 'movie' and data.get('tmdb') and labels[prefix + '.Playcount'] in ('', '0') \
                and not conds[prefix + '.IsResumable']:
            return data['tmdb']
    return None


def context(args):
    os.makedirs(OUT, exist_ok=True)
    result = {'label': args.label, 'when': datetime.now().isoformat(timespec='seconds'), 'slow': args.slow, 'menu': {}}
    try:
        start_kodi(args.slow)
        wait_quiet(least=MIN_WAIT)
        sections = visible_sections()
        trigger = context_trigger()
        result['trigger'] = trigger
        for name, section, list_id in MENU_CARDS:
            focus_menu(sections.index(section))
            time.sleep(0.8)
            builtin('SetFocus({})'.format(list_id))
            time.sleep(0.8)
            times = []
            for _ in range(args.reps):
                if args.busy:                          # the rows are reloading while the key is pressed
                    ops({'op': 'call', 'module': 'wcs.skin.refresh', 'func': 'changed', 'args': [BUSY_CHANGE]})
                    time.sleep(0.25)
                times.append(after(marked({'op': 'builtin', 'cmd': trigger}), r'^context menu open'))
                if args.busy:
                    wait_quiet(quiet=3.0)
                time.sleep(0.6)
                builtin('Dialog.Close(all,true)')
                time.sleep(1.0)
            result['menu'][name] = times
            print('{}: {}'.format(name, times), flush=True)
        focus_menu(sections.index('my_movies'))
        time.sleep(0.8)
        movie = unwatched_movie()
        times = []
        if movie:
            command = 'RunPlugin(plugin://plugin.video.milionar/?action=ctx_mark&type=movie&tmdb={}&watched=0)'.format(movie)
            for _ in range(args.reps):
                try:                                   # the menu runs its own actions in the process that shows it
                    when = marked({'op': 'call', 'module': 'wcs.skin.context', 'func': 'run_chosen', 'args': [command]})
                    result['action_path'] = 'in the service'
                except SystemExit:                     # before: every menu action started the plugin
                    when = marked({'op': 'builtin', 'cmd': command})
                    result['action_path'] = 'plugin start'
                times.append(after(when, r'^action ctx_mark done'))
                time.sleep(1.0)
        result['action'] = times
        print('action ({}): {}'.format(result.get('action_path'), times), flush=True)
    finally:
        with open(os.path.join(OUT, args.label + '.json'), 'w', encoding='utf-8') as handle:
            json.dump(result, handle, ensure_ascii=False, indent=1)
        if args.slow:
            start_kodi(False)
        focus_menu(0)
    print('saved', os.path.relpath(os.path.join(OUT, args.label + '.json')))


def clicks(args):
    """A hero button ("Více informací" of a film in Filmy): the key -> the Home detail is open."""
    os.makedirs(OUT, exist_ok=True)
    result = {'label': args.label, 'when': datetime.now().isoformat(timespec='seconds'), 'slow': args.slow, 'busy': args.busy}
    try:
        start_kodi(args.slow)
        wait_quiet(least=MIN_WAIT)
        sections = visible_sections()
        focus_menu(sections.index('my_movies'))
        time.sleep(0.8)
        times = []
        for _ in range(args.reps):
            builtin('SetFocus(29902)')                 # the second hero button of Filmy
            time.sleep(0.6)
            if args.busy:
                ops({'op': 'call', 'module': 'wcs.skin.refresh', 'func': 'changed', 'args': [BUSY_CHANGE]})
                time.sleep(0.25)
            times.append(after(marked({'op': 'builtin', 'cmd': 'Action(Select)'}), r'^action show_wcs_details done', timeout=30))
            time.sleep(1.5)
            builtin('Action(Back)')
            wait_quiet(quiet=3.0) if args.busy else time.sleep(2.0)
        result['button'] = times
        print('hero button -> detail open: {}'.format(times), flush=True)
    finally:
        with open(os.path.join(OUT, args.label + '.json'), 'w', encoding='utf-8') as handle:
            json.dump(result, handle, ensure_ascii=False, indent=1)
        if args.slow:
            start_kodi(False)
        focus_menu(0)
    print('saved', os.path.relpath(os.path.join(OUT, args.label + '.json')))


def context_compare(labels):
    data = [json.load(open(os.path.join(OUT, label + '.json'), encoding='utf-8')) for label in labels]
    print('{:<34}'.format('median seconds') + ''.join('{:>16}'.format(item['label'][:16]) for item in data))
    for name in [card[0] for card in MENU_CARDS]:
        print('{:<34}'.format('menu on a ' + name) + ''.join('{:>16}'.format(str(median(item['menu'].get(name, [])))) for item in data))
    print('{:<34}'.format('menu action (mark) -> done') + ''.join('{:>16}'.format(str(median(item.get('action', [])))) for item in data))


def median(values):
    values = [value for value in values if isinstance(value, (int, float))]
    return round(statistics.median(values), 2) if values else None


def summary(result):
    reps = result['reps']
    sections = reps[0]['visible']
    out = {'label': result['label'], 'reps': len(reps), 'sections': {}}
    for section in sections:
        entries = [rep['avail']['sections'].get(section, {}) for rep in reps]
        out['sections'][section] = {
            'first_s': median([entry.get('first_s') for entry in entries]),
            'all_s': median([entry.get('all_s') for entry in entries]),
            'rows': '{}/{}'.format(entries[0].get('rows_with_cards'), entries[0].get('rows')),
            'tour_s': median([rep['tour'].get(section) for rep in reps])}
    for key in ('skin_loaded_s', 'everything_s', 'cpu_s', 'rss_mb', 'plugin_calls', 'bound'):
        out[key] = median([rep['avail'].get(key) for rep in reps])
    out['change_screen_s'] = median([rep['change'].get('screen_s') for rep in reps])
    out['change_everything_s'] = median([rep['change'].get('everything_s') for rep in reps])
    out['jump_section'] = reps[0]['jump'].get('section')
    out['jump_first_s'] = median([rep['jump'].get('first_s') for rep in reps])
    out['jump_all_s'] = median([rep['jump'].get('all_s') for rep in reps])
    return out


def show(*summaries):
    def cell(value):
        return '{:>14}'.format('-' if value is None else value)
    print('{:<44}'.format('') + ''.join(cell(item['label'][:14]) for item in summaries))
    rows = [('Kodi start -> skin loaded (s)', 'skin_loaded_s'), ('Kodi start -> every section loaded (s)', 'everything_s'),
            ('CPU time used until then (s)', 'cpu_s'), ('memory (MB)', 'rss_mb'), ('plugin calls for rows', 'plugin_calls'),
            ('rows bound by the service', 'bound'), ('change -> rows on screen (s)', 'change_screen_s'),
            ('change -> every section (s)', 'change_everything_s'), ('jump to the last section: first row (s)', 'jump_first_s'),
            ('jump to the last section: all rows (s)', 'jump_all_s')]
    for title, key in rows:
        print('{:<44}'.format(title) + ''.join(cell(item.get(key)) for item in summaries))
    for section in summaries[0]['sections']:
        for title, key in (('first row after Kodi start (s)', 'first_s'), ('all rows after Kodi start (s)', 'all_s'),
                           ('rows with cards', 'rows'), ('loaded: key -> rows (s)', 'tour_s')):
            print('{:<44}'.format('  {:<10} {}'.format(section, title)) + ''.join(
                cell(item['sections'].get(section, {}).get(key)) for item in summaries))


def main():
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    commands = parser.add_subparsers(dest='command', required=True)
    runner = commands.add_parser('run')
    runner.add_argument('label')
    runner.add_argument('--reps', type=int, default=3)
    runner.add_argument('--all-sections', action='store_true')
    runner.add_argument('--slow', action='store_true')
    comparer = commands.add_parser('compare')
    comparer.add_argument('labels', nargs='+')
    menus = commands.add_parser('context')
    menus.add_argument('label')
    menus.add_argument('--reps', type=int, default=5)
    menus.add_argument('--slow', action='store_true')
    menus.add_argument('--busy', action='store_true', help='press the key while the rows are reloading')
    clicker = commands.add_parser('clicks')
    clicker.add_argument('label')
    clicker.add_argument('--reps', type=int, default=5)
    clicker.add_argument('--slow', action='store_true')
    clicker.add_argument('--busy', action='store_true')
    menu_comparer = commands.add_parser('context-compare')
    menu_comparer.add_argument('labels', nargs='+')
    args = parser.parse_args()
    if args.command == 'run':
        run(args)
    elif args.command == 'context':
        context(args)
    elif args.command == 'clicks':
        clicks(args)
    elif args.command == 'context-compare':
        context_compare(args.labels)
    else:
        show(*[summary(json.load(open(os.path.join(OUT, label + '.json'), encoding='utf-8'))) for label in args.labels])


if __name__ == '__main__':
    main()
