Files
asepharyana 1cdb82a76f Sync config from arch
- hypr/apps.lua
- hypr/autostart.lua
- hypr/envs.lua
- hypr/hyprland.lua
- hypr/hyprsunset.conf
- hypr/input.lua
- hypr/looknfeel.lua
- hypr/omasettings.lua
- hypr/xdph.conf
- omarchy/branding/about.txt
- omarchy/branding/screensaver.txt
- omarchy/extensions/omarchy-menu.jsonc
- omarchy/hooks/battery-low.d/play-warning-sound.sample
- omarchy/hooks/font-set.d/show-font-notification.sample
- omarchy/hooks/post-boot.d/weather.sample
- omarchy/hooks/post-update.d/install-voxtype.hook
- omarchy/hooks/post-update.d/setup-agent.hook
- omarchy/hooks/post-update.d/setup-fingerprint.hook
- omarchy/hooks/post-update.d/show-update-notification.sample
- omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample
- omarchy/hooks/theme-set.d/show-theme-notification.sample
- omarchy/shell.json
- omarchy/shell.toml
- omarchy/theme.name
- omarchy/themes/azure-glow/README.md
- omarchy/themes/azure-glow/alacritty.toml
- omarchy/themes/azure-glow/btop.theme
- omarchy/themes/azure-glow/hyprland.conf
- omarchy/themes/azure-glow/hyprlock.conf
- omarchy/themes/azure-glow/icons.theme
- … 269 more
2026-09-23 15:19:12 +07:00

485 lines
20 KiB
Python

