fix(engine): ask Windows for a fine timer tick while a session runs - #52
Merged
Merged
Conversation
The injector holds a delayed packet with Condition.wait(timeout=...), and Windows rounds that timeout up to the system timer tick - 15.6 ms unless the process asks for better. That rounding was the whole added-latency error. Measured on the real capture path (ping through live WinDivert, two runs): a CONSTANT +12.6 ms of round-trip overshoot regardless of the setting - +12.2 ms at --latency 10, +12.8 ms at --latency 50, where a proportional error would have been ~62 ms at the higher one. The control run at --latency 0 measured +0.4 / -0.3 ms, so it was not the capture path, the driver or the link. - winenv: request_fine_timers / release_fine_timers (Windows only, no-op elsewhere), taken for the life of a SESSION and given back after the injector is joined. The pair costs ~1.3 us, so nothing holds a finer tick while the tool is idle. - winenv: _allow_fine_timers_in_background(). timeBeginPeriod ALONE is a fix that works and then stops - Windows 11 throttles a background process's request, and this tool lives in the background. Measured: the request kept succeeding with a balanced log while the effect vanished after ~10 s (wait(10 ms): 10.1, 10.3, then 15.6 for every later session in the process). SetProcessInformation/ProcessPowerThrottling with IGNORE_TIMER_RESOLUTION and state 0 opts out, once per process. - engine: _fine_timers tracks whether the request was GRANTED, because the OS refcounts them per process and releasing one we never took cancels somebody else's. - tests: four in test_failsafe.py; the two balance tests verified by mutation (no release -> ['request']; unconditional release -> ['request', 'release']). - both changelogs (convention 39). Result through the engine, eight sessions in one process: overshoot +0.22 to +0.55 ms (was +8.3), worst case ~25 ms -> ~11 ms. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…timer fix The user-facing entry quoted figures from the synthetic harness. Replaced with the ping measurement, which the reader can reproduce: 40 packets per setting through live WinDivert, +1 ms of overshoot at both --latency 10 and --latency 50 (was +12.6 ms at both). Also records two measurement lessons in the internal log, because the first acceptance run read as a half fix: - a mean over ten pings is outlier-dominated here (~1% of packets sit in a tail, two impaired packets per ping), which moved it by 4-8 ms; the median was right, and even that run's minima were already exactly nominal. - the tail belongs to the link: in the 40-packet run the untouched baseline produced the worst outlier of the whole session (112 ms, against 61 and 135 with the tool in the path). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
Engineering audit finding F3. The injector holds a delayed packet with
Condition.wait(timeout=...), and Windows rounds that timeout UP to the system timer tick -15.6 ms unless the process asks for better. That rounding was the whole of the added-latency
error, and it was a fixed surcharge rather than a percentage, so it swamped the small settings
this tool is most used for (LAN, game server, VoIP hop).
Measured on the REAL capture path before the fix (ping through live WinDivert,
--dst-ip 8.8.8.8, two independent runs): a constant +12.6 ms of round-trip overshoot - +12.2 ms at--latency 10and +12.8 ms at--latency 50, where a proportional error would have been ~62 msat the higher setting. The control run at
--latency 0measured +0.4 and -0.3 ms, so it was notthe capture path, the driver or the link.
After (same ping test, 40 packets per setting)
--latency 10--latency 50Same +1 ms at both settings, so the surcharge is gone rather than scaled. p90 is +5 ms at both.
How
winenv.request_fine_timers()/release_fine_timers()- Windows only, no-op elsewhere. Takenfor the life of a SESSION in
BeanEngine.start()and given back in_stop_lockedafter theinjector thread is joined. Session-scoped rather than process-scoped: the pair costs ~1.3 us
(measured), so nothing holds a finer tick while the tool sits idle.
winenv._allow_fine_timers_in_background()-timeBeginPeriodalone is a fix that works andthen stops. Windows 11 throttles a background process's timer resolution request, and this
tool lives in the background (start a session, switch to the app under test). Measured: the
request kept returning success with a perfectly balanced request/release log while the effect
vanished after roughly ten seconds - in one process,
Condition.wait(10 ms)went 10.1, 10.3,then 15.6 ms for every later session.
SetProcessInformationwithProcessPowerThrottling+PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION(state 0) opts out, once per process. Withit the same wait held 10.1-10.7 ms across 40 s of sampling and eight back-to-back sessions.
BeanEngine._fine_timerstracks whether the request was GRANTED. The OS refcounts these perprocess, so releasing one we never took cancels somebody else's.
For the reviewer
NtQueryTimerResolutionreported a current tick of 1.0 ms (something else on the machine washolding it) while our own waits were still rounded to 15.6 ms, because since Windows 10 2004
the tick is per-process. A future session that checks the global number and concludes "the
timer is already fine" would be reading a number that does not apply to it.
with
['request']; releasing unconditionally turns the second red with['request', 'release']. An unbalanced pair is otherwise invisible from inside the program - itjust means the process keeps a finer system timer for life.
as a half fix, at +6.3 / +9.5 ms. That mean is outlier-dominated: about 1% of packets sit in a
tail and a ping carries two impaired packets, so one outlier moves a ten-sample mean by 4-8 ms.
Its own minima were already exactly nominal. In the 40-packet run the untouched baseline
produced the worst outlier of the whole session (112 ms, against 61 and 135 with the tool in
the path).
Windows every call in this change is a no-op.
Checklist
python -m pytest testspasses locally (674 tests, 0 failures, elevated shell).python smoke_gui.pypasses.tests/test_failsafe.py; two verified by mutation).lang/en.jsonandlang/pl.jsonupdated - not applicable, no UI strings in this change.CHANGELOG.md; technical ones and new tests inCHANGELOG-INTERNAL.md, under[Unreleased].type(scope): summary).VERSION.txt.🤖 Generated with Claude Code