diff --git a/docs/verb-reference.md b/docs/verb-reference.md index 48de1e69..c820df2d 100644 --- a/docs/verb-reference.md +++ b/docs/verb-reference.md @@ -1379,13 +1379,13 @@ Not made by the test suite: this call needs more than the two-node headless sess ### `sim.faster` -Double how much simulated time one tick covers, trading detail for how long a run takes to watch. +Double how much simulated time one tick covers, which makes the run go twice as fast until the machine cannot keep up. **Takes** nothing. -**Answers** `step_ms`. `step_ms` is the new tick length. Nothing bounds it: enough calls will step past whole transmissions. +**Answers** `step_ms`. `step_ms` is the new tick length, capped at 1000. A tick is ten milliseconds of wall time and the engine still steps in its own 10 ms, so nothing is skipped: past what the machine can step in a tick the run simply goes as fast as it can, and the transport's realtime figure says how fast that is. -**Example** - run twice as coarsely +**Example** - run twice as fast ```json {"id":1,"method":"sim.faster","params":{}} @@ -1551,9 +1551,9 @@ Halve how much simulated time one tick covers. **Takes** nothing. -**Answers** `step_ms`. `step_ms` is the new tick length, which reaches zero after enough calls and stops the clock advancing at all. +**Answers** `step_ms`. `step_ms` is the new tick length, never below 1. Below the engine's own 10 ms step, a tick steps only once it has been owed a whole step, so 5 is half real time. -**Example** - run twice as finely +**Example** - run at half the speed ```json {"id":1,"method":"sim.slower","params":{}} @@ -1569,7 +1569,7 @@ Set how much simulated time one tick covers, which is the honest form of a speed | parameter | type | | what | |---|---|---|---| -| `step_ms` | number | optional | simulated milliseconds per tick, which is what the engine actually paces on | +| `step_ms` | number | optional | simulated milliseconds per tick, from 1 to 1000; a tick is ten milliseconds of wall time, so 10 is real time and 1000 is as fast as the machine can step | | `factor` | number | optional | a multiple of ten milliseconds per tick, read only when `step_ms` is absent, for scripts written against the old socket | **Answers** `step_ms`. `step_ms` is what it settled on. Neither parameter positive leaves the tick alone and reports it. diff --git a/docs/verbs.json b/docs/verbs.json index 38e15670..dc74085c 100644 --- a/docs/verbs.json +++ b/docs/verbs.json @@ -4709,14 +4709,14 @@ } }, "sim.faster": { - "what": "double how much simulated time one tick covers, trading detail for how long a run takes to watch", + "what": "double how much simulated time one tick covers, which makes the run go twice as fast until the machine cannot keep up", "returns": [ "step_ms" ], - "answers": "`step_ms` is the new tick length. Nothing bounds it: enough calls will step past whole transmissions.", + "answers": "`step_ms` is the new tick length, capped at 1000. A tick is ten milliseconds of wall time and the engine still steps in its own 10 ms, so nothing is skipped: past what the machine can step in a tick the run simply goes as fast as it can, and the transport's realtime figure says how fast that is.", "example": { "params": {}, - "what": "run twice as coarsely", + "what": "run twice as fast", "runnable": true } }, @@ -4867,10 +4867,10 @@ "returns": [ "step_ms" ], - "answers": "`step_ms` is the new tick length, which reaches zero after enough calls and stops the clock advancing at all.", + "answers": "`step_ms` is the new tick length, never below 1. Below the engine's own 10 ms step, a tick steps only once it has been owed a whole step, so 5 is half real time.", "example": { "params": {}, - "what": "run twice as finely", + "what": "run at half the speed", "runnable": true } }, @@ -4880,7 +4880,7 @@ { "name": "step_ms", "type": "number", - "what": "simulated milliseconds per tick, which is what the engine actually paces on" + "what": "simulated milliseconds per tick, from 1 to 1000; a tick is ten milliseconds of wall time, so 10 is real time and 1000 is as fast as the machine can step" }, { "name": "factor", diff --git a/internal/app/session/clockstep.go b/internal/app/session/clockstep.go new file mode 100644 index 00000000..8860361f --- /dev/null +++ b/internal/app/session/clockstep.go @@ -0,0 +1,51 @@ +package session + +import ( + "context" + + "github.com/MeshBench/meshbench/internal/app/state" +) + +// stepTick advances the engine by one tick's worth of simulated time. +// +// A tick's worth, not one engine step. The engine's step is fixed at its own +// 10 ms, and the tick used to take exactly one of them whatever the tick size +// said - then set the clock back from the engine - so sim.faster, sim.slower +// and sim.speed changed the number on the transport and nothing else: every +// run in a session went at real time, with the processor mostly idle. +// +// So the tick is owed its size and pays it in whole engine steps. A tick +// shorter than a step pays every few ticks, which is slower than real time; a +// tick longer than the machine can cover in its 10 ms of wall time simply runs +// as fast as the machine can go, which is what the lockstep clock exists to +// allow: firmware reads simulated time, so nothing in it notices. +// +// Due sends are checked after every step rather than once a tick, so a send at +// 20 s still goes at 20 s and not at the end of a 500 ms tick. The tick stops at +// the end of a timed run, so sim.run lands where it was asked to. +func (s *Sim) stepTick(w *state.World, tickMs uint32) { + // Close takes the engine away, from the workbench's own shutdown + // goroutine, while this tick stays installed - so a step arriving after a + // close finds nothing there and the panic takes the store's goroutine + // with it. + eng := s.eng + if eng == nil { + return + } + step := eng.Config.StepMs + if step == 0 { + step = 10 + } + s.tickOwed += tickMs + for s.tickOwed >= step { + if until := w.RunUntilMs; until != 0 && eng.NowMs() >= until { + s.tickOwed = 0 + return + } + _ = eng.Step(context.Background()) + s.tickOwed -= step + // After the step, so a send lands on a mesh whose clock has already + // reached its moment. + s.fireDueSends(w) + } +} diff --git a/internal/app/session/clockstep_test.go b/internal/app/session/clockstep_test.go new file mode 100644 index 00000000..920bda4a --- /dev/null +++ b/internal/app/session/clockstep_test.go @@ -0,0 +1,56 @@ +package session + +import ( + "testing" + + "github.com/MeshBench/meshbench/internal/app/state" + "github.com/MeshBench/meshbench/internal/world/scenario" +) + +// A tick covers the tick's size in simulated time, not one engine step. It +// took one step whatever the speed control said, so "faster" relabelled the +// transport and the run went at real time regardless. +func TestATickCoversItsSize(t *testing.T) { + s := &Sim{gpuAsked: true} + s.build([]scenario.Node{repeaterNode("Alpha"), repeaterNode("Bravo")}, 869.618) + w := &state.World{} + before := s.eng.NowMs() + s.stepTick(w, 250) + if got := s.eng.NowMs() - before; got != 250 { + t.Fatalf("a 250 ms tick advanced the engine %d ms", got) + } +} + +// A timed run ends where it was asked to, even when the tick is larger than +// what is left of it. +func TestATickStopsAtTheEndOfATimedRun(t *testing.T) { + s := &Sim{gpuAsked: true} + s.build([]scenario.Node{repeaterNode("Alpha"), repeaterNode("Bravo")}, 869.618) + w := &state.World{RunUntilMs: s.eng.NowMs() + 120} + s.stepTick(w, 1000) + if s.eng.NowMs() != w.RunUntilMs { + t.Fatalf("the run was to end at %d and the engine is at %d", w.RunUntilMs, s.eng.NowMs()) + } + // And the next run does not inherit what the last tick was owed. + w.RunUntilMs = 0 + before := s.eng.NowMs() + s.stepTick(w, 10) + if got := s.eng.NowMs() - before; got != 10 { + t.Fatalf("a 10 ms tick after a timed run advanced the engine %d ms", got) + } +} + +// A tick shorter than an engine step steps only once it has been owed a whole +// one: a 5 ms tick is half speed, not real time. +func TestAShortTickStepsEveryOtherTime(t *testing.T) { + s := &Sim{gpuAsked: true} + s.build([]scenario.Node{repeaterNode("Alpha"), repeaterNode("Bravo")}, 869.618) + w := &state.World{} + before := s.eng.NowMs() + for i := 0; i < 4; i++ { + s.stepTick(w, 5) + } + if got := s.eng.NowMs() - before; got != 20 { + t.Fatalf("four 5 ms ticks advanced the engine %d ms, want 20", got) + } +} diff --git a/internal/app/session/engine.go b/internal/app/session/engine.go index 8f075075..2cfcdb35 100644 --- a/internal/app/session/engine.go +++ b/internal/app/session/engine.go @@ -33,6 +33,9 @@ type Sim struct { // sendClock is when each scheduled send last fired, so a repeating one // repeats per interval rather than per tick. sendClock sendClock + // tickOwed is simulated time a tick has been given and the engine has not + // yet stepped through, because a tick can be shorter than one step. + tickOwed uint32 // statesMu guards states, which Reflash writes from its own goroutine and // the store goroutine reads to answer nodes.stats. statesMu sync.Mutex diff --git a/internal/app/session/simctl.verbs.json b/internal/app/session/simctl.verbs.json index 7a0c7012..8b54d6fd 100644 --- a/internal/app/session/simctl.verbs.json +++ b/internal/app/session/simctl.verbs.json @@ -84,7 +84,7 @@ { "name": "step_ms", "type": "number", - "what": "simulated milliseconds per tick, which is what the engine actually paces on" + "what": "simulated milliseconds per tick, from 1 to 1000; a tick is ten milliseconds of wall time, so 10 is real time and 1000 is as fast as the machine can step" }, { "name": "factor", diff --git a/internal/app/session/verbs.go b/internal/app/session/verbs.go index fb88fb86..c47ec48d 100644 --- a/internal/app/session/verbs.go +++ b/internal/app/session/verbs.go @@ -174,25 +174,14 @@ func Register(st *state.Store, s *Sim) { for i, n := range f.nodes { index[n.Name] = i } - w.Tick = func(uint32) { + w.Tick = func(tickMs uint32) { w.FirmwareRunning, w.FirmwareStarting = s.firmwareCount(), s.starting.Load() // A sweep cell paces its own engine against wall time and counts on // that pacing. Stepping it again here would not make the run // faster - it would make its airtime accounting wrong - so while a // cell owns the clock this only reads. if !s.benchOwnsTheClock() { - // Close takes the engine away, from the workbench's own - // shutdown goroutine, while this tick stays installed - so a - // step arriving after a close finds nothing there and the - // panic takes the store's goroutine with it. fireDueSends - // below has always asked; this is the call that did not. - if eng := s.eng; eng != nil { - _ = eng.Step(context.Background()) - } - // Whatever the schedule is due to say at this moment of - // simulated time. After the step, so a send lands on a mesh - // whose clock has already reached its moment. - s.fireDueSends(w) + s.stepTick(w, tickMs) } // A rebuild anywhere leaves the matrix cold; this is the one // place every run passes through, so it is where the warm is diff --git a/internal/app/session/verbsclock.verbs.json b/internal/app/session/verbsclock.verbs.json index b27dd8f5..d9ad3d3f 100644 --- a/internal/app/session/verbsclock.verbs.json +++ b/internal/app/session/verbsclock.verbs.json @@ -1,13 +1,13 @@ { "sim.faster": { - "what": "double how much simulated time one tick covers, trading detail for how long a run takes to watch", + "what": "double how much simulated time one tick covers, which makes the run go twice as fast until the machine cannot keep up", "returns": [ "step_ms" ], - "answers": "`step_ms` is the new tick length. Nothing bounds it: enough calls will step past whole transmissions.", + "answers": "`step_ms` is the new tick length, capped at 1000. A tick is ten milliseconds of wall time and the engine still steps in its own 10 ms, so nothing is skipped: past what the machine can step in a tick the run simply goes as fast as it can, and the transport's realtime figure says how fast that is.", "example": { "params": {}, - "what": "run twice as coarsely", + "what": "run twice as fast", "runnable": true } }, @@ -39,10 +39,10 @@ "returns": [ "step_ms" ], - "answers": "`step_ms` is the new tick length, which reaches zero after enough calls and stops the clock advancing at all.", + "answers": "`step_ms` is the new tick length, never below 1. Below the engine's own 10 ms step, a tick steps only once it has been owed a whole step, so 5 is half real time.", "example": { "params": {}, - "what": "run twice as finely", + "what": "run at half the speed", "runnable": true } },