Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions docs/verb-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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":{}}
Expand Down Expand Up @@ -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":{}}
Expand All @@ -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.
Expand Down
12 changes: 6 additions & 6 deletions docs/verbs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
},
Expand Down Expand Up @@ -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
}
},
Expand All @@ -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",
Expand Down
51 changes: 51 additions & 0 deletions internal/app/session/clockstep.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
56 changes: 56 additions & 0 deletions internal/app/session/clockstep_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
3 changes: 3 additions & 0 deletions internal/app/session/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion internal/app/session/simctl.verbs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
15 changes: 2 additions & 13 deletions internal/app/session/verbs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions internal/app/session/verbsclock.verbs.json
Original file line number Diff line number Diff line change
@@ -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
}
},
Expand Down Expand Up @@ -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
}
},
Expand Down
Loading