#!/usr/bin/env python3 """Prove the primary runtime's compiled shape and actual inproc GPU execution.""" import argparse import hashlib import importlib.util import json from pathlib import Path import re import shlex import subprocess import tempfile from unittest.mock import patch ROOT = Path(__file__).resolve().parents[2] RSP_CASES = {backend + '.Split.P5fRsp.F1WireScenario.EachWireFrameHasZeroResidualPulls' for backend in ('DirectGLES', 'DirectVulkan')} def build_proof(build, mode): library = build / 'libMobileGL.so' commands = json.loads((build / 'compile_commands.json').read_text()) production = [row for row in commands if row['file'].replace('\\', '/').endswith('/MobileGL/ConfigLoader.cpp')] if not production: raise RuntimeError('no ConfigLoader production compilation found') definitions = [] for row in production: argv = row.get('arguments') or shlex.split(row['command']) flags = {arg[2:].split('=', 1)[0]: arg[2:].split('=', 1)[1] if '=' in arg else '1' for arg in argv if arg.startswith('-D')} # P13: "monolith" means the transport is absent (no MG_Remote), not that the pipe is # pulled - the monolith build pushes too, so MGPipeApply symbols are expected in both. expected = mode == 'disaggregated' if (flags.get('MOBILEGL_BUILD_DISAGGREGATED') == '1') != expected: raise RuntimeError(f'production macros disagree with {mode}: {flags}') definitions.append({'source': row['file'], 'definitions': flags}) symbols = subprocess.check_output(['nm', '--defined-only', str(library)], text=True) if len(symbols.splitlines()) < 1000: raise RuntimeError('library appears stripped; symbol absence would prove nothing') remote = sum('MG_Remote' in line for line in symbols.splitlines()) applier = sum('MGPipeApply' in line for line in symbols.splitlines()) if (mode == 'disaggregated' and (not remote or not applier)) or (mode == 'monolith' and remote): raise RuntimeError(f'{mode} symbol mismatch: MG_Remote={remote}, MGPipeApply={applier}') return {'mode': mode, 'source_sha': subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip(), 'library': str(library), 'sha256': hashlib.sha256(library.read_bytes()).hexdigest(), 'defined_symbols': len(symbols.splitlines()), 'remote_symbols': remote, 'applier_symbols': applier, 'production_compilation': definitions} def inproc_log_proof(text): marker = 'Config: MOBILEGL_TRANSPORT=inproc - the MGPipe record stream' configs = re.findall(r'Config: IPC[^\r\n]*', text) required = ('strict=1', 'role-split-state=1', 'run-ahead=1') fatals = re.findall(r'^.*Fatal\{.*$', text, re.MULTILINE) if marker not in text or not configs or not all( all(re.search(r'\b' + re.escape(setting) + r'\b', line) for setting in required) for line in configs) or fatals: raise RuntimeError('library log does not prove real inproc / strict / role state / zero Fatal') return {'transport': 'inproc', 'ipc_config_lines': configs, 'fatal_count': 0} def logs_proof(discovery, junit): spec = importlib.util.spec_from_file_location('split_log_paths', ROOT / 'MobileGL/MG_IntegrationTest/Harness/split_log_paths.py') helper = importlib.util.module_from_spec(spec) spec.loader.exec_module(helper) lane = json.loads(discovery.read_text()) names = [test['name'] for test in lane['tests']] if len(names) != 2 or set(names) != RSP_CASES: raise RuntimeError(f'primary inproc proof must execute both exact backend cases: {names}') helper.require_green(lane, str(junit), sorted(RSP_CASES)) logs = helper.marker_log_paths(lane) proof = {} for name in sorted(RSP_CASES): # MOBILEGL_LOG_FILE_PATH is a base name. In split builds neither role # writes that literal path, and the server half owns applier failures. paths = [Path(path) for path in helper.role_paths(logs[name])] text = '\n'.join(path.read_text(encoding='utf-8', errors='replace') for path in paths) proof[name] = {'private_logs': [str(path) for path in paths], **inproc_log_proof(text)} return proof def self_test(): marker = 'Config: MOBILEGL_TRANSPORT=inproc - the MGPipe record stream crosses a real ring' config = 'Config: IPC run-ahead=1 strict=1 role-split-state=1' good = marker + '\n' + config inproc_log_proof(good) for bad in ('', 'Config: Accepted env variable: MOBILEGL_TRANSPORT=inproc\n' + config, good.replace('strict=1', 'strict=0'), good.replace('role-split-state=1', 'role-split-state=0'), good.replace('run-ahead=1', 'run-ahead=0'), good + '\nFatal{ProtocolCorruption, example}'): try: inproc_log_proof(bad) except RuntimeError: pass else: raise AssertionError('invalid runtime log passed') with tempfile.TemporaryDirectory(prefix='mobilegl-runtime-proof-') as directory: build = Path(directory) discovery = build / 'lane.json' junit = build / 'lane.xml' discovery.write_text(json.dumps({'tests': [ {'name': name, 'properties': [{'name': 'ENVIRONMENT', 'value': ['MOBILEGL_LOG_FILE_PATH=' + str(build / (name + '.log'))]}]} for name in sorted(RSP_CASES)]})) junit.write_text('' + ''.join(f'' for name in sorted(RSP_CASES)) + '') for name in RSP_CASES: (build / (name + '.client.log')).write_text(good) (build / (name + '.server.log')).write_text('server role\n') logs_proof(discovery, junit) server = build / (sorted(RSP_CASES)[0] + '.server.log') server.write_text('Fatal{ProtocolCorruption, server-side example}\n') try: logs_proof(discovery, junit) except RuntimeError: pass else: raise AssertionError('server-side Fatal passed the runtime log proof') server.unlink() try: logs_proof(discovery, junit) except FileNotFoundError: pass else: raise AssertionError('missing server log passed the runtime log proof') (build / 'libMobileGL.so').write_bytes(b'checker fixture, not a real runtime') normal = ''.join(f'00000000 T fixture{i}\n' for i in range(1000)) pushed = normal + '00000000 T MGPipeApply_fixture\n' remote = pushed + '00000000 T MG_Remote_fixture\n' for mode, flags, symbols, succeeds in ( ('monolith', '', normal, True), ('monolith', '-DMOBILEGL_PIPE_PUSH=1', pushed, True), ('disaggregated', '-DMOBILEGL_BUILD_DISAGGREGATED=1 -DMOBILEGL_PIPE_PUSH=1', remote, True), ('disaggregated', '-DMOBILEGL_BUILD_DISAGGREGATED=1', remote, True), ('disaggregated', '-DMOBILEGL_PIPE_PUSH=1', remote, False), ('disaggregated', '-DMOBILEGL_BUILD_DISAGGREGATED=1 -DMOBILEGL_PIPE_PUSH=1', normal, False), ('disaggregated', '-DMOBILEGL_BUILD_DISAGGREGATED=1', normal + '00000000 T MG_Remote_fixture\n', False), ('monolith', '', remote, False), ('monolith', '-DMOBILEGL_BUILD_DISAGGREGATED=1', pushed, False), ('monolith', '', '00000000 T stripped\n', False), ): (build / 'compile_commands.json').write_text(json.dumps([ {'file': '/fixture/MobileGL/ConfigLoader.cpp', 'command': f'clang++ {flags} -c ConfigLoader.cpp'}])) with patch('subprocess.check_output', side_effect=lambda args, **kwargs: symbols if args[0] == 'nm' else 'fixture-sha\n'): try: build_proof(build, mode) except RuntimeError: if succeeds: raise else: if not succeeds: raise AssertionError('invalid compiled shape passed') print('runtime mode proof: compiled-shape and actual-transport positive/negative controls passed') def main(): parser = argparse.ArgumentParser(description=__doc__) sub = parser.add_subparsers(dest='command', required=True) sub.add_parser('self-test') build = sub.add_parser('build') build.add_argument('--build-dir', type=Path, required=True) build.add_argument('--mode', choices=('disaggregated', 'monolith'), required=True) logs = sub.add_parser('logs') logs.add_argument('--discovery', type=Path, required=True) logs.add_argument('--junit', type=Path, required=True) for child in (build, logs): child.add_argument('--output', type=Path, required=True) args = parser.parse_args() if args.command == 'self-test': self_test() return proof = build_proof(args.build_dir, args.mode) if args.command == 'build' else logs_proof(args.discovery, args.junit) args.output.write_text(json.dumps(proof, indent=2), encoding='utf-8') print(f'{args.command} runtime proof written to {args.output}') if __name__ == '__main__': main()