#!/usr/bin/env python3 """P13 W5 inventory: every preprocessor group whose condition names MOBILEGL_BUILD_DISAGGREGATED, classified by what its DISAGG-taken branch talks about. w5_classify.py [--root DIR] [--csv OUT] [--summary] Classes (W5 design ยง3.1): A record terms, no transport token, no MG_Remote name -> becomes MOBILEGL_BUILD_RECORD_ARM B names MG_Remote, no record term -> stays DISAGG C names MG_Remote and record terms -> hand split D transport token and record terms, no MG_Remote name -> hand split E transport token, no record term, no MG_Remote name -> stays DISAGG F none of the three -> reviewed, biased to record N negated (`#if !MOBILEGL_BUILD_DISAGGREGATED`) or compound condition -> listed for the hand pass Comments are stripped before matching. Nested DISAGG groups are listed with their depth; a rewrite touches the outermost of a run and leaves inner transport groups as they are. """ import argparse import csv import pathlib import re import sys SCAN = ['MobileGL'] SKIP_PARTS = {'MG_Remote', 'generated', '3rdparty'} EXTS = {'.h', '.hpp', '.cpp', '.cc', '.inc', '.def'} MACRO = 'MOBILEGL_BUILD_DISAGGREGATED' DIRECTIVE = re.compile(r'^\s*#\s*(if|ifdef|ifndef|elif|else|endif)\b(.*)$') TRANSPORT = [r'ClientSession', r'ServerSession', r'ServerLoop', r'Transport::', r'MG_Config::Ipc\b', r'Ipc\.', r'SharedImage', r'AdoptT0', r'\bT0\b', r'externalAhb', r'RunsAsTheServerRole', r'CapsMirror', r'SessionLatch', r'ServerRole', r'SplitRoles', r'TransportMode::Spawn', r'TransportMode::InProcess', r'ServerOwnedWindow', r'ServerSetContextLive', r'DeviceLost', r'RoleSplit', r'ApplyThread', r'MGPipeServerArm', r'MGPipeSessionLive', r'ClientBlock', r'EndedServerSession', r'OnApplyThread', r'Transport\s*!=', r'Transport\s*==', # Second pass (S2): transport-shaped words that name no MG_Remote symbol. r'Window', r'SwapInterval', r'ContextLive', r'Ahb', r'AHardwareBuffer', r'HardwareBuffer', r'[Dd]ma[Bb]uf', r'Gbm', r'SyncFile', r'sync_file', r'Session', r'Peer', r'Spawn', r'Tcp', r'Socket', # Device loss and its recovery are a session's (the session ends, not the process). r'Lost', r'Epoch', r'ResetNotif', r'Recover', r'StreamGate', r'ReplacementShare', r'ReplaceProcessDefault'] RECORD = [r'DataArmIsRecord', r'RecordArm', r'Staged', r'\bWire', r'MGPipe', r'Handle', r'Record\b', r'Twin', r'Respecif', r'Readback', r'Verb', r'PersistentMap', r'GpuWrite', r'Emit', r'Applier', r'Slot', r'Residual', r'ContextValues', r'Cso\b', r'CSO', r'Placeholder', r'Mipmap', r'Shadow'] TRANSPORT_RE = re.compile('|'.join(TRANSPORT)) RECORD_RE = re.compile('|'.join(RECORD)) REMOTE_RE = re.compile(r'MG_Remote') def strip_comments(text): text = re.sub(r'/\*.*?\*/', ' ', text, flags=re.S) return re.sub(r'//[^\n]*', '', text) def groups(lines): """Yield dicts for each #if group naming MACRO: open/else/close lines, condition, depth, body.""" stack = [] # entries: {'open', 'cond', 'kind', 'names', 'branches': [(line, kind)], 'depth'} out = [] for no, raw in enumerate(lines, 1): m = DIRECTIVE.match(raw) if not m: continue kind, rest = m.group(1), strip_comments(m.group(2)).strip() if kind in ('if', 'ifdef', 'ifndef'): names = MACRO in rest depth = sum(1 for s in stack if s['names']) stack.append({'open': no, 'cond': (kind + ' ' + rest).strip(), 'names': names, 'branches': [(no, kind)], 'depth': depth}) elif kind in ('elif', 'else'): if stack: stack[-1]['branches'].append((no, kind)) elif kind == 'endif': if not stack: continue g = stack.pop() g['close'] = no if g['names']: out.append(g) return out def classify(path, lines): rows = [] for g in groups(lines): cond = g['cond'] plain = re.fullmatch(r'(if\s+' + MACRO + r'|ifdef\s+' + MACRO + r'|if\s+defined\s*\(?\s*' + MACRO + r'\s*\)?)', cond) is not None # The DISAGG-taken branch: first branch for a plain condition. first_end = g['branches'][1][0] if len(g['branches']) > 1 else g['close'] body = strip_comments(''.join(lines[g['open']:first_end - 1])) remote = bool(REMOTE_RE.search(body)) transport = sorted(set(t.group(0) for t in TRANSPORT_RE.finditer(body))) record = bool(RECORD_RE.search(body)) if not plain: cls = 'N' elif remote and record: cls = 'C' elif remote: cls = 'B' elif transport and record: cls = 'D' elif transport: cls = 'E' elif record: cls = 'A' else: cls = 'F' rows.append({'file': path, 'open': g['open'], 'close': g['close'], 'lines': g['close'] - g['open'] + 1, 'depth': g['depth'], 'class': cls, 'cond': cond, 'has_else': len(g['branches']) > 1, 'transport': ' '.join(transport)[:120]}) return rows def main(): ap = argparse.ArgumentParser() ap.add_argument('--root', default='.') ap.add_argument('--csv') ap.add_argument('--summary', action='store_true') args = ap.parse_args() root = pathlib.Path(args.root) rows = [] for top in SCAN: for p in sorted((root / top).rglob('*')): if p.suffix not in EXTS or not p.is_file(): continue rel = p.relative_to(root) if SKIP_PARTS & set(rel.parts): continue lines = p.read_text(encoding='utf-8', errors='replace').splitlines(keepends=True) rows.extend(classify(rel.as_posix(), lines)) if args.csv: with open(args.csv, 'w', newline='', encoding='utf-8') as f: w = csv.DictWriter(f, fieldnames=list(rows[0].keys()) if rows else ['file']) w.writeheader() w.writerows(rows) if args.summary or not args.csv: by = {} for r in rows: top = r['file'].split('/')[1] if '/' in r['file'] else r['file'] if top.startswith('MG_Backend') and len(r['file'].split('/')) > 2: top = '/'.join(r['file'].split('/')[1:3]) key = (top, r['class']) n, l = by.get(key, (0, 0)) by[key] = (n + 1, l + r['lines']) classes = 'ABCDEFN' print('%-28s' % 'dir' + ''.join('%14s' % c for c in classes)) for top in sorted(set(k[0] for k in by)): print('%-28s' % top + ''.join('%14s' % ('%d/%d' % by.get((top, c), (0, 0))) for c in classes)) tot = {c: (sum(by[k][0] for k in by if k[1] == c), sum(by[k][1] for k in by if k[1] == c)) for c in classes} print('%-28s' % 'TOTAL blocks/lines' + ''.join('%14s' % ('%d/%d' % tot[c]) for c in classes)) return 0 if __name__ == '__main__': sys.exit(main())