observe-index.py 3.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. #!/usr/bin/env python3
  2. """Observe one owned benchmark child on Linux, without a wall-clock timeout.
  3. Usage: python3 observe-index.py NEW_RUN_DIR -- node measure-index.cjs ENGINE FIXTURE NEW_RUN_DIR native
  4. The child retains the caller's environment/affinity. No host settings are changed.
  5. """
  6. import json
  7. import os
  8. from pathlib import Path
  9. import subprocess
  10. import sys
  11. import time
  12. def read(path):
  13. try:
  14. return Path(path).read_text()
  15. except (FileNotFoundError, PermissionError, ProcessLookupError):
  16. return None
  17. def sample(pid):
  18. tasks = {}
  19. try:
  20. for task in Path(f'/proc/{pid}/task').iterdir():
  21. tasks[task.name] = {name: read(task / name) for name in ('stat', 'schedstat', 'wchan')}
  22. except (FileNotFoundError, ProcessLookupError):
  23. pass
  24. # Both common cgroup layouts; unavailable counters are recorded as null.
  25. paths = [
  26. '/proc/stat', '/proc/meminfo', '/proc/loadavg', '/proc/diskstats',
  27. '/proc/pressure/cpu', '/proc/pressure/io', '/proc/pressure/memory',
  28. '/sys/fs/cgroup/cpu.stat', '/sys/fs/cgroup/cpu.max',
  29. '/sys/fs/cgroup/memory.current', '/sys/fs/cgroup/memory.max',
  30. '/sys/fs/cgroup/cpu,cpuacct/cpu.stat',
  31. '/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us',
  32. '/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us',
  33. '/sys/fs/cgroup/memory/memory.usage_in_bytes',
  34. '/sys/fs/cgroup/memory/memory.limit_in_bytes',
  35. ]
  36. return {'epochMs': time.time() * 1000,
  37. 'process': {name: read(f'/proc/{pid}/{name}') for name in ('stat', 'status', 'io')},
  38. 'threads': tasks, 'host': {path: read(path) for path in paths}}
  39. def main():
  40. if sys.platform != 'linux' or len(sys.argv) < 4 or sys.argv[2] != '--':
  41. raise SystemExit(__doc__)
  42. out = Path(sys.argv[1]).resolve()
  43. out.mkdir(parents=True, exist_ok=False) # Preserve completed/interrupted runs.
  44. argv = sys.argv[3:]
  45. command = {'argv': argv, 'startedEpochMs': time.time() * 1000,
  46. 'affinity': sorted(os.sched_getaffinity(0)), 'wallTimeout': None}
  47. command_path = out / 'command.json'
  48. command_path.write_text(json.dumps(command, indent=2))
  49. start = time.monotonic()
  50. with (out / 'console.log').open('w') as log, (out / 'resources.ndjson').open('w') as resources:
  51. # Inherit the foreground process group: Ctrl-C reaches this owned child
  52. # too. Never signal a PID discovered outside this invocation.
  53. child = subprocess.Popen(argv, stdout=log, stderr=subprocess.STDOUT)
  54. command['pid'] = child.pid
  55. command_path.write_text(json.dumps(command, indent=2))
  56. next_notice = start
  57. try:
  58. while child.poll() is None:
  59. resources.write(json.dumps(sample(child.pid)) + '\n')
  60. resources.flush()
  61. now = time.monotonic()
  62. if now >= next_notice:
  63. print(f'Benchmark PID {child.pid}: {now-start:.0f}s elapsed; progress in {out}', flush=True)
  64. next_notice = now + 30
  65. time.sleep(2)
  66. except BaseException:
  67. # Record an explicit interrupted outcome, even when no final result
  68. # exists. Give only our child a chance to stop, then reap it.
  69. child.terminate()
  70. try:
  71. child.wait(timeout=10)
  72. except subprocess.TimeoutExpired:
  73. child.kill()
  74. child.wait()
  75. command['interrupted'] = True
  76. raise
  77. finally:
  78. command.update(exit=child.poll(), wallSec=time.monotonic()-start)
  79. command_path.write_text(json.dumps(command, indent=2))
  80. return child.returncode
  81. if __name__ == '__main__':
  82. sys.exit(main())