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

mlange-42 / ark-pixel / 13944821963

19 Mar 2025 10:55AM CUT coverage: 89.432%. Remained the same
13944821963

push

github

web-flow
Upgrade to Ark v0.4.0 (#16)

1464 of 1637 relevant lines covered (89.43%)

221047.74 hits per line

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

92.26
/monitor/monitor.go
1
package monitor
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
        tsMemoryUsed
26
        tsTickPerSec
27
        tsLast
28
)
29

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

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

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

95
// Initialize the system
96
func (m *Monitor) Initialize(w *ecs.World, win *opengl.Window) {
2✔
97
        if m.PlotCapacity <= 0 {
4✔
98
                m.PlotCapacity = 300
2✔
99
        }
2✔
100
        if m.SampleInterval <= 0 {
4✔
101
                m.SampleInterval = time.Second
2✔
102
        }
2✔
103
        m.lastPlotUpdate = time.Now()
2✔
104
        m.startTime = m.lastPlotUpdate
2✔
105

2✔
106
        m.drawer = *imdraw.New(nil)
2✔
107

2✔
108
        m.scale = calcScaleCorrection()
2✔
109

2✔
110
        m.summary = text.New(px.V(0, 0), defaultFont)
2✔
111
        m.summary.AlignedTo(px.BottomRight)
2✔
112

2✔
113
        m.timeSeries = newTimeSeries(m.PlotCapacity)
2✔
114
        for i := 0; i < len(m.timeSeries.Text); i++ {
12✔
115
                m.timeSeries.Text[i] = text.New(px.V(0, 0), defaultFont)
10✔
116
        }
10✔
117
        fmt.Fprintf(m.timeSeries.Text[tsEntities], "Entities")
2✔
118
        fmt.Fprintf(m.timeSeries.Text[tsEntityCap], "Capacity")
2✔
119
        fmt.Fprintf(m.timeSeries.Text[tsMemory], "Memory")
2✔
120
        fmt.Fprintf(m.timeSeries.Text[tsMemoryUsed], "Memory used")
2✔
121
        fmt.Fprintf(m.timeSeries.Text[tsTickPerSec], "TPS")
2✔
122

2✔
123
        m.text = text.New(px.V(0, 0), defaultFont).AlignedTo(px.TopRight)
2✔
124
        m.text.Color = color.RGBA{200, 200, 200, 255}
2✔
125

2✔
126
        m.textRight = text.New(px.V(0, 0), defaultFont).AlignedTo(px.TopLeft)
2✔
127
        m.textRight.Color = color.RGBA{200, 200, 200, 255}
2✔
128

2✔
129
        m.step = 0
2✔
130
}
131

132
// Update the drawer.
133
func (m *Monitor) Update(w *ecs.World) {
200✔
134
        t := time.Now()
200✔
135
        m.frameTimer.Update(m.step, t)
200✔
136

200✔
137
        if !m.HidePlots && t.Sub(m.lastPlotUpdate) >= m.SampleInterval {
206✔
138
                stats := w.Stats()
6✔
139
                m.archetypes.Update(stats)
6✔
140
                m.timeSeries.append(
6✔
141
                        stats.Entities.Used, stats.Entities.Total,
6✔
142
                        stats.Memory, stats.MemoryUsed,
6✔
143
                        int(m.frameTimer.FPS()*1000000),
6✔
144
                )
6✔
145
                m.lastPlotUpdate = t
6✔
146
        }
6✔
147
        m.step++
200✔
148
}
149

150
// UpdateInputs handles input events of the previous frame update.
151
func (m *Monitor) UpdateInputs(w *ecs.World, win *opengl.Window) {}
200✔
152

