test(engine): split profiler coverage by module
This commit is contained in:
parent
cb51fc73fc
commit
46bf5a2bd2
|
|
@ -277,6 +277,11 @@ const ENGINE_TEST_DOMAIN_REDIRECTS: Array<{
|
|||
from: 'tests/engine/geometry/basic',
|
||||
to: 'tests/engine/geometry/angles.test.ts, tests/engine/geometry/rotation.test.ts, tests/engine/geometry/bounds.test.ts, or tests/engine/geometry/visual-bounds.test.ts',
|
||||
source: 'the geometry module under test'
|
||||
},
|
||||
{
|
||||
from: 'tests/engine/profiler/basic',
|
||||
to: 'tests/engine/profiler/frame-stats.test.ts, draw-call-counter.test.ts, gpu-timer.test.ts, phase-timer.test.ts, capture-stack.test.ts, or speedscope.test.ts',
|
||||
source: 'the profiler module under test'
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,185 +0,0 @@
|
|||
import { describe, it, expect } from 'bun:test'
|
||||
|
||||
import {
|
||||
FrameStats,
|
||||
DrawCallCounter,
|
||||
PhaseTimer,
|
||||
GPUTimer,
|
||||
CaptureStack,
|
||||
toSpeedscopeJSON
|
||||
} from '@open-pencil/core'
|
||||
|
||||
describe('FrameStats', () => {
|
||||
it('records frames and computes rolling averages', () => {
|
||||
const stats = new FrameStats()
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
stats.recordFrame(5)
|
||||
}
|
||||
|
||||
expect(stats.avgCpuTime).toBe(5)
|
||||
expect(stats.smoothedFps).toBeGreaterThan(0)
|
||||
expect(stats.getFrameTimeHistory()).toBeInstanceOf(Float64Array)
|
||||
expect(stats.getBufferCount()).toBe(10)
|
||||
})
|
||||
|
||||
it('tracks external fields', () => {
|
||||
const stats = new FrameStats()
|
||||
stats.totalNodes = 100
|
||||
stats.culledNodes = 20
|
||||
stats.drawCalls = 50
|
||||
stats.scenePictureCacheHit = true
|
||||
|
||||
expect(stats.totalNodes).toBe(100)
|
||||
expect(stats.culledNodes).toBe(20)
|
||||
expect(stats.drawCalls).toBe(50)
|
||||
expect(stats.scenePictureCacheHit).toBe(true)
|
||||
})
|
||||
|
||||
it('handles GPU time as NaN initially', () => {
|
||||
const stats = new FrameStats()
|
||||
stats.recordFrame(1)
|
||||
expect(stats.gpuTime).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DrawCallCounter', () => {
|
||||
it('handles null GL context', () => {
|
||||
const counter = new DrawCallCounter(null)
|
||||
expect(counter.count).toBe(0)
|
||||
expect(counter.reset()).toBe(0)
|
||||
counter.destroy()
|
||||
})
|
||||
|
||||
it('wraps draw calls only while enabled', () => {
|
||||
let rawDrawCalls = 0
|
||||
const gl = {
|
||||
drawArrays: () => rawDrawCalls++,
|
||||
drawElements: () => rawDrawCalls++,
|
||||
drawArraysInstanced: () => rawDrawCalls++,
|
||||
drawElementsInstanced: () => rawDrawCalls++
|
||||
} as WebGL2RenderingContext
|
||||
const originalDrawArrays = gl.drawArrays
|
||||
|
||||
const counter = new DrawCallCounter(gl)
|
||||
gl.drawArrays(0, 0, 0)
|
||||
expect(counter.count).toBe(0)
|
||||
expect(rawDrawCalls).toBe(1)
|
||||
|
||||
counter.enable()
|
||||
expect(gl.drawArrays).not.toBe(originalDrawArrays)
|
||||
gl.drawArrays(0, 0, 0)
|
||||
expect(counter.count).toBe(1)
|
||||
expect(rawDrawCalls).toBe(2)
|
||||
|
||||
counter.disable()
|
||||
expect(gl.drawArrays).toBe(originalDrawArrays)
|
||||
gl.drawArrays(0, 0, 0)
|
||||
expect(counter.count).toBe(1)
|
||||
expect(rawDrawCalls).toBe(3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GPUTimer', () => {
|
||||
it('handles null GL context', () => {
|
||||
const timer = new GPUTimer(null)
|
||||
expect(timer.available).toBe(false)
|
||||
expect(timer.lastGpuTimeMs).toBeNaN()
|
||||
timer.beginFrame()
|
||||
timer.endFrame()
|
||||
expect(timer.pollResults()).toBeNull()
|
||||
timer.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('PhaseTimer', () => {
|
||||
it('is disabled by default', () => {
|
||||
const timer = new PhaseTimer()
|
||||
expect(timer.enabled).toBe(false)
|
||||
})
|
||||
|
||||
it('records measures when enabled', () => {
|
||||
const timer = new PhaseTimer()
|
||||
timer.enabled = true
|
||||
timer.beginPhase('test')
|
||||
timer.endPhase('test')
|
||||
timer.clearPhases()
|
||||
})
|
||||
|
||||
it('is a no-op when disabled', () => {
|
||||
const timer = new PhaseTimer()
|
||||
timer.beginPhase('test')
|
||||
timer.endPhase('test')
|
||||
})
|
||||
})
|
||||
|
||||
describe('CaptureStack', () => {
|
||||
it('builds a tree of node profiles', () => {
|
||||
const stack = new CaptureStack()
|
||||
stack.reset(performance.now())
|
||||
|
||||
stack.begin('node-1', 'Frame 1', 'FRAME', false)
|
||||
stack.begin('node-2', 'Rect', 'RECTANGLE', false)
|
||||
stack.end(2)
|
||||
stack.end(3)
|
||||
|
||||
const roots = stack.getRootProfiles()
|
||||
expect(roots).toHaveLength(1)
|
||||
expect(roots[0].nodeId).toBe('node-1')
|
||||
expect(roots[0].children).toHaveLength(1)
|
||||
expect(roots[0].children[0].nodeId).toBe('node-2')
|
||||
expect(roots[0].children[0].drawCalls).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('toSpeedscopeJSON', () => {
|
||||
it('produces valid speedscope JSON', () => {
|
||||
const capture = {
|
||||
timestamp: 0,
|
||||
totalTimeMs: 10,
|
||||
cpuTimeMs: 8,
|
||||
gpuTimeMs: 6,
|
||||
totalNodes: 5,
|
||||
culledNodes: 1,
|
||||
drawCalls: 10,
|
||||
scenePictureCacheHit: false,
|
||||
rootProfiles: [
|
||||
{
|
||||
nodeId: 'n1',
|
||||
name: 'Frame',
|
||||
type: 'FRAME',
|
||||
depth: 0,
|
||||
startTime: 0,
|
||||
endTime: 10,
|
||||
selfTime: 5,
|
||||
drawCalls: 3,
|
||||
culled: false,
|
||||
children: [
|
||||
{
|
||||
nodeId: 'n2',
|
||||
name: 'Rect',
|
||||
type: 'RECTANGLE',
|
||||
depth: 1,
|
||||
startTime: 2,
|
||||
endTime: 7,
|
||||
selfTime: 5,
|
||||
drawCalls: 2,
|
||||
culled: false,
|
||||
children: []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const json = toSpeedscopeJSON(capture)
|
||||
const parsed = JSON.parse(json)
|
||||
|
||||
expect(parsed.$schema).toBe('https://www.speedscope.app/file-format-schema.json')
|
||||
expect(parsed.profiles).toHaveLength(1)
|
||||
expect(parsed.profiles[0].type).toBe('evented')
|
||||
expect(parsed.profiles[0].unit).toBe('milliseconds')
|
||||
expect(parsed.shared.frames).toHaveLength(2)
|
||||
expect(parsed.profiles[0].events).toHaveLength(4)
|
||||
})
|
||||
})
|
||||
23
tests/engine/profiler/capture-stack.test.ts
Normal file
23
tests/engine/profiler/capture-stack.test.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { describe, it, expect } from 'bun:test'
|
||||
|
||||
import { CaptureStack } from '@open-pencil/core'
|
||||
|
||||
describe('CaptureStack', () => {
|
||||
it('builds a tree of node profiles', () => {
|
||||
const stack = new CaptureStack()
|
||||
stack.reset(performance.now())
|
||||
|
||||
stack.begin('node-1', 'Frame 1', 'FRAME', false)
|
||||
stack.begin('node-2', 'Rect', 'RECTANGLE', false)
|
||||
stack.end(2)
|
||||
stack.end(3)
|
||||
|
||||
const roots = stack.getRootProfiles()
|
||||
expect(roots).toHaveLength(1)
|
||||
expect(roots[0].nodeId).toBe('node-1')
|
||||
expect(roots[0].children).toHaveLength(1)
|
||||
expect(roots[0].children[0].nodeId).toBe('node-2')
|
||||
expect(roots[0].children[0].drawCalls).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
41
tests/engine/profiler/draw-call-counter.test.ts
Normal file
41
tests/engine/profiler/draw-call-counter.test.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { describe, it, expect } from 'bun:test'
|
||||
|
||||
import { DrawCallCounter } from '@open-pencil/core'
|
||||
|
||||
describe('DrawCallCounter', () => {
|
||||
it('handles null GL context', () => {
|
||||
const counter = new DrawCallCounter(null)
|
||||
expect(counter.count).toBe(0)
|
||||
expect(counter.reset()).toBe(0)
|
||||
counter.destroy()
|
||||
})
|
||||
|
||||
it('wraps draw calls only while enabled', () => {
|
||||
let rawDrawCalls = 0
|
||||
const gl = {
|
||||
drawArrays: () => rawDrawCalls++,
|
||||
drawElements: () => rawDrawCalls++,
|
||||
drawArraysInstanced: () => rawDrawCalls++,
|
||||
drawElementsInstanced: () => rawDrawCalls++
|
||||
} as WebGL2RenderingContext
|
||||
const originalDrawArrays = gl.drawArrays
|
||||
|
||||
const counter = new DrawCallCounter(gl)
|
||||
gl.drawArrays(0, 0, 0)
|
||||
expect(counter.count).toBe(0)
|
||||
expect(rawDrawCalls).toBe(1)
|
||||
|
||||
counter.enable()
|
||||
expect(gl.drawArrays).not.toBe(originalDrawArrays)
|
||||
gl.drawArrays(0, 0, 0)
|
||||
expect(counter.count).toBe(1)
|
||||
expect(rawDrawCalls).toBe(2)
|
||||
|
||||
counter.disable()
|
||||
expect(gl.drawArrays).toBe(originalDrawArrays)
|
||||
gl.drawArrays(0, 0, 0)
|
||||
expect(counter.count).toBe(1)
|
||||
expect(rawDrawCalls).toBe(3)
|
||||
})
|
||||
})
|
||||
|
||||
38
tests/engine/profiler/frame-stats.test.ts
Normal file
38
tests/engine/profiler/frame-stats.test.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { describe, it, expect } from 'bun:test'
|
||||
|
||||
import { FrameStats } from '@open-pencil/core'
|
||||
|
||||
describe('FrameStats', () => {
|
||||
it('records frames and computes rolling averages', () => {
|
||||
const stats = new FrameStats()
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
stats.recordFrame(5)
|
||||
}
|
||||
|
||||
expect(stats.avgCpuTime).toBe(5)
|
||||
expect(stats.smoothedFps).toBeGreaterThan(0)
|
||||
expect(stats.getFrameTimeHistory()).toBeInstanceOf(Float64Array)
|
||||
expect(stats.getBufferCount()).toBe(10)
|
||||
})
|
||||
|
||||
it('tracks external fields', () => {
|
||||
const stats = new FrameStats()
|
||||
stats.totalNodes = 100
|
||||
stats.culledNodes = 20
|
||||
stats.drawCalls = 50
|
||||
stats.scenePictureCacheHit = true
|
||||
|
||||
expect(stats.totalNodes).toBe(100)
|
||||
expect(stats.culledNodes).toBe(20)
|
||||
expect(stats.drawCalls).toBe(50)
|
||||
expect(stats.scenePictureCacheHit).toBe(true)
|
||||
})
|
||||
|
||||
it('handles GPU time as NaN initially', () => {
|
||||
const stats = new FrameStats()
|
||||
stats.recordFrame(1)
|
||||
expect(stats.gpuTime).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
16
tests/engine/profiler/gpu-timer.test.ts
Normal file
16
tests/engine/profiler/gpu-timer.test.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { describe, it, expect } from 'bun:test'
|
||||
|
||||
import { GPUTimer } from '@open-pencil/core'
|
||||
|
||||
describe('GPUTimer', () => {
|
||||
it('handles null GL context', () => {
|
||||
const timer = new GPUTimer(null)
|
||||
expect(timer.available).toBe(false)
|
||||
expect(timer.lastGpuTimeMs).toBeNaN()
|
||||
timer.beginFrame()
|
||||
timer.endFrame()
|
||||
expect(timer.pollResults()).toBeNull()
|
||||
timer.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
25
tests/engine/profiler/phase-timer.test.ts
Normal file
25
tests/engine/profiler/phase-timer.test.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { describe, it, expect } from 'bun:test'
|
||||
|
||||
import { PhaseTimer } from '@open-pencil/core'
|
||||
|
||||
describe('PhaseTimer', () => {
|
||||
it('is disabled by default', () => {
|
||||
const timer = new PhaseTimer()
|
||||
expect(timer.enabled).toBe(false)
|
||||
})
|
||||
|
||||
it('records measures when enabled', () => {
|
||||
const timer = new PhaseTimer()
|
||||
timer.enabled = true
|
||||
timer.beginPhase('test')
|
||||
timer.endPhase('test')
|
||||
timer.clearPhases()
|
||||
})
|
||||
|
||||
it('is a no-op when disabled', () => {
|
||||
const timer = new PhaseTimer()
|
||||
timer.beginPhase('test')
|
||||
timer.endPhase('test')
|
||||
})
|
||||
})
|
||||
|
||||
55
tests/engine/profiler/speedscope.test.ts
Normal file
55
tests/engine/profiler/speedscope.test.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { describe, it, expect } from 'bun:test'
|
||||
|
||||
import { toSpeedscopeJSON } from '@open-pencil/core'
|
||||
|
||||
describe('toSpeedscopeJSON', () => {
|
||||
it('produces valid speedscope JSON', () => {
|
||||
const capture = {
|
||||
timestamp: 0,
|
||||
totalTimeMs: 10,
|
||||
cpuTimeMs: 8,
|
||||
gpuTimeMs: 6,
|
||||
totalNodes: 5,
|
||||
culledNodes: 1,
|
||||
drawCalls: 10,
|
||||
scenePictureCacheHit: false,
|
||||
rootProfiles: [
|
||||
{
|
||||
nodeId: 'n1',
|
||||
name: 'Frame',
|
||||
type: 'FRAME',
|
||||
depth: 0,
|
||||
startTime: 0,
|
||||
endTime: 10,
|
||||
selfTime: 5,
|
||||
drawCalls: 3,
|
||||
culled: false,
|
||||
children: [
|
||||
{
|
||||
nodeId: 'n2',
|
||||
name: 'Rect',
|
||||
type: 'RECTANGLE',
|
||||
depth: 1,
|
||||
startTime: 2,
|
||||
endTime: 7,
|
||||
selfTime: 5,
|
||||
drawCalls: 2,
|
||||
culled: false,
|
||||
children: []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const json = toSpeedscopeJSON(capture)
|
||||
const parsed = JSON.parse(json)
|
||||
|
||||
expect(parsed.$schema).toBe('https://www.speedscope.app/file-format-schema.json')
|
||||
expect(parsed.profiles).toHaveLength(1)
|
||||
expect(parsed.profiles[0].type).toBe('evented')
|
||||
expect(parsed.profiles[0].unit).toBe('milliseconds')
|
||||
expect(parsed.shared.frames).toHaveLength(2)
|
||||
expect(parsed.profiles[0].events).toHaveLength(4)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue