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

mlange-42 / ark-pixel / 13710361931

06 Mar 2025 11:36PM CUT coverage: 21.025% (-58.0%) from 79.07%
13710361931

push

github

web-flow
Add Controls and Monitor (#3)

51 of 480 new or added lines in 3 files covered. (10.63%)

119 of 566 relevant lines covered (21.02%)

6.0 hits per line

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

3.5
/plot/monitor.go
1
package plot
2

3
import (
4
        "fmt"
5
        "image/color"
6
        "math"
7
        "strings"
8
        "time"
9

10
        px "github.com/gopxl/pixel/v2"
11
        "github.com/gopxl/pixel/v2/backends/opengl"
12
        "github.com/gopxl/pixel/v2/ext/imdraw"
13
        "github.com/gopxl/pixel/v2/ext/text"
14
        "github.com/mlange-42/ark-pixel/window"
15
        "github.com/mlange-42/ark/ecs"
16
        "github.com/mlange-42/ark/ecs/stats"
17
)
18

19
type timeSeriesType uint8
20

21
const (
22
        tsEntities timeSeriesType = iota
23
        tsEntityCap
24
        tsMemory
25
        tsTickPerSec
26
        tsLast
27
)
28

29
var (
30
        colorGreen     = color.RGBA{0, 130, 40, 255}
31
        colorDarkGreen = color.RGBA{20, 80, 25, 255}
32
        colorCyan      = color.RGBA{0, 100, 120, 255}
33
        colorDarkCyan  = color.RGBA{20, 50, 70, 255}
34
)
35

36
// NewMonitorWindow creates a window with [Monitor] drawer, for immediate use as a system.
37
// See [Monitor] for details.
38
//
39
// Also adds [Controls] for pausing/resuming the simulation.
40
func NewMonitorWindow(drawInterval int) *window.Window {
1✔
41
        return (&window.Window{
1✔
42
                Title:        "Monitor",
1✔
43
                DrawInterval: drawInterval,
1✔
44
        }).With(
1✔
45
                &Monitor{
1✔
46
                        SampleInterval: time.Second / 3,
1✔
47
                },
1✔
48
                &Controls{},
1✔
49
        )
1✔
50
}
1✔
51

52
// Monitor drawer for visualizing world and performance statistics.
53
//
54
// Symbology:
55
//   - Green bars: archetypes without entity relations
56
//   - Cyan bars: archetypes with entity relations
57
//   - Light green/cyan: currently used
58
//   - Dark green/cyan: reserved
59
//
60
// Top info:
61
//   - Tick: current model tick
62
//   - Ent: total number of entities
63
//   - Nodes: active/total nodes in archetype graph
64
//   - Comp: number of component types
65
//   - Cache: number of cached filters
66
//   - Mem: total memory reserved for entities and components
67
//   - TPS: (simulation) ticks per second
68
//   - TPT: time per (simulation) tick
69
//   - Time: total run time of the simulation
70
type Monitor struct {
71
        PlotCapacity   int           // Number of values in time series plots. Optional, default 300.
72
        SampleInterval time.Duration // Approx. time between measurements for time series plots. Optional, default 1 second.
73
        HidePlots      bool          // Hides time series plots
74
        HideArchetypes bool          // Hides archetype stats
75
        scale          float64
76
        drawer         imdraw.IMDraw
77
        summary        *text.Text
78
        timeSeries     timeSeries
79
        frameTimer     frameTimer
80
        archetypes     archetypes
81
        text           *text.Text
82
        startTime      time.Time
83
        lastPlotUpdate time.Time
84
        step           int64
85
}
86

87
// Initialize the system
NEW
88
func (m *Monitor) Initialize(w *ecs.World, win *opengl.Window) {
×
NEW
89
        if m.PlotCapacity <= 0 {
×
NEW
90
                m.PlotCapacity = 300
×
NEW
91
        }
×
NEW
92
        if m.SampleInterval <= 0 {
×
NEW
93
                m.SampleInterval = time.Second
×
NEW
94
        }
×
NEW
95
        m.lastPlotUpdate = time.Now()
×
NEW
96
        m.startTime = m.lastPlotUpdate
×
NEW
97

×
NEW
98
        m.drawer = *imdraw.New(nil)
×
NEW
99

×
NEW
100
        m.scale = calcScaleCorrection()
×
NEW
101

×
NEW
102
        m.summary = text.New(px.V(0, 0), defaultFont)
×
NEW
103
        m.summary.AlignedTo(px.BottomRight)
×
NEW
104

×
NEW
105
        m.timeSeries = newTimeSeries(m.PlotCapacity)
×
NEW
106
        for i := 0; i < len(m.timeSeries.Text); i++ {
×
NEW
107
                m.timeSeries.Text[i] = text.New(px.V(0, 0), defaultFont)
×
NEW
108
        }
×
NEW
109
        fmt.Fprintf(m.timeSeries.Text[tsEntities], "Entities")
×
NEW
110
        fmt.Fprintf(m.timeSeries.Text[tsEntityCap], "Capacity")
×
NEW
111
        fmt.Fprintf(m.timeSeries.Text[tsMemory], "Memory")
×
NEW
112
        fmt.Fprintf(m.timeSeries.Text[tsTickPerSec], "TPS")
×
NEW
113

×
NEW
114
        m.text = text.New(px.V(0, 0), defaultFont)
×
NEW
115
        m.text.Color = color.RGBA{200, 200, 200, 255}
×
NEW
116

×
NEW
117
        m.step = 0
×
118
}
119

120
// Update the drawer.
NEW
121
func (m *Monitor) Update(w *ecs.World) {
×
NEW
122
        t := time.Now()
×
NEW
123
        m.frameTimer.Update(m.step, t)
×
NEW
124

×
NEW
125
        if !m.HidePlots && t.Sub(m.lastPlotUpdate) >= m.SampleInterval {
×
NEW
126
                stats := w.Stats()
×
NEW
127
                m.archetypes.Update(stats)
×
NEW
128
                m.timeSeries.append(
×
NEW
129
                        stats.Entities.Used, stats.Entities.Total, stats.Memory,
×
NEW
130
                        int(m.frameTimer.FPS()*1000000),
×
NEW
131
                )
×
NEW
132
                m.lastPlotUpdate = t
×
NEW
133
        }
×
NEW
134
        m.step++
×
135
}
136

137
// UpdateInputs handles input events of the previous frame update.
NEW
138
func (m *Monitor) UpdateInputs(w *ecs.World, win *opengl.Window) {}
×
139

140
// Draw the system
NEW
141
func (m *Monitor) Draw(w *ecs.World, win *opengl.Window) {
×
NEW
142
        stats := w.Stats()
×
NEW
143
        m.archetypes.Update(stats)
×
NEW
144

×
NEW
145
        width := win.Canvas().Bounds().W()
×
NEW
146
        height := win.Canvas().Bounds().H()
×
NEW
147

×
NEW
148
        m.summary.Clear()
×
NEW
149
        mem, units := toMemText(stats.Memory)
×
NEW
150
        split := width < 1080
×
NEW
151
        fmt.Fprintf(
×
NEW
152
                m.summary, "Tick: %8d  |  Ent.: %7d  |  Archetypes: %3d  |  Comp: %3d  |  Cache: %3d",
×
NEW
153
                m.step, stats.Entities.Used, len(stats.Archetypes), stats.ComponentCount, stats.CachedFilters,
×
NEW
154
        )
×
NEW
155
        if split {
×
NEW
156
                fmt.Fprintf(
×
NEW
157
                        m.summary, "\nMem: %6.1f %s  |  TPS: %8.1f  |  TPT: %6.2f ms  |  Time: %s",
×
NEW
158
                        mem, units, m.frameTimer.FPS(),
×
NEW
159
                        float64(m.frameTimer.FrameTime().Microseconds())/1000, time.Since(m.startTime).Round(time.Second),
×
NEW
160
                )
×
NEW
161
        } else {
×
NEW
162
                fmt.Fprintf(
×
NEW
163
                        m.summary, "  |  Mem: %6.1f %s  |  TPS: %6.1f  |  TPT: %6.2f ms  |  Time: %s",
×
NEW
164
                        mem, units, m.frameTimer.FPS(),
×
NEW
165
                        float64(m.frameTimer.FrameTime().Microseconds())/1000, time.Since(m.startTime).Round(time.Second),
×
NEW
166
                )
×
NEW
167
        }
×
168

NEW
169
        numNodes := len(m.archetypes.Components)
×
NEW
170
        maxCapacity := 0
×
NEW
171
        for i := 0; i < numNodes; i++ {
×
NEW
172
                cap := stats.Archetypes[m.archetypes.Indices[i]].Capacity
×
NEW
173
                if cap > maxCapacity {
×
NEW
174
                        maxCapacity = cap
×
NEW
175
                }
×
176
        }
NEW
177
        dr := &m.drawer
×
NEW
178
        x0 := 6.0
×
NEW
179
        y0 := height - 18.0
×
NEW
180

×
NEW
181
        m.summary.Draw(win, px.IM.Moved(px.V(x0, y0+10)))
×
NEW
182
        y0 -= 10
×
NEW
183

×
NEW
184
        if split {
×
NEW
185
                y0 -= 10
×
NEW
186
        }
×
187

NEW
188
        if !m.HidePlots {
×
NEW
189
                plotY0 := y0
×
NEW
190
                plotHeight := (y0 - 40) / 3
×
NEW
191
                if plotHeight > 150 {
×
NEW
192
                        plotHeight = 150
×
NEW
193
                }
×
NEW
194
                plotWidth := (width - 20) * 0.25
×
NEW
195
                if m.HideArchetypes {
×
NEW
196
                        plotWidth = width - 20
×
NEW
197
                }
×
NEW
198
                m.drawPlot(win, x0, plotY0-plotHeight, plotWidth, plotHeight, tsEntities, tsEntityCap)
×
NEW
199
                plotY0 -= plotHeight + 10
×
NEW
200
                m.drawPlot(win, x0, plotY0-plotHeight, plotWidth, plotHeight, tsMemory)
×
NEW
201
                plotY0 -= plotHeight + 10
×
NEW
202
                m.drawPlot(win, x0, plotY0-plotHeight, plotWidth, plotHeight, tsTickPerSec)
×
NEW
203

×
NEW
204
                x0 += math.Ceil(plotWidth + 10)
×
205
        }
206

NEW
207
        archHeight := math.Ceil((y0 - 10) / float64(numNodes+1))
×
NEW
208
        if !m.HideArchetypes {
×
NEW
209
                if archHeight >= 8 {
×
NEW
210
                        archWidth := width - x0 - 10
×
NEW
211
                        if archHeight > 20 {
×
NEW
212
                                archHeight = 20
×
NEW
213
                        }
×
NEW
214
                        m.drawArchetypeScales(
×
NEW
215
                                win, x0, y0-archHeight, archWidth, archHeight, maxCapacity,
×
NEW
216
                        )
×
NEW
217
                        for i := 0; i < numNodes; i++ {
×
NEW
218
                                idx := m.archetypes.Indices[i]
×
NEW
219
                                m.drawArchetype(
×
NEW
220
                                        win, x0, y0-float64(i+2)*archHeight, archWidth, archHeight,
×
NEW
221
                                        maxCapacity, &stats.Archetypes[idx], m.archetypes.Components[i],
×
NEW
222
                                )
×
NEW
223
                        }
×
NEW
224
                } else {
×
NEW
225
                        m.text.Clear()
×
NEW
226
                        fmt.Fprintf(m.text, "Too many archetypes")
×
NEW
227
                        m.text.Draw(win, px.IM.Moved(px.V(x0, y0-10)))
×
NEW
228
                }
×
229
        }
230

NEW
231
        dr.Draw(win)
×
NEW
232
        dr.Clear()
×
233
}
234

NEW
235
func (m *Monitor) drawArchetypeScales(win *opengl.Window, x, y, w, h float64, max int) {
×
NEW
236
        dr := &m.drawer
×
NEW
237
        step := calcTicksStep(float64(max), 8)
×
NEW
238
        if step < 1 {
×
NEW
239
                return
×
NEW
240
        }
×
NEW
241
        drawStep := w * step / float64(max)
×
NEW
242

×
NEW
243
        dr.Color = color.RGBA{140, 140, 140, 255}
×
NEW
244
        dr.Push(px.V(x, y+2), px.V(x+w, y+2))
×
NEW
245
        dr.Line(1)
×
NEW
246
        dr.Reset()
×
NEW
247

×
NEW
248
        steps := int(float64(max) / step)
×
NEW
249
        for i := 0; i <= steps; i++ {
×
NEW
250
                xi := float64(i)
×
NEW
251
                dr.Push(px.V(x+xi*drawStep, y+2), px.V(x+xi*drawStep, y+7))
×
NEW
252
                dr.Line(1)
×
NEW
253
                dr.Reset()
×
NEW
254

×
NEW
255
                val := i * int(step)
×
NEW
256
                m.text.Clear()
×
NEW
257
                fmt.Fprintf(m.text, "%d", val)
×
NEW
258
                m.text.Draw(win, px.IM.Moved(px.V(math.Floor(x+xi*drawStep-m.text.Bounds().W()/2), y+10)))
×
NEW
259
        }
×
260
}
261

NEW
262
func (m *Monitor) drawArchetype(win *opengl.Window, x, y, w, h float64, max int, arch *stats.Archetype, text *text.Text) {
×
NEW
263
        dr := &m.drawer
×
NEW
264

×
NEW
265
        cap := float64(arch.Capacity) / float64(max)
×
NEW
266
        cnt := float64(arch.Size) / float64(max)
×
NEW
267

×
NEW
268
        if arch.NumRelations > 0 {
×
NEW
269
                dr.Color = colorCyan
×
NEW
270
        } else {
×
NEW
271
                dr.Color = colorGreen
×
NEW
272
        }
×
NEW
273
        dr.Push(px.V(x, y), px.V(x+w*cnt, y+h))
×
NEW
274
        dr.Rectangle(0)
×
NEW
275
        dr.Reset()
×
NEW
276

×
NEW
277
        if arch.NumRelations > 0 {
×
NEW
278
                dr.Color = colorDarkCyan
×
NEW
279
        } else {
×
NEW
280
                dr.Color = colorDarkGreen
×
NEW
281
        }
×
NEW
282
        dr.Push(px.V(x+w*cnt, y), px.V(x+w*cap, y+h))
×
NEW
283
        dr.Rectangle(0)
×
NEW
284
        dr.Reset()
×
NEW
285

×
NEW
286
        dr.Color = color.RGBA{40, 40, 40, 255}
×
NEW
287
        dr.Push(px.V(x, y), px.V(x+w, y+h))
×
NEW
288
        dr.Rectangle(1)
×
NEW
289
        dr.Reset()
×
NEW
290

×
NEW
291
        dr.Draw(win)
×
NEW
292
        dr.Clear()
×
NEW
293

×
NEW
294
        text.Draw(win, px.IM.Moved(px.V(x+3, y+3)))
×
NEW
295

×
NEW
296
        if arch.NumRelations > 0 {
×
NEW
297
                m.text.Clear()
×
NEW
298
                fmt.Fprintf(m.text, "%5d / %5d", len(arch.Tables), len(arch.Tables)+arch.FreeTables)
×
NEW
299
                m.text.Draw(win, px.IM.Moved(px.V(x+3, y+3)))
×
NEW
300
        }
×
301
}
302

NEW
303
func (m *Monitor) drawPlot(win *opengl.Window, x, y, w, h float64, series ...timeSeriesType) {
×
NEW
304
        dr := &m.drawer
×
NEW
305

×
NEW
306
        dr.Color = color.RGBA{0, 25, 10, 255}
×
NEW
307
        dr.Push(px.V(x, y), px.V(x+w, y+h))
×
NEW
308
        dr.Rectangle(0)
×
NEW
309
        dr.Reset()
×
NEW
310

×
NEW
311
        yMax := 0
×
NEW
312
        for _, series := range series {
×
NEW
313
                values := m.timeSeries.Values[series]
×
NEW
314
                l := values.Len()
×
NEW
315
                for i := 0; i < l; i++ {
×
NEW
316
                        v := values.Get(i)
×
NEW
317
                        if v > yMax {
×
NEW
318
                                yMax = v
×
NEW
319
                        }
×
320
                }
321
        }
322

NEW
323
        dr.Color = color.White
×
NEW
324
        for _, series := range series {
×
NEW
325
                values := m.timeSeries.Values[series]
×
NEW
326
                numValues := values.Len()
×
NEW
327
                if numValues > 0 {
×
NEW
328
                        xStep := w / float64(numValues-1)
×
NEW
329
                        yScale := 0.95 * h / float64(yMax)
×
NEW
330

×
NEW
331
                        for i := 0; i < numValues-1; i++ {
×
NEW
332
                                xi := float64(i)
×
NEW
333
                                x1, x2 := xi*xStep, xi*xStep+xStep
×
NEW
334
                                y1, y2 := float64(values.Get(i))*yScale, float64(values.Get(i+1))*yScale
×
NEW
335

×
NEW
336
                                dr.Push(px.V(x+x1, y+y1), px.V(x+x2, y+y2))
×
NEW
337
                                dr.Line(1)
×
NEW
338
                                dr.Reset()
×
NEW
339
                        }
×
340
                }
341
        }
342

NEW
343
        dr.Color = color.RGBA{140, 140, 140, 255}
×
NEW
344
        dr.Push(px.V(x, y), px.V(x+w, y+h))
×
NEW
345
        dr.Rectangle(1)
×
NEW
346
        dr.Reset()
×
NEW
347

×
NEW
348
        dr.Draw(win)
×
NEW
349
        dr.Clear()
×
NEW
350

×
NEW
351
        if len(series) > 0 {
×
NEW
352
                text := m.timeSeries.Text[series[0]]
×
NEW
353
                text.Draw(win, px.IM.Moved(px.V(x+w-text.Bounds().W()-3, y+3)))
×
NEW
354
        }
×
355
}
356

NEW
357
func toMemText(bytes int) (float64, string) {
×
NEW
358
        if bytes <= 10*1_024_000 {
×
NEW
359
                return float64(bytes) / 1024, "kB"
×
NEW
360
        }
×
NEW
361
        return float64(bytes) / 1_024_000, "MB"
×
362
}
363

364
type timeSeries struct {
365
        Values [tsLast]ringBuffer[int]
366
        Text   [tsLast]*text.Text
367
}
368

NEW
369
func newTimeSeries(cap int) timeSeries {
×
NEW
370
        ts := timeSeries{}
×
NEW
371
        for i := 0; i < int(tsLast); i++ {
×
NEW
372
                ts.Values[i] = newRingBuffer[int](cap)
×
NEW
373
        }
×
NEW
374
        return ts
×
375
}
376

NEW
377
func (t *timeSeries) append(entities, entityCap, memory, tps int) {
×
NEW
378
        t.Values[tsEntities].Add(entities)
×
NEW
379
        t.Values[tsEntityCap].Add(entityCap)
×
NEW
380
        t.Values[tsMemory].Add(memory)
×
NEW
381
        t.Values[tsTickPerSec].Add(tps)
×
NEW
382
}
×
383

384
type frameTimer struct {
385
        lastTick  int64
386
        lastTime  time.Time
387
        frameTime time.Duration
388
}
389

NEW
390
func (t *frameTimer) Update(tick int64, tm time.Time) {
×
NEW
391
        delta := tm.Sub(t.lastTime)
×
NEW
392

×
NEW
393
        if delta < time.Second {
×
NEW
394
                return
×
NEW
395
        }
×
396

NEW
397
        ticks := tick - t.lastTick
×
NEW
398

×
NEW
399
        if ticks > 0 {
×
NEW
400
                t.frameTime = delta / time.Duration(ticks)
×
NEW
401
        }
×
402

NEW
403
        t.lastTick = tick
×
NEW
404
        t.lastTime = tm
×
405
}
406

NEW
407
func (t *frameTimer) FrameTime() time.Duration {
×
NEW
408
        return t.frameTime
×
NEW
409
}
×
410

NEW
411
func (t *frameTimer) FPS() float64 {
×
NEW
412
        if t.frameTime == 0 {
×
NEW
413
                return 0
×
NEW
414
        }
×
NEW
415
        return 1.0 / t.frameTime.Seconds()
×
416
}
417

418
type archetypes struct {
419
        Components []*text.Text
420
        Indices    []int
421
}
422

NEW
423
func (a *archetypes) Update(stats *stats.World) {
×
NEW
424
        newLen := len(stats.Archetypes)
×
NEW
425
        oldLen := len(a.Components)
×
NEW
426

×
NEW
427
        if newLen == oldLen {
×
NEW
428
                return
×
NEW
429
        }
×
430

NEW
431
        a.Components = a.Components[:0]
×
NEW
432
        a.Indices = a.Indices[:0]
×
NEW
433

×
NEW
434
        numNodes := len(stats.Archetypes)
×
NEW
435
        for i := 0; i < numNodes; i++ {
×
NEW
436
                node := &stats.Archetypes[i]
×
NEW
437
                text := text.New(px.V(0, 0), defaultFont)
×
NEW
438
                text.Color = color.RGBA{200, 200, 200, 255}
×
NEW
439
                sb := strings.Builder{}
×
NEW
440
                sb.WriteString(fmt.Sprintf("              %4d B: ", node.MemoryPerEntity))
×
NEW
441
                types := node.ComponentTypes
×
NEW
442
                for j := 0; j < len(types); j++ {
×
NEW
443
                        sb.WriteString(types[j].Name())
×
NEW
444
                        sb.WriteString(" ")
×
NEW
445
                }
×
NEW
446
                text.WriteString(sb.String())
×
NEW
447
                a.Components = append(a.Components, text)
×
NEW
448
                a.Indices = append(a.Indices, i)
×
449
        }
450
}
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