153
// Draw the system
154
func (m *Monitor) Draw(w *ecs.World, win *opengl.Window) {
200✔
155
        stats := w.Stats()
200✔
156
        m.archetypes.Update(stats)
200✔
157

200✔
158
        width := win.Canvas().Bounds().W()
200✔
159
        height := win.Canvas().Bounds().H()
200✔
160

200✔
161
        m.summary.Clear()
200✔
162
        mem, units := toMemText(stats.Memory)
200✔
163
        split := width < 1080
200✔
164
        fmt.Fprintf(
200✔
165
                m.summary, "Tick: %8d  |  Ent.: %7d  |  Archetypes: %3d  |  Comp: %3d  |  Cache: %3d",
200✔
166
                m.step, stats.Entities.Used, len(stats.Archetypes), len(stats.ComponentTypes), stats.CachedFilters,
200✔
167
        )
200✔
168
        if split {
400✔
169
                fmt.Fprintf(
200✔
170
                        m.summary, "\nMem: %6.1f %s  |  TPS: %8.1f  |  TPT: %6.2f ms  |  Time: %s",
200✔
171
                        mem, units, m.frameTimer.FPS(),
200✔
172
                        float64(m.frameTimer.FrameTime().Microseconds())/1000, time.Since(m.startTime).Round(time.Second),
200✔
173
                )
200✔
174
        } else {
200✔
175
                fmt.Fprintf(
×
176
                        m.summary, "  |  Mem: %6.1f %s  |  TPS: %6.1f  |  TPT: %6.2f ms  |  Time: %s",
×
177
                        mem, units, m.frameTimer.FPS(),
×
178
                        float64(m.frameTimer.FrameTime().Microseconds())/1000, time.Since(m.startTime).Round(time.Second),
×
179
                )
×
180
        }
×
181

182
        numNodes := len(m.archetypes.Components)
200✔
183
        maxCapacity := 0
200✔
184
        for i := 0; i < numNodes; i++ {
400✔
185
                cap := stats.Archetypes[m.archetypes.Indices[i]].Capacity
200✔
186
                if cap > maxCapacity {
400✔
187
                        maxCapacity = cap
200✔
188
                }
200✔
189
        }
190
        dr := &m.drawer
200✔
191
        x0 := 6.0
200✔
192
        y0 := height - 18.0
200✔
193

200✔
194
        m.summary.Draw(win, px.IM.Moved(px.V(x0, y0+10)))
200✔
195
        y0 -= 10
200✔
196

200✔
197
        if split {
400✔
198
                y0 -= 10
200✔
199
        }
200✔
200

201
        if !m.HidePlots {
400✔
202
                plotY0 := y0
200✔
203
                plotHeight := (y0 - 40) / 3
200✔
204
                if plotHeight > 150 {
400✔
205
                        plotHeight = 150
200✔
206
                }
200✔
207
                plotWidth := (width - 20) * 0.25
200✔
208
                if m.HideArchetypes {
200✔
209
                        plotWidth = width - 20
×
210
                }
×
211
                m.drawPlot(win, x0, plotY0-plotHeight, plotWidth, plotHeight, tsEntities, tsEntityCap)
200✔
212
                plotY0 -= plotHeight + 10
200✔
213
                m.drawPlot(win, x0, plotY0-plotHeight, plotWidth, plotHeight, tsMemory, tsMemoryUsed)
200✔
214
                plotY0 -= plotHeight + 10
200✔
215
                m.drawPlot(win, x0, plotY0-plotHeight, plotWidth, plotHeight, tsTickPerSec)
200✔
216

200✔
217
                x0 += math.Ceil(plotWidth + 10)
200✔
218
        }
219

220
        archHeight := math.Ceil((y0 - 10) / float64(numNodes+1))
200✔
221
        if !m.HideArchetypes {
400✔
222
                if archHeight >= 8 {
400✔
223
                        archWidth := width - x0 - 10
200✔
224
                        if archHeight > 20 {
400✔
225
                                archHeight = 20
200✔
226
                        }
200✔
227
                        m.drawArchetypeScales(
200✔
228
                                win, x0, y0-archHeight, archWidth, archHeight, maxCapacity,
200✔
229
                        )
200✔
230
                        for i := 0; i < numNodes; i++ {
400✔
231
                                idx := m.archetypes.Indices[i]
200✔
232
                                m.drawArchetype(
200✔
233
                                        win, x0, y0-float64(i+2)*archHeight, archWidth, archHeight,
200✔
234
                                        maxCapacity, &stats.Archetypes[idx], m.archetypes.Components[i],
200✔
235
                                )
200✔
236
                        }
200✔
237
                } else {
×
238
                        m.text.Clear()
×
239
                        fmt.Fprintf(m.text, "Too many archetypes")
×
240
                        m.text.Draw(win, px.IM.Moved(px.V(x0, y0-10)))
×
241
                }
×
242
        }
243

244
        dr.Draw(win)
200✔
245
        dr.Clear()
200✔
246
}
247

