a73x

test/native_stress.py

Ref:   Size: 6.7 KiB   History

#!/usr/bin/env python3
"""Raw cat /dev/random with same-daemon and separate-daemon GUI neighbours.

Run through `make native-stress`: its build graph supplies both release binaries.
Linux /proc provides independent producer identity and daemon read-progress
oracles. Producer write accounting misses GNU cat's splice path.
"""
import hashlib
import json
import os
from pathlib import Path
import shlex
import signal
import subprocess
import sys
import time

from native_lifecycle import LifecycleRig, start_workspace
from native_resize import by_id
from native_tiling import eventually, output_flood, require


class StressRig(LifecycleRig):
    # Sample completed painted state more closely than the general integration
    # harness's 40 ms polling. These remain observed upper bounds, not GPU timings.
    def artifact(self, command, suffix):
        self.serial += 1
        path = self.root / (str(self.serial) + suffix)
        self.send(command + ':' + str(path))
        deadline = time.monotonic() + 5
        while not path.exists() and self.gui.poll() is None and time.monotonic() < deadline:
            time.sleep(.005)
        require(path.exists(), 'GUI did not produce ' + command)
        return path

    def painted(self, pane, marker):
        deadline = time.monotonic() + 2
        while time.monotonic() < deadline:
            state = self.state()
            if marker in by_id(state)[pane]['painted_text']:
                return state
            time.sleep(.005)
        raise AssertionError('neighbour did not paint ' + marker)


def main():
    require(sys.platform == 'linux', 'native-stress currently requires Linux /proc')
    r = StressRig(*sys.argv[1:3])
    producer = None
    report = {'artifacts': str(r.root), 'samples': []}
    report_path = r.root / 'stress-result.json'

    def cat_alive():
        try:
            return Path(f'/proc/{producer}/cmdline').read_bytes().startswith(b'cat\0/dev/random\0')
        except FileNotFoundError:
            return False

    def read_count(pid):
        fields = dict(line.split(': ', 1) for line in Path(f'/proc/{pid}/io').read_text().splitlines())
        return int(fields['rchar'])

    def probe(pane, sock, session, iteration):
        r.focus(pane)
        marker = f'RAW-NEIGHBOUR-{iteration}-{pane}'
        before = time.monotonic()
        # The contiguous marker is absent from shell input, so echo alone cannot
        # satisfy the check before the shell executes our command.
        r.shell("printf '\\033[32mRAW-NEIGHBOUR-%s\\033[0m\\n' " + shlex.quote(f'{iteration}-{pane}'))
        state = r.painted(pane, marker)
        delay_ms = (time.monotonic() - before) * 1000
        report['samples'].append({'pane': pane, 'session': session,
                                  'observed_input_to_painted_ms': round(delay_ms, 1)})
        require(delay_ms < 250, f'neighbour exceeded 250 ms: {delay_ms:.1f} ms')
        require(all(p['phase'] == 'attached' for p in state['panes']), 'pane lost attachment')
        # Independently check the authoritative grid outside the latency interval.
        r.wait_marker(sock, session, marker)

    try:
        version = subprocess.check_output([r.muxg, '--version'], text=True)
        require('ReleaseSafe' in version or 'ReleaseFast' in version, 'stress requires release binaries')
        report['binaries'] = {path: hashlib.sha256(Path(path).read_bytes()).hexdigest()
                              for path in (r.mux, r.muxg)}
        refs = start_workspace(r)
        output_flood(r, refs)
        ids = list(refs)
        flooded, separate, shared = ids
        report['panes'] = {'flooded': flooded, 'separate_daemon': separate, 'shared_daemon': shared}
        daemon_pid = next(proc.pid for sock, proc in r.daemons if sock == refs[flooded][0])
        r.focus(flooded)
        pid_path = r.root / 'random.pid'
        r.shell('sh -c ' + shlex.quote('printf "%s\\n" $$ > ' + shlex.quote(str(pid_path)) +
                                     '; exec cat /dev/random'))
        eventually(lambda: pid_path.exists() and pid_path.stat().st_size, 'cat did not start')
        producer = int(pid_path.read_text())
        eventually(cat_alive, 'owned producer did not exec cat /dev/random')
        started = time.monotonic()
        baseline = read_count(daemon_pid)
        last_count = baseline
        round_no = 0
        restarted = False
        while time.monotonic() - started < 30:
            require(cat_alive(), 'raw producer exited early')
            for pane in (separate, shared):
                probe(pane, *refs[pane], round_no)
            if not restarted and time.monotonic() - started >= 10:
                r.chord('p')
                r.wait_state(lambda s: s['recovery'] is not None)
                r.key('escape')
                r.wait_state(lambda s: s['recovery'] is None)
                r.drag('beside', dx=-r.state()['cell_w'] * 3)
                r.kernel_sizes()
                report['before_reopen_frames'], report['before_reopen_p99_us'] = r.frames()
                r.quit()
                r.launch_gui([], 'gui-raw-restored')
                r.kernel_sizes()
                report['reopened_during_flood'] = True
                restarted = True
            time.sleep(.15)
            count = read_count(daemon_pid)
            require(count - last_count > 16 * 1024, 'raw flood made insufficient progress this round')
            last_count = count
            round_no += 1
        report['active_seconds'] = round(time.monotonic() - started, 1)
        report['daemon_bytes_read'] = last_count - baseline
        report['final_frames'], report['final_p99_us'] = r.frames()
        require(restarted and cat_alive(), 'reopen did not run during active raw output')
        require(report['daemon_bytes_read'] > 256 * 1024, 'raw flood made insufficient total progress')
        require(report['before_reopen_frames'] > 0 and report['final_frames'] > 0, 'no frames during flood')
        require(max(report['before_reopen_p99_us'], report['final_p99_us']) < 20000,
                'raw flood exceeded 20 ms frame p99 budget')
        os.kill(producer, signal.SIGTERM)
        producer = None
        for pane in (separate, shared):
            probe(pane, *refs[pane], 'after-stop')
        r.quit()
        r.assert_cli_untouched()
        report['passed'] = True
        print(json.dumps(report, indent=2), flush=True)
        print('native stress OK', flush=True)
    except BaseException:
        r.failure_artifacts()
        raise
    finally:
        try:
            if producer is not None and cat_alive():
                os.kill(producer, signal.SIGTERM)
        except ProcessLookupError:
            pass
        try:
            report_path.write_text(json.dumps(report, indent=2) + '\n')
        finally:
            r.close()


if __name__ == '__main__':
    main()