Files
aego/runtime/player/stats.go
T
sanspie 5eb1832907
ci-platforms / darwin amd64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 15m28s
ci / release (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / checks (push) Successful in 7m47s
Complete Stage 12 for v0.12.0
2026-11-05 11:46:53 +03:00

204 lines
6.7 KiB
Go

package player
import (
"runtime/metrics"
"aego/core/clock"
"aego/engine"
"aego/gpu"
"aego/platform"
"aego/render"
)
var statNames = [...]string{
"/gc/cycles/total:gc-cycles",
"/gc/heap/allocs:objects",
"/gc/heap/allocs:bytes",
"/memory/classes/heap/objects:bytes",
"/gc/heap/goal:bytes",
}
type Stats struct {
Checkpoint *CheckpointObservation `json:"checkpoint,omitempty"`
ModuleCounters map[string]uint64 `json:"moduleCounters,omitempty"`
Frames uint64 `json:"frames"`
Ticks uint64 `json:"ticks"`
GC uint64 `json:"gc"`
AllocObjects uint64 `json:"allocObjects"`
AllocBytes uint64 `json:"allocBytes"`
HeapBytes uint64 `json:"heapBytes"`
HeapGoal uint64 `json:"heapGoal"`
Zones map[string]uint64 `json:"zones,omitempty"`
FrameBytes uint64 `json:"frameBytes,omitempty"`
Bridge *gpu.BridgeStats `json:"bridge,omitempty"`
GPUNs uint64 `json:"gpuNs"`
DirtyShadowMasks uint64 `json:"dirtyShadowMasks"`
ShadowCacheHits uint64 `json:"shadowCacheHits"`
ShadowGeometryNS uint64 `json:"shadowGeometryNs"`
ResolvePasses uint64 `json:"resolvePasses"`
PostFXPasses uint64 `json:"postFXPasses"`
PostFXAuxPixels uint64 `json:"postFXAuxPixels"`
PostFXReplacedClears uint64 `json:"postFXReplacedClears"`
PostFXTargets uint64 `json:"postFXTargets"`
PostFXFailures uint64 `json:"postFXFailures"`
GPUFrames uint64 `json:"gpuFrames"`
Draws uint64 `json:"draws"`
Sprites uint64 `json:"sprites"`
Keys uint64 `json:"keys"`
Clamped float64 `json:"clamped"`
MaxReal float64 `json:"maxReal"`
Lost bool `json:"lost"`
Restores int `json:"restores"`
Renderer string `json:"renderer"`
}
type gpuLoss interface {
Lost() bool
Restores() int
}
type sampler struct {
render func() render.Stats
gpuNs uint64
gpuFrames uint64
fxPasses, fxTargets, fxFailures, fxPixels, fxClears uint64
dirtyShadowMasks, shadowCacheHits, shadowGeometryNS, resolvePasses uint64
draws uint64
sprites uint64
keys uint64
maxReal float64
metrics []metrics.Sample
zones []uint64
win *window
}
type window struct {
from, to uint64
start Stats
opened func()
closed func(start, end Stats)
}
func (s *sampler) frame(w *engine.World) {
if s.render != nil {
r := s.render()
if r.GPUValid {
s.gpuNs += r.GPUNS
s.gpuFrames++
}
s.dirtyShadowMasks += uint64(r.DirtyShadowMasks)
s.shadowCacheHits += uint64(r.ShadowCacheHits)
s.shadowGeometryNS += r.ShadowGeometryNS
s.resolvePasses += uint64(r.ResolvePasses)
s.fxPasses += uint64(r.PostFXPasses)
s.fxTargets += uint64(r.PostFXTargets)
s.fxPixels += r.PostFXAuxPixels
s.fxClears += r.PostFXReplacedClears
s.fxFailures += uint64(r.PostFXFailures)
s.draws += uint64(r.DrawCalls)
s.sprites += uint64(r.Visible)
}
if w != nil && w.Time != nil {
s.maxReal = max(s.maxReal, w.Time.Span(clock.Real).Delta)
}
}
func (s *sampler) event(ev platform.Event) {
if ev.Kind == platform.EvKeyDown || ev.Kind == platform.EvKeyUp {
s.keys++
}
}
func (s *sampler) value(i int) uint64 {
if v := s.metrics[i].Value; v.Kind() == metrics.KindUint64 {
return v.Uint64()
}
return 0
}
func (p *Player) Stats() Stats {
s := &p.sampler
if s.metrics == nil {
s.metrics = make([]metrics.Sample, len(statNames))
for i, n := range statNames {
s.metrics[i].Name = n
}
}
metrics.Read(s.metrics)
e := p.Engine
w := e.MainWorld()
out := Stats{
Frames: w.Time.Frame(), Ticks: w.Time.Tick(), GC: s.value(0), AllocObjects: s.value(1), AllocBytes: s.value(2),
HeapBytes: s.value(3), HeapGoal: s.value(4),
DirtyShadowMasks: s.dirtyShadowMasks, ShadowCacheHits: s.shadowCacheHits,
ShadowGeometryNS: s.shadowGeometryNS, ResolvePasses: s.resolvePasses,
PostFXAuxPixels: s.fxPixels, PostFXReplacedClears: s.fxClears,
PostFXPasses: s.fxPasses, PostFXTargets: s.fxTargets, PostFXFailures: s.fxFailures,
GPUNs: s.gpuNs, GPUFrames: s.gpuFrames, Draws: s.draws, Sprites: s.sprites, Keys: s.keys,
Clamped: w.Time.ClampedTime(), MaxReal: s.maxReal, Renderer: e.Caps().Renderer,
}
if p.checkpoint != nil {
v := *p.checkpoint
out.Checkpoint = &v
}
if p.cfg.Probe {
w.EachOwner(func(_ string, owner any) {
if recorder, ok := owner.(interface{ ProbeCounters(map[string]uint64) }); ok {
if out.ModuleCounters == nil {
out.ModuleCounters = make(map[string]uint64)
}
recorder.ProbeCounters(out.ModuleCounters)
}
})
s.zones = e.AllocTotals(s.zones[:0])
out.Zones = make(map[string]uint64, len(s.zones))
for i, n := range s.zones {
out.Zones[engine.AllocZone(i)] = n
}
out.FrameBytes = e.FrameBytes()
}
if b, ok := e.GPU().(gpu.BridgeReporter); ok {
st := b.BridgeStats()
out.Bridge = &st
}
if l, ok := e.GPU().(gpuLoss); ok {
out.Lost, out.Restores = l.Lost(), l.Restores()
}
return out
}
func (p *Player) Window(from, to uint64, opened func(), closed func(start, end Stats)) bool {
if to <= from || p.Engine.MainWorld().Time.Frame() >= from {
return false
}
p.sampler.win = &window{from: from, to: to, opened: opened, closed: closed}
return true
}
func (p *Player) markWindow() {
w := p.sampler.win
if w == nil {
return
}
switch p.Engine.MainWorld().Time.Frame() {
case w.from:
if p.cfg.MemProfile {
p.memStart = memSnapshot()
}
w.start = p.Stats()
w.opened()
case w.to:
p.sampler.win = nil
end := p.Stats()
if p.memStart != nil {
r := memDiff(p.memStart, memSnapshot())
p.allocs, p.memStart = &r, nil
}
w.closed(w.start, end)
}
}
func (p *Player) ResetMaxReal() {
p.sampler.maxReal = 0
}