Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions fission/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@ yarn.lock
test-results
coverage
src/test/**/__screenshots__
src/bench/baseline.json

tsconfig.tsbuildinfo
5 changes: 5 additions & 0 deletions fission/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
"test:coverage": "vitest run --coverage",
"test:asan": "sh ./scripts/test-asan.sh",
"test:multiplayer": "VITE_RUN_MULTIPLAYER_TEST=true vitest src/test/multiplayer",
"bench": "vitest bench --mode test",
"bench:baseline": "vitest bench --mode test --project chromium --outputJson src/bench/baseline.json",
"bench:chrome": "vitest bench --mode test --project chromium",
"bench:firefox": "vitest bench --mode test --project firefox",
"bench:check": "vitest bench --mode test --project chromium --compare src/bench/baseline.json",
"build": "tsc && vite build",
"build:prod": "tsc && vite build --base=/fission/ --outDir dist/prod",
"build:dev": "tsc && vite build --base=/fission-closed/ --outDir dist/dev",
Expand Down
46 changes: 19 additions & 27 deletions fission/src/bench/MirabufParser.bench.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,26 @@
import MirabufParser from "@/mirabuf/MirabufParser"
import { bench, describe } from "vitest"
import type { mirabuf } from "@/proto/mirabuf"
import { getMiraAssembly } from "@/test/GetAssets"
import { bench, beforeAll, describe } from "vitest"
import MirabufParser from "@/mirabuf/MirabufParser"

// Top-level await: runs once at module load before any bench executes.
// beforeAll does not fire in Vitest browser bench mode, so this is the workaround.
const [dozer, multiJoint, field2018] = (await Promise.all([
getMiraAssembly("DOZER"),
getMiraAssembly("MULTI_JOINT"),
getMiraAssembly(2018),
])) as [mirabuf.Assembly, mirabuf.Assembly, mirabuf.Assembly]

