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

mlange-42 / ark-pixel / 13719732838

07 Mar 2025 11:28AM CUT coverage: 29.148% (+8.1%) from 21.025%
13719732838

push

github

web-flow
Add entity inspector (#4)

76 of 103 new or added lines in 1 file covered. (73.79%)

195 of 669 relevant lines covered (29.15%)

31.42 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
88
func (m *Monitor) Initialize(w *ecs.World, win *opengl.Window) {
×
89
        if m.PlotCapacity <= 0 {
×
90
                m.PlotCapacity = 300
×
91
        }
×
92
        if m.SampleInterval <= 0 {
×
93
                m.SampleInterval = time.Second
×
94
        }
×
95
        m.lastPlotUpdate = time.Now()
×
96
        m.startTime = m.lastPlotUpdate
×
97

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

×
100
        m.scale = calcScaleCorrection()
×
101

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

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

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

×
117
        m.step = 0
×
118
}
119

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

357
func toMemText(bytes int) (float64, string) {
×
358
        if bytes <= 10*1_024_000 {
×
359
                return float64(bytes) / 1024, "kB"
×
360
        }
×
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

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

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

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

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

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

397
        ticks := tick - t.lastTick
×
398

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

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

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

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

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

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

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

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

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