swayfx-mosaic/tooling/probes/step20c-presentation-identity/analyze.py

165 lines
9.1 KiB
Python

"""Correlate native headless output events without inventing frame identity."""
import hashlib
import json
from pathlib import Path
import re
import sys
from PIL import Image
if len(sys.argv) != 2:
raise SystemExit('usage: analyze.py LAB')
lab = Path(sys.argv[1]).resolve()
results = lab / 'presentation-results'
results.mkdir(exist_ok=True)
def fields(line):
return {k: v for k, v in re.findall(r'([a-z_]+)=([^ ]+)', line)}
def save(name, value):
(results / name).write_text(json.dumps(value, indent=2) + '\n')
events, commits, presents, scenes, inputs, pixels, damage, matrix, transitions = [], [], [], [], [], [], [], [], []
repaint_results = []
for run in sorted((lab / 'runs').glob('*')):
status_path = run / 'exit-status.json'
if not status_path.exists():
continue
run_id = run.name
status = json.loads(status_path.read_text())
output_by_seq = {}
active_commit = None
for line in (run / 'compositor.log').read_text().splitlines():
match = re.search(r'\b(PI_OUTPUT_COMMIT|PI_OUTPUT_PRESENT|PI_SCENE|PI_OWNER|PI_POPUP|TEMPORAL_DAMAGE|SPIKE_DAMAGE|TEMPORAL_OUTPUT_COMMIT)\b(.*)', line)
if not match:
continue
kind = match.group(1)
data = {'run': run_id, 'kind': kind, **fields(match.group(2))}
events.append(data)
if kind == 'PI_OUTPUT_COMMIT':
key = (data['output'], int(data['commit_seq']))
if key in output_by_seq:
raise RuntimeError(f'duplicate native commit sequence in {run_id}: {key}')
data['commit_seq'] = key[1]
data['present_count'] = 0
data['scene'] = None
data['owners'] = []
data['popups'] = []
commits.append(data)
output_by_seq[key] = data
active_commit = data
elif kind == 'PI_OUTPUT_PRESENT':
data['commit_seq'] = int(data['commit_seq'])
data['matched'] = (data['output'], data['commit_seq']) in output_by_seq
if data['matched']:
output_by_seq[(data['output'], data['commit_seq'])]['present_count'] += 1
presents.append(data)
active_commit = None
elif kind in ('PI_SCENE', 'PI_OWNER', 'PI_POPUP'):
# CONTRACT: these synchronous callback lines belong to the
# immediately preceding commit callback, never to a later frame.
if active_commit is not None and data['output'] == active_commit['output']:
if kind == 'PI_SCENE':
active_commit['scene'] = data
scenes.append({'run': run_id, 'output': data['output'],
'commit_seq': active_commit['commit_seq'], **data})
elif kind == 'PI_OWNER':
active_commit['owners'].append(data)
else:
active_commit['popups'].append(data)
else:
if kind in ('TEMPORAL_DAMAGE', 'SPIKE_DAMAGE'):
damage.append(data)
if kind == 'TEMPORAL_OUTPUT_COMMIT':
repaint_results.append(data)
active_commit = None
runner_events = [json.loads(line) for line in (run / 'events.jsonl').read_text().splitlines()]
starts = [e for e in runner_events if e['kind'] == 'transition_start']
for index, start in enumerate(starts):
begin = start['monotonic_ns']
end = starts[index + 1]['monotonic_ns'] if index + 1 < len(starts) else None
own_commits = [c for c in commits if c['run'] == run_id and
c['output'] == 'HEADLESS-1' and int(c['ns']) >= begin and
(end is None or int(c['ns']) < end)]
own_presents = [p for p in presents if p['run'] == run_id and
p['output'] == 'HEADLESS-1' and int(p['ns']) >= begin and
(end is None or int(p['ns']) < end)]
transitions.append({'run': run_id, 'series': start['series'],
'index': start['index'], 'requested_scroll': start['target'],
'start_ns': begin, 'next_command_ns': end,
'commit_count_in_interval': len(own_commits),
'present_count_in_interval': len(own_presents),
'observed_commit_scrolls': [c['scene'].get('scroll')
if c['scene'] else None for c in own_commits],
'command_to_frame_identity': 'UNPROVEN'})
for event in runner_events:
if event['kind'] in ('pointer_request', 'camera_observed', 'ipc', 'client_request'):
inputs.append({'run': run_id, **event})
if event['kind'] != 'capture':
continue
path = run / event['path']
image = Image.open(path).convert('RGB')
counts = {name: image.get_flattened_data().count(rgb)
for name, rgb in {'A_yellow': (224, 192, 32),
'B_green': (32, 208, 160)}.items()}
before, after = event['before_ns'], event['after_ns']
nearby = [p['commit_seq'] for p in presents if p['run'] == run_id and
p['output'] == event['output'] and before <= int(p['ns']) <= after]
pixels.append({'run': run_id, 'output': event['output'], 'tag': event['tag'],
'path': event['path'], 'sha256': hashlib.sha256(path.read_bytes()).hexdigest(),
'before_ns': before, 'after_ns': after, 'size': list(image.size),
'roi': [400, 90, 800, 310] if event['output'] == 'HEADLESS-1'
else [0, 90, 200, 310], 'exact_color_pixels': counts,
'present_events_during_capture': nearby,
'commit_seq_of_captured_buffer': None})
client = (run / 'client.log').read_text() if (run / 'client.log').exists() else ''
compositor_text = (run / 'compositor.log').read_text()
protocol_error = bool(re.search(r'protocol error|invalid serial',
compositor_text + client, re.I))
grabs = re.findall(r'POPUP_[AB] GRAB_REQUEST origin=wl_pointer.button '
r'target=\w+ serial=(\d+) used=(\d+)', client)
button_serials = set(re.findall(r'POINTER_BUTTON target=\w+ serial=(\d+) '
r'button=272 state=1', client))
valid_grabs = len(grabs) >= 2 and all(
serial == used and serial in button_serials for serial, used in grabs)
for line in client.splitlines():
if any(token in line for token in ('GRAB_REQUEST', 'POINTER_ENTER', 'POINTER_BUTTON',
'POPUP_DONE', 'MAP', 'DESTROY')):
inputs.append({'run': run_id, 'kind': 'client_line', 'line': line})
own_commits = [c for c in commits if c['run'] == run_id]
own_presents = [p for p in presents if p['run'] == run_id]
matrix.append({'run': run_id, 'runner': 'FAIL' if status['failure'] else 'PASS',
'accepted_commits': len(own_commits), 'present_events': len(own_presents),
'matched_present_events': sum(p['matched'] for p in own_presents),
'commits_without_present': sum(c['present_count'] == 0 for c in own_commits),
'scene_snapshots': sum(c['scene'] is not None for c in own_commits),
'valid_button_serials': 'PASS' if valid_grabs else 'INCONCLUSIVE',
'protocol_error': protocol_error,
'capture_count': sum(p['run'] == run_id for p in pixels),
'failure': status['failure']})
for name, value in [('commits.json', commits), ('presentations.json', presents),
('camera-states.json', scenes), ('damage-analysis.json', damage),
('input-analysis.json', inputs), ('pixel-analysis.json', pixels),
('matrix.json', matrix), ('transitions.json', transitions)]:
save(name, value)
(results / 'events.jsonl').write_text(''.join(json.dumps(e) + '\n' for e in events))
summary = {'accepted_commits': len(commits), 'present_events': len(presents),
'matched_present_events': sum(p['matched'] for p in presents),
'unmatched_present_events': sum(not p['matched'] for p in presents),
'commits_without_present': sum(c['present_count'] == 0 for c in commits),
'headless_presented_true': sum(p['presented'] == '1' for p in presents),
'presented_false': sum(p['presented'] == '0' for p in presents),
'repaint_commit_calls_rejected': sum(r['accepted'] == '0' for r in repaint_results),
'scene_snapshots': len(scenes), 'capture_count': len(pixels),
'captures_with_present_during_capture': sum(bool(p['present_events_during_capture'])
for p in pixels),
'runs': len(matrix), 'runner_failures': sum(x['runner'] == 'FAIL' for x in matrix),
'invalid_or_unproven_grabs': sum(x['valid_button_serials'] != 'PASS' for x in matrix),
'protocol_error_runs': sum(x['protocol_error'] for x in matrix),
'buffer_content_identity': 'UNPROVEN'}
save('summary.json', summary)
print(json.dumps(summary))