Added niceness / thread priority management.
This commit is contained in:
parent
3d6c7251e9
commit
47bc23cd16
1 changed files with 82 additions and 2 deletions
84
logger.py
84
logger.py
|
|
@ -20,6 +20,7 @@ header -- see capture.py.
|
|||
|
||||
import argparse
|
||||
import csv
|
||||
import os
|
||||
import queue
|
||||
import sys
|
||||
import threading
|
||||
|
|
@ -51,6 +52,53 @@ FLUSH_INTERVAL_S = 0.5
|
|||
# inside the bracket budget.
|
||||
GIL_SWITCH_INTERVAL_S = 0.0005
|
||||
|
||||
# Thread priority. The sampling loop runs on the main thread and is the only
|
||||
# latency-sensitive part; the writer merely formats a few fields per sample.
|
||||
#
|
||||
# Lowering the writer needs no privilege and is always done: only the *relative*
|
||||
# priority matters, and on Linux nice is per-thread, so it does not touch the
|
||||
# sampler. Raising the sampler needs CAP_SYS_NICE and is opt-in via
|
||||
# --high-priority, which fails loudly rather than degrading silently -- asking
|
||||
# for it and not getting it is worth knowing about.
|
||||
SAMPLER_NICE = -10
|
||||
WRITER_NICE = 10
|
||||
|
||||
|
||||
def restore_ownership(path):
|
||||
"""Hand a file created under sudo back to the invoking user.
|
||||
|
||||
--high-priority needs root, and anything root writes stays root-owned --
|
||||
which earlier in this project produced captures the normal user could not
|
||||
rewrite. Undo that here so privilege is needed for scheduling and nothing
|
||||
else leaks from it.
|
||||
"""
|
||||
if os.geteuid() != 0:
|
||||
return
|
||||
uid, gid = os.environ.get("SUDO_UID"), os.environ.get("SUDO_GID")
|
||||
if uid is None:
|
||||
return
|
||||
try:
|
||||
os.chown(path, int(uid), int(gid) if gid else -1)
|
||||
except OSError as exc:
|
||||
print(f"WARNING: could not hand {path} back to uid {uid}: {exc}",
|
||||
file=sys.stderr)
|
||||
|
||||
|
||||
def set_thread_nice(value):
|
||||
"""Renice the calling thread. Returns the nice actually in effect, or None.
|
||||
|
||||
Linux threads are tasks, so PRIO_PROCESS with a native thread id applies to
|
||||
just this thread. Elsewhere this may be a no-op or affect the process, which
|
||||
is why failure is tolerated rather than fatal -- priority is an optimisation,
|
||||
not a correctness requirement.
|
||||
"""
|
||||
try:
|
||||
tid = threading.get_native_id()
|
||||
os.setpriority(os.PRIO_PROCESS, tid, value)
|
||||
return os.getpriority(os.PRIO_PROCESS, tid)
|
||||
except (AttributeError, OSError):
|
||||
return None
|
||||
|
||||
# Miss detection uses the DRDY bracket -- the span between the last poll showing
|
||||
# DRDY clear and the poll showing it set -- rather than the read-to-read
|
||||
# interval.
|
||||
|
|
@ -142,12 +190,17 @@ def parse_args():
|
|||
help="free-text label recorded in the capture header, e.g. "
|
||||
"the supply under test. Keeps the configuration with the "
|
||||
"data instead of only in the filename")
|
||||
p.add_argument("--high-priority", action="store_true",
|
||||
help=f"raise the sampling thread to nice {SAMPLER_NICE}. "
|
||||
"Needs CAP_SYS_NICE, so run under sudo; the run aborts "
|
||||
"if the priority cannot be set. Output files are handed "
|
||||
"back to the invoking user afterwards")
|
||||
p.add_argument("--scan-only", action="store_true",
|
||||
help="scan the bus, report what responded, and exit")
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
def print_plan(cfg, bus_speed, requested_rate):
|
||||
def print_plan(cfg, bus_speed, requested_rate, sampler_nice=None):
|
||||
"""Show how the configuration was derived, so it can be checked not trusted."""
|
||||
lsb = rm3100.tesla_per_count(cfg.cycle_count) * rm3100.NT_PER_TESLA
|
||||
dither = rm3100.expected_noise_nt(cfg.cycle_count) / lsb
|
||||
|
|
@ -172,6 +225,12 @@ def print_plan(cfg, bus_speed, requested_rate):
|
|||
print(f" duty {cfg.duty:.1%} integration / period")
|
||||
print(f" bus {bus_speed} kHz {bus * 1e3:.3f} ms/sample "
|
||||
f"{share:.1%} of the period")
|
||||
if sampler_nice is not None:
|
||||
detail = (f"nice {sampler_nice}, writer at {WRITER_NICE}"
|
||||
if sampler_nice < 0 else
|
||||
f"nice {sampler_nice}, writer at {WRITER_NICE} "
|
||||
"(--high-priority raises the sampler, needs sudo)")
|
||||
print(f" priority {detail}")
|
||||
for note in cfg.notes:
|
||||
print(f" note: {note}")
|
||||
# Bus speed is validated but never changed silently -- swapping it would
|
||||
|
|
@ -249,8 +308,11 @@ def write_header(handle, meta):
|
|||
|
||||
def writer_thread(q, path, meta, stats):
|
||||
"""Drain samples to CSV and drive the console, off the sampling thread."""
|
||||
# Stand aside for the sampler: this thread is not latency-sensitive.
|
||||
set_thread_nice(WRITER_NICE)
|
||||
lsb_nt = float(meta["tesla_per_count"]) * rm3100.NT_PER_TESLA
|
||||
with open(path, "w", newline="") as handle:
|
||||
restore_ownership(path)
|
||||
write_header(handle, meta)
|
||||
out = csv.writer(handle)
|
||||
out.writerow(CSV_FIELDS)
|
||||
|
|
@ -504,6 +566,24 @@ def sample_loop(sensor, q, duration, dt_nominal, stats):
|
|||
def main():
|
||||
args = parse_args()
|
||||
sys.setswitchinterval(GIL_SWITCH_INTERVAL_S)
|
||||
sampler_nice = None
|
||||
if args.high_priority:
|
||||
sampler_nice = set_thread_nice(SAMPLER_NICE)
|
||||
if sampler_nice is None:
|
||||
sys.exit(
|
||||
f"--high-priority needs CAP_SYS_NICE to set nice "
|
||||
f"{SAMPLER_NICE}, and this process does not have it.\n"
|
||||
"Re-run under sudo, using the venv interpreter by absolute "
|
||||
"path:\n"
|
||||
f" sudo {sys.executable} {' '.join(sys.argv)}\n"
|
||||
"Or drop the flag -- the writer thread already steps aside, "
|
||||
f"which is the half that needs no privilege.")
|
||||
else:
|
||||
try:
|
||||
sampler_nice = os.getpriority(os.PRIO_PROCESS,
|
||||
threading.get_native_id())
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
|
||||
if args.cycle_count is not None and not 1 <= args.cycle_count <= 0xFFFF:
|
||||
sys.exit(f"--cycle-count {args.cycle_count} outside 1..65535")
|
||||
|
|
@ -538,7 +618,7 @@ def main():
|
|||
sys.exit(str(exc))
|
||||
|
||||
print()
|
||||
print_plan(cfg, args.bus_speed, args.rate)
|
||||
print_plan(cfg, args.bus_speed, args.rate, sampler_nice)
|
||||
print()
|
||||
nominal_rate = cfg.predicted_hz
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue