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

mlange-42 / arche-pixel / 9206262010

23 May 2024 10:17AM CUT coverage: 88.275% (+0.03%) from 88.246%
9206262010

push

github

web-flow
Add optional property `XLim` to `Lines` plot (#54)

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

1438 of 1629 relevant lines covered (88.28%)

222101.43 hits per line

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

86.39
/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/arche-pixel/window"
15
        "github.com/mlange-42/arche/ecs"
16
        "github.com/mlange-42/arche/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) {
2✔
89
        if m.PlotCapacity <= 0 {
4✔
90
                m.PlotCapacity = 300
2✔
91
        }
2✔
92
        if m.SampleInterval <= 0 {
3✔
93
                m.SampleInterval = time.Second
1✔
94
        }
1✔
95
        m.lastPlotUpdate = time.Now()
2✔
96
        m.startTime = m.lastPlotUpdate
2✔
97

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

2✔
100
        m.scale = calcScaleCorrection()
2✔
101

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

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

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

2✔
117
        m.step = 0
2✔
118
}
119

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

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

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

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

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

110✔
148
        m.summary.Clear()
110✔
149
        mem, units := toMemText(stats.Memory)
110✔
150
        split := width < 1080
110✔
151
        fmt.Fprintf(
110✔
152
                m.summary, "Tick: %8d  |  Ent.: %7d  |  Nodes: %3d/%3d  |  Comp: %3d  |  Cache: %3d",
110✔
153
                m.step, stats.Entities.Used, stats.ActiveNodeCount, len(stats.Nodes), stats.ComponentCount, stats.CachedFilters,
110✔
154
        )
110✔
155
        if split {
220✔
156
                fmt.Fprintf(
110✔
157
                        m.summary, "\nMem: %6.1f %s  |  TPS: %8.1f  |  TPT: %6.2f ms  |  Time: %s",
110✔
158
                        mem, units, m.frameTimer.FPS(),
110✔
159
                        float64(m.frameTimer.FrameTime().Microseconds())/1000, time.Since(m.startTime).Round(time.Second),
110✔
160
                )
110✔
161
        } else {
110✔
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)
110✔
170
        maxCapacity := 0
110✔
171
        for i := 0; i < numNodes; i++ {
220✔
172
                cap := stats.Nodes[m.archetypes.Indices[i]].Capacity
110✔
173
                if cap > maxCapacity {
220✔
174
                        maxCapacity = cap
110✔
175
                }
110✔
176
        }
177
        dr := &m.drawer
110✔
178
        x0 := 6.0
110✔
179
        y0 := height - 18.0
110✔
180

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

110✔
184
        if split {
220✔
185
                y0 -= 10
110✔
186
        }
110✔
187

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

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

207
        archHeight := math.Ceil((y0 - 10) / float64(numNodes+1))
110✔
208
        if !m.HideArchetypes {
220✔
209
                if archHeight >= 8 {
220✔
210
                        archWidth := width - x0 - 10
110✔
211
                        if archHeight > 20 {
220✔
212
                                archHeight = 20
110✔
213
                        }
110✔
214
                        m.drawArchetypeScales(
110✔
215
                                win, x0, y0-archHeight, archWidth, archHeight, maxCapacity,
110✔
216
                        )
110✔
217
                        for i := 0; i < numNodes; i++ {
220✔
218
                                idx := m.archetypes.Indices[i]
110✔
219
                                m.drawArchetype(
110✔
220
                                        win, x0, y0-float64(i+2)*archHeight, archWidth, archHeight,
110✔
221
                                        maxCapacity, &stats.Nodes[idx], m.archetypes.Components[i],
110✔
222
                                )
110✔
223
                        }
110✔
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)
110✔
232
        dr.Clear()
110✔
233
}
234

235
func (m *Monitor) drawArchetypeScales(win *opengl.Window, x, y, w, h float64, max int) {
110✔
236
        dr := &m.drawer
110✔
237
        step := calcTicksStep(float64(max), 8)
110✔
238
        if step < 1 {
220✔
239
                return
110✔
240
        }
110✔
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, node *stats.Node, text *text.Text) {
110✔
263
        dr := &m.drawer
110✔
264

110✔
265
        cap := float64(node.Capacity) / float64(max)
110✔
266
        cnt := float64(node.Size) / float64(max)
110✔
267

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

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

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

110✔
291
        dr.Draw(win)
110✔
292
        dr.Clear()
110✔
293

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

110✔
296
        if node.HasRelation {
110✔
297
                m.text.Clear()
×
298
                fmt.Fprintf(m.text, "%5d / %5d", node.ActiveArchetypeCount, node.ArchetypeCount)
×
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) {
330✔
304
        dr := &m.drawer
330✔
305

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

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

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

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

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

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

330✔
348
        dr.Draw(win)
330✔
349
        dr.Clear()
330✔
350

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

357
func toMemText(bytes int) (float64, string) {
210✔
358
        if bytes <= 10*1_024_000 {
420✔
359
                return float64(bytes) / 1024, "kB"
210✔
360
        }
210✔
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 {
2✔
370
        ts := timeSeries{}
2✔
371
        for i := 0; i < int(tsLast); i++ {
10✔
372
                ts.Values[i] = newRingBuffer[int](cap)
8✔
373
        }
8✔
374
        return ts
2✔
375
}
376

377
func (t *timeSeries) append(entities, entityCap, memory, tps int) {
12✔
378
        t.Values[tsEntities].Add(entities)
12✔
379
        t.Values[tsEntityCap].Add(entityCap)
12✔
380
        t.Values[tsMemory].Add(memory)
12✔
381
        t.Values[tsTickPerSec].Add(tps)
12✔
382
}
12✔
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) {
300✔
391
        delta := tm.Sub(t.lastTime)
300✔
392

300✔
393
        if delta < time.Second {
588✔
394
                return
288✔
395
        }
288✔
396

397
        ticks := tick - t.lastTick
12✔
398

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

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

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

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

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

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

122✔
427
        if newLen == oldLen {
242✔
428
                return
120✔
429
        }
120✔
430

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

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