swayfx-mosaic/tooling/probes/step20c-applied-state-input/analyze.py

128 lines
6.6 KiB
Python

"""Parse private applied scene, buffer lifetime and input traces without invented frame IDs."""
import argparse
from collections import Counter
import hashlib
import json
from pathlib import Path
import re
import statistics
ap = argparse.ArgumentParser()
ap.add_argument('lab', type=Path)
ap.add_argument('run', type=Path)
args = ap.parse_args()
run = args.run.resolve()
lab = args.lab.resolve()
if lab not in run.parents or not (run / 'compositor.log').is_file():
ap.error('run must be inside the prepared lab')
out = lab / 'results-asi' / run.name
out.mkdir(parents=True, exist_ok=True)
kinds = ('ASI_MUTATION', 'BI_STAGE', 'ASI_HIT', 'ASI_WARP',
'ASI_BUTTON_REQUEST', 'ASI_DELIVERY', 'ASI_LOCKED', 'ASI_RELEASE',
'PI_OUTPUT_COMMIT', 'PI_OUTPUT_PRESENT', 'BI_BUFFER', 'BI_READ',
'BI_COMMIT', 'BI_EVENT_BUFFER', 'BI_PRESENT_READ', 'BI_PRESENT_HOLD')
records = {k: [] for k in kinds}
for number, line in enumerate((run / 'compositor.log').read_text(errors='replace').splitlines(), 1):
for kind in kinds:
marker = kind + ' '
if marker not in line:
continue
payload = line.split(marker, 1)[1]
fields = dict(re.findall(r'(\w+)=([^ ]+)', payload))
records[kind].append({'line': number, 'kind': kind, **fields})
break
mode = json.loads((run / 'asi-mode.json').read_text()) if (run / 'asi-mode.json').exists() else {'mode': 'pointer_control', 'readback_dir': None}
read_dir = Path(mode['readback_dir']) if mode['readback_dir'] else None
raw = {}
if read_dir:
for f in sorted(read_dir.glob('buffer-*.xrgb')):
raw[f.name] = {'bytes': f.stat().st_size,
'sha256': hashlib.sha256(f.read_bytes()).hexdigest()}
commits = {(x['output'], x['commit_seq']): x for x in records['PI_OUTPUT_COMMIT']}
presents = {(x['output'], x['commit_seq']): x for x in records['PI_OUTPUT_PRESENT']}
selected = records['BI_COMMIT']
releases = records['ASI_RELEASE']
release_count = Counter(x['id'] for x in releases)
selected_ids = {x['id'] for x in selected}
release_ids = set(release_count)
selected_pairs = {(x['output'], x['commit_seq']) for x in selected if x['accepted'] == '1'}
matched = selected_pairs & set(presents)
client = (run / 'client.log').read_text(errors='replace') if (run / 'client.log').exists() else ''
client_events = [line for line in client.splitlines() if any(
key in line for key in ('POPUP_A MAP', 'POPUP_B MAP', 'POPUP_DONE',
'POINTER_BUTTON target=', 'GRAB_REQUEST'))]
summary = {
'mode': mode['mode'], 'native_commits': len(commits),
'native_presents': len(presents), 'selected_commits': len(selected_pairs),
'selected_presents': len(matched), 'selected_buffers': len(records['BI_BUFFER']),
'raw_snapshots': len([x for x in raw if '-present' not in x]),
'present_snapshots': len([x for x in raw if '-present' in x]),
'lock_count': len(records['ASI_LOCKED']), 'release_count': len(releases),
'double_release_ids': sorted(k for k,v in release_count.items() if v != 1),
'unreleased_ids': sorted(selected_ids - release_ids),
'cross_output_release_count': sum(1 for x in releases if x.get('reason') == 'superseded'),
'release_reasons': dict(Counter(x.get('reason') for x in releases)),
'mutation_events': len(records['ASI_MUTATION']),
'scene_stages': len(records['BI_STAGE']),
'hit_tests': len(records['ASI_HIT']),
'seat_notifications': len(records['ASI_DELIVERY']),
'client_popup_maps': {'A': client.count('POPUP_A MAP'), 'B': client.count('POPUP_B MAP')},
'client_B_buttons': len(re.findall(r'POINTER_BUTTON target=POPUP_B[^\n]*state=1', client)),
'client_popup_done': {'A': client.count('POPUP_A POPUP_DONE'),
'B': client.count('POPUP_B POPUP_DONE')},
}
correlation = []
pixel_analysis = []
for item in selected:
key = (item['output'], item['commit_seq'])
earlier = [stage for stage in records['BI_STAGE']
if stage.get('phase') == 'POST_BUILD' and
stage['output'] == item['output'] and
int(stage['ns']) <= int(item['ns'])]
stage = earlier[-1] if earlier else None
correlation.append({'id': item['id'], 'output': item['output'],
'commit_seq': item['commit_seq'],
'accepted': item['accepted'],
'native_commit': key in commits,
'headless_present': key in presents,
'applied_stage': stage})
if read_dir:
f = read_dir / ('buffer-' + item['id'] + '.xrgb')
if f.is_file() and f.stat().st_size == 800 * 600 * 4:
from PIL import Image
data = f.read_bytes()
image = Image.frombytes('RGB', (800, 600), data, 'raw', 'BGRX', 0, -1)
colors = {color: count for count, color in image.getcolors(480000)}
second = read_dir / ('buffer-' + item['id'] + '-present.xrgb')
pixel_analysis.append({'id': item['id'], 'commit_seq': item['commit_seq'],
'epoch': stage.get('epoch') if stage else None,
'scroll': stage.get('scroll') if stage else None,
'tile_scene': stage.get('tile_scene') if stage else None,
'clip': stage.get('clip') if stage else None,
'A_yellow': colors.get((224, 192, 32), 0),
'B_green': colors.get((32, 208, 160), 0),
'raw_sha256': hashlib.sha256(data).hexdigest(),
'present_read_equal': second.read_bytes() == data if second.is_file() else None})
post_to_commit = []
stages = records['BI_STAGE']
for i, stage in enumerate(stages):
if stage.get('phase') != 'POST_BUILD' or stage['output'] != 'HEADLESS-1':
continue
later = next((x for x in stages[i + 1:] if x['output'] == stage['output']
and x['phase'] in ('PRE_BUILD', 'COMMIT_ACCEPTED')), None)
if later and later['phase'] == 'COMMIT_ACCEPTED':
post_to_commit.append(int(later['ns']) - int(stage['ns']))
timing = {'post_build_to_commit_median_ns':
statistics.median(post_to_commit) if post_to_commit else None,
'sample_count': len(post_to_commit)}
for name, obj in (('correlation', correlation), ('pixel-analysis', pixel_analysis),
('timing', timing), ('scene-states', {'build': records['BI_STAGE'],
'hits': records['ASI_HIT'], 'mutations': records['ASI_MUTATION']}),
('records', records), ('raw-buffers', raw),
('client-events', client_events), ('summary', summary)):
(out / (name + '.json')).write_text(json.dumps(obj, indent=2) + '\n')
print(json.dumps(summary, indent=2))