A command that overruns a timeout of 60 s or more makes WinQuick discard a
healthy prepared guest, rebuild it five times and cold boot, turning a 60 s
timeout into 242 s of wall time. The next run then pays another 10.9 s to
rebuild the state.
Nothing is corrupted and it recovers on its own, so this is a cost and
diagnostics problem rather than a correctness one. Reporting it because the
trigger is a normal workload: a build or test suite that runs longer than a
generous --timeout is exactly the case the flag exists for.
Environment
|
|
| WinQuick |
0.4.2 (homebrew tap) |
| QEMU |
11.1.1 (homebrew bottle) |
| Host |
Apple M1, 8 cores, 16 GiB, macOS 26.4 |
| Guest |
Validation OS arm64, 10.0.26100.9278 |
Reproduce
winquick run --verbose --timeout 60 -- cmd /c "ping -n 200 127.0.0.1 >nul"
Reproduced on every attempt.
[ 70.2s] winquick: the guest has not acknowledged the command yet but has moved 152 MiB
- it is working, not halted; waiting for the command instead
[ 70.3s] winquick: the prepared guest timed out without ever taking the command;
discarding it and booting cold for this run
[ 70.3s] winquick: preparing a reusable Windows image (one-off, takes a few seconds)
[ 80.9s] winquick: ready state built in 10.6s (423 MiB)
[ 92.6s] winquick: newly built ready state did not work (attempt 1 of 5):
timed out waiting for WQGO.TXT from the guest
... attempts 2, 3, 4, 5, each ~22s, all failing identically ...
[181.9s] winquick: 5 prepared guests in a row came back silent, but this QEMU has
restored one before; leaving the fast path on
[182.0s] winquick: cold boot
[242.0s] winquick: the command did not finish within 60 s - raise it with `--timeout`
The lines at 70.2 s and 70.3 s contradict each other: the run proves the guest
is alive, then concludes it never took the command.
Cause
warm_execute decides whether the guest accepted the command by re-probing the
raw GO flag (src/runner.rs:1344):
let took_it = mailbox::probe(&mbox, mailbox::GO).is_none();
A guest hammering the FAT volume has not flushed its delete of that flag, so it
reads as never having taken the command, even though PROOF_OF_LIFE_BYTES and
still_moving proved it alive one line earlier. That verdict reaches
a_cold_boot_would_help (src/runner.rs:1053), which discards the ready state
and falls through to the cold path.
I am not suggesting the byte counters be trusted instead. The comment at
src/runner.rs:1080 explains why that is worse: a guest wedged at 98% of a
processor also looks alive by that measure, and trusting it once left a machine
failing every run for eight hours. Preferring the flag is the safe side of that
tradeoff, and ACKNOWLEDGEMENT_IS_CERTAIN bounds the damage by keeping short
timeouts off the cold path entirely.
The part that looks worth changing is what happens afterwards.
The five prepare attempts cannot succeed here
PREPARE_ATTEMPTS = 5 (src/runner.rs:1241) is there for a freeze that comes
back silent. In this cascade all five attempts fail with the same
timed out waiting for WQGO.TXT at about 11.7 s each, and those failures are
caused by the command rather than by an unlucky freeze.
Control, running winquick reset first and then one command, three trials each:
| fresh prepare, then |
failed prepare attempts |
total |
cmd /c ver |
0 (3 of 3 trials) |
11 s |
ping -n 200, overrunning the timeout |
3 to 5 consecutive |
147 s to 242 s |
A freshly built state serves a light command first time, every time. The same
build fails repeatedly when the injected command is the heavy one, because each
newly prepared guest is handed the same command and trips the same unflushed
acknowledgement. The loop is rebuilding state to fix something the state was
never responsible for, at roughly 22 s per attempt.
A second trace shows the same thing without any misdiagnosis involved. With
--timeout 59 starting from no ready state, attempts 1 to 3 failed identically
and attempt 4 succeeded, then ran to the timeout:
[ 0.0s] winquick: no ready state yet
[ 22.4s] winquick: newly built ready state did not work (attempt 1 of 5): timed out waiting for WQGO.TXT
[ 44.8s] winquick: newly built ready state did not work (attempt 2 of 5): timed out waiting for WQGO.TXT
[ 67.2s] winquick: newly built ready state did not work (attempt 3 of 5): timed out waiting for WQGO.TXT
[147.0s] winquick: the guest has not acknowledged the command yet but has moved 145 MiB
- it is working, not halted; waiting for the command instead
[147.0s] winquick: the command did not finish within 59 s - raise it with `--timeout`
Suggestions
None of these require trusting the byte counters.
- Stop the retry loop when a rebuilt state fails the same way as the one it
replaced. That is evidence about the command, not the state, and it would cut
about 88 s from the cascade while leaving the silent-freeze protection alone.
- Reword the two messages. "it is working, not halted" and "timed out without
ever taking the command" land 0.1 s apart and read as a contradiction.
Neither mentions the likeliest cause, which is a command that needs longer.
- Reconsider the 60 s threshold. The comment calls it well below the 300 s
default, which holds for the default, but --timeout 60 is the round number
a user reaches for first, and it opens the gate exactly there.
Minor, separate
winquick run --help describes --timeout as "give up after this many
seconds". The clock starts after first contact, not at launch, so a timed-out
run takes FIRST_CONTACT plus the timeout. Measured at 15 s for --timeout 5
across 7 runs and 20 s for --timeout 10. The behaviour is deliberate and well
argued in the source; only the help text is silent about the extra 10 s.
A command that overruns a timeout of 60 s or more makes WinQuick discard a
healthy prepared guest, rebuild it five times and cold boot, turning a 60 s
timeout into 242 s of wall time. The next run then pays another 10.9 s to
rebuild the state.
Nothing is corrupted and it recovers on its own, so this is a cost and
diagnostics problem rather than a correctness one. Reporting it because the
trigger is a normal workload: a build or test suite that runs longer than a
generous
--timeoutis exactly the case the flag exists for.Environment
Reproduce
winquick run --verbose --timeout 60 -- cmd /c "ping -n 200 127.0.0.1 >nul"Reproduced on every attempt.
The lines at 70.2 s and 70.3 s contradict each other: the run proves the guest
is alive, then concludes it never took the command.
Cause
warm_executedecides whether the guest accepted the command by re-probing theraw
GOflag (src/runner.rs:1344):A guest hammering the FAT volume has not flushed its delete of that flag, so it
reads as never having taken the command, even though
PROOF_OF_LIFE_BYTESandstill_movingproved it alive one line earlier. That verdict reachesa_cold_boot_would_help(src/runner.rs:1053), which discards the ready stateand falls through to the cold path.
I am not suggesting the byte counters be trusted instead. The comment at
src/runner.rs:1080explains why that is worse: a guest wedged at 98% of aprocessor also looks alive by that measure, and trusting it once left a machine
failing every run for eight hours. Preferring the flag is the safe side of that
tradeoff, and
ACKNOWLEDGEMENT_IS_CERTAINbounds the damage by keeping shorttimeouts off the cold path entirely.
The part that looks worth changing is what happens afterwards.
The five prepare attempts cannot succeed here
PREPARE_ATTEMPTS = 5(src/runner.rs:1241) is there for a freeze that comesback silent. In this cascade all five attempts fail with the same
timed out waiting for WQGO.TXTat about 11.7 s each, and those failures arecaused by the command rather than by an unlucky freeze.
Control, running
winquick resetfirst and then one command, three trials each:cmd /c verping -n 200, overrunning the timeoutA freshly built state serves a light command first time, every time. The same
build fails repeatedly when the injected command is the heavy one, because each
newly prepared guest is handed the same command and trips the same unflushed
acknowledgement. The loop is rebuilding state to fix something the state was
never responsible for, at roughly 22 s per attempt.
A second trace shows the same thing without any misdiagnosis involved. With
--timeout 59starting from no ready state, attempts 1 to 3 failed identicallyand attempt 4 succeeded, then ran to the timeout:
Suggestions
None of these require trusting the byte counters.
replaced. That is evidence about the command, not the state, and it would cut
about 88 s from the cascade while leaving the silent-freeze protection alone.
ever taking the command" land 0.1 s apart and read as a contradiction.
Neither mentions the likeliest cause, which is a command that needs longer.
default, which holds for the default, but
--timeout 60is the round numbera user reaches for first, and it opens the gate exactly there.
Minor, separate
winquick run --helpdescribes--timeoutas "give up after this manyseconds". The clock starts after first contact, not at launch, so a timed-out
run takes
FIRST_CONTACTplus the timeout. Measured at 15 s for--timeout 5across 7 runs and 20 s for
--timeout 10. The behaviour is deliberate and wellargued in the source; only the help text is silent about the extra 10 s.