describe("MirabufParser", () => {
bench("Parse Dozer", () => {
new MirabufParser(dozer)
})

describe("Mirabuf Parsing", () => {
describe("2018", () => {
let assembly: mirabuf.Assembly | undefined
beforeAll(async () => {
assembly = await getMiraAssembly(2018)
})
bench(
"parse",
() => {
new MirabufParser(assembly!)
},
{ time: 100 }
)
bench("Parse Multi-Joint Wheels", () => {
new MirabufParser(multiJoint)
})

describe("Dozer", () => {
let assembly: mirabuf.Assembly | undefined
beforeAll(async () => {
assembly = await getMiraAssembly("DOZER")
})
bench(
"parse",
() => {
new MirabufParser(assembly!)
},
{ time: 100 }
)
bench("Parse FRC Field 2018", () => {
new MirabufParser(field2018)
})
})
128 changes: 128 additions & 0 deletions fission/src/bench/PhysicsSystem.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import * as THREE from "three"
import { bench, describe } from "vitest"
import JOLT from "@/util/loading/JoltSyncLoader"
import PhysicsSystem, { STANDARD_SIMULATION_PERIOD } from "@/systems/physics/PhysicsSystem"
import MirabufParser from "@/mirabuf/MirabufParser"
import { getMiraAssembly } from "@/test/GetAssets"

function addFloor(system: PhysicsSystem) {
const floor = system.createBox(new THREE.Vector3(50, 0.5, 50), undefined, new THREE.Vector3(0, -0.5, 0), undefined)
system.addBodyToSystem(floor.GetID(), false)
}

// --- Simulation step systems ---

const emptySystem = new PhysicsSystem()
addFloor(emptySystem)

const eightBodySystem = new PhysicsSystem()
addFloor(eightBodySystem)
for (let i = 0; i < 8; i++) {
const body = eightBodySystem.createBox(
new THREE.Vector3(0.25, 0.25, 0.25),
1.0,
new THREE.Vector3((i % 4) * 0.6, Math.floor(i / 4) + 0.5, 0),
undefined
)
eightBodySystem.addBodyToSystem(body.GetID(), true)
}

const fiftyBodySystem = new PhysicsSystem()
addFloor(fiftyBodySystem)
for (let i = 0; i < 50; i++) {
const body = fiftyBodySystem.createBox(
new THREE.Vector3(0.25, 0.25, 0.25),
0.5,
new THREE.Vector3((i % 10) * 0.6, Math.floor(i / 10) * 0.6 + 0.5, 0),
undefined
)
fiftyBodySystem.addBodyToSystem(body.GetID(), true)
}

// --- Raycast system ---
// Pass destroy=false on each rayCast call so the Vec3s survive across iterations.

const raycastSystem = new PhysicsSystem()
const rayTarget = raycastSystem.createBox(new THREE.Vector3(1, 1, 1), undefined, new THREE.Vector3(0, 5, 0), undefined)
raycastSystem.addBodyToSystem(rayTarget.GetID(), false)
const RAY_FROM = new JOLT.Vec3(0, 0, 0)
const RAY_HIT_DIR = new JOLT.Vec3(0, 10, 0)
const RAY_MISS_DIR = new JOLT.Vec3(100, 0, 0)

// --- Body creation system ---

const creationSystem = new PhysicsSystem()
const HALF_EXTENTS = new THREE.Vector3(0.5, 0.5, 0.5)
const CUBE_HULL_POINTS = new Float32Array([
0.5, -0.5, 0.5, -0.5, -0.5, 0.5, -0.5, -0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, -0.5, 0.5, 0.5, -0.5, 0.5, -0.5,
0.5, 0.5, -0.5,
])

// --- Assembly spawn systems (async: need real mirabuf assemblies) ---

const [dozerAssembly, multiJointAssembly] = await Promise.all([
getMiraAssembly("DOZER"),
getMiraAssembly("MULTI_JOINT"),
])
const dozerParser = new MirabufParser(dozerAssembly!)
const multiJointParser = new MirabufParser(multiJointAssembly!)

// Separate systems per spawn bench so their internal state stays independent.
const spawnSystemDozer = new PhysicsSystem()
const spawnSystemMultiJoint = new PhysicsSystem()

// ────────────────────────────────────────────────────────────────────────────

describe("PhysicsSystem — simulation step", () => {
bench("update — empty world", () => {
emptySystem.update(STANDARD_SIMULATION_PERIOD)
})

bench("update — 8 dynamic bodies", () => {
eightBodySystem.update(STANDARD_SIMULATION_PERIOD)
})

bench("update — 50 dynamic bodies", () => {
fiftyBodySystem.update(STANDARD_SIMULATION_PERIOD)
})
})

describe("PhysicsSystem — body creation", () => {
bench("createBox + add + destroy", () => {
const body = creationSystem.createBox(HALF_EXTENTS, 1.0, undefined, undefined)
creationSystem.addBodyToSystem(body.GetID(), false)
creationSystem.destroyBodies(body)
})

bench("createConvexHull — 8-point cube", () => {
const result = creationSystem.createConvexHull(CUBE_HULL_POINTS)
if (result.IsValid()) result.Get().Release()
})
})

describe("PhysicsSystem — raycast", () => {
bench("rayCast — hit", () => {
raycastSystem.rayCast(RAY_FROM, RAY_HIT_DIR, false)
})

bench("rayCast — miss", () => {
raycastSystem.rayCast(RAY_FROM, RAY_MISS_DIR, false)
})
})

describe("PhysicsSystem — assembly spawn", () => {
// createMechanismFromParser allocates a LayerReserve (one of 8 robot slots).
// destroyMechanism removes bodies and constraints but does not release the slot,
// so we do it manually to keep the pool from exhausting across iterations.
bench("spawn + destroy Dozer (7 bodies, 6 joints)", () => {
const mech = spawnSystemDozer.createMechanismFromParser(dozerParser)
spawnSystemDozer.destroyMechanism(mech)
mech.layerReserve?.release()
})

bench("spawn + destroy Multi-Joint Wheels (9 bodies, 8 joints)", () => {
const mech = spawnSystemMultiJoint.createMechanismFromParser(multiJointParser)
spawnSystemMultiJoint.destroyMechanism(mech)
mech.layerReserve?.release()
})
})
11 changes: 11 additions & 0 deletions fission/src/bench/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Benchmark Documentation

## To Run

`bun run bench`

## Output Table Format

```
name hz (tests / second) min (ms) max (ms) mean (ms) p75 (ms) p99 (ms) p995 (ms) p999 (ms) rme (root mean error) samples
```
71 changes: 71 additions & 0 deletions fission/src/bench/SceneRenderer.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { server } from "@vitest/browser/context"
import { bench, describe } from "vitest"
import MirabufInstance from "@/mirabuf/MirabufInstance"
import MirabufParser from "@/mirabuf/MirabufParser"
import type { mirabuf } from "@/proto/mirabuf"
import SceneRenderer from "@/systems/scene/SceneRenderer"
import World from "@/systems/World"
import { getMiraAssembly } from "@/test/GetAssets"

const STANDARD_FRAME_DELTA = 1 / 60

// Top-level await: assemblies load once before any bench runs. beforeAll does not fire
// in Vitest browser bench mode (same workaround as the other .bench.ts files).
const [dozer, multiJoint, field2023] = (await Promise.all([
getMiraAssembly("DOZER"),
getMiraAssembly("MULTI_JOINT"),
getMiraAssembly(2023),
])) as [mirabuf.Assembly, mirabuf.Assembly, mirabuf.Assembly]

// MirabufInstance sets its materials up against World.sceneRenderer during construction,
// so a live World is required even though each scenario renders on its own SceneRenderer.
World.initWorld()

interface Scene {
renderer: SceneRenderer
gl: WebGLRenderingContext | WebGL2RenderingContext
}

// Build a standalone renderer with the given assemblies' geometry added directly to its
// scene. We bypass MirabufSceneObject/physics on purpose: this isolates the per-frame
// *render* cost (draw calls, shadow map, postprocessing) — what PerformanceMonitor reacts
// to and what dominates a real frame. Each scenario owns its own WebGL context so the
// scenes stay independent and directly comparable.
function makeScene(assemblies: mirabuf.Assembly[]): Scene {
const renderer = new SceneRenderer()
for (const assembly of assemblies) {
const instance = new MirabufInstance(new MirabufParser(assembly))
instance.addToScene(renderer.scene)
}
renderer.updateCanvasSize()

const gl = renderer.renderer.getContext()
// Warm up: the first frames compile shaders and allocate GPU buffers (hundreds of ms).
// Render a few and drain the GPU so that one-time cost never lands in a sample.
for (let i = 0; i < 5; i++) {
renderer.update(STANDARD_FRAME_DELTA)
gl.finish()
}
return { renderer, gl }
}

const empty = makeScene([])
const oneRobot = makeScene([dozer])
const robotAndField = makeScene([dozer, field2023])
const twoRobotsAndField = makeScene([dozer, multiJoint, field2023])

// composer.render() only queues GPU commands; gl.finish() blocks until they complete, so
// the timed region reflects the true frame cost instead of command-submission noise.
function renderFrame({ renderer, gl }: Scene) {
renderer.update(STANDARD_FRAME_DELTA)
gl.finish()
}

// Skip on firefox: WebGL is unreliable in the firefox instance under GitHub Actions
// (same guard as MirabufRealLoad.test.ts).
describe.skipIf(server.browser === "firefox")("SceneRenderer — full frame render", () => {
bench("render — empty scene (ground + skybox)", () => renderFrame(empty))
bench("render — 1 robot (Dozer)", () => renderFrame(oneRobot))
bench("render — 1 robot + field (2023)", () => renderFrame(robotAndField))
bench("render — 2 robots + field (2023)", () => renderFrame(twoRobotsAndField))
})
31 changes: 27 additions & 4 deletions fission/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,25 @@ type ProxyOptions = Proxies[string]
// https://vitejs.dev/config/
export default defineConfig(({ mode }): ViteUserConfig => {
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) }
const useLocalAssets = localAssetsExist && (mode === "test" || process.env.NODE_ENV == "development")
process.env.VITE_MULTIPLAYER_PORT = mode === "test" ? "3001" : "9002"
// @vitest/browser spawns its vite server with mode "test" for `vitest test` and
// "benchmark" for `vitest bench`; both should use local assets when available
// (private mirabuf assets such as Multi-Joint Wheels only exist locally).
const useLocalAssets =
localAssetsExist && (mode === "test" || mode === "benchmark" || process.env.NODE_ENV == "development")

if (!localAssetsExist && (mode === "test" || process.env.NODE_ENV == "development")) {
if (!localAssetsExist && mode !== "production") {
console.warn("Can't find local assets, do you need to run `npm run assetpack`?")
}
console.log(`Using ${useLocalAssets ? "local" : "remote"} mirabuf assets`)

const proxies: Proxies = {}
// In dev mode NODE_ENV is "development"; in vitest (test or bench) it is "test"
// regardless of what mode @vitest/browser uses when spawning the browser vite server.
const localAssetPort = process.env.NODE_ENV === "development" ? serverPort : 3001
const assetProxy: ProxyOptions = useLocalAssets
? {
target: `http://localhost:${mode === "test" ? 3001 : serverPort}`,
target: `http://localhost:${localAssetPort}`,
changeOrigin: true,
secure: false,
rewrite: path => path.replace(/^\/api/, "/Downloadables"),
Expand Down Expand Up @@ -105,7 +113,22 @@ export default defineConfig(({ mode }): ViteUserConfig => {
globalSetup: ["src/test/TestSetup.server.ts"],
testTimeout: 10000,
globals: true,
environment: "jsdom",
environment: "node",
reporters: process.env.GITHUB_ACTIONS
? [
"github-actions",
"default",
{
onTestRunEnd(_modules: unknown, unhandled: unknown[], reason: TestRunEndReason) {
if (reason === "passed" && unhandled.length === 0) {
console.error("GH ACTIONS VITEST PASSED")
} else {
console.error(unhandled)
}
},
},
]
: ["default"],
browser: {
enabled: true,
provider: "playwright",
Expand Down
Loading