248
func (m *Monitor) drawArchetypeScales(win *opengl.Window, x, y, w, h float64, max int) {
200✔
249
        dr := &m.drawer
200✔
250
        step := calcTicksStep(float64(max), 8)
200✔
251
        if step < 1 {
200✔
252
                return
×
253
        }
×
254
        drawStep := w * step / float64(max)
200✔
255

200✔
256
        dr.Color = color.RGBA{140, 140, 140, 255}
200✔
257
        dr.Push(px.V(x, y+2), px.V(x+w, y+2))
200✔
258
        dr.Line(1)
200✔
259
        dr.Reset()
200✔
260

200✔
261
        steps := int(float64(max) / step)
200✔
262
        for i := 0; i <= steps; i++ {
1,400✔
263
                xi := float64(i)
1,200✔
264
                dr.Push(px.V(x+xi*drawStep, y+2), px.V(x+xi*drawStep, y+7))
1,200✔
265
                dr.Line(1)
1,200✔
266
                dr.Reset()
1,200✔
267

1,200✔
268
                val := i * int(step)
1,200✔
269
                m.text.Clear()
1,200✔
270
                fmt.Fprintf(m.text, "%d", val)
1,200✔
271
                m.text.Draw(win, px.IM.Moved(px.V(math.Floor(x+xi*drawStep-m.text.Bounds().W()/2), y+10)))
1,200✔
272
        }
1,200✔
273
}
274

275
func (m *Monitor) drawArchetype(win *opengl.Window, x, y, w, h float64, max int, arch *stats.Archetype, text *text.Text) {
200✔
276
        dr := &m.drawer
200✔
277

200✔
278
        cap := float64(arch.Capacity) / float64(max)
200✔
279
        cnt := float64(arch.Size) / float64(max)
200✔
280

200✔
281
        if arch.NumRelations > 0 {
200✔
282
                dr.Color = colorCyan
×
283
        } else {
200✔
284
                dr.Color = colorGreen
200✔
285
        }
200✔
286
        dr.Push(px.V(x, y), px.V(x+w*cnt, y+h))
200✔
287
        dr.Rectangle(0)
200✔
288
        dr.Reset()
200✔
289

200✔
290
        if arch.NumRelations > 0 {
200✔
291
                dr.Color = colorDarkCyan
×
292
        } else {
200✔
293
                dr.Color = colorDarkGreen
200✔
294
        }
200✔
295
        dr.Push(px.V(x+w*cnt, y), px.V(x+w*cap, y+h))
200✔
296
        dr.Rectangle(0)
200✔
297
        dr.Reset()
200✔
298

200✔
299
        dr.Color = color.RGBA{40, 40, 40, 255}
200✔
300
        dr.Push(px.V(x, y), px.V(x+w, y+h))
200✔
301
        dr.Rectangle(1)
200✔
302
        dr.Reset()
200✔
303

200✔
304
        dr.Draw(win)
200✔
305
        dr.Clear()
200✔
306

200✔
307
        text.Draw(win, px.IM.Moved(px.V(x+3, y+3)))
200✔
308

200✔
309
        if arch.NumRelations > 0 {
200✔
310
                m.text.Clear()
×
311
                fmt.Fprintf(m.text, "%5d / %5d", len(arch.Tables), len(arch.Tables)+arch.FreeTables)
×
312
                m.text.Draw(win, px.IM.Moved(px.V(x+5, y+3)))
×
313
        }
×
314

315
        m.textRight.Clear()
200✔
316
        fmt.Fprintf(m.textRight, "%d", arch.Size)
200✔
317
        m.textRight.Draw(win, px.IM.Moved(px.V(x+w-5, y+3)))
200✔
318
}
319

320
func (m *Monitor) drawPlot(win *opengl.Window, x, y, w, h float64, series ...timeSeriesType) {
600✔
321
        dr := &m.drawer
600✔
322

600✔
323
        dr.Color = color.RGBA{0, 25, 10, 255}
600✔
324
        dr.Push(px.V(x, y), px.V(x+w, y+h))
600✔
325
        dr.Rectangle(0)
600✔
326
        dr.Reset()
600✔
327

600✔
328
        yMax := 0
600✔
329
        for _, series := range series {
1,600✔
330
                values := m.timeSeries.Values[series]
1,000✔
331
                l := values.Len()
1,000✔
332
                for i := 0; i < l; i++ {
2,130✔
333
                        v := values.Get(i)
1,130✔
334
                        if v > yMax {
1,402✔
335
                                yMax = v
272✔
336
                        }
272✔
337
                }
338
        }
339

340
        dr.Color = color.White
600✔
341
        for _, series := range series {
1,600✔
342
                values := m.timeSeries.Values[series]
1,000✔
343
                numValues := values.Len()
1,000✔
344
                if numValues > 0 {
1,680✔
345
                        xStep := w / float64(numValues-1)
680✔
346
                        yScale := 0.95 * h / float64(yMax)
680✔
347

680✔
348
                        for i := 0; i < numValues-1; i++ {
1,130✔
349
                                xi := float64(i)
450✔
350
                                x1, x2 := xi*xStep, xi*xStep+xStep
450✔
351
                                y1, y2 := float64(values.Get(i))*yScale, float64(values.Get(i+1))*yScale
450✔
352

450✔
353
                                dr.Push(px.V(x+x1, y+y1), px.V(x+x2, y+y2))
450✔
354
                                dr.Line(1)
450✔
355
                                dr.Reset()
450✔
356
                        }
450✔
357
                }
358
        }
359

360
        dr.Color = color.RGBA{140, 140, 140, 255}
600✔
361
        dr.Push(px.V(x, y), px.V(x+w, y+h))
600✔
362
        dr.Rectangle(1)
600✔
363
        dr.Reset()
600✔
364

600✔
365
        dr.Draw(win)
600✔
366
        dr.Clear()
600✔
367

600✔
368
        if len(series) > 0 {
1,200✔
369
                text := m.timeSeries.Text[series[0]]
600✔
370
                text.Draw(win, px.IM.Moved(px.V(x+w-text.Bounds().W()-3, y+3)))
600✔
371
        }
600✔
372
}
373

