a789495a98
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
251 lines
8.3 KiB
Python
251 lines
8.3 KiB
Python
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import pty
|
|
import select
|
|
import signal
|
|
import socket
|
|
import subprocess
|
|
import tempfile
|
|
import time
|
|
from pathlib import Path
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description="Profile a single resumed jcode spawn")
|
|
parser.add_argument("--binary", default="./target/selfdev/jcode")
|
|
parser.add_argument("--timeout", type=float, default=20.0)
|
|
parser.add_argument("--cwd", default=os.getcwd())
|
|
parser.add_argument("--json", action="store_true", help="Emit JSON summary")
|
|
return parser.parse_args()
|
|
|
|
|
|
def wait_for_socket(path: Path, timeout_s: float = 10.0) -> None:
|
|
deadline = time.time() + timeout_s
|
|
while time.time() < deadline:
|
|
if path.exists():
|
|
try:
|
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
sock.settimeout(0.2)
|
|
sock.connect(str(path))
|
|
sock.close()
|
|
return
|
|
except OSError:
|
|
pass
|
|
time.sleep(0.01)
|
|
raise RuntimeError(f"socket not ready: {path}")
|
|
|
|
|
|
def create_session(debug_sock: Path, cwd: str) -> str:
|
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
sock.connect(str(debug_sock))
|
|
req = {"type": "debug_command", "id": 1, "command": f"create_session:{cwd}"}
|
|
sock.sendall((json.dumps(req) + "\n").encode())
|
|
buf = b""
|
|
while True:
|
|
chunk = sock.recv(65536)
|
|
if not chunk:
|
|
break
|
|
buf += chunk
|
|
while b"\n" in buf:
|
|
line, buf = buf.split(b"\n", 1)
|
|
resp = json.loads(line.decode())
|
|
if resp.get("type") == "ack":
|
|
continue
|
|
if resp.get("type") == "error":
|
|
raise RuntimeError(resp.get("message") or resp)
|
|
if resp.get("type") != "debug_response":
|
|
continue
|
|
if not resp.get("ok", True):
|
|
raise RuntimeError(resp.get("output") or resp)
|
|
output = json.loads(resp["output"])
|
|
return output["session_id"]
|
|
raise RuntimeError("missing debug response")
|
|
|
|
|
|
def reply_queries(master_fd: int, buffer: bytes) -> bytes:
|
|
replies = [
|
|
(b"\x1b[6n", b"\x1b[1;1R"),
|
|
(b"\x1b[c", b"\x1b[?62;c"),
|
|
(b"\x1b]10;?\x1b\\", b"\x1b]10;rgb:ffff/ffff/ffff\x1b\\"),
|
|
(b"\x1b]11;?\x1b\\", b"\x1b]11;rgb:0000/0000/0000\x1b\\"),
|
|
(b"\x1b]10;?\x07", b"\x1b]10;rgb:ffff/ffff/ffff\x07"),
|
|
(b"\x1b]11;?\x07", b"\x1b]11;rgb:0000/0000/0000\x07"),
|
|
(b"\x1b[14t", b"\x1b[4;600;800t"),
|
|
(b"\x1b[16t", b"\x1b[6;16;8t"),
|
|
(b"\x1b[18t", b"\x1b[8;24;80t"),
|
|
(b"\x1b[?1016$p", b"\x1b[?1016;1$y"),
|
|
(b"\x1b[?2027$p", b"\x1b[?2027;1$y"),
|
|
(b"\x1b[?2031$p", b"\x1b[?2031;1$y"),
|
|
(b"\x1b[?1004$p", b"\x1b[?1004;1$y"),
|
|
(b"\x1b[?2004$p", b"\x1b[?2004;1$y"),
|
|
(b"\x1b[?2026$p", b"\x1b[?2026;1$y"),
|
|
]
|
|
changed = True
|
|
while changed:
|
|
changed = False
|
|
for query, response in replies:
|
|
if query in buffer:
|
|
os.write(master_fd, response)
|
|
buffer = buffer.replace(query, b"")
|
|
changed = True
|
|
return buffer
|
|
|
|
|
|
def latest_log_file(log_dir: Path) -> Path:
|
|
logs = sorted(log_dir.glob("jcode-*.log"), key=lambda p: p.stat().st_mtime)
|
|
if not logs:
|
|
raise RuntimeError(f"no log files found in {log_dir}")
|
|
return logs[-1]
|
|
|
|
|
|
def extract_timing_lines(log_path: Path) -> list[str]:
|
|
timing_lines = []
|
|
for line in log_path.read_text(errors="replace").splitlines():
|
|
if "[TIMING]" in line or "Startup Profile (" in line:
|
|
timing_lines.append(line)
|
|
return timing_lines
|
|
|
|
|
|
def profile_single_spawn(binary: str, cwd: str, timeout_s: float) -> dict:
|
|
root = Path(tempfile.mkdtemp(prefix="jcode-single-profile-"))
|
|
home = root / "home"
|
|
runtime_dir = root / "run"
|
|
socket_path = runtime_dir / "jcode.sock"
|
|
debug_socket_path = runtime_dir / "jcode-debug.sock"
|
|
home.mkdir(parents=True, exist_ok=True)
|
|
runtime_dir.mkdir(parents=True, exist_ok=True)
|
|
env = os.environ.copy()
|
|
env.update(
|
|
{
|
|
"JCODE_HOME": str(home),
|
|
"JCODE_RUNTIME_DIR": str(runtime_dir),
|
|
"JCODE_SOCKET": str(socket_path),
|
|
"JCODE_DEBUG_SOCKET": str(debug_socket_path),
|
|
"JCODE_SWARM_ENABLED": "0",
|
|
"JCODE_NO_TELEMETRY": "1",
|
|
"JCODE_TRACE": "1",
|
|
"JCODE_TEMP_SERVER": "1",
|
|
"JCODE_SERVER_OWNER_PID": str(os.getpid()),
|
|
}
|
|
)
|
|
|
|
server_proc = subprocess.Popen(
|
|
[binary, "--no-update", "--no-selfdev", "serve", "--socket", str(socket_path)],
|
|
env=env,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
preexec_fn=os.setsid,
|
|
)
|
|
try:
|
|
wait_for_socket(socket_path, timeout_s=min(timeout_s, 10.0))
|
|
wait_for_socket(debug_socket_path, timeout_s=min(timeout_s, 10.0))
|
|
session_id = create_session(debug_socket_path, cwd)
|
|
|
|
master_fd, slave_fd = pty.openpty()
|
|
client_start = time.perf_counter()
|
|
client_proc = subprocess.Popen(
|
|
[
|
|
binary,
|
|
"--no-update",
|
|
"--no-selfdev",
|
|
"--socket",
|
|
str(socket_path),
|
|
"--fresh-spawn",
|
|
"--resume",
|
|
session_id,
|
|
],
|
|
stdin=slave_fd,
|
|
stdout=slave_fd,
|
|
stderr=slave_fd,
|
|
env=env,
|
|
preexec_fn=os.setsid,
|
|
)
|
|
os.close(slave_fd)
|
|
os.set_blocking(master_fd, False)
|
|
|
|
buffer = b""
|
|
first_output_ms = None
|
|
last_output_at = None
|
|
deadline = time.perf_counter() + timeout_s
|
|
settle_after_output_s = 0.2
|
|
while time.perf_counter() < deadline:
|
|
if client_proc.poll() is not None:
|
|
break
|
|
rlist, _, _ = select.select([master_fd], [], [], 0.05)
|
|
if rlist:
|
|
chunk = os.read(master_fd, 65536)
|
|
if not chunk:
|
|
break
|
|
if first_output_ms is None:
|
|
first_output_ms = (time.perf_counter() - client_start) * 1000.0
|
|
last_output_at = time.perf_counter()
|
|
buffer += chunk
|
|
buffer = reply_queries(master_fd, buffer)
|
|
lower = buffer.lower()
|
|
if b"loading session" in lower or b"jcode" in lower or len(buffer) > 4096:
|
|
if time.perf_counter() - last_output_at >= settle_after_output_s:
|
|
break
|
|
elif last_output_at is not None and time.perf_counter() - last_output_at >= settle_after_output_s:
|
|
break
|
|
|
|
os.close(master_fd)
|
|
try:
|
|
os.killpg(client_proc.pid, signal.SIGTERM)
|
|
except ProcessLookupError:
|
|
pass
|
|
try:
|
|
client_proc.wait(timeout=0.2)
|
|
except Exception:
|
|
try:
|
|
os.killpg(client_proc.pid, signal.SIGKILL)
|
|
except ProcessLookupError:
|
|
pass
|
|
|
|
log_path = latest_log_file(home / "logs")
|
|
return {
|
|
"temp_root": str(root),
|
|
"log_path": str(log_path),
|
|
"session_id": session_id,
|
|
"first_output_ms": first_output_ms,
|
|
"timing_lines": extract_timing_lines(log_path),
|
|
}
|
|
finally:
|
|
try:
|
|
os.killpg(server_proc.pid, signal.SIGTERM)
|
|
except ProcessLookupError:
|
|
pass
|
|
try:
|
|
server_proc.wait(timeout=1.0)
|
|
except Exception:
|
|
try:
|
|
os.killpg(server_proc.pid, signal.SIGKILL)
|
|
except ProcessLookupError:
|
|
pass
|
|
|
|
|
|
def main() -> None:
|
|
args = parse_args()
|
|
result = profile_single_spawn(args.binary, args.cwd, args.timeout)
|
|
if args.json:
|
|
print(json.dumps(result, indent=2))
|
|
return
|
|
|
|
print(f"temp root: {result['temp_root']}")
|
|
print(f"session id: {result['session_id']}")
|
|
print(f"log path: {result['log_path']}")
|
|
if result["first_output_ms"] is not None:
|
|
print(f"first output: {result['first_output_ms']:.1f}ms")
|
|
else:
|
|
print("first output: none")
|
|
print("timings:")
|
|
for line in result["timing_lines"]:
|
|
print(f" {line}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|