ci / checks (push) Failing after 5m21s
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 7m49s
ci / release (push) Skipped
633 lines
21 KiB
Go
633 lines
21 KiB
Go
package profiler
|
||
|
||
import (
|
||
"fmt"
|
||
"math"
|
||
"runtime/metrics"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"aego/core/mathx"
|
||
"aego/core/stats"
|
||
"aego/core/trace"
|
||
"aego/editor/core/context"
|
||
"aego/editor/core/play"
|
||
"aego/editor/core/services"
|
||
"aego/editor/core/workspace"
|
||
"aego/engine"
|
||
"aego/engine/i18n"
|
||
"aego/render"
|
||
"aego/ui/core"
|
||
"aego/ui/widgets"
|
||
)
|
||
|
||
const history = 120
|
||
|
||
const (
|
||
mFrame = iota
|
||
mUpdate
|
||
mRender
|
||
mDraws
|
||
mSprites
|
||
mGarbage
|
||
mGameUpdate
|
||
mGameRender
|
||
mGameSprites
|
||
mGameNodes
|
||
mScriptInstr
|
||
mScriptHost
|
||
mScriptHeap
|
||
mCoroutines
|
||
mEmits
|
||
metricCount
|
||
)
|
||
|
||
type bucket struct {
|
||
avg [metricCount]float32
|
||
peak [metricCount]float32
|
||
frames int
|
||
intervals int
|
||
elapsed float64
|
||
}
|
||
|
||
func (b *bucket) fps() float64 {
|
||
if b.elapsed <= 0 {
|
||
return 0
|
||
}
|
||
return float64(b.intervals) / b.elapsed
|
||
}
|
||
|
||
type mark struct {
|
||
title string
|
||
y float32
|
||
}
|
||
|
||
type Panel struct {
|
||
ring [history]bucket
|
||
head int
|
||
n int
|
||
valid [metricCount]int
|
||
acc bucket
|
||
start float64
|
||
lastSample float64
|
||
haveSample bool
|
||
interval int
|
||
paused bool
|
||
playing bool
|
||
scroll float32
|
||
content float32
|
||
top float32
|
||
scripts context.ScriptStats
|
||
hasScripts bool
|
||
game play.Stats
|
||
materials render.Stats
|
||
mem []metrics.Sample
|
||
probe bool
|
||
allocs []uint64
|
||
slow int
|
||
alertAt float64
|
||
titles []string
|
||
marks []mark
|
||
tl Timeline
|
||
deps Deps
|
||
}
|
||
|
||
type Deps struct {
|
||
Tracer *trace.Tracer
|
||
Root string
|
||
Notices services.Notices
|
||
Sections func() []context.ProfilerSection
|
||
Nav services.Navigation
|
||
PhaseAllocs func([]uint64) []uint64
|
||
SetProbe func(bool)
|
||
Memory func() MemStats
|
||
Throttled func() bool
|
||
}
|
||
|
||
func (p *Panel) Sections() []string {
|
||
return p.titles
|
||
}
|
||
|
||
func (p *Panel) section(ui *core.Context, title string) {
|
||
p.titles = append(p.titles, title)
|
||
p.marks = append(p.marks, mark{title: title, y: ui.Cursor().Y - p.top})
|
||
widgets.Label(ui, title)
|
||
}
|
||
|
||
func (p *Panel) forget(from int) {
|
||
for i := from; i < metricCount; i++ {
|
||
p.valid[i] = 0
|
||
p.acc.avg[i], p.acc.peak[i] = 0, 0
|
||
}
|
||
}
|
||
|
||
func (p *Panel) count(m int) int {
|
||
return min(p.n, p.valid[m])
|
||
}
|
||
|
||
func intervals() [4]float64 {
|
||
return [4]float64{0.1, 0.25, 0.5, 1}
|
||
}
|
||
|
||
var memNames = [...]string{
|
||
"/memory/classes/heap/objects:bytes",
|
||
"/gc/heap/goal:bytes",
|
||
"/memory/classes/total:bytes",
|
||
"/sched/goroutines:goroutines",
|
||
"/gc/cycles/total:gc-cycles",
|
||
"/gc/heap/live:bytes",
|
||
}
|
||
|
||
func New(d Deps) *Panel {
|
||
p := &Panel{interval: 1, deps: d}
|
||
p.mem = make([]metrics.Sample, len(memNames))
|
||
for i, n := range memNames {
|
||
p.mem[i].Name = n
|
||
}
|
||
return p
|
||
}
|
||
|
||
func mib(b uint64) string {
|
||
return strconv.FormatFloat(float64(b)/(1<<20), 'f', 1, 64) + " MiB"
|
||
}
|
||
|
||
type MemStats struct {
|
||
// Mapped is Go-managed address space. Process is reserved for callers
|
||
// supplying their own process measurement (for example, process RSS).
|
||
Mapped uint64
|
||
Live uint64
|
||
Heap uint64
|
||
Goal uint64
|
||
Process uint64
|
||
Goroutines uint64
|
||
GC uint64
|
||
}
|
||
|
||
func (p *Panel) readMemory() MemStats {
|
||
if p.deps.Memory != nil {
|
||
return p.deps.Memory()
|
||
}
|
||
metrics.Read(p.mem)
|
||
v := func(i int) uint64 {
|
||
if p.mem[i].Value.Kind() == metrics.KindUint64 {
|
||
return p.mem[i].Value.Uint64()
|
||
}
|
||
return 0
|
||
}
|
||
return MemStats{Heap: v(0), Goal: v(1), Mapped: v(2), Goroutines: v(3), GC: v(4), Live: v(5)}
|
||
}
|
||
|
||
func (p *Panel) memory(ui *core.Context) {
|
||
m := p.readMemory()
|
||
if m.Mapped == 0 && m.Process > 0 && m.Live == 0 {
|
||
// An external sample may provide process memory, but no Go live/mapped measurement.
|
||
widgets.LabelDim(ui, i18n.Tr("editor.profiler.go_heap_heap_goal_goal", "Go heap {heap} goal {goal} process {process} goroutines {formatuint} GC {formatuint2}", i18n.Str("heap", mib(m.Heap)), i18n.Str("goal", mib(m.Goal)), i18n.Str("process", mib(m.Process)), i18n.Str("formatuint", strconv.FormatUint(m.Goroutines, 10)), i18n.Str("formatuint2", strconv.FormatUint(m.GC, 10))))
|
||
} else {
|
||
live := i18n.Tr("editor.profiler.no_live_sample", "awaiting GC sample")
|
||
if m.Live > 0 || m.GC > 0 {
|
||
live = mib(m.Live)
|
||
}
|
||
widgets.LabelDim(ui, i18n.Tr("editor.profiler.live_heap", "Live at last GC {live} Allocated now {heap} Next GC goal {goal}", i18n.Str("live", live), i18n.Str("heap", mib(m.Heap)), i18n.Str("goal", mib(m.Goal))))
|
||
widgets.LabelDim(ui, i18n.Tr("editor.profiler.runtime_mapped", "Go mapped {mapped} Goroutines {count} GC cycles {cycles}", i18n.Str("mapped", mib(m.Mapped)), i18n.Str("count", strconv.FormatUint(m.Goroutines, 10)), i18n.Str("cycles", strconv.FormatUint(m.GC, 10))))
|
||
}
|
||
if p.hasScripts {
|
||
widgets.LabelDim(ui, i18n.Tr("editor.profiler.script_heap_heapobjects_objects_heapbytes", "script heap {heapobjects} objects, {heapbytes}", i18n.Str("heapobjects", strconv.Itoa(p.scripts.HeapObjects)), i18n.Str("heapbytes", mib(uint64(p.scripts.HeapBytes)))))
|
||
}
|
||
label := i18n.Tr("editor.profiler.allocations_by_phase_off", "Allocations by phase: off")
|
||
if p.probe {
|
||
label = i18n.Tr("editor.profiler.allocations_by_phase_on", "Allocations by phase: on")
|
||
}
|
||
if widgets.Button(ui, label) && p.deps.SetProbe != nil {
|
||
p.probe = !p.probe
|
||
p.deps.SetProbe(p.probe)
|
||
}
|
||
if !p.probe || p.deps.PhaseAllocs == nil {
|
||
return
|
||
}
|
||
p.allocs = p.deps.PhaseAllocs(p.allocs[:0])
|
||
for i, n := range p.allocs {
|
||
if n > 0 {
|
||
widgets.LabelDim(ui, i18n.Tr("editor.profiler.v_formatuint_objects", "{v} {formatuint} objects", i18n.Str("v", engine.Phase(i).String()), i18n.Str("formatuint", strconv.FormatUint(n, 10))))
|
||
}
|
||
}
|
||
widgets.LabelDim(ui, i18n.Tr("editor.profiler.approximate_counted_process_wide_from", "Approximate: counted process-wide from phase start to end; a play world ticked inside a phase counts there too."))
|
||
}
|
||
|
||
func (p *Panel) slowest() string {
|
||
f := p.deps.Tracer.Frame(0)
|
||
if f == nil {
|
||
return ""
|
||
}
|
||
best, name := uint32(0), ""
|
||
for _, e := range f.Events {
|
||
if e.Depth == 1 && e.Dur > best {
|
||
best, name = e.Dur, p.deps.Tracer.Name(e.Zone)
|
||
}
|
||
}
|
||
return name
|
||
}
|
||
|
||
func (p *Panel) budget(now float64, f *stats.Frame) {
|
||
if f == nil {
|
||
return
|
||
}
|
||
if p.deps.Throttled != nil && p.deps.Throttled() {
|
||
p.slow = 0
|
||
return
|
||
}
|
||
if f.FrameTime*1000 > 2*1000.0/60 {
|
||
p.slow++
|
||
} else {
|
||
p.slow = 0
|
||
}
|
||
if p.slow != 3 || now-p.alertAt < 5 || p.deps.Notices == nil {
|
||
return
|
||
}
|
||
p.alertAt = now
|
||
msg := i18n.Tr("editor.profiler.frame_took_formatfloat_ms_for", "Frame took {formatfloat} ms for 3 frames in a row", i18n.Str("formatfloat", strconv.FormatFloat(f.FrameTime*1000, 'f', 1, 64)))
|
||
if z := p.slowest(); z != "" {
|
||
msg += i18n.Tr("editor.profiler.slowest_zone_z", ", slowest zone {z}", i18n.Str("z", z))
|
||
}
|
||
p.deps.Notices.Notify(context.LevelWarn, msg)
|
||
}
|
||
|
||
func (p *Panel) ID() string {
|
||
return workspace.Profiler
|
||
}
|
||
|
||
func (p *Panel) Title() string {
|
||
return i18n.Tr("editor.profiler.title", "Profiler")
|
||
}
|
||
|
||
func (p *Panel) seconds() float64 {
|
||
return intervals()[p.interval]
|
||
}
|
||
|
||
func gameLine(g *play.Stats) string {
|
||
return i18n.Tr("editor.profiler.frame_formatuint_tick_formatuint2_alpha", "frame {formatuint} tick {formatuint2} alpha {formatfloat} scale ×{formatfloat2} dropped ticks {formatuint3}", i18n.Str("formatuint", strconv.FormatUint(g.Frame, 10)), i18n.Str("formatuint2", strconv.FormatUint(g.Tick, 10)), i18n.Str("formatfloat", strconv.FormatFloat(g.Alpha, 'f', 3, 64)), i18n.Str("formatfloat2", strconv.FormatFloat(g.Scale, 'g', 3, 64)), i18n.Str("formatuint3", strconv.FormatUint(g.DroppedFixed, 10)))
|
||
}
|
||
|
||
func (p *Panel) Record(now float64, f *stats.Frame, r render.Stats, g play.Stats, sc context.ScriptStats, hasSc, playing bool) {
|
||
p.game = g
|
||
p.budget(now, f)
|
||
if playing && !p.playing {
|
||
p.forget(mGameUpdate)
|
||
}
|
||
p.playing = playing
|
||
if !playing {
|
||
p.hasScripts = false
|
||
}
|
||
if p.paused || f == nil {
|
||
p.haveSample = false
|
||
return
|
||
}
|
||
p.materials = r
|
||
if hasSc && playing && !p.hasScripts {
|
||
p.forget(mScriptInstr)
|
||
}
|
||
p.scripts, p.hasScripts = sc, hasSc && playing
|
||
var s [metricCount]float32
|
||
s[mFrame] = float32(f.FrameTime * 1000)
|
||
s[mUpdate] = float32(f.UpdateTime * 1000)
|
||
s[mRender] = float32(f.RenderTime * 1000)
|
||
s[mDraws] = float32(r.DrawCalls)
|
||
s[mSprites] = float32(r.Visible)
|
||
s[mGarbage] = float32(f.AllocBytes) / 1024
|
||
if playing {
|
||
s[mGameUpdate] = float32(g.Update * 1000)
|
||
s[mGameRender] = float32(g.Render * 1000)
|
||
s[mGameSprites] = float32(g.Sprites)
|
||
s[mGameNodes] = float32(g.Nodes)
|
||
if hasSc {
|
||
s[mScriptInstr] = float32(sc.Instructions)
|
||
s[mScriptHost] = float32(sc.HostCalls)
|
||
s[mScriptHeap] = float32(sc.HeapBytes) / 1024
|
||
s[mCoroutines] = float32(sc.Coroutines.Active)
|
||
s[mEmits] = float32(sc.Signals.EmitsPerFrame)
|
||
}
|
||
}
|
||
if p.acc.frames == 0 {
|
||
p.start = now
|
||
}
|
||
if p.haveSample {
|
||
dt := now - p.lastSample
|
||
if dt > 0 && !math.IsInf(dt, 0) {
|
||
p.acc.elapsed += dt
|
||
p.acc.intervals++
|
||
}
|
||
}
|
||
p.lastSample, p.haveSample = now, true
|
||
for i := range s {
|
||
p.acc.avg[i] += s[i]
|
||
p.acc.peak[i] = max(p.acc.peak[i], s[i])
|
||
}
|
||
p.acc.frames++
|
||
if now-p.start < p.seconds() {
|
||
return
|
||
}
|
||
for i := range p.acc.avg {
|
||
p.acc.avg[i] /= float32(p.acc.frames)
|
||
}
|
||
p.ring[p.head] = p.acc
|
||
p.head = (p.head + 1) % history
|
||
if p.n < history {
|
||
p.n++
|
||
}
|
||
for i := range p.valid {
|
||
p.valid[i] = min(p.valid[i]+1, history)
|
||
}
|
||
p.acc = bucket{}
|
||
}
|
||
|
||
func ms(d time.Duration) string {
|
||
return strconv.FormatFloat(d.Seconds()*1000, 'f', 2, 64)
|
||
}
|
||
|
||
func (p *Panel) scriptTable(ui *core.Context) {
|
||
th := &ui.Theme
|
||
s := &p.scripts
|
||
widgets.LabelDim(ui, i18n.Tr("editor.profiler.instances_instances_update_update_fixed", "instances {instances} update {update} fixed {fixed} faults {faults} budget aborts {formatuint} heap objects {heapobjects} GC {formatuint2} / {gctime} ms", i18n.Str("instances", strconv.Itoa(s.Instances)), i18n.Str("update", strconv.Itoa(s.Update)), i18n.Str("fixed", strconv.Itoa(s.Fixed)), i18n.Str("faults", strconv.Itoa(s.Faults)), i18n.Str("formatuint", strconv.FormatUint(s.BudgetAborts, 10)), i18n.Str("heapobjects", strconv.Itoa(s.HeapObjects)), i18n.Str("formatuint2", strconv.FormatUint(s.GCRuns, 10)), i18n.Str("gctime", ms(s.GCTime))))
|
||
co := &s.Coroutines
|
||
widgets.LabelDim(ui, i18n.Tr("editor.profiler.coroutines_active_active_sleeping_sleeping", "coroutines active {active} sleeping {sleeping} resumes/frame {formatuint} canceled {formatuint2}", i18n.Str("active", strconv.Itoa(co.Active)), i18n.Str("sleeping", strconv.Itoa(co.Sleeping)), i18n.Str("formatuint", strconv.FormatUint(co.ResumesPerFrame, 10)), i18n.Str("formatuint2", strconv.FormatUint(co.Canceled, 10))))
|
||
sg := &s.Signals
|
||
widgets.LabelDim(ui, i18n.Tr("editor.profiler.signal_connections_connections_emits_frame", "signal connections {connections} emits/frame {formatuint} handlers/frame {formatuint2}", i18n.Str("connections", strconv.Itoa(sg.Connections)), i18n.Str("formatuint", strconv.FormatUint(sg.EmitsPerFrame, 10)), i18n.Str("formatuint2", strconv.FormatUint(sg.HandlersPerFrame, 10))))
|
||
cols := [5]float32{0, 0.5, 0.64, 0.78, 0.9}
|
||
names := [5]string{"script", "instances", "calls", "ms", "faults"}
|
||
row := func(vals [5]string, col mathx.Color) {
|
||
r := ui.Cursor().Next(0)
|
||
ui.PushClip(r)
|
||
for i, v := range vals {
|
||
ui.Text(v, mathx.Vec2{X: r.X + r.W*cols[i], Y: ui.TextY(r)}, col)
|
||
}
|
||
ui.PopClip()
|
||
}
|
||
row(names, th.TextDim)
|
||
for _, t := range s.Types {
|
||
row([5]string{t.Name, strconv.Itoa(t.Instances), strconv.FormatUint(t.Calls, 10), ms(t.Time), strconv.Itoa(t.Faults)}, th.Text)
|
||
}
|
||
}
|
||
|
||
func (p *Panel) at(i int) *bucket {
|
||
return &p.ring[(p.head-p.n+i+history)%history]
|
||
}
|
||
|
||
func (p *Panel) series(m int) (last, avg, peak float32) {
|
||
n := p.count(m)
|
||
if n == 0 {
|
||
return 0, 0, 0
|
||
}
|
||
var sum float32
|
||
for i := p.n - n; i < p.n; i++ {
|
||
b := p.at(i)
|
||
sum += b.avg[m]
|
||
peak = max(peak, b.peak[m])
|
||
}
|
||
return p.at(p.n - 1).avg[m], sum / float32(n), peak
|
||
}
|
||
|
||
func fmtf(v float32, prec int) string {
|
||
return strconv.FormatFloat(float64(v), 'f', prec, 32)
|
||
}
|
||
|
||
func (p *Panel) graph(ui *core.Context, title, unit string, prec int, budget float32, col mathx.Color, m int) {
|
||
th := &ui.Theme
|
||
last, avg, peak := p.series(m)
|
||
n := p.count(m)
|
||
first := p.n - n
|
||
|
||
hr := ui.Cursor().Next(th.RowHeight)
|
||
if offscreen(ui, hr.Y, th.RowHeight*3.6) {
|
||
ui.Cursor().Next(th.RowHeight * 2.6)
|
||
return
|
||
}
|
||
nameW, valW := hr.W*0.42, hr.W*0.2
|
||
cell := func(c mathx.Rect, s string, tc mathx.Color, align core.Align) {
|
||
ui.PushClip(c)
|
||
ui.TextIn(c, s, tc, align)
|
||
ui.PopClip()
|
||
}
|
||
ui.Rect(mathx.Rect{X: hr.X, Y: hr.Y + (hr.H-9)/2, W: 3, H: 9}, col)
|
||
valCol := th.Text
|
||
if budget > 0 && last > budget {
|
||
valCol = th.Warn
|
||
}
|
||
cell(mathx.Rect{X: hr.X + 4, Y: hr.Y, W: nameW - 4, H: hr.H}, title, th.Text, core.AlignLeft)
|
||
cell(mathx.Rect{X: hr.X + nameW, Y: hr.Y, W: valW, H: hr.H}, fmtf(last, prec)+unit, valCol, core.AlignLeft)
|
||
cell(mathx.Rect{X: hr.X + nameW + valW, Y: hr.Y, W: hr.W - nameW - valW, H: hr.H}, "avg "+fmtf(avg, prec)+" max "+fmtf(peak, prec), th.TextDim, core.AlignRight)
|
||
|
||
r := ui.Cursor().Next(th.RowHeight * 2.6)
|
||
ui.PushClip(r)
|
||
defer ui.PopClip()
|
||
ui.Rect(r, th.PanelAlt)
|
||
top := max(peak, budget, 1e-6) * 1.15
|
||
bw := r.W / history
|
||
if n > 0 {
|
||
floor := r.Y + r.H
|
||
off := float32(history-n) * bw
|
||
fill, env, line := col, col, col
|
||
fill.A, env.A = 0.22, 0.09
|
||
yOf := func(v float32) float32 { return r.H * v / top }
|
||
sample := func(u float32) (float32, float32) {
|
||
k := int(u)
|
||
if k >= n-1 {
|
||
b := p.at(first + n - 1)
|
||
return b.avg[m], b.peak[m]
|
||
}
|
||
f := u - float32(k)
|
||
a, b := p.at(first+k), p.at(first+k+1)
|
||
return a.avg[m] + (b.avg[m]-a.avg[m])*f, a.peak[m] + (b.peak[m]-a.peak[m])*f
|
||
}
|
||
prev := float32(-1)
|
||
for px := int(off); px < int(r.W); px++ {
|
||
x := r.X + float32(px)
|
||
av, pk := sample((float32(px) - off) / bw)
|
||
ah, ph := yOf(av), yOf(pk)
|
||
ui.Rect(mathx.Rect{X: x, Y: floor - ph, W: 1, H: ph}, env)
|
||
ui.Rect(mathx.Rect{X: x, Y: floor - ah, W: 1, H: ah}, fill)
|
||
lo, hi := ah, ah
|
||
if prev >= 0 {
|
||
lo, hi = min(ah, prev), max(ah, prev)
|
||
}
|
||
ui.Rect(mathx.Rect{X: x, Y: floor - hi - 1, W: 1, H: hi - lo + 2}, line)
|
||
prev = ah
|
||
}
|
||
}
|
||
if budget > 0 {
|
||
warn := th.Warn
|
||
warn.A = 0.7
|
||
ui.Rect(mathx.Rect{X: r.X, Y: r.Y + r.H - r.H*budget/top, W: r.W, H: 1}, warn)
|
||
}
|
||
ui.TextIn(mathx.Rect{X: r.X, Y: r.Y + 2, W: r.W - 5, H: ui.LineHeight()}, fmtf(top, prec)+unit, th.TextDisabled, core.AlignRight)
|
||
if !ui.Hit(r) || n == 0 {
|
||
return
|
||
}
|
||
i := int((ui.Mouse().X-r.X)/bw) - (history - n)
|
||
if i < 0 || i >= n {
|
||
return
|
||
}
|
||
b := p.at(first + i)
|
||
x := r.X + float32(history-n+i)*bw
|
||
ui.Rect(mathx.Rect{X: x, Y: r.Y, W: max(1, bw-1), H: r.H}, ui.Theme.Highlight)
|
||
ago := float64(n-1-i) * p.seconds()
|
||
label := i18n.Tr("editor.profiler.fmtf_fmtf2_unit_formatfloat_s", "{fmtf} / {fmtf2}{unit} {formatfloat} s ago", i18n.Str("fmtf", fmtf(b.avg[m], prec)), i18n.Str("fmtf2", fmtf(b.peak[m], prec)), i18n.Str("unit", unit), i18n.Str("formatfloat", strconv.FormatFloat(ago, 'f', 1, 64)))
|
||
size := ui.TextSize(label)
|
||
box := mathx.Rect{X: r.X + r.W - size.X - 10, Y: r.Y + 2, W: size.X + 8, H: size.Y + 2}
|
||
ui.Rect(box, th.Background)
|
||
ui.Text(label, mathx.Vec2{X: box.X + 4, Y: box.Y + 1}, th.Text)
|
||
}
|
||
|
||
func (p *Panel) toolbar(ui *core.Context, bar mathx.Rect) {
|
||
th := &ui.Theme
|
||
label := i18n.Tr("editor.profiler.pause", "Pause")
|
||
if p.paused {
|
||
label = i18n.Tr("editor.profiler.resume", "Resume")
|
||
}
|
||
if widgets.ButtonRect(ui, ui.ID("pause"), mathx.Rect{X: bar.X, Y: bar.Y, W: 80, H: bar.H}, label) {
|
||
p.paused = !p.paused
|
||
}
|
||
names := []string{"0.1 s", "0.25 s", "0.5 s", "1 s"}
|
||
if widgets.Combo(ui, ui.ID("interval"), mathx.Rect{X: bar.X + 86, Y: bar.Y, W: 80, H: bar.H}, &p.interval, names, false) {
|
||
p.n, p.head, p.acc = 0, 0, bucket{}
|
||
p.valid = [metricCount]int{}
|
||
}
|
||
if p.n == 0 {
|
||
return
|
||
}
|
||
fps := "—"
|
||
if v := p.at(p.n - 1).fps(); v > 0 {
|
||
fps = strconv.FormatFloat(v, 'f', 1, 64)
|
||
}
|
||
span := strconv.FormatFloat(p.seconds()*history, 'f', 0, 64)
|
||
info := mathx.Rect{X: bar.X + 176, Y: bar.Y, W: max(0, bar.W-176), H: bar.H}
|
||
ui.PushClip(info)
|
||
ui.Text(i18n.Tr("editor.profiler.fps_v_window_span_s", "fps {v} window {span} s", i18n.Str("v", fps), i18n.Str("span", span)), mathx.Vec2{X: info.X, Y: ui.TextY(bar)}, th.Text)
|
||
ui.PopClip()
|
||
}
|
||
|
||
func (p *Panel) sticky(ui *core.Context, body mathx.Rect) {
|
||
th := &ui.Theme
|
||
cur := -1
|
||
for i, m := range p.marks {
|
||
if m.y < p.scroll {
|
||
cur = i
|
||
}
|
||
}
|
||
if cur < 0 {
|
||
return
|
||
}
|
||
y := body.Y
|
||
if next := cur + 1; next < len(p.marks) {
|
||
if gap := p.marks[next].y - p.scroll; gap < th.RowHeight {
|
||
y -= th.RowHeight - gap
|
||
}
|
||
}
|
||
h := mathx.Rect{X: body.X, Y: y, W: body.W, H: th.RowHeight}
|
||
ui.Rect(h, th.Panel)
|
||
ui.Rect(mathx.Rect{X: h.X, Y: h.Y + h.H - 1, W: h.W, H: 1}, th.Border)
|
||
ui.TextIn(h, p.marks[cur].title, th.Text, core.AlignLeft)
|
||
}
|
||
|
||
func (p *Panel) Draw(f *services.Frame, r mathx.Rect) {
|
||
ui := f.UI
|
||
th := &ui.Theme
|
||
body := r
|
||
if ui.MouseOver(ui.ID("scroll"), body) {
|
||
if w := ui.ScrollY(th.RowHeight * 3); w != 0 {
|
||
p.scroll -= w
|
||
}
|
||
}
|
||
p.scroll = mathx.Clamp(p.scroll, 0, max(0, p.content-body.H+th.Padding))
|
||
p.top = body.Y - p.scroll
|
||
ui.PushClip(body)
|
||
y := p.top
|
||
if p.deps.Tracer != nil {
|
||
p.tl.Draw(ui, p.deps, mathx.Rect{X: r.X + th.Padding, Y: y + th.Padding, W: r.W - 2*th.Padding, H: TimelineHeight - th.Padding})
|
||
y += TimelineHeight
|
||
}
|
||
bar := mathx.Rect{X: r.X + th.Padding, Y: y + th.Padding, W: r.W - 2*th.Padding, H: th.RowHeight}
|
||
p.toolbar(ui, bar)
|
||
y = bar.Y + bar.H + th.Spacing
|
||
ui.PushCursor(core.Cursor{X: body.X + th.Padding, Y: y, W: body.W - 2*th.Padding, RowH: th.RowHeight, Pad: th.Padding, Space: th.Spacing})
|
||
p.titles, p.marks = p.titles[:0], p.marks[:0]
|
||
p.section(ui, i18n.Tr("editor.profiler.frame", "Frame"))
|
||
p.graph(ui, i18n.Tr("editor.profiler.graph.frame", "frame"), " ms", 2, 1000.0/60, th.Accent, mFrame)
|
||
p.graph(ui, i18n.Tr("editor.profiler.graph.update", "update"), " ms", 2, 0, th.AxisY, mUpdate)
|
||
p.graph(ui, i18n.Tr("editor.profiler.graph.render", "render"), " ms", 2, 0, th.AxisX, mRender)
|
||
p.graph(ui, i18n.Tr("editor.profiler.graph.draw_calls", "draw calls"), "", 0, 0, th.Selection, mDraws)
|
||
p.graph(ui, i18n.Tr("editor.profiler.graph.sprites", "sprites"), "", 0, 0, th.Selection, mSprites)
|
||
p.graph(ui, i18n.Tr("editor.profiler.graph.garbage", "garbage"), " KB", 1, 64, th.Warn, mGarbage)
|
||
widgets.Separator(ui)
|
||
p.section(ui, i18n.Tr("editor.profiler.materials", "Materials and shaders"))
|
||
p.materialTable(ui)
|
||
p.section(ui, i18n.Tr("editor.profiler.memory", "Memory"))
|
||
p.memory(ui)
|
||
if p.playing {
|
||
widgets.Separator(ui)
|
||
p.section(ui, i18n.Tr("editor.profiler.game", "Game"))
|
||
p.graph(ui, i18n.Tr("editor.profiler.graph.game_update", "game update"), " ms", 2, 0, th.AxisY, mGameUpdate)
|
||
p.graph(ui, i18n.Tr("editor.profiler.graph.game_render", "game render"), " ms", 2, 0, th.AxisX, mGameRender)
|
||
p.graph(ui, i18n.Tr("editor.profiler.graph.game_sprites", "game sprites"), "", 0, 0, th.Selection, mGameSprites)
|
||
p.graph(ui, i18n.Tr("editor.profiler.graph.game_nodes", "game nodes"), "", 0, 0, th.Selection, mGameNodes)
|
||
widgets.LabelDim(ui, gameLine(&p.game))
|
||
if p.hasScripts {
|
||
widgets.Separator(ui)
|
||
p.section(ui, i18n.Tr("editor.profiler.scripts", "Scripts"))
|
||
p.graph(ui, i18n.Tr("editor.profiler.graph.script_instructions", "script instructions"), "", 0, 0, th.Accent, mScriptInstr)
|
||
p.graph(ui, i18n.Tr("editor.profiler.graph.script_host_calls", "script host calls"), "", 0, 0, th.AxisY, mScriptHost)
|
||
p.graph(ui, i18n.Tr("editor.profiler.graph.script_heap", "script heap"), " KB", 1, 0, th.Warn, mScriptHeap)
|
||
p.graph(ui, i18n.Tr("editor.profiler.graph.coroutines", "coroutines"), "", 0, 0, th.AxisZ, mCoroutines)
|
||
p.graph(ui, i18n.Tr("editor.profiler.graph.signal_emits", "signal emits"), "", 0, 0, th.Selection, mEmits)
|
||
p.scriptTable(ui)
|
||
}
|
||
}
|
||
var secs []context.ProfilerSection
|
||
if p.deps.Sections != nil {
|
||
secs = p.deps.Sections()
|
||
}
|
||
for _, sec := range secs {
|
||
if sec.Play && !p.playing {
|
||
continue
|
||
}
|
||
widgets.Separator(ui)
|
||
p.section(ui, sec.Title.String())
|
||
sec.Draw()
|
||
}
|
||
if !p.playing {
|
||
widgets.LabelDim(ui, playHint(secs))
|
||
}
|
||
p.content = ui.Cursor().Y - p.top
|
||
ui.PopCursor()
|
||
p.sticky(ui, body)
|
||
ui.PopClip()
|
||
}
|
||
|
||
func playHint(list []context.ProfilerSection) string {
|
||
var extra []string
|
||
for _, sec := range list {
|
||
if sec.Play {
|
||
extra = append(extra, strings.ToLower(sec.Title.String()))
|
||
}
|
||
}
|
||
if len(extra) == 0 {
|
||
return i18n.Tr("editor.profiler.press_play_to_see_game", "Press Play to see game and script timings")
|
||
}
|
||
return i18n.Tr("editor.profiler.press_play_to_see_game_2", "Press Play to see game, script and {join} timings", i18n.Str("join", strings.Join(extra, ", ")))
|
||
}
|
||
|
||
// offscreen reports whether a graph from y down h is outside the clip, so it is skipped: its columns would be clipped but still sent to the GPU.
|
||
func offscreen(ui *core.Context, y, h float32) bool {
|
||
clip, ok := ui.ClipRect()
|
||
return ok && (y > clip.Y+clip.H || y+h < clip.Y)
|
||
}
|
||
|
||
func (p *Panel) materialTable(ui *core.Context) {
|
||
r := &p.materials
|
||
widgets.LabelDim(ui, fmt.Sprintf("Materials %d programs %d variants %d pending %d", r.MaterialCount, r.MaterialPrograms, r.MaterialVariants, r.MaterialPending))
|
||
widgets.LabelDim(ui, fmt.Sprintf("Uploads/frame: frame %d B view %d B material %d B", r.FrameUBOBytes, r.ViewUBOBytes, r.MaterialUBOBytes))
|
||
widgets.LabelDim(ui, fmt.Sprintf("Instance bytes: sprites %d B material lanes %d B", r.InstanceBytes, r.MaterialLaneBytes))
|
||
widgets.LabelDim(ui, fmt.Sprintf("Pipeline switches/frame %d", r.PipelineSwitches))
|
||
widgets.LabelDim(ui, fmt.Sprintf("Batch splits/frame: material %d texture/sampler %d pipeline %d clip %d mesh %d", r.MaterialBreaks, r.TextureBreaks, r.PipelineBreaks, r.ClipBreaks, r.MeshBreaks))
|
||
widgets.LabelDim(ui, fmt.Sprintf("Since renderer start: compile failures %d committed reloads %d", r.MaterialCompileFailures, r.MaterialReloadGenerations))
|
||
}
|