374
func toMemText(bytes int) (float64, string) {
300✔
375
        if bytes <= 10*1_024_000 {
600✔
376
                return float64(bytes) / 1024, "kB"
300✔
377
        }
300✔
378
        return float64(bytes) / 1_024_000, "MB"
×
379
}
380

381
type timeSeries struct {
382
        Values [tsLast]ringBuffer[int]
383
        Text   [tsLast]*text.Text
384
}
385

386
func newTimeSeries(cap int) timeSeries {
2✔
387
        ts := timeSeries{}
2✔
388
        for i := 0; i < int(tsLast); i++ {
12✔
389
                ts.Values[i] = newRingBuffer[int](cap)
10✔
390
        }
10✔
391
        return ts
2✔
392
}
393

394
func (t *timeSeries) append(entities, entityCap, memory, memoryUsed, tps int) {
6✔
395
        t.Values[tsEntities].Add(entities)
6✔
396
        t.Values[tsEntityCap].Add(entityCap)
6✔
397
        t.Values[tsMemory].Add(memory)
6✔
398
        t.Values[tsMemoryUsed].Add(memoryUsed)
6✔
399
        t.Values[tsTickPerSec].Add(tps)
6✔
400
}
6✔
401

402
type frameTimer struct {
403
        lastTick  int64
404
        lastTime  time.Time
405
        frameTime time.Duration
406
}
407

408
func (t *frameTimer) Update(tick int64, tm time.Time) {
300✔
409
        delta := tm.Sub(t.lastTime)
300✔
410

300✔
411
        if delta < time.Second {
588✔
412
                return
288✔
413
        }
288✔
414

415
        ticks := tick - t.lastTick
12✔
416

12✔
417
        if ticks > 0 {
21✔
418
                t.frameTime = delta / time.Duration(ticks)
9✔
419
        }
9✔
420

421
        t.lastTick = tick
12✔
422
        t.lastTime = tm
12✔
423
}
424

425
func (t *frameTimer) FrameTime() time.Duration {
300✔
426
        return t.frameTime
300✔
427
}
300✔
428

429
func (t *frameTimer) FPS() float64 {
306✔
430
        if t.frameTime == 0 {
402✔
431
                return 0
96✔
432
        }
96✔
433
        return 1.0 / t.frameTime.Seconds()
210✔
434
}
435

436
type archetypes struct {
437
        Components []*text.Text
438
        Indices    []int
439
}
440

441
func (a *archetypes) Update(stats *stats.World) {
206✔
442
        newLen := len(stats.Archetypes)
206✔
443
        oldLen := len(a.Components)
206✔
444

206✔
445
        if newLen == oldLen {
410✔
446
                return
204✔
447
        }
204✔
448

449
        a.Components = a.Components[:0]
2✔
450
        a.Indices = a.Indices[:0]
2✔
451

2✔
452
        numNodes := len(stats.Archetypes)
2✔
453
        for i := 0; i < numNodes; i++ {
4✔
454
                node := &stats.Archetypes[i]
2✔
455
                text := text.New(px.V(0, 0), defaultFont)
2✔
456
                text.Color = color.RGBA{200, 200, 200, 255}
2✔
457
                sb := strings.Builder{}
2✔
458
                sb.WriteString(fmt.Sprintf("              %4d B  ", node.MemoryPerEntity))
2✔
459
                types := node.ComponentTypes
2✔
460
                for j := 0; j < len(types); j++ {
2✔
461
                        sb.WriteString(types[j].Name())
×
462
                        sb.WriteString(" ")
×
463
                }
×
464
                text.WriteString(sb.String())
2✔
465
                a.Components = append(a.Components, text)
2✔
466
                a.Indices = append(a.Indices, i)
2✔
467
        }
468
}
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