Repository navigation
v0.40.0
·
2 commits
to main
since this release
Immutable
release. Only release title and notes can be modified.
Performance
- Incremental process table (Linux): a per-pid classification cache
(crates/adsum-detect/src/cache/procs.rs) replaces the full re-read of
every process each tick. A tick now enumerates/procfor liveness,
sweeps the dead, revalidates cached hits (one cmdline read per detected
game; the matcher re-runs if the argv fingerprint drifts) and classifies
only new pids. Cached misses are not re-read; a miss that becomes a hit
surfaces as an EXEC event or a watcher-loss flush.- Steady state: zero cmdline reads, automaton walks and allocations.
On the bundled database shape (1062 processes): 5.05 → 0.38 ms/tick
(−92%), 3152 → 3 allocs/tick, 40.3 KB → 54 B/tick of churn. - Every watcher loss channel (
cn_procgaps, backlog sheds, socket
overruns, watcher death/resubscribe) forces one full-read tick; bundle
swaps invalidate through an epoch in the generationArc. - Watcher disabled/dead, other OSes and one-shot diagnostics keep the
classic full sweep, byte-for-byte.
- Steady state: zero cmdline reads, automaton walks and allocations.
- Zero-allocation
/procenumeration: rawgetdents64into a reused,
aligned arena replacesstd::fs::read_dir. ~3 allocations per entry →
0 (~29 MB/h of churn removed at 1000 processes), which also keeps the
glibc arenas and the RSS floor low. - Allocation-free snapshot lane:
ScannedEntry.id/.nameand
ProcessDetectedEvent::Removednow useAppId/Arc<str>, and the scan
loop's snapshotVecis reused across ticks. Per tick: zero
allocations; the daemon pump'sScannedGameconstruction is two
refcount bumps instead of two deep copies. - Aux-table arena: the four
SortedIndextables (steam/epic/name/
nodot) pack keys into one contiguous per-generation arena. Miss-heavy
lookups are 13–39% faster and per-key malloc headers are gone. Build
order, first-wins ties and lookups are unchanged (pinned by tests). - Steam install-tail matching:
match_install_tail_intobinary-searches
a precomputed sorted tail index (rebuilt ondirsmutation) instead of
walking every install prefix per probe, so large libraries no longer
multiply each sweep or EXEC storm. Longest-wins and ambiguity refusal
are preserved; a parity oracle test compares it with the old linear
walk.refresh_if_stalenow fingerprints over the map in place. - Borrow pass: five places that copied where a borrow suffices:
matching: the command-line AppId fallback no longer wraps the borrowed
&strin a freshAppIdper process per tick.cache/appid: oneAppIdshared by refcount instead of two copies.runtime: first-sighting inserts are refcount bumps, notto_string().types/cmd:ActivityCmd.cmdisCow<'static, str>, so synthesized
commands borrow their verb (wire format unchanged).bridge/handoff:is_process_aliveformats/proc/<pid>into a stack
buffer.
- Hoisted locks and buffers: the exclusions
RwLockis read once per
tick/event instead of per process; the dead-pid sweep,is_suspended
andread_cwd_intoreuse caller-owned sets and buffers. - Windows/macOS: the snapshot enumerator folds
\→/at fill time so
match_process'sCowstaysBorrowed;sysinfoslots are refilled in
place and gaincompact_slots;process_start_msrefreshes with
ProcessRefreshKind::nothing()instead ofeverything(). - Minor:
Exclusions.executablesis anFxHashSet; the persist gate
swaps its dirty flag withAcqRel(wasSeqCst);broadcast_raw
answers both encoding questions in one pass.
Changed
- File organization: one concern per module. The near-twin module
pairs are gone —scan.rs(classification) is nowmatching/with
the pipeline inmod.rsand its rule families innormalize(the one
name-folding rule),paths(what it means for a declared name to name
a path),args(borrowed command-line token scans) andtiers(the
aux match tiers + shared hit tail); the tick orchestration moved from
scanner.rstosweep.rs;database.rs(bundle swaps) became
generations.rs, leavingdb.rsas the single wire-model module.
runtime.rssplit intoruntime/{mod,workers,lanes,cadence,loops}—
start()reads as lifecycle and the two 150+ line thread closures are
named bodies (run_scan_loop,run_refresh_loop). The reuse-buffer
family moved to a top-levelscratch.rswith a new
EnumerateScratchgroup (pid list, getdents arena, liveness/work
lists) so the enumeration state no longer rides inside the per-pid
reader scratch, and the two per-pid caches grouped undercache/
(appid+procs) with their shared trust model documented once.
os_matchesmoved besideOsNameintypes.rs;first_sightings/
apply_ignore_listmoved beside the lanes that consume them. Public
paths that embedders use are preserved (adsum_detect::proc_start,
runtime::ScanGuard/idle_wait, thematching::re-exports); the
test tree mirrors the source (tests/unit/matching/,
tests/unit/cache/,tests/unit/platform/). No behavior change: the
full suite passes untouched, rustdoc is warning-free. - Platform backends behind one seam: the OS-specific code that was
scattered as#[cfg]islands inside shared files (six reader pairs in
scan.rs, two enumerations inscanner.rs, the netlink wiring in
runtime.rs, four arena-release arms inserver.rs) now lives under
crates/adsum-detect/src/platform/—linux/(/procreaders,
getdents64enumerator, netlink dispatch/watcher),snapshot/(the
sysinfobackend for every non-Linux target),macos/(the.app
bundle-interior shape — deliberately ungated: the tier is data-driven
and must run, and be tested, everywhere),windows/(documented
empty: nothing Windows-only is left in the crate), plus unified
memory.rs(arena release) andproc_start.rs(public path
adsum_detect::proc_startunchanged). The seam is a static-method
ProcessEnumeratortrait (traits/enumerator.rs:begin_tick+
read_one), so the tick orchestration — BOTH the full sweep and the
incremental cache — is now 100% cfg-free and runs on every target;
the incremental mode is gated only bycache_mode_live(watcher
enabled AND live), which today can only hold on Linux. No behavior
change: the full test suite passes untouched, and theparse_proc_pid
suite moved with its function. - Watcher opt-in under systemd: the user-unit guide now shows a drop-in
that resets the namespace-creating sandbox directives socn_proccan
deliver, with the filesystem and device isolation loss stated explicitly.
The default unit keeps its sandbox and polling.
Added
- SteamAppId tier on Windows and macOS:
read_steam_app_idwas a
Nonestub off Linux, leaving the 57% of database entries with no
declared executables without their authoritative tier on those
platforms. sysinfo 0.39 reads the environ block on both (PEB through
the process handle on Windows, the sysctl arg/env route on macOS); the
reader uses aSystemscoped to the call — the shared snapshot never
accumulates environ copies — and the existing per-pid memo
(Present/Absent/Stale+ invalidation sequence) keeps it to one
read per process lifetime. Permission-gated processes memoize
Absent, exactly like an unreadable/proc/<pid>/environ. - Steam and Heroic discovery off Linux: fallback Steam roots now
include the registry'sSteamPath(HKCU\Software\Valve\Steamvia
reg query— dependency-free; discovery runs at startup plus every
120th tick) and theProgram Files (x86)/Program Filesdefaults on
Windows, and~/Library/Application Support/Steamon macOS; Heroic's
cached library is found under%APPDATA%\heroic(Windows) and
~/Library/Application Support/heroic(macOS). The
ADSUM_STEAM_ROOT/ADSUM_HEROIC_ROOToverrides stay exclusive.
Without these, the Steam-library and-epicapptiers were silently
dead off Linux ($HOME/XDG shapes do not exist on Windows). DetectablesBundlecarries a monotonicepoch(stamped inside the
generationArc, so a reader can never pair it with a foreign
bundle): the cache-invalidation signal for refreshes and override
edits, costing one full-read tick per swap.watch()takes aninvalidateflag it sets on every loss channel it
can observe from inside (sequence gap, shed,ENOBUFS); the retry
loop sets it on both edges of the watch.
Fixed
- Proc-events watcher: a non-initial user or PID namespace now turns
the watcher off with one warning instead of retrying a self-test the
kernel can never satisfy.watcher_liveis set only after the self-test
parses an event and cleared on every exit. A refusal is recognized both
as a nonzeroNLMSG_ERRORand as aPROC_EVENT_NONEack with an error
in the event body, and retries back off from 5 seconds to a 5-minute cap
instead of waiting a fixed 5 minutes. A watch that proved delivery resets
that backoff, so a late failure retries soon instead of inheriting a stale
cap. - Proc-events listener leak: every exit after a successful subscription
now sendsPROC_CN_MCAST_IGNOREbefore closing the netlink socket. The
kernel decrements its global listener count only on that message, so a
stopped watcher, a failed self-test or a lost receiver no longer leaves
the count positive. A leaked count let sandboxed sockets receive events
they were never granted. - PID namespace gate: the watcher also reads
NSpidfrom
/proc/self/statusand stays off when it sees several values, which
means a nested PID namespace. A procfs mounted inside that namespace
collapses the line to one value, so the self-test remains the final word. - Steam demos whose executable is not in the database (VEIN Demo is the
motivating case) are now detected when the process runs under the AppId's
own install dir (steamapps/common/<installdir>/). The install dir is
independent evidence, not an AppId-only match: declared launchers,
prerequisite installers, Steam helpers and the exclusion list keep their
current veto. - A clean install no longer fails with
226/NAMESPACEwhen the cache
directory is missing: the unit creates it before the sandbox mounts
(CacheDirectory=) instead of listing it inReadWritePaths=. The
installer creates the legacy default directories only when the
installed unit still lists them there.
What's Changed
- Proc events: own the listener slot, retry state and namespace gates by @yiesko in #35
- chore(deps): Bump tokio in / by @dependabot[bot] in #33
- chore(deps): Bump ureq in / by @dependabot[bot] in #34
Full Changelog: v0.39.0...v0.40.0