Commit b48ee82a authored by Andrey Filippov's avatar Andrey Filippov

CLAUDE: perfd - 'measure' now PINS the CPU frequency (min=max) + per-hold overrides

Andrey's observation on the first perfd-held run: CPU boosting to 4.9 GHz,
100 C. Calibration (openssl all-core): the i9-11900K spikes 4.8 GHz @ 85 C,
hits 100 C within seconds and throttles to a wobbling 3.2-3.5 GHz - the
'performance' boost bias is itself a nondeterminism source AND rides Tjmax.
- measure profile: CPU pinned via sysfs scaling_min=max (default 3.2 GHz =
  the measured all-core throttle equilibrium, so the pin holds even in the
  worst phase and runs cooler; PERFD_CPU_LOCK_KHZ) + the existing GPU lock.
  PPD 'performance' hold now only backs the max profile.
- HOLD accepts bounds-checked per-hold overrides (HOLD measure cpu=KHZ
  gpu=MHZ; perfctl passes them through) so lock-value calibration needs no
  daemon reconfiguration/sudo.
- Holds now per-connection with values from the newest holder of the
  winning profile; STATUS extended.
User-mode tested: override parsing/apply, bounds rejection, permission
errors surfaced, auto-release. Root paths activate on reinstall.
Co-authored-by: 's avatarClaude Opus 4.8 <claude-opus-4-8@anthropic.com>
Co-authored-by: 's avatarClaude Fable 5 <claude-fable-5@anthropic.com>
parent 74d8341c
...@@ -24,11 +24,17 @@ install command above when absent (and continues normally). ...@@ -24,11 +24,17 @@ install command above when absent (and continues normally).
perfctl hold max # production profile until Ctrl-C perfctl hold max # production profile until Ctrl-C
Profiles: Profiles:
- `measure` — CPU `performance` + GPU persistence + clocks **locked** - `measure` — CPU frequency **pinned** (`scaling_min=max`, default 3.2 GHz,
(default 2550 MHz, `PERFD_GPU_LOCK_MHZ` in the unit): run-to-run timing `PERFD_CPU_LOCK_KHZ`) + GPU persistence + GPU clocks **locked** (default
reproducibility (±1% instead of ±10% with free-running boost) for per-item 2550 MHz, `PERFD_GPU_LOCK_MHZ`): run-to-run timing reproducibility (±1%
performance evaluation. instead of ±10% with free-running boost) for per-item performance
- `max` — CPU `performance` + GPU persistence, clocks unlocked: throughput. evaluation. Pinned, not `performance`: the i9-11900K at free boost hits
4.9 GHz → 100 °C → throttle wobble within seconds of an all-core phase
(measured 07/17/2026) — boost bias is itself a nondeterminism source.
Per-hold overrides: `perfctl hold measure cpu=3400000 gpu=2400 -- <cmd>`
(bounds-checked; for calibration without reconfiguration).
- `max` — CPU `performance` profile (free boost) + GPU persistence, clocks
unlocked: throughput.
A hold is tied to the client's connection — process exit or crash releases A hold is tied to the client's connection — process exit or crash releases
it automatically (refcounted across concurrent holders; `measure` outranks it automatically (refcounted across concurrent holders; `measure` outranks
......
...@@ -11,6 +11,7 @@ ExecStart=/usr/bin/python3 /usr/local/lib/elphel-perfd/elphel_perfd.py ...@@ -11,6 +11,7 @@ ExecStart=/usr/bin/python3 /usr/local/lib/elphel-perfd/elphel_perfd.py
# Config overrides (defaults live in the script): # Config overrides (defaults live in the script):
#Environment=PERFD_PORT=48890 #Environment=PERFD_PORT=48890
#Environment=PERFD_GPU_LOCK_MHZ=2550 #Environment=PERFD_GPU_LOCK_MHZ=2550
#Environment=PERFD_CPU_LOCK_KHZ=3200000
#Environment=PERFD_GPU_INDEX=0 #Environment=PERFD_GPU_INDEX=0
Restart=on-failure Restart=on-failure
RestartSec=2 RestartSec=2
......
...@@ -13,49 +13,60 @@ programs use it through the same socket/CLI). Implemented by Claude. ...@@ -13,49 +13,60 @@ programs use it through the same socket/CLI). Implemented by Claude.
Java 8/11 clients need no extra libraries): Java 8/11 clients need no extra libraries):
PING -> OK pong PING -> OK pong
STATUS -> OK {json} STATUS -> OK {json}
HOLD measure -> OK held measure ... (until connection closes) HOLD measure [cpu=KHZ] [gpu=MHZ] -> OK held measure ...
HOLD max -> OK held max ... HOLD max -> OK held max ...
RELEASE -> OK released RELEASE -> OK released
A hold is TIED TO THE CONNECTION: client exit/crash closes the socket and A hold is TIED TO THE CONNECTION: client exit/crash closes the socket and
the hold is released automatically (crash-safe, like a media player's the hold is released automatically (crash-safe, like a media player's
sleep inhibitor). Multiple concurrent holders are refcounted; the sleep inhibitor). Multiple concurrent holders are refcounted; the
strongest profile wins (measure > max). strongest profile wins (measure > max); lock values come from the newest
holder of the winning profile. cpu=/gpu= overrides are bounds-checked and
exist so lock-value calibration needs no daemon reconfiguration.
- Profiles: - Profiles:
measure : CPU 'performance' (power-profiles-daemon hold) + measure : CPU frequency PINNED (intel_pstate scaling_min=max=KHZ) +
GPU persistence ON + clocks LOCKED at GPU_LOCK_MHZ GPU persistence ON + GPU clocks LOCKED at MHZ
-> run-to-run deterministic timing (per-item A/B evaluation) -> run-to-run deterministic timing for per-item A/B
max : CPU 'performance' + GPU persistence ON, clocks unlocked evaluation. PINNED, not 'performance': the i9-11900K at
-> production throughput free boost hits 4.9 GHz -> 100 C -> throttle wobble
(3.2-3.5 GHz) within seconds of an all-core phase - the
boost bias itself is a nondeterminism source (measured
07/17/2026).
max : CPU 'performance' profile (power-profiles-daemon hold,
free boost) + GPU persistence ON, clocks unlocked
-> production throughput.
(idle) : everything released; also enforced on daemon stop. (idle) : everything released; also enforced on daemon stop.
- GPU ops shell out to nvidia-smi with fixed argument lists (no client - GPU ops shell out to nvidia-smi with fixed argument lists; CPU pinning
input ever reaches a command line); CPU hold uses the same D-Bus call the writes sysfs cpufreq directly (root). No client input ever reaches a
old attic/elphel-agent-tools/bin/cpu_perf_hold.py used (which this daemon command line. Supersedes attic/elphel-agent-tools/bin/cpu_perf_hold.py.
supersedes).
Config via environment (set in the systemd unit or /etc/default/elphel-perfd): Config via environment (systemd unit):
PERFD_PORT control port (default 48890) PERFD_PORT control port (default 48890)
PERFD_GPU_LOCK_MHZ 'measure' lock (default 2550; RTX 5060 Ti burst PERFD_GPU_LOCK_MHZ 'measure' GPU lock (default 2550; RTX 5060 Ti burst
boost measured ~2595 @ 23 W / 49 C on 07/17/2026 - boost measured ~2595 @ 23 W / 49 C - thermally trivial)
thermally trivial, so the value is about PERFD_CPU_LOCK_KHZ 'measure' CPU pin (default 3200000; i9-11900K
reproducibility, not speed) all-core THROTTLE equilibrium measured 3.2-3.5 GHz
@ 100 C - the pin must sit at/below it to stay
deterministic AND cooler)
PERFD_GPU_INDEX GPU index (default 0) PERFD_GPU_INDEX GPU index (default 0)
""" """
import glob
import json import json
import os import os
import signal import signal
import socket
import socketserver import socketserver
import subprocess import subprocess
import sys import sys
import threading import threading
import time
PORT = int(os.environ.get("PERFD_PORT", "48890")) PORT = int(os.environ.get("PERFD_PORT", "48890"))
GPU_LOCK_MHZ = int(os.environ.get("PERFD_GPU_LOCK_MHZ", "2550")) GPU_LOCK_MHZ = int(os.environ.get("PERFD_GPU_LOCK_MHZ", "2550"))
CPU_LOCK_KHZ = int(os.environ.get("PERFD_CPU_LOCK_KHZ", "3200000"))
GPU_INDEX = os.environ.get("PERFD_GPU_INDEX", "0") GPU_INDEX = os.environ.get("PERFD_GPU_INDEX", "0")
NVIDIA_SMI = "/usr/bin/nvidia-smi" NVIDIA_SMI = "/usr/bin/nvidia-smi"
PROFILES = ("measure", "max") # strongest first PROFILES = ("measure", "max") # strongest first
GPU_MHZ_BOUNDS = (180, 3090)
CPU_KHZ_BOUNDS = (800000, 5300000)
def log(msg): def log(msg):
...@@ -76,29 +87,30 @@ def _smi(*args): ...@@ -76,29 +87,30 @@ def _smi(*args):
class GpuControl: class GpuControl:
def __init__(self): def __init__(self):
self.locked = False self.locked_mhz = None
def apply(self, profile): def apply(self, profile, gpu_mhz):
errs = [] errs = []
rc, out = _smi("-pm", "1") rc, out = _smi("-pm", "1")
if rc != 0: if rc != 0:
errs.append("persistence: " + out) errs.append("persistence: " + out)
if profile == "measure": if profile == "measure":
rc, out = _smi("-lgc", "{0},{0}".format(GPU_LOCK_MHZ)) rc, out = _smi("-lgc", "{0},{0}".format(gpu_mhz))
if rc != 0: if rc != 0:
errs.append("lgc: " + out) errs.append("lgc: " + out)
self.locked_mhz = None
else: else:
self.locked = True self.locked_mhz = gpu_mhz
else: # max - make sure no stale lock survives a profile downgrade else: # max - no stale lock may survive a profile downgrade
self.release_lock() self.release_lock()
return errs return errs
def release_lock(self): def release_lock(self):
if self.locked: if self.locked_mhz is not None:
rc, out = _smi("-rgc") rc, out = _smi("-rgc")
if rc != 0: if rc != 0:
log("WARNING: -rgc failed: " + out) log("WARNING: -rgc failed: " + out)
self.locked = False self.locked_mhz = None
def release_all(self): def release_all(self):
# persistence mode is left ON (harmless, avoids driver re-init cost) # persistence mode is left ON (harmless, avoids driver re-init cost)
...@@ -107,17 +119,28 @@ class GpuControl: ...@@ -107,17 +119,28 @@ class GpuControl:
def status(self): def status(self):
rc, out = _smi("--query-gpu=clocks.sm,temperature.gpu,power.draw", rc, out = _smi("--query-gpu=clocks.sm,temperature.gpu,power.draw",
"--format=csv,noheader") "--format=csv,noheader")
return {"locked": self.locked, return {"locked_mhz": self.locked_mhz,
"lock_mhz": GPU_LOCK_MHZ if self.locked else None,
"telemetry": out if rc == 0 else "unavailable"} "telemetry": out if rc == 0 else "unavailable"}
# ---------------------------------------------------------------- CPU control # ---------------------------------------------------------------- CPU control
class CpuControl: class CpuControl:
"""Hold the 'performance' profile via power-profiles-daemon (D-Bus) - """measure: PIN the frequency (scaling_min=max=KHZ on every cpufreq
the same mechanism cpu_perf_hold.py used; falls back to a no-op with a policy) - deterministic and below the thermal wall. max: hold the
warning if the D-Bus service is unavailable.""" 'performance' profile via power-profiles-daemon (free boost), the same
call the retired cpu_perf_hold.py used."""
def __init__(self): def __init__(self):
self.policies = sorted(glob.glob(
"/sys/devices/system/cpu/cpufreq/policy*"))
self.hw_bounds = {}
for p in self.policies:
try:
lo = int(open(p + "/cpuinfo_min_freq").read())
hi = int(open(p + "/cpuinfo_max_freq").read())
self.hw_bounds[p] = (lo, hi)
except OSError:
pass
self.pinned_khz = None
self.cookie = None self.cookie = None
self.iface = None self.iface = None
try: try:
...@@ -129,9 +152,40 @@ class CpuControl: ...@@ -129,9 +152,40 @@ class CpuControl:
"org.freedesktop.UPower.PowerProfiles") "org.freedesktop.UPower.PowerProfiles")
except Exception as e: except Exception as e:
log("WARNING: power-profiles-daemon unavailable (" + str(e) + log("WARNING: power-profiles-daemon unavailable (" + str(e) +
") - CPU holds will be no-ops") ") - 'max' CPU holds will be no-ops")
@staticmethod
def _write(path, value):
with open(path, "w") as f:
f.write(str(value))
def pin(self, khz):
errs = []
for p, (lo, hi) in self.hw_bounds.items():
k = max(lo, min(hi, khz))
try:
# safe order: open the floor, set the ceiling, raise the floor
self._write(p + "/scaling_min_freq", lo)
self._write(p + "/scaling_max_freq", k)
self._write(p + "/scaling_min_freq", k)
except OSError as e:
errs.append(os.path.basename(p) + ": " + str(e))
if not errs:
self.pinned_khz = khz
return errs
def unpin(self):
if self.pinned_khz is None:
return
for p, (lo, hi) in self.hw_bounds.items():
try:
self._write(p + "/scaling_min_freq", lo)
self._write(p + "/scaling_max_freq", hi)
except OSError as e:
log("WARNING: unpin " + os.path.basename(p) + ": " + str(e))
self.pinned_khz = None
def hold(self): def hold_ppd(self):
if (self.iface is not None) and (self.cookie is None): if (self.iface is not None) and (self.cookie is None):
try: try:
self.cookie = self.iface.HoldProfile( self.cookie = self.iface.HoldProfile(
...@@ -140,7 +194,7 @@ class CpuControl: ...@@ -140,7 +194,7 @@ class CpuControl:
except Exception as e: except Exception as e:
log("WARNING: CPU HoldProfile failed: " + str(e)) log("WARNING: CPU HoldProfile failed: " + str(e))
def release(self): def release_ppd(self):
if (self.iface is not None) and (self.cookie is not None): if (self.iface is not None) and (self.cookie is not None):
try: try:
self.iface.ReleaseProfile(self.cookie) self.iface.ReleaseProfile(self.cookie)
...@@ -148,24 +202,45 @@ class CpuControl: ...@@ -148,24 +202,45 @@ class CpuControl:
log("WARNING: CPU ReleaseProfile failed: " + str(e)) log("WARNING: CPU ReleaseProfile failed: " + str(e))
self.cookie = None self.cookie = None
def apply(self, profile, cpu_khz):
if profile == "measure":
self.release_ppd()
return self.pin(cpu_khz)
else: # max
self.unpin()
self.hold_ppd()
return []
def release_all(self):
self.unpin()
self.release_ppd()
def status(self): def status(self):
return {"held": self.cookie is not None} return {"pinned_khz": self.pinned_khz,
"ppd_held": self.cookie is not None,
"policies": len(self.hw_bounds)}
# ------------------------------------------------------------------ hold book # ------------------------------------------------------------------ hold book
class HoldBook: class HoldBook:
"""Refcounted holds; the strongest requested profile is applied.""" """Per-connection holds; the strongest profile wins; lock values come
from the NEWEST holder of the winning profile."""
def __init__(self): def __init__(self):
self.lock = threading.Lock() self.lock = threading.Lock()
self.counts = {p: 0 for p in PROFILES} self.holds = {} # conn_id -> (seq, profile, cpu_khz, gpu_mhz)
self.applied = None self.seq = 0
self.applied = None # (profile, cpu_khz, gpu_mhz) or None
self.gpu = GpuControl() self.gpu = GpuControl()
self.cpu = CpuControl() self.cpu = CpuControl()
def _target(self): def _target(self):
for p in PROFILES: # strongest first for prof in PROFILES: # strongest first
if self.counts[p] > 0: best = None
return p for (seq, p, ck, gm) in self.holds.values():
if (p == prof) and ((best is None) or (seq > best[0])):
best = (seq, p, ck, gm)
if best is not None:
return best[1:]
return None return None
def _apply_target(self): def _apply_target(self):
...@@ -174,50 +249,77 @@ class HoldBook: ...@@ -174,50 +249,77 @@ class HoldBook:
return [] return []
errs = [] errs = []
if target is None: if target is None:
self.cpu.release() self.cpu.release_all()
self.gpu.release_all() self.gpu.release_all()
log("all holds released - default power management") log("all holds released - default power management")
else: else:
self.cpu.hold() profile, cpu_khz, gpu_mhz = target
errs = self.gpu.apply(target) errs = self.cpu.apply(profile, cpu_khz) + \
log("profile '%s' applied (gpu errors: %s)" % self.gpu.apply(profile, gpu_mhz)
(target, errs if errs else "none")) log("profile '%s' applied (cpu=%d kHz gpu=%d MHz; errors: %s)" %
(profile, cpu_khz, gpu_mhz, errs if errs else "none"))
self.applied = target self.applied = target
return errs return errs
def acquire(self, profile): def acquire(self, conn_id, profile, cpu_khz, gpu_mhz):
with self.lock: with self.lock:
self.counts[profile] += 1 self.seq += 1
self.holds[conn_id] = (self.seq, profile, cpu_khz, gpu_mhz)
return self._apply_target() return self._apply_target()
def release(self, profile): def release(self, conn_id):
with self.lock: with self.lock:
if self.counts[profile] > 0: self.holds.pop(conn_id, None)
self.counts[profile] -= 1
self._apply_target() self._apply_target()
def release_everything(self): def release_everything(self):
with self.lock: with self.lock:
for p in PROFILES: self.holds.clear()
self.counts[p] = 0
self._apply_target() self._apply_target()
def status(self): def status(self):
with self.lock: with self.lock:
return {"applied": self.applied, return {"applied": (None if self.applied is None else {
"holds": dict(self.counts), "profile": self.applied[0],
"cpu_khz": self.applied[1],
"gpu_mhz": self.applied[2]}),
"holders": len(self.holds),
"cpu": self.cpu.status(), "cpu": self.cpu.status(),
"gpu": self.gpu.status(), "gpu": self.gpu.status(),
"gpu_lock_mhz_configured": GPU_LOCK_MHZ} "defaults": {"cpu_khz": CPU_LOCK_KHZ,
"gpu_mhz": GPU_LOCK_MHZ}}
BOOK = HoldBook() BOOK = HoldBook()
# ------------------------------------------------------------------ TCP layer # ------------------------------------------------------------------ TCP layer
def _parse_hold(parts):
"""HOLD measure [cpu=KHZ] [gpu=MHZ] -> (profile, cpu_khz, gpu_mhz) or None."""
if (len(parts) < 2) or (parts[1].lower() not in PROFILES):
return None
profile, cpu_khz, gpu_mhz = parts[1].lower(), CPU_LOCK_KHZ, GPU_LOCK_MHZ
for tok in parts[2:]:
if "=" not in tok:
return None
key, _, val = tok.partition("=")
try:
v = int(val)
except ValueError:
return None
if key == "cpu" and CPU_KHZ_BOUNDS[0] <= v <= CPU_KHZ_BOUNDS[1]:
cpu_khz = v
elif key == "gpu" and GPU_MHZ_BOUNDS[0] <= v <= GPU_MHZ_BOUNDS[1]:
gpu_mhz = v
else:
return None
return profile, cpu_khz, gpu_mhz
class Handler(socketserver.StreamRequestHandler): class Handler(socketserver.StreamRequestHandler):
def handle(self): def handle(self):
held = None # this connection's profile conn_id = id(self)
holding = False
try: try:
for raw in self.rfile: for raw in self.rfile:
parts = raw.decode("utf-8", "replace").strip().split() parts = raw.decode("utf-8", "replace").strip().split()
...@@ -228,19 +330,20 @@ class Handler(socketserver.StreamRequestHandler): ...@@ -228,19 +330,20 @@ class Handler(socketserver.StreamRequestHandler):
self._reply("OK pong") self._reply("OK pong")
elif cmd == "STATUS": elif cmd == "STATUS":
self._reply("OK " + json.dumps(BOOK.status())) self._reply("OK " + json.dumps(BOOK.status()))
elif cmd == "HOLD" and (len(parts) == 2) and \ elif cmd == "HOLD":
(parts[1].lower() in PROFILES): parsed = _parse_hold(parts)
prof = parts[1].lower() if parsed is None:
if held is not None: # one hold per connection self._reply("ERR usage: HOLD measure|max [cpu=KHZ] [gpu=MHZ] (bounds cpu %d..%d, gpu %d..%d)" %
BOOK.release(held) (CPU_KHZ_BOUNDS + GPU_MHZ_BOUNDS))
errs = BOOK.acquire(prof) continue
held = prof errs = BOOK.acquire(conn_id, *parsed)
self._reply("OK held %s%s" % (prof, holding = True
(" gpu_errors=" + ";".join(errs)) if errs else "")) self._reply("OK held %s cpu=%d gpu=%d%s" %
(parsed[0], parsed[1], parsed[2],
(" errors=" + ";".join(errs)) if errs else ""))
elif cmd == "RELEASE": elif cmd == "RELEASE":
if held is not None: BOOK.release(conn_id)
BOOK.release(held) holding = False
held = None
self._reply("OK released") self._reply("OK released")
elif cmd == "QUIT": elif cmd == "QUIT":
break break
...@@ -249,8 +352,8 @@ class Handler(socketserver.StreamRequestHandler): ...@@ -249,8 +352,8 @@ class Handler(socketserver.StreamRequestHandler):
except Exception: except Exception:
pass pass
finally: finally:
if held is not None: # crash-safe auto-release if holding: # crash-safe auto-release
BOOK.release(held) BOOK.release(conn_id)
def _reply(self, line): def _reply(self, line):
self.wfile.write((line + "\n").encode("utf-8")) self.wfile.write((line + "\n").encode("utf-8"))
...@@ -270,8 +373,8 @@ def main(): ...@@ -270,8 +373,8 @@ def main():
signal.signal(signal.SIGTERM, shutdown) signal.signal(signal.SIGTERM, shutdown)
signal.signal(signal.SIGINT, shutdown) signal.signal(signal.SIGINT, shutdown)
srv = Server(("127.0.0.1", PORT), Handler) # localhost ONLY srv = Server(("127.0.0.1", PORT), Handler) # localhost ONLY
log("listening on 127.0.0.1:%d (gpu lock for 'measure' = %d MHz)" % log("listening on 127.0.0.1:%d (measure defaults: cpu=%d kHz, gpu=%d MHz)" %
(PORT, GPU_LOCK_MHZ)) (PORT, CPU_LOCK_KHZ, GPU_LOCK_MHZ))
try: try:
srv.serve_forever() srv.serve_forever()
finally: finally:
......
...@@ -4,11 +4,14 @@ perfctl - unprivileged client for elphel-perfd (127.0.0.1:48890). ...@@ -4,11 +4,14 @@ perfctl - unprivileged client for elphel-perfd (127.0.0.1:48890).
perfctl status one-line daemon/clock status perfctl status one-line daemon/clock status
perfctl ping probe (exit 0 = daemon installed) perfctl ping probe (exit 0 = daemon installed)
perfctl hold <measure|max> hold until Ctrl-C perfctl hold <measure|max> [cpu=KHZ] [gpu=MHZ]
perfctl hold <measure|max> -- CMD [ARGS...] hold until Ctrl-C (overrides optional)
perfctl hold <measure|max> [cpu=KHZ] [gpu=MHZ] -- CMD [ARGS...]
hold for the duration of CMD hold for the duration of CMD
The hold is tied to this process's connection: if perfctl dies, the daemon The hold is tied to this process's connection: if perfctl dies, the daemon
releases automatically. By Claude on 07/17/2026. releases automatically. cpu=/gpu= override the daemon's 'measure' lock
defaults (bounds-checked server-side) - for calibration without
reconfiguration. By Claude on 07/17/2026.
""" """
import os import os
import signal import signal
...@@ -47,16 +50,20 @@ def main(): ...@@ -47,16 +50,20 @@ def main():
if args[0] == "status": if args[0] == "status":
print(ask(f, "STATUS")) print(ask(f, "STATUS"))
return 0 return 0
# hold # hold [cpu=KHZ] [gpu=MHZ] [-- CMD...]
if len(args) < 2 or args[1] not in ("measure", "max"): if len(args) < 2 or args[1] not in ("measure", "max"):
print("perfctl: hold needs a profile: measure | max") print("perfctl: hold needs a profile: measure | max")
return 2 return 2
reply = ask(f, "HOLD " + args[1]) overrides = []
rest = args[2:]
while rest and ("=" in rest[0]) and (rest[0] != "--"):
overrides.append(rest.pop(0))
reply = ask(f, " ".join(["HOLD", args[1]] + overrides))
print("perfctl: " + reply) print("perfctl: " + reply)
if not reply.startswith("OK"): if not reply.startswith("OK"):
return 1 return 1
if len(args) > 2 and args[2] == "--": if rest and rest[0] == "--":
rc = subprocess.call(args[3:]) rc = subprocess.call(rest[1:])
ask(f, "RELEASE") ask(f, "RELEASE")
return rc return rc
print("perfctl: holding '%s' until Ctrl-C ..." % args[1]) print("perfctl: holding '%s' until Ctrl-C ..." % args[1])
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment