Files
docview/tests/measure_interaction.py
T
2026-09-21 13:41:40 +09:00

96 lines
5.3 KiB
Python

#!/usr/bin/env python3
"""Run the opt-in production GUI T20 interaction workload and pin its evidence."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import platform
import subprocess
import sys
import time
def hashed(path, label):
with path.open("rb") as stream:
digest = hashlib.file_digest(stream, "sha256").hexdigest()
return {"path": label, "size": path.stat().st_size, "sha256": digest}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--binary", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
source = Path(__file__).resolve().parents[1]
binary = args.binary.resolve()
output = args.output.resolve()
if not binary.is_file():
parser.error("--binary must be an existing test_app executable")
if output.exists():
parser.error("--output must be a new directory to preserve earlier evidence")
if os.environ.get("QTWEBENGINE_DISABLE_SANDBOX") or any(x in os.environ.get("QTWEBENGINE_CHROMIUM_FLAGS", "")
for x in ("--no-sandbox", "--disable-setuid-sandbox")):
parser.error("Sandbox disabling is not allowed for this measurement")
output.mkdir(parents=True)
suffix = ".exe" if platform.system() == "Windows" else ""
binaries = [binary, binary.parent / ("docview-pdf-worker" + suffix), binary.parent / ("docview-archive-worker" + suffix)]
app = binary.parent / ("docview" + suffix)
if app.exists():
binaries.append(app) # Recorded for comparison; the tested shell is test_app.
pdfium = source / ".deps/pdfium/lib/libpdfium.so"
if pdfium.exists():
binaries.append(pdfium)
source_files = [source / "CMakeLists.txt", source / "resources.qrc", source / "tests/test_app.cpp",
source / "tests/interaction_measurement.h", Path(__file__).resolve()]
for directory in ("src", "qml", "resources"):
source_files.extend(p for p in (source / directory).rglob("*") if p.is_file() and p.suffix in (".cpp", ".h", ".qml", ".js", ".toml"))
source_files = sorted(set(source_files))
before_binaries = [hashed(p, p.name) for p in binaries]
before_sources = [hashed(p, str(p.relative_to(source))) for p in source_files]
environment = os.environ.copy()
environment["DOCVIEW_INTERACTION_OUTPUT"] = str(output)
command = [str(binary), "measureInteractionWorkload"]
start = time.monotonic()
try:
run = subprocess.run(command, env=environment, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
timeout=180, check=False)
log, code = run.stdout, run.returncode
except subprocess.TimeoutExpired as error:
log, code = error.stdout or b"", 124
(output / "test.log").write_bytes(log)
after_binaries = [hashed(p, p.name) for p in binaries]
after_sources = [hashed(p, str(p.relative_to(source))) for p in source_files]
measurement_path = output / "measurement.json"
measurement = json.loads(measurement_path.read_text()) if measurement_path.exists() else {}
cpu = ""
if Path("/proc/cpuinfo").exists():
cpu = next((line.partition(":")[2].strip() for line in Path("/proc/cpuinfo").read_text().splitlines()
if line.startswith("model name")), "")
mem = ""
if Path("/proc/meminfo").exists():
mem = next((line.partition(":")[2].strip() for line in Path("/proc/meminfo").read_text().splitlines()
if line.startswith("MemTotal:")), "")
stable = before_binaries == after_binaries and before_sources == after_sources
record = {"schemaVersion": 1, "command": [binary.name, "measureInteractionWorkload"],
"testedExecutable": binary.name, "buildDirectory": binary.parent.name,
"exitCode": code, "elapsedSeconds": time.monotonic() - start,
"success": code == 0 and measurement.get("success") is True and stable,
"binaryAndSourceHashesStableDuringRun": stable, "binaries": before_binaries,
"sources": before_sources, "host": {"platform": platform.platform(), "cpu": cpu,
"logicalCpuCount": os.cpu_count(), "memTotal": mem},
"measurement": hashed(measurement_path, "measurement.json") if measurement_path.exists() else None,
"limitations": ["Development-host synthetic workload, not the provisional reference machine.",
"Xvfb/software rendering when selected by the caller; no native GPU or physical scanout acceptance.",
"frameSwapped is a presentation-queue notification; Web is not correlated to Chromium frame IDs.",
"GUI timer gaps include host scheduling and instrumentation; they are not a direct measurement of exclusively application-caused stalls.",
"No OS cache flush. No Windows/Ubuntu acceptance inferred.",
"Normal app CTest deliberately skips this opt-in slot; this dedicated run executes it."]}
(output / "record.json").write_text(json.dumps(record, indent=2, ensure_ascii=False) + "\n")
print(json.dumps({"success": record["success"], "output": str(output), "exitCode": code}, ensure_ascii=False))
return 0 if record["success"] else 1
if __name__ == "__main__":
sys.exit(main())