pluginbench 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pluginbench/__init__.py +3 -0
- pluginbench/__main__.py +3 -0
- pluginbench/assessment-charts.html +790 -0
- pluginbench/assessment.py +316 -0
- pluginbench/assessment_charts.py +1225 -0
- pluginbench/assessment_metrics.py +1956 -0
- pluginbench/assessment_models.py +127 -0
- pluginbench/baselines.py +792 -0
- pluginbench/claude.py +374 -0
- pluginbench/cli.py +672 -0
- pluginbench/code_diff.py +207 -0
- pluginbench/codex.py +451 -0
- pluginbench/codex_events.py +585 -0
- pluginbench/collectors.py +591 -0
- pluginbench/compare.py +927 -0
- pluginbench/components.py +129 -0
- pluginbench/coverage_report.py +610 -0
- pluginbench/datasets.py +207 -0
- pluginbench/evaluation.example.yaml +45 -0
- pluginbench/evaluation.py +1651 -0
- pluginbench/evaluation_report.py +1004 -0
- pluginbench/fallow_parse.mjs +101 -0
- pluginbench/harbor_backend.py +2946 -0
- pluginbench/isolation.py +314 -0
- pluginbench/judge.py +683 -0
- pluginbench/language_analysis.py +915 -0
- pluginbench/ledger.py +217 -0
- pluginbench/measurement_profiles.py +616 -0
- pluginbench/native_treatment_runtime.js +221 -0
- pluginbench/native_treatments.py +155 -0
- pluginbench/observer_data/go-analysis-v1/go.mod +10 -0
- pluginbench/observer_data/go-analysis-v1/go.sum +10 -0
- pluginbench/observer_data/go-analysis-v1/main.go +64 -0
- pluginbench/observer_data/oxc-parser-0.126.0/package-lock.json +449 -0
- pluginbench/observer_data/oxc-parser-0.126.0/package.json +5 -0
- pluginbench/parallel.py +101 -0
- pluginbench/processes.py +100 -0
- pluginbench/remeasure.py +992 -0
- pluginbench/run_lock.py +227 -0
- pluginbench/run_status.py +518 -0
- pluginbench/study.py +1858 -0
- pluginbench/study_review.py +924 -0
- pluginbench/submissions.py +486 -0
- pluginbench/task_sources.py +403 -0
- pluginbench/treatments.py +388 -0
- pluginbench/trial_containers.py +94 -0
- pluginbench/vendor/ECHARTS-LICENSE +222 -0
- pluginbench/vendor/ECHARTS-NOTICE +5 -0
- pluginbench/vendor/ZRENDER-LICENSE +29 -0
- pluginbench/vendor/echarts.min.js +48 -0
- pluginbench-0.5.0.dist-info/METADATA +294 -0
- pluginbench-0.5.0.dist-info/RECORD +58 -0
- pluginbench-0.5.0.dist-info/WHEEL +4 -0
- pluginbench-0.5.0.dist-info/entry_points.txt +2 -0
- pluginbench-0.5.0.dist-info/licenses/LICENSE +201 -0
- pluginbench-0.5.0.dist-info/licenses/src/pluginbench/vendor/ECHARTS-LICENSE +222 -0
- pluginbench-0.5.0.dist-info/licenses/src/pluginbench/vendor/ECHARTS-NOTICE +5 -0
- pluginbench-0.5.0.dist-info/licenses/src/pluginbench/vendor/ZRENDER-LICENSE +29 -0
pluginbench/run_lock.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Own one run directory: a lock naming its process group, and a stop that signals only that group.
|
|
2
|
+
|
|
3
|
+
This lives outside processes.py on purpose: processes.py is part of the vanilla launcher hash,
|
|
4
|
+
so editing it would change every vanilla cache key.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import fcntl
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import signal
|
|
13
|
+
import subprocess
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
from collections.abc import Iterator
|
|
17
|
+
from contextlib import contextmanager
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from datetime import UTC, datetime
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from types import FrameType
|
|
22
|
+
|
|
23
|
+
LOCK_NAME = "run.lock"
|
|
24
|
+
_PROBE_GRACE = 0.5
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RunMoved(ValueError):
|
|
28
|
+
"""The run directory is no longer at the path its lock recorded."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def process_start(pid: int) -> str:
|
|
32
|
+
"""The kernel's start time for `pid`, so a reused PID never matches a recorded owner."""
|
|
33
|
+
if Path("/proc/self/stat").is_file():
|
|
34
|
+
try:
|
|
35
|
+
stat = Path(f"/proc/{pid}/stat").read_text()
|
|
36
|
+
except FileNotFoundError as exc:
|
|
37
|
+
raise ProcessLookupError(pid) from exc
|
|
38
|
+
# Field 22 is starttime. Split after the last ")" because the command name may hold
|
|
39
|
+
# spaces or parentheses; the first field after it is field 3.
|
|
40
|
+
return stat.rsplit(")", 1)[1].split()[19]
|
|
41
|
+
result = subprocess.run(
|
|
42
|
+
["ps", "-o", "lstart=", "-p", str(pid)], capture_output=True, text=True, check=False
|
|
43
|
+
)
|
|
44
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
45
|
+
raise ProcessLookupError(pid)
|
|
46
|
+
return result.stdout.strip()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class RunLock:
|
|
51
|
+
root: Path
|
|
52
|
+
fd: int
|
|
53
|
+
|
|
54
|
+
def assert_in_place(self) -> None:
|
|
55
|
+
"""Raise RunMoved when the root path no longer leads to the lock this process holds."""
|
|
56
|
+
held = os.fstat(self.fd)
|
|
57
|
+
try:
|
|
58
|
+
current = os.stat(self.root / LOCK_NAME)
|
|
59
|
+
except FileNotFoundError:
|
|
60
|
+
current = None
|
|
61
|
+
if current is None or (current.st_dev, current.st_ino) != (held.st_dev, held.st_ino):
|
|
62
|
+
raise RunMoved(f"run directory moved away from {self.root}; no further work starts")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# Roots whose lock this process holds now. The owner record can lag a handover, so it cannot
|
|
66
|
+
# say whether this process still holds a lock.
|
|
67
|
+
_held: set[Path] = set()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def held_here(root: Path) -> bool:
|
|
71
|
+
"""Whether this process holds root/run.lock right now."""
|
|
72
|
+
return root.resolve() in _held
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _owner(fd: int) -> dict[str, object] | None:
|
|
76
|
+
text = os.pread(fd, 1 << 16, 0).decode()
|
|
77
|
+
if not text.strip():
|
|
78
|
+
return None
|
|
79
|
+
value = json.loads(text)
|
|
80
|
+
if not isinstance(value, dict):
|
|
81
|
+
raise ValueError("run.lock must hold a JSON object")
|
|
82
|
+
return value
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _interrupt(signum: int, frame: FrameType | None) -> None:
|
|
86
|
+
# Reuse the interrupt path: no new attempts start and saved work is checkpointed.
|
|
87
|
+
raise KeyboardInterrupt("stopped by SIGTERM")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@contextmanager
|
|
91
|
+
def deferred_stop() -> Iterator[None]:
|
|
92
|
+
"""Hold SIGTERM and SIGINT until the block ends, then deliver them in arrival order.
|
|
93
|
+
|
|
94
|
+
For writes that must land together: `stop` and Ctrl-C otherwise interrupt between any two
|
|
95
|
+
bytecodes. Keep the block to those writes. Only the main thread runs signal handlers, so
|
|
96
|
+
elsewhere nothing can interrupt the block and it runs as is.
|
|
97
|
+
"""
|
|
98
|
+
if threading.current_thread() is not threading.main_thread():
|
|
99
|
+
yield
|
|
100
|
+
return
|
|
101
|
+
pending: list[int] = []
|
|
102
|
+
|
|
103
|
+
def defer(signum: int, frame: FrameType | None) -> None:
|
|
104
|
+
pending.append(signum)
|
|
105
|
+
|
|
106
|
+
# An ignored signal stays ignored. A handler installed outside Python cannot be put back.
|
|
107
|
+
previous = {
|
|
108
|
+
item: handler
|
|
109
|
+
for item in (signal.SIGTERM, signal.SIGINT)
|
|
110
|
+
if (handler := signal.getsignal(item)) not in (None, signal.SIG_IGN)
|
|
111
|
+
}
|
|
112
|
+
try:
|
|
113
|
+
for item in previous:
|
|
114
|
+
signal.signal(item, defer)
|
|
115
|
+
yield
|
|
116
|
+
finally:
|
|
117
|
+
failure: BaseException | None = None
|
|
118
|
+
while True:
|
|
119
|
+
try:
|
|
120
|
+
for item, handler in previous.items():
|
|
121
|
+
signal.signal(item, handler)
|
|
122
|
+
break
|
|
123
|
+
except BaseException as exc:
|
|
124
|
+
# A signal that arrives now runs a handler already restored, which may raise
|
|
125
|
+
# anywhere in the loop. Keep the first error and restore every handler again.
|
|
126
|
+
failure = failure or exc
|
|
127
|
+
if failure is not None:
|
|
128
|
+
raise failure
|
|
129
|
+
# In arrival order: a handler that returns must not swallow a later stop.
|
|
130
|
+
for signum in pending:
|
|
131
|
+
signal.raise_signal(signum)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@contextmanager
|
|
135
|
+
def hold(root: Path) -> Iterator[RunLock]:
|
|
136
|
+
"""Own `root` while the block runs; SIGTERM becomes KeyboardInterrupt in this process.
|
|
137
|
+
|
|
138
|
+
The flock is released when the process exits, even by SIGKILL, so a lock that can be
|
|
139
|
+
acquired always belongs to a dead owner. The file stays behind and keeps the last root.
|
|
140
|
+
"""
|
|
141
|
+
root = root.resolve()
|
|
142
|
+
fd = os.open(root / LOCK_NAME, os.O_RDWR | os.O_CREAT, 0o644)
|
|
143
|
+
try:
|
|
144
|
+
# `status` probes ownership with a brief shared flock; wait that out, not a real owner.
|
|
145
|
+
deadline = time.monotonic() + _PROBE_GRACE
|
|
146
|
+
while True:
|
|
147
|
+
try:
|
|
148
|
+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
149
|
+
break
|
|
150
|
+
except BlockingIOError as exc:
|
|
151
|
+
if time.monotonic() > deadline:
|
|
152
|
+
raise ValueError(
|
|
153
|
+
f"run is already active: {root}; stop it with `pluginbench stop {root}`"
|
|
154
|
+
) from exc
|
|
155
|
+
time.sleep(0.02)
|
|
156
|
+
previous = _owner(fd)
|
|
157
|
+
if previous is not None and previous.get("root") != str(root):
|
|
158
|
+
raise ValueError(
|
|
159
|
+
f"run was at {previous.get('root')} and is now at {root}; if you moved a stopped "
|
|
160
|
+
f"run on purpose, delete {root / LOCK_NAME} and run it again"
|
|
161
|
+
)
|
|
162
|
+
owner = {
|
|
163
|
+
"pid": os.getpid(),
|
|
164
|
+
"pgid": os.getpgrp(),
|
|
165
|
+
"process_start": process_start(os.getpid()),
|
|
166
|
+
"started_at": datetime.now(UTC).isoformat(),
|
|
167
|
+
"root": str(root),
|
|
168
|
+
}
|
|
169
|
+
os.ftruncate(fd, 0)
|
|
170
|
+
os.pwrite(fd, (json.dumps(owner, indent=2) + "\n").encode(), 0)
|
|
171
|
+
os.fsync(fd)
|
|
172
|
+
previous_handler = signal.signal(signal.SIGTERM, _interrupt)
|
|
173
|
+
try:
|
|
174
|
+
_held.add(root)
|
|
175
|
+
yield RunLock(root, fd)
|
|
176
|
+
finally:
|
|
177
|
+
_held.discard(root)
|
|
178
|
+
signal.signal(signal.SIGTERM, previous_handler)
|
|
179
|
+
finally:
|
|
180
|
+
os.close(fd)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def owner(root: Path) -> dict[str, object] | None:
|
|
184
|
+
"""The recorded owner while a live process holds root/run.lock; otherwise None."""
|
|
185
|
+
path = root.resolve() / LOCK_NAME
|
|
186
|
+
if not path.is_file():
|
|
187
|
+
return None
|
|
188
|
+
fd = os.open(path, os.O_RDONLY)
|
|
189
|
+
try:
|
|
190
|
+
try:
|
|
191
|
+
fcntl.flock(fd, fcntl.LOCK_SH | fcntl.LOCK_NB)
|
|
192
|
+
except BlockingIOError:
|
|
193
|
+
# Held: the owner may not have written its record yet.
|
|
194
|
+
return _owner(fd) or {}
|
|
195
|
+
return None
|
|
196
|
+
finally:
|
|
197
|
+
os.close(fd)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def stop(root: Path) -> dict[str, object] | None:
|
|
201
|
+
"""SIGTERM the process group recorded in root/run.lock; None when no live run owns it.
|
|
202
|
+
|
|
203
|
+
Nothing is signalled unless the lock is held and its recorded process still has the
|
|
204
|
+
recorded start time and group. This command's own group is never signalled.
|
|
205
|
+
"""
|
|
206
|
+
if not (root.resolve() / LOCK_NAME).is_file():
|
|
207
|
+
raise ValueError(f"no run lock in {root}; it never started here")
|
|
208
|
+
current = owner(root)
|
|
209
|
+
if current is None:
|
|
210
|
+
return None
|
|
211
|
+
if not current:
|
|
212
|
+
raise ValueError("run.lock is held but names no owner yet; try again")
|
|
213
|
+
pid, pgid = current.get("pid"), current.get("pgid")
|
|
214
|
+
if type(pid) is not int or type(pgid) is not int:
|
|
215
|
+
raise ValueError("run.lock owner must record integer pid and pgid")
|
|
216
|
+
try:
|
|
217
|
+
matches = process_start(pid) == current.get("process_start") and os.getpgid(pid) == pgid
|
|
218
|
+
except ProcessLookupError:
|
|
219
|
+
matches = False
|
|
220
|
+
if not matches:
|
|
221
|
+
raise ValueError(
|
|
222
|
+
f"run.lock owner {pid} no longer matches its recorded process; nothing signalled"
|
|
223
|
+
)
|
|
224
|
+
if pgid == os.getpgrp():
|
|
225
|
+
raise ValueError("refusing to signal this command's own process group")
|
|
226
|
+
os.killpg(pgid, signal.SIGTERM)
|
|
227
|
+
return current
|