A late vehicle stalls its own tick, not everyone else's flight - #17
Conversation
skysim renders camera frames and no deployment can ask it to: the entrypoint maps SKYSIM_TILES, SKYSIM_DT and the rest into flags but has no camera mapping, so --camera-fps is never passed and the render service is never built. That is the state on dev — the service is running the rendering build, the gateway is pointed at it, and there are no pictures. SKYSIM_CAMERA_FPS is the switch, matching the CLI where --camera-fps is what decides whether the render service exists at all. Size, quality, threads, fov, pitch and range apply once it is on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cgM68QE3FDz7S2pQAfTaZ
skysim defaults to strict — barrier every tick, abort on a miss — because that is what determinism and CI replays need. A long-lived server wants the opposite: vehicles join and leave, some lag, and none of that should take the fleet down. There was no way to say so. The entrypoint mapped --dt but not --time-mode, so every containerised deployment ran strict unless it hand-wrote SKYSIM_EXTRA_ARGS. Dev does run strict, and it is why a vehicle that reserved its slot correctly then vanished from the fleet: its 200 Hz scheduler answers one tick in four of an 800 Hz world, and strict mode drops the straggler. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cgM68QE3FDz7S2pQAfTaZ
add_if_set was introduced and then only the camera block used it — the same change hand-wrote SKYSIM_TIME_MODE in the old three-line form, next to SKYSIM_DT and SKYSIM_SPAWN_HOME already written that way. Hoisted above first use; nine lines become three. --camera-size stops restating 256x144, which src/main.cpp already defaults to. Written out in the entrypoint, the README and the terraform task definition, it meant changing the default in main.cpp would silently change nothing in a container. Verified by running the entrypoint with exec stubbed, for camera off, everything set, and camera on with size unset. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cgM68QE3FDz7S2pQAfTaZ
Interactive mode paced the world off the wall clock and evicted whatever could not keep up. That is the wrong thing to trade away. A vehicle that misses its slot gets its physics advanced without it, and the autopilot on the other side of the JSON link derives its whole scheduler rate from the timestamp delta we send — so a straggler was not merely late, it was flying a different simulation from the one it was being graded on. Now a late vehicle stalls its own tick and holds the world while it catches up. Lateness costs wall-clock pacing, which is recoverable and visible in the metrics; it no longer costs flight fidelity, which is neither. --hold-ticks becomes --straggler-timeout, expressed in seconds because a bound on how long to wait is a duration, not a count of frames whose length is itself the thing under discussion. Also: the MJPEG endpoint re-sent whatever was in the frame store on a fixed cadence, so roughly 59% of what went down the wire was a frame the client already had. Frames now carry their sim time and go out only when it changes. Measured on a local SITL, 24.7 fps and 62 KB/s to 9.8 fps and 22.7 KB/s, with nothing lost — the surplus was all duplicates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cgM68QE3FDz7S2pQAfTaZ
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughThe change adds environment-driven runtime flags and camera configuration, returns timestamped camera frames, deduplicates MJPEG output, and replaces stale-PWM stepping with barrier-based interactive timing and straggler handling. Vehicle telemetry, documentation, tests, and harness settings are updated. ChangesRuntime and control flow
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant InteractiveLoop
participant VehicleFrames
participant PhysicsStep
participant SnapshotPublisher
InteractiveLoop->>VehicleFrames: wait for current input frames
VehicleFrames-->>InteractiveLoop: provide fresh or missing frames
InteractiveLoop->>PhysicsStep: step only vehicles with frames
InteractiveLoop->>SnapshotPublisher: publish snapshots during stalls
Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (3)
README.md (1)
193-197: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDocument the two-stage timeout lifecycle.
--straggler-timeoutfreezes a silent vehicle after 2 seconds.--gracedespawns it after an additional 30 seconds by default. Update the README to distinguish these timeouts.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@README.md` around lines 193 - 197, Update the README section describing interactive mode and --straggler-timeout to document the two-stage lifecycle: a silent vehicle freezes after the straggler timeout, then is despawned after the additional --grace period, defaulting to 30 seconds. Clearly distinguish both timeout settings and retain the existing default of 2 seconds for --straggler-timeout.tests/test_api.cpp (1)
270-275: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTest duplicate-frame suppression with a stable timestamp.
The callback changes
sim_time_son every poll. Every poll therefore appears to be a new frame. The test only verifies that one multipart frame arrives, so a duplicate-per-poll implementation would still pass.Return a stable frame during several polls and assert one multipart boundary. Then advance
sim_time_sand assert that a second boundary arrives.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_api.cpp` around lines 270 - 275, Update the MJPEG callback test around the frame-producing callback to keep the frame timestamp stable across several polls, then assert that only one multipart boundary is emitted. Advance the simulated timestamp afterward and assert that a second boundary is received, ensuring duplicate-frame suppression and new-frame delivery are both tested.tests/test_render.cpp (1)
276-288: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAssert simulation-time propagation.
This test checks only
FrameStore::Frame::jpeg. It can pass ifRenderService::framelosessim_time_s. The MJPEG route insrc/api/control_server.cpp, Lines 341-363, usessim_time_sto suppress duplicate frames. Poll into a completeFrameStore::Frameand assert that its timestamp equalspose.sim_time_s.As per the PR objectives and the downstream MJPEG consumer contract, this test should cover timestamp propagation.
Proposed test update
- std::vector<uint8_t> frame; - for (int i = 0; i < 200 && frame.empty(); ++i) { + skysim::render::FrameStore::Frame frame; + for (int i = 0; i < 200 && frame.jpeg.empty(); ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); - frame = service.frame(1).jpeg; + frame = service.frame(1); } - CHECK(!frame.empty()); - CHECK(frame.size() > 2 && frame[0] == 0xFF && frame[1] == 0xD8); + CHECK(!frame.jpeg.empty()); + CHECK(frame.sim_time_s == pose.sim_time_s); + CHECK(frame.jpeg.size() > 2 && frame.jpeg[0] == 0xFF && frame.jpeg[1] == 0xD8);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_render.cpp` around lines 276 - 288, Update the frame polling in the render-service test to retrieve the complete FrameStore::Frame rather than only its jpeg field, and assert that the returned frame’s sim_time_s equals the published pose’s sim_time_s. Preserve the existing JPEG validity and despawn-clearing assertions while covering timestamp propagation through RenderService::frame.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@README.md`:
- Line 236: Update the --straggler-timeout entry in the README options table to
show that it requires a seconds argument, using the syntax --straggler-timeout
<seconds> rather than presenting it as a valueless switch.
In `@src/core/clock.h`:
- Around line 23-26: The TickGate documentation in src/core/clock.h lines 23-26
must define the barrier over connected vehicles rather than all vehicles, and
explicitly state that interactive freeze removes a vehicle from the barrier.
Update docs/DESIGN.md lines 38-42 to clarify that timeout, freeze, and blocking
apply only after a vehicle has connected.
In `@src/main.cpp`:
- Around line 2-3: Update run_strict to track any_connected independently from
any_fresh, preserving any_fresh solely for tick readiness. Base the
strict_timeout_s expiration check on any_connected so the run aborts when all
previously connected vehicles stop sending frames, while retaining existing
behavior for fresh-frame processing.
- Around line 926-950: Update wait_for_frames to exclude vehicles whose frozen
flag is already true from the pre-deadline barrier, while continuing to poll all
vehicles in the subsequent readiness loop so frozen slots can thaw when frames
resume. Keep the existing connected-slot waiting behavior for non-frozen
vehicles.
- Around line 173-174: Update the --straggler-timeout branch in parse_args to
validate the parsed value before assigning straggler_timeout_s: reject
non-finite values, negatives, and values whose later tick conversion cannot fit
in int, reporting the argument as invalid. Only assign accepted values so line
893’s conversion receives a finite, non-negative, representable timeout.
---
Nitpick comments:
In `@README.md`:
- Around line 193-197: Update the README section describing interactive mode and
--straggler-timeout to document the two-stage lifecycle: a silent vehicle
freezes after the straggler timeout, then is despawned after the additional
--grace period, defaulting to 30 seconds. Clearly distinguish both timeout
settings and retain the existing default of 2 seconds for --straggler-timeout.
In `@tests/test_api.cpp`:
- Around line 270-275: Update the MJPEG callback test around the frame-producing
callback to keep the frame timestamp stable across several polls, then assert
that only one multipart boundary is emitted. Advance the simulated timestamp
afterward and assert that a second boundary is received, ensuring
duplicate-frame suppression and new-frame delivery are both tested.
In `@tests/test_render.cpp`:
- Around line 276-288: Update the frame polling in the render-service test to
retrieve the complete FrameStore::Frame rather than only its jpeg field, and
assert that the returned frame’s sim_time_s equals the published pose’s
sim_time_s. Preserve the existing JPEG validity and despawn-clearing assertions
while covering timestamp propagation through RenderService::frame.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 498cc8ce-284b-47c3-922f-e7898c100bfb
📒 Files selected for processing (12)
README.mddocker-entrypoint.shdocs/DESIGN.mdsrc/api/control_server.cppsrc/api/control_server.hsrc/core/clock.hsrc/main.cppsrc/render/render_service.htests/test_api.cpptests/test_render.cpptools/harness/params/skysim.parmtools/harness/straggler.py
Three from review, all about the barrier's population. A frozen vehicle stayed in wait_for_frames. The barrier below excludes it — that is what freezing means — but the pre-deadline wait did not, so the fleet spent the whole frame grace every tick on a vehicle it had already agreed to step without. Freezing one dead SITL was supposed to end that stall, not move it earlier in the tick. Strict mode only started its abort timer when some vehicle was fresh, so the case the abort exists for — every SITL going silent at once — was the one it sat through forever. The gate is now "has anything connected", which still lets an empty world wait for its first arrival. --straggler-timeout went through atof into an int tick count with nothing in between. "abc" is nan and "1e400" is inf; converting either to int is undefined, so the freeze threshold became whatever that produced, silently, on the flag that says how long to tolerate silence. Plus the docs that described a barrier over all vehicles when it has never been over more than the connected ones. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cgM68QE3FDz7S2pQAfTaZ
|
All five applied in bc9314c. Exclude frozen slots from the pre-deadline wait — confirmed and embarrassing: the barrier below excludes a frozen vehicle, which is the entire meaning of freezing it, but Abort when all connected vehicles are silent — confirmed. Validate Both doc findings — taken. Verified: 17/17 ctest pass, clean build. Note for whoever merges: the red check here is |
What
Interactive mode paced the world off the wall clock and evicted whatever could not keep up. That is the wrong thing to trade away.
A vehicle that misses its slot gets its physics advanced without it — and the autopilot on the other side of the JSON link derives its entire scheduler rate from the timestamp delta we send (
SIM_JSON.cppcallsadjust_frame_time(1.0 / deltat)). So a straggler was not merely late. It was flying a different simulation from the one it was being graded on.Now a late vehicle stalls its own tick and holds the world while it catches up. Lateness costs wall-clock pacing, which is recoverable and shows up in the metrics; it no longer costs flight fidelity, which is neither.
--hold-ticksbecomes--straggler-timeout, in seconds — a bound on how long to wait is a duration, not a count of frames whose length is itself the thing under discussion.Also: the MJPEG stream was 59% duplicates
The endpoint re-sent whatever sat in the frame store on a fixed cadence, regardless of whether the renderer had produced anything new. Frames now carry their sim time and go out only when it changes.
Measured on a local SITL:
Nothing was lost — the surplus was all frames the client already had.
Note for whoever merges
The red check on this repo's PRs is
Build not triggered: Pull request approval required for starting a build— a CodeBuild policy gate, not a code failure. Unit tests (GCC and Clang), simulator happy paths, full coverage and performance budgets are all green.🤖 Generated with Claude Code
https://claude.ai/code/session_017cgM68QE3FDz7S2pQAfTaZ
Summary by CodeRabbit