swayfx-mosaic/tooling/probes/step20c-mutation-coverage/analyze.py

140 lines
7.2 KiB
Python

"""Correlate bounded headless mutations without assigning input a frame ID."""
import argparse
from collections import Counter
import hashlib
import json
from pathlib import Path
import re
import shutil
from PIL import Image
ap = argparse.ArgumentParser()
ap.add_argument('lab', type=Path)
args = ap.parse_args()
lab = args.lab.resolve()
repo = Path(__file__).resolve().parents[3]
out = lab / 'mutation-results'
out.mkdir(exist_ok=True)
fixture = Path(__file__).parent
shutil.copyfile(fixture / 'inventory.json', out / 'inventory.json')
def fields(line, marker):
if marker not in line:
return None
return dict(re.findall(r'(\w+)=([^ ]+)', line.split(marker, 1)[1]))
def read_run(mode, kind):
matches = sorted((lab / 'runs').glob(f'{mode}-{kind}-asi0-*'))
if len(matches) != 1:
raise RuntimeError(f'expected one {mode}/{kind} run, got {len(matches)}')
run = matches[0]
events = [json.loads(x) for x in (run / 'events.jsonl').read_text().splitlines()]
log = (run / 'compositor.log').read_text(errors='replace').splitlines()
records = {key: [f for line in log if (f := fields(line, key + ' '))]
for key in ('ASI_MUTATION', 'ASI_HIT', 'BI_STAGE',
'PI_OUTPUT_COMMIT', 'PI_OUTPUT_PRESENT',
'ASI_LOCKED', 'ASI_RELEASE')}
commits = {(x['output'], x['commit_seq']) for x in records['PI_OUTPUT_COMMIT']}
presents = {(x['output'], x['commit_seq']) for x in records['PI_OUTPUT_PRESENT']}
client = (run / 'client.log').read_text(errors='replace')
status = json.loads((run / 'exit-status.json').read_text())
return run, events, records, client, status, commits, presents
matrix = {}
for mode in ('control', 'private_standard', 'private_mosaic'):
for kind in ('cycles', 'mutation_content', 'mutation_input_region'):
run, events, rec, client, status, commits, presents = read_run(mode, kind)
entry = {
'run': str(run), 'mode': mode, 'scenario': kind,
'runner_failure': status['failure'], 'captures': status['captures'],
'commits': len(commits), 'presents': len(presents),
'unpaired_commits': sorted([list(x) for x in commits - presents]),
'hit_count': len(rec['ASI_HIT']),
'epoch_events': len(rec['ASI_MUTATION']),
'buffer_locks': len(rec['ASI_LOCKED']),
'buffer_releases': len(rec['ASI_RELEASE']),
'popup_grabs': client.count('GRAB_REQUEST'),
'popup_creates': [x for x in client.splitlines() if 'POPUP_' in x and ' CREATE ' in x],
'popup_destroys': [x for x in client.splitlines() if 'POPUP_' in x and ' DESTROY ' in x],
}
if kind == 'cycles':
entry['A_buttons_after_B_close'] = len(re.findall(
r'POINTER_BUTTON target=POPUP_A[^\n]*button=273 state=1', client))
entry['camera_stages'] = [
{k: x.get(k) for k in ('output', 'scroll', 'tile_scene', 'clip', 'epoch')}
for x in rec['BI_STAGE'] if x.get('phase') == 'COMMIT_ACCEPTED'
and x.get('output') == 'HEADLESS-1'
and x.get('scroll') in ('400', '1280')]
elif kind == 'mutation_content':
entry['client_new_buffer'] = 'COMMAND recolor_top DONE' in client and (
client.count('TOPLEVEL MAP') >= 2)
entry['pixels'] = {tag: Image.open(run / f'left-content-{tag}.png').getpixel((342, 175))
for tag in ('before', 'after')}
boundary = [e for e in events if e['kind'] == 'client_request'
and e['value'] == 'recolor_top'][0]['monotonic_ns']
entry['commits_after_request'] = [x for x in rec['PI_OUTPUT_COMMIT']
if x['output'] == 'HEADLESS-1' and int(x['ns']) > boundary]
entry['stages_after_request'] = [{k: x.get(k) for k in
('commit_seq', 'scroll', 'tile_scene', 'clip', 'epoch', 'ns')}
for x in rec['BI_STAGE'] if x.get('phase') == 'COMMIT_ACCEPTED'
and x['output'] == 'HEADLESS-1' and int(x['ns']) > boundary]
else:
entry['pixels'] = {tag: Image.open(run / f'left-region-{tag}.png').getpixel((342, 175))
for tag in ('full-before', 'empty', 'full-after')}
entry['client_top_button_presses'] = len(re.findall(
r'POINTER_BUTTON target=TOPLEVEL[^\n]*button=273 state=1', client))
entry['hit_sequence'] = [
{k: x.get(k) for k in ('ns', 'epoch', 'lx', 'ly', 'node', 'surface',
'logical_scroll', 'tile_scene', 'clip')}
for x in rec['ASI_HIT'] if x.get('lx') == '-458.00' or
(mode == 'private_standard' and x.get('lx') == '-458.00')]
matrix[f'{mode}/{kind}'] = entry
(out / 'scenario-matrix.json').write_text(json.dumps(matrix, indent=2) + '\n')
hold_runs = sorted((lab / 'runs').glob('private_mosaic-mutation_hold_exit-asi1-*'))
if len(hold_runs) != 1:
raise RuntimeError(f'expected one real output-destroy run, got {len(hold_runs)}')
hold = hold_runs[0]
hold_log = (hold / 'compositor.log').read_text(errors='replace').splitlines()
hold_status = json.loads((hold / 'exit-status.json').read_text())
lifetime = {
'runtime_output_destroy': {
'run': str(hold), 'runner_failure': hold_status['failure'],
'compositor_exit': hold_status['compositor'],
'locks': [fields(x, 'ASI_LOCKED ') for x in hold_log if 'ASI_LOCKED ' in x],
'releases': [fields(x, 'ASI_RELEASE ') for x in hold_log if 'ASI_RELEASE ' in x],
'selected_present_retained': any('ASI_TEST_HOLD id=1 ' in x for x in hold_log),
},
'runtime_rejected_commit': 'NOT TESTED',
'synthetic_rejected_commit': 'see lifetime-model.c; no backend triggered',
}
(out / 'lifetime.json').write_text(json.dumps(lifetime, indent=2) + '\n')
sources = [
fixture / 'mutation-client.patch', fixture / 'prepare.sh', fixture / 'run.py', fixture / 'analyze.py',
fixture / 'inventory.json', fixture / 'lifetime-model.c',
lab / 'nested-client.c', lab / 'mosaic/sway/desktop/transaction.c',
lab / 'mosaic/sway/desktop/output.c', lab / 'mosaic/sway/desktop/xdg_shell.c',
lab / 'mosaic/sway/input/cursor.c', lab / 'mosaic/sway/input/seat.c',
lab / 'scenefx/types/scene/wlr_scene.c',
lab / 'wlroots/types/scene/surface.c',
lab / 'wlroots/types/scene/subsurface_tree.c',
]
with (out / 'source-sha256.txt').open('w') as dst:
for p in sources:
dst.write(f'{hashlib.sha256(p.read_bytes()).hexdigest()} {p}\n')
with (out / 'artifact-sha256.txt').open('w') as dst:
for p in sorted((lab / 'runs').glob('*')):
if not p.is_dir() or ('asi0' not in p.name and 'mutation_hold_exit-asi1' not in p.name):
continue
for f in sorted(p.rglob('*')):
if f.is_file():
dst.write(f'{hashlib.sha256(f.read_bytes()).hexdigest()} {f}\n')
for p in (out / 'inventory.json', out / 'scenario-matrix.json',
out / 'lifetime.json', out / 'source-sha256.txt'):
dst.write(f'{hashlib.sha256(p.read_bytes()).hexdigest()} {p}\n')
print(f'MATRIX={out / "scenario-matrix.json"} SOURCES={len(sources)} '
f'ARTIFACTS={len((out / "artifact-sha256.txt").read_text().splitlines())}')