"""Persistent ping and TCP-handshake probes, and the rolling windows they feed.
One long-lived `ping` process per target rather than one process per sample.
At two samples a second, spawning a process each time would mean 172,800
forks a day inside a laptop's idle budget; `ping -i` already does the timing
for us, and `-O` makes it say so out loud when a packet goes missing.
"""
import collections
import re
import shutil
import socket
import statistics
import subprocess
import threading
import time
from collections import deque
def nearest_rank(ordered, p: float):
"""Nearest-rank percentile of a NON-EMPTY sorted sequence.
One implementation shared by the probe stats, the per-app socket stats
and the speed baseline, so the three cannot drift apart — they read the
same figure off the same rule. The index formula already collapses to
element 0 for a single-element input, so no length special-case.
"""
i = min(len(ordered) - 1, max(0, int(round(p * (len(ordered) - 1)))))
return ordered[i]
# [1787562260.703963] 64 bytes from 10.10.0.1: icmp_seq=1 ttl=64 time=9.13 ms
RE_REPLY = re.compile(r"^\[(\d+\.\d+)\].*icmp_seq=(\d+).*time=([\d.]+)\s*ms")
# [1787562369.690501] no answer yet for icmp_seq=1
RE_PENDING = re.compile(r"^\[(\d+\.\d+)\]\s+no answer yet for icmp_seq=(\d+)")
# [...] From 10.10.0.147 icmp_seq=1 Destination Host Unreachable
RE_UNREACH = re.compile(r"^\[(\d+\.\d+)\].*icmp_seq=(\d+).*(?:Unreachable|unreachable)")
# How much recent history judges an instrument, and the fewest samples that
# can carry a judgement at all.
#
# Shared, deliberately, by the two places that ask "what has this instrument
# been doing lately": the bench, which ranks instruments over a window, and
# TcpProbe, which needs its own recent p50 to tell a retransmit from a slow
# path. They live here rather than on the bench because `instruments` imports
# this module and not the other way round, and an alias in the direction the
# imports already run is the only one Python will take.
#
# The point of aliasing rather than repeating the number: two literals plus a
# test catches drift on the next test run, an alias makes the drift
# impossible. `test_the_window_matches_what_the_bench_ranks_on` is kept even
# though it now passes by construction — it catches someone replacing an
# alias with a literal, which is the drift it was written against.
RECENT_WINDOW_S = 300.0
RECENT_MIN_SAMPLES = 8
class Series:
"""A rolling window of (timestamp, rtt_ms or None) for one target.
None means the probe went out and nothing came back. Keeping losses in
the same series as the replies is what lets a single pass compute both
latency and loss over any sub-window.
"""
def __init__(self, window_s: float = 1830.0):
self.window_s = window_s
self._samples = deque()
self._lock = threading.Lock()
def add(self, t: float, rtt, loaded: bool = False):
"""Record one probe result, tagged with whether the link was busy.
The tag is what makes bufferbloat visible: the same connection can
answer in 15 ms while idle and 300 ms while a download runs, and a
score built only on the idle number calls that line excellent right
up until someone uses it.
"""
with self._lock:
self._samples.append((t, rtt, bool(loaded)))
cutoff = t - self.window_s
while self._samples and self._samples[0][0] < cutoff:
self._samples.popleft()
def since(self, seconds: float):
cutoff = time.time() - seconds
with self._lock:
return [s for s in self._samples if s[0] >= cutoff]
def all(self):
with self._lock:
return list(self._samples)
@staticmethod
def split_by_load(samples):
"""(idle, loaded) — probes taken while the link was quiet vs busy.
Samples are indexed rather than unpacked throughout, so a caller
holding older two-element samples still reads as idle instead of
raising.
"""
idle = [s for s in samples if not (len(s) > 2 and s[2])]
loaded = [s for s in samples if len(s) > 2 and s[2]]
return idle, loaded
@staticmethod
def stats(samples) -> dict:
"""Latency percentiles, jitter and loss over the samples given.
Jitter is mean absolute difference between consecutive replies
(RFC 3550's IPDV), not standard deviation: a connection that
alternates 10/40/10/40 ms feels far worse than one that drifts
smoothly across the same range, and only IPDV says so.
"""
total = len(samples)
if total == 0:
return {"count": 0, "loss": None, "p50": None, "p75": None,
"p95": None, "jitter": None, "last": None, "max": None}
rtts = [s[1] for s in samples if s[1] is not None]
lost = total - len(rtts)
loss = lost / total
if not rtts:
return {"count": total, "loss": loss, "p50": None, "p75": None,
"p95": None, "jitter": None, "last": None, "max": None}
ordered = sorted(rtts)
deltas = [abs(rtts[i] - rtts[i - 1]) for i in range(1, len(rtts))]
last = next((x[1] for x in reversed(samples) if x[1] is not None), None)
return {
"count": total,
"loss": loss,
"p50": round(statistics.median(ordered), 2),
"p75": round(nearest_rank(ordered, 0.75), 2),
"p95": round(nearest_rank(ordered, 0.95), 2),
"max": round(ordered[-1], 2),
"jitter": round(statistics.fmean(deltas), 2) if deltas else 0.0,
"last": round(last, 2) if last is not None else None,
}
class PingProbe(threading.Thread):
"""Runs one `ping` forever, restarting it if it dies, feeding a Series.
A probe never raises into the daemon: if `ping` is missing, the target
stops resolving, or the interface goes away, the thread backs off and
keeps trying while the series simply records losses.
"""
def __init__(self, target: str, series: Series, interval_ms: int = 500,
name: str = "", loaded_fn=None):
super().__init__(name=f"probe-{name or target}", daemon=True)
self.target = target
self.series = series
# Asked at the moment a sample lands, so each probe is tagged with
# the link state it actually experienced rather than whatever the
# link was doing when the window is later read.
self.loaded_fn = loaded_fn
self.interval = max(0.2, interval_ms / 1000.0)
self._stop = threading.Event()
self._proc = None
# seq -> timestamp first seen unanswered, drained by _expire()
self._pending = {}
# seq -> when it was charged as lost. A packet the grace period gave
# up on can still be reported afterwards — the gateway's Destination
# Host Unreachable for it arrives later than the grace, in the real
# recording by 0.35 s — and without this it would be charged twice.
# Held for one further grace period, which is as long as a late report
# can be believed to belong to that packet.
self._charged = {}
def _loaded(self) -> bool:
try:
return bool(self.loaded_fn()) if self.loaded_fn else False
except Exception:
return False # a probe never raises into the daemon
def stop(self):
self._stop.set()
proc = self._proc
if proc and proc.poll() is None:
try:
proc.terminate()
except OSError:
pass
def set_interval(self, seconds: float):
"""Change cadence in place — a benched instrument idles, a seated
one probes at full rate, without tearing the thread down. `ping`
takes its interval on the command line, so the running process is
retired and the run loop respawns it with the new one."""
seconds = max(0.2, float(seconds))
if abs(seconds - self.interval) < 1e-9:
return
self.interval = seconds
proc = self._proc
if proc and proc.poll() is None:
try:
proc.terminate()
except OSError:
pass
def _reset_tracking(self):
"""Forget both maps together.
`ping` numbers from 1 again on every respawn, so a seq remembered past
the process that produced it would suppress a real loss on the next
one — turning a guard against overcharging into an undercount, which
is the same defect facing the other way.
"""
self._pending.clear()
self._charged.clear()
def _expire(self, now: float):
"""A packet still unanswered after the grace period is a lost packet.
`ping -O` reports "no answer yet" as soon as it sends the next probe,
but a slow reply can still land, so a pending seq is only counted as
lost once it is too old to come back.
"""
grace = self.interval * 2.5 + 1.0
for seq, t in list(self._pending.items()):
if now - t > grace:
del self._pending[seq]
self._charged[seq] = now
self.series.add(t, None, self._loaded())
# Bounded by the same clock that fills it: a seq stops being
# remembered once no report about it could still arrive.
for seq, t in list(self._charged.items()):
if now - t > grace:
del self._charged[seq]
def run(self):
backoff = 1.0
while not self._stop.is_set():
if not shutil.which("ping") or not self.target:
time.sleep(min(backoff, 30.0))
backoff = min(backoff * 2, 30.0)
continue
try:
self._run_once()
backoff = 1.0
except Exception:
# Never let a parse or spawn failure take the daemon with it.
time.sleep(min(backoff, 30.0))
backoff = min(backoff * 2, 30.0)
def _run_once(self):
cmd = ["ping", "-n", "-O", "-D", "-i", f"{self.interval:g}",
"-W", "1", self.target]
self._reset_tracking()
self._proc = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
text=True, bufsize=1,
)
try:
for line in self._proc.stdout:
if self._stop.is_set():
break
self._consume(line)
# ping exited: whatever was outstanding never arrived.
for seq, t in self._pending.items():
self.series.add(t, None, self._loaded())
self._reset_tracking()
finally:
proc, self._proc = self._proc, None
if proc:
try:
proc.terminate()
proc.wait(timeout=2)
except subprocess.TimeoutExpired:
# Would not go quietly: do not leave it running.
try:
proc.kill()
proc.wait(timeout=2)
except (OSError, subprocess.TimeoutExpired):
pass
except OSError:
pass
def _consume(self, line: str):
m = RE_REPLY.match(line)
if m:
t, seq, rtt = float(m.group(1)), int(m.group(2)), float(m.group(3))
self._pending.pop(seq, None)
# A reply this late cannot un-lose the packet — the window it
# belonged to has already been read — and recording the RTT as
# well would put two samples on the wire's one packet.
if seq not in self._charged:
self.series.add(t, rtt, self._loaded())
self._expire(t)
return
m = RE_UNREACH.match(line)
if m:
t, seq = float(m.group(1)), int(m.group(2))
self._pending.pop(seq, None)
if seq not in self._charged:
self.series.add(t, None, self._loaded())
self._expire(t)
return
m = RE_PENDING.match(line)
if m:
t, seq = float(m.group(1)), int(m.group(2))
# `ping -O` repeats "no answer yet" for the same seq, so one that
# has already been charged must not be put back on the pending
# list to be charged a second time.
if seq not in self._charged:
self._pending.setdefault(seq, t)
self._expire(t)
class TcpProbe(threading.Thread):
"""Connect-time RTT to the anchor's TLS port, feeding a Series.
ICMP measures what routers choose to answer, and they answer it fast:
many devices handle it in hardware, in an ASIC or via XDP, while real
traffic waits in the user-space path behind the queues that actually
hold it up. Anything on the way can also reply on the destination's
behalf, because there is nothing in ICMP to prove otherwise.
A TCP handshake cannot be shortcut that way. The SYN has to reach a
listener that completes it, over port 443 where the user's own traffic
goes, so its round trip is the one applications experience. One
connection per sample, opened and closed — no payload, no TLS, nothing
kept.
Since 0.2.0 these are seated instruments in the bench (instruments.py):
the two best of four feed the scored internet leg, so a TCP series moves
the score whenever it holds a seat. The anchor's ICMP figure is still
recorded beside it per minute (`lag_icmp`) so the switch stays auditable.
"""
CONNECT_TIMEOUT_S = 2.0
# Linux and macOS both start TCP's retransmit timer at one second, so a
# handshake that comes back at or past this did not measure a slow path:
# its SYN was dropped and the kernel sent another. The number is the
# kernel's constant, not the network's round trip, and folding it into a
# latency percentile reports the line as slow when what happened is that
# a packet was lost.
#
# The connect timeout was already drawing this line, in the wrong place
# and for the wrong reason: a handshake needing TWO retransmits waits
# 1 s + 2 s, exceeds CONNECT_TIMEOUT_S and is recorded as loss, while one
# needing a single retransmit returns at ~1 s and was recorded as a round
# trip. The same event, accounted two opposite ways, with the boundary
# wherever the timeout happened to fall.
# One initial RTO, with slop for timer granularity and scheduling. The
# first retransmit fires at 1000 ms on Linux, macOS and Windows alike.
RETRANSMIT_MARGIN_MS = 900.0
# The baseline is this instrument's own recent p50 over the shared window
# above — aliases, not copies, so "recent" cannot come to mean two things.
RETRANSMIT_WINDOW_S = RECENT_WINDOW_S
RETRANSMIT_MIN_SAMPLES = RECENT_MIN_SAMPLES
def __init__(self, target: str, series: Series, interval_s: float = 1.0,
name: str = "", loaded_fn=None, port: int = 443):
super().__init__(name=f"tcp-{name or target}", daemon=True)
self.target = target
self.port = port
self.series = series
self.interval = max(0.25, interval_s)
self.loaded_fn = loaded_fn
self._stop = threading.Event()
self.ever_connected = False
# The recent round trips this probe has actually seen, for the
# comparison above. Bounded, and its own — the series it feeds is
# merged with other instruments and cannot answer "what does THIS
# path usually do". Held as (when, rtt) so the window is a duration
# rather than a count, which is what makes it survive a cadence
# change: a benched instrument probes at a fraction of the rate.
self._recent = collections.deque(maxlen=1024)
# Samples this probe declined to call latency, so the reclassification
# can be seen rather than inferred from a loss rate. A dropped SYN and
# a slow line are different faults with different owners.
self.retransmits = 0
self.unclassified = 0
def stop(self):
self._stop.set()
def set_interval(self, seconds: float):
"""Picked up on the next cycle; nothing to tear down here."""
self.interval = max(0.25, float(seconds))
def _loaded(self) -> bool:
try:
return bool(self.loaded_fn()) if self.loaded_fn else False
except Exception:
return False
def _once(self):
started = time.time()
t0 = time.monotonic()
try:
sock = socket.create_connection((self.target, self.port),
timeout=self.CONNECT_TIMEOUT_S)
except (OSError, ValueError):
self.series.add(started, None, self._loaded())
return
rtt = (time.monotonic() - t0) * 1000.0
try:
sock.close()
except OSError:
pass
# The handshake completed, so the target is reachable, whatever the
# kernel had to do to get there.
self.ever_connected = True
verdict = self._classify(started, rtt)
if verdict == "retransmit":
# Loss on new connections, which is what it is. Recorded the same
# way a refused or timed-out connect already is, so it charges the
# loss term and Reliability rather than the latency percentiles.
self.retransmits += 1
self.series.add(started, None, self._loaded())
return
if verdict == "unknown":
# Past the floor before this probe has a baseline to judge it
# against. It is either a retransmit or a genuinely slow path and
# nothing here can tell which, so it is not recorded as either —
# inventing a loss and publishing a suspect latency are both
# claims, and the honest move is to make neither.
#
# It still feeds the baseline, and that is not an oversight. A
# link whose real round trip is past the floor — p50 1200 ms, say
# — has every sample land here, so a deque that only accepted
# classified samples would never reach its minimum, the baseline
# would never form, and the instrument would stay unclassified
# for ever: nothing recorded, count never growing, `penalty()`
# returning None, and the bench able neither to seat it nor to
# call it dead. A silent unrankable instrument, invisible because
# it is not failing, merely absent.
self.unclassified += 1
self._recent.append((started, rtt))
return
self._recent.append((started, rtt))
self.series.add(started, round(rtt, 2), self._loaded())
def _baseline_ms(self, now: float):
"""This instrument's own recent p50, or None while it has too few."""
cutoff = now - self.RETRANSMIT_WINDOW_S
recent = [rtt for t, rtt in self._recent if t >= cutoff]
if len(recent) < self.RETRANSMIT_MIN_SAMPLES:
return None
return statistics.median(recent)
def _classify(self, now: float, rtt_ms: float) -> str:
""""reply", "retransmit" or "unknown" for a handshake that completed.
A connect rescued by a retransmitted SYN is a lost packet, not a slow
path, and the threshold has to be relative or it mislabels distance as
loss: one full RTO ABOVE what this instrument usually sees. A satellite
link whose p50 is 600 ms gets a threshold of 1500, so a 1045 ms sample
there stays the measurement it is.
"""
if rtt_ms < self.RETRANSMIT_MARGIN_MS:
return "reply"
baseline = self._baseline_ms(now)
if baseline is None:
return "unknown"
# Note what does NOT reach the baseline once one exists: a sample this
# returns "retransmit" for. Feeding those back would raise the
# threshold on the instrument's own retransmits and the rule would
# quietly stop firing exactly where it is needed most.
return "retransmit" if rtt_ms >= baseline + self.RETRANSMIT_MARGIN_MS \
else "reply"
def run(self):
while not self._stop.is_set():
if not self.target:
self._stop.wait(5.0)
continue
t0 = time.monotonic()
try:
self._once()
except Exception:
# Never let a socket or DNS failure take the daemon with it.
pass
self._stop.wait(max(0.0, self.interval - (time.monotonic() - t0)))