• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mlange-42 / arche-model / 6698858792

30 Oct 2023 09:19PM CUT coverage: 98.208% (+0.8%) from 97.455%
6698858792

push

github

web-flow
Fix full CPU load when paused (#47)

As reported in mlange-42/arche#304, systems spin at maximum speed when paused.

This PR fixes the issue by calculating the theoretical next update time even when paused (assuming 30 TPS). Also, UI system update logic needs to be performed even when there are no UI systems.

Fixes mlange-42/arche#304

Changes:

* prevent paused spinning by still calculating next update
* add a test for pausing the model/Systems
* limit FPS/TPS when paused
* execute UI system update logic also when there are no UI systems
* update changelog

30 of 30 new or added lines in 1 file covered. (100.0%)

548 of 558 relevant lines covered (98.21%)

124.11 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

100.0
/model/systems.go
1
package model
2

3
import (
4
        "fmt"
5
        "time"
6

7
        "github.com/mlange-42/arche-model/resource"
8
        "github.com/mlange-42/arche/ecs"
9
        "github.com/mlange-42/arche/generic"
10
)
11

12
// System is the interface for ECS systems.
13
//
14
// See also [UISystem] for systems with an independent graphics step.
15
type System interface {
16
        Initialize(w *ecs.World) // Initialize the system.
17
        Update(w *ecs.World)     // Update the system.
18
        Finalize(w *ecs.World)   // Finalize the system.
19
}
20

21
// UISystem is the interface for ECS systems that display UI in an independent graphics step.
22
//
23
// See also [System] for normal systems.
24
type UISystem interface {
25
        InitializeUI(w *ecs.World) // InitializeUI the system.
26
        UpdateUI(w *ecs.World)     // UpdateUI/update the system.
27
        PostUpdateUI(w *ecs.World) // PostUpdateUI does the final part of updating, e.g. update the GL window.
28
        FinalizeUI(w *ecs.World)   // FinalizeUI the system.
29
}
30

31
// Systems manages and schedules ECS [System] and [UISystem] instances.
32
//
33
// [System] instances are updated with a frequency given by TPS (ticks per second).
34
// [UISystem] instances are updated independently of normal systems, with a frequency given by FPS (frames per second).
35
//
36
// [Systems] is an embed in [Model] and it's methods are usually only used through a [Model] instance.
37
// By also being a resource of each [Model], however, systems can access it and e.g. remove themselves from a model.
38
type Systems struct {
39
        // Ticks per second for normal systems.
40
        // Values <= 0 (the default) mean as fast as possible.
41
        TPS float64
42
        // Frames per second for UI systems.
43
        // A zero/unset value defaults to 30 FPS. Values < 0 sync FPS with TPS.
44
        // With fast movement, a value of 60 may be required for fluent graphics.
45
        FPS float64
46
        // Whether the simulation is currently paused.
47
        // When paused, only UI updates but no normal updates are performed.
48
        Paused bool
49

50
        world      *ecs.World
51
        systems    []System
52
        uiSystems  []UISystem
53
        toRemove   []System
54
        uiToRemove []UISystem
55

56
        nextDraw   time.Time
57
        nextUpdate time.Time
58

59
        initialized bool
60
        locked      bool
61

62
        tickRes generic.Resource[resource.Tick]
63
        termRes generic.Resource[resource.Termination]
64
}
65

66
// Systems returns the normal/non-UI systems.
67
func (s *Systems) Systems() []System {
3✔
68
        return s.systems
3✔
69
}
3✔
70

71
// UISystems returns the UI systems.
72
func (s *Systems) UISystems() []UISystem {
3✔
73
        return s.uiSystems
3✔
74
}
3✔
75

76
// AddSystem adds a [System] to the model.
77
//
78
// Panics if the system is also a [UISystem].
79
// To add systems that implement both [System] and [UISystem], use [Systems.AddUISystem]
80
func (s *Systems) AddSystem(sys System) {
36✔
81
        if s.initialized {
39✔
82
                panic("adding systems after model initialization is not implemented yet")
3✔
83
        }
84
        if sys, ok := sys.(UISystem); ok {
36✔
85
                panic(fmt.Sprintf("System %T is also an UI system. Must be added via AddSystem.", sys))
3✔
86
        }
87
        s.systems = append(s.systems, sys)
30✔
88
}
89

90
// AddUISystem adds an [UISystem] to the model.
91
//
92
// Adds the [UISystem] also as a normal [System] if it implements the interface.
93
func (s *Systems) AddUISystem(sys UISystem) {
14✔
94
        if s.initialized {
17✔
95
                panic("adding systems after model initialization is not implemented yet")
3✔
96
        }
97
        s.uiSystems = append(s.uiSystems, sys)
11✔
98
        if sys, ok := sys.(System); ok {
14✔
99
                s.systems = append(s.systems, sys)
3✔
100
        }
3✔
101
}
102

103
// RemoveSystem removes a system from the model.
104
//
105
// Systems can also be removed during a model run.
106
// However, this will take effect only after the end of the full model step.
107
func (s *Systems) RemoveSystem(sys System) {
10✔
108
        if sys, ok := sys.(UISystem); ok {
13✔
109
                panic(fmt.Sprintf("System %T is also an UI system. Must be removed via RemoveUISystem.", sys))
3✔
110
        }
111
        s.toRemove = append(s.toRemove, sys)
7✔
112
        if !s.locked {
11✔
113
                s.removeSystems()
4✔
114
        }
4✔
115
}
116

117
// RemoveUISystem removes an UI system from the model.
118
//
119
// Systems can also be removed during a model run.
120
// However, this will take effect only after the end of the full model step.
121
func (s *Systems) RemoveUISystem(sys UISystem) {
12✔
122
        s.uiToRemove = append(s.uiToRemove, sys)
12✔
123
        if !s.locked {
21✔
124
                s.removeSystems()
9✔
125
        }
9✔
126
}
127

128
// Removes systems that were removed during the model step.
129
func (s *Systems) removeSystems() {
1,461✔
130
        rem := s.toRemove
1,461✔
131
        remUI := s.uiToRemove
1,461✔
132

1,461✔
133
        s.toRemove = s.toRemove[:0]
1,461✔
134
        s.uiToRemove = s.uiToRemove[:0]
1,461✔
135

1,461✔
136
        for _, sys := range rem {
1,468✔
137
                s.removeSystem(sys)
7✔
138
        }
7✔
139
        for _, sys := range remUI {
1,470✔
140
                if sys, ok := sys.(System); ok {
18✔
141
                        s.removeSystem(sys)
6✔
142
                }
6✔
143
                s.removeUISystem(sys)
9✔
144
        }
145
}
146

147
func (s *Systems) removeSystem(sys System) {
16✔
148
        if s.locked {
19✔
149
                panic("can't remove a system in locked state")
3✔
150
        }
151
        idx := -1
13✔
152
        for i := 0; i < len(s.systems); i++ {
35✔
153
                if sys == s.systems[i] {
29✔
154
                        idx = i
7✔
155
                        break
7✔
156
                }
157
        }
158
        if idx < 0 {
19✔
159
                panic(fmt.Sprintf("can't remove system %T: not in the model", sys))
6✔
160
        }
161
        s.systems[idx].Finalize(s.world)
7✔
162
        s.systems = append(s.systems[:idx], s.systems[idx+1:]...)
7✔
163
}
164

165
func (s *Systems) removeUISystem(sys UISystem) {
12✔
166
        if s.locked {
15✔
167
                panic("can't remove a system in locked state")
3✔
168
        }
169
        idx := -1
9✔
170
        for i := 0; i < len(s.uiSystems); i++ {
15✔
171
                if sys == s.uiSystems[i] {
12✔
172
                        idx = i
6✔
173
                        break
6✔
174
                }
175
        }
176
        if idx < 0 {
12✔
177
                panic(fmt.Sprintf("can't remove UI system %T: not in the model", sys))
3✔
178
        }
179
        s.uiSystems[idx].FinalizeUI(s.world)
6✔
180
        s.uiSystems = append(s.uiSystems[:idx], s.uiSystems[idx+1:]...)
6✔
181
}
182

183
// Initialize all systems.
184
func (s *Systems) initialize() {
26✔
185
        if s.initialized {
29✔
186
                panic("model is already initialized")
3✔
187
        }
188

189
        if s.FPS == 0 {
25✔
190
                s.FPS = 30
2✔
191
        }
2✔
192

193
        s.tickRes = generic.NewResource[resource.Tick](s.world)
23✔
194
        s.termRes = generic.NewResource[resource.Termination](s.world)
23✔
195

23✔
196
        s.locked = true
23✔
197
        for _, sys := range s.systems {
55✔
198
                sys.Initialize(s.world)
32✔
199
        }
32✔
200
        for _, sys := range s.uiSystems {
34✔
201
                sys.InitializeUI(s.world)
11✔
202
        }
11✔
203
        s.locked = false
23✔
204
        s.removeSystems()
23✔
205
        s.initialized = true
23✔
206

23✔
207
        s.nextDraw = time.Time{}
23✔
208
        s.nextUpdate = time.Time{}
23✔
209
}
210

211
// Update all systems.
212
func (s *Systems) update() {
1,402✔
213
        s.locked = true
1,402✔
214
        update := s.updateSystems()
1,402✔
215
        s.updateUISystems(update)
1,402✔
216
        s.locked = false
1,402✔
217

1,402✔
218
        s.removeSystems()
1,402✔
219

1,402✔
220
        if update {
2,682✔
221
                time := s.tickRes.Get()
1,280✔
222
                time.Tick++
1,280✔
223
        } else {
1,402✔
224
                s.wait()
122✔
225
        }
122✔
226
}
227

228
// Calculates and waits the time until the next update of UI update.
229
func (s *Systems) wait() {
122✔
230
        nextUpdate := s.nextUpdate
122✔
231

122✔
232
        if (s.Paused || s.FPS > 0) && s.nextDraw.Before(nextUpdate) {
201✔
233
                nextUpdate = s.nextDraw
79✔
234
        }
79✔
235

236
        t := time.Now()
122✔
237
        wait := nextUpdate.Sub(t)
122✔
238

122✔
239
        if wait > 0 {
243✔
240
                time.Sleep(wait)
121✔
241
        }
121✔
242
}
243

244
// Update normal systems.
245
func (s *Systems) updateSystems() bool {
1,402✔
246
        update := false
1,402✔
247
        if s.Paused {
1,503✔
248
                update = !time.Now().Before(s.nextUpdate)
101✔
249
                if update {
136✔
250
                        tps := s.limitedFps(s.TPS, 10)
35✔
251
                        s.nextUpdate = nextTime(s.nextUpdate, tps)
35✔
252
                }
35✔
253
                return false
101✔
254
        }
255
        if s.TPS <= 0 {
2,566✔
256
                update = true
1,265✔
257
                for _, sys := range s.systems {
2,602✔
258
                        sys.Update(s.world)
1,337✔
259
                }
1,337✔
260
        } else {
36✔
261
                update = !time.Now().Before(s.nextUpdate)
36✔
262
                if update {
51✔
263
                        s.nextUpdate = nextTime(s.nextUpdate, s.TPS)
15✔
264
                        for _, sys := range s.systems {
30✔
265
                                sys.Update(s.world)
15✔
266
                        }
15✔
267
                }
268
        }
269
        return update
1,301✔
270
}
271

272
// Update UI systems.
273
func (s *Systems) updateUISystems(updated bool) {
1,402✔
274
        if !s.Paused && s.FPS <= 0 {
1,410✔
275
                if updated {
13✔
276
                        for _, sys := range s.uiSystems {
10✔
277
                                sys.UpdateUI(s.world)
5✔
278
                        }
5✔
279
                        for _, sys := range s.uiSystems {
10✔
280
                                sys.PostUpdateUI(s.world)
5✔
281
                        }
5✔
282
                }
283
        } else {
1,394✔
284
                if !time.Now().Before(s.nextDraw) {
1,555✔
285
                        fps := s.FPS
161✔
286
                        if s.Paused {
262✔
287
                                fps = s.limitedFps(s.FPS, 30)
101✔
288
                        }
101✔
289
                        s.nextDraw = nextTime(s.nextDraw, fps)
161✔
290
                        for _, sys := range s.uiSystems {
298✔
291
                                sys.UpdateUI(s.world)
137✔
292
                        }
137✔
293
                        for _, sys := range s.uiSystems {
298✔
294
                                sys.PostUpdateUI(s.world)
137✔
295
                        }
137✔
296
                }
297
        }
298
}
299

300
// Finalize all systems.
301
func (s *Systems) finalize() {
23✔
302
        s.locked = true
23✔
303
        for _, sys := range s.systems {
52✔
304
                sys.Finalize(s.world)
29✔
305
        }
29✔
306
        for _, sys := range s.uiSystems {
31✔
307
                sys.FinalizeUI(s.world)
8✔
308
        }
8✔
309
        s.locked = false
23✔
310
        s.removeSystems()
23✔
311
}
312

313
// Run the model.
314
func (s *Systems) run() {
23✔
315
        if !s.initialized {
46✔
316
                s.initialize()
23✔
317
        }
23✔
318

319
        time := s.tickRes.Get()
23✔
320
        time.Tick = 0
23✔
321
        terminate := s.termRes.Get()
23✔
322

23✔
323
        for !terminate.Terminate {
1,425✔
324
                s.update()
1,402✔
325
        }
1,402✔
326

327
        s.finalize()
23✔
328
}
329

330
// Removes all systems.
331
func (s *Systems) reset() {
16✔
332
        s.systems = []System{}
16✔
333
        s.uiSystems = []UISystem{}
16✔
334
        s.toRemove = s.toRemove[:0]
16✔
335
        s.uiToRemove = s.uiToRemove[:0]
16✔
336

16✔
337
        s.nextDraw = time.Time{}
16✔
338
        s.nextUpdate = time.Time{}
16✔
339

16✔
340
        s.initialized = false
16✔
341
        s.tickRes = generic.Resource[resource.Tick]{}
16✔
342
}
16✔
343

344
// Calculates frame rate capped to target
345
func (s *Systems) limitedFps(actual, target float64) float64 {
136✔
346
        if actual > target || actual <= 0 {
171✔
347
                return target
35✔
348
        }
35✔
349
        return actual
101✔
350
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc