Materia includes a comprehensive profiling system designed to help you identify performance bottlenecks and optimize your 3D applications. The profiling infrastructure provides:
import io.materia.profiling.*
// Enable profiling
PerformanceProfiler.configure(ProfilerConfig(enabled = true))
// In your render loop
fun renderFrame() {
PerformanceProfiler.startFrame()
// Your rendering code
renderer.render(scene, camera)
PerformanceProfiler.endFrame()
}
// Get statistics
val stats = PerformanceProfiler.getFrameStats()
println("Average FPS: ${stats.averageFps}")
println("Average frame time: ${stats.averageFrameTime / 1_000_000}ms")
val dashboard = ProfilingDashboard()
// Enable with default configuration
dashboard.enable()
// Check performance status
if (!dashboard.isPerformanceAcceptable()) {
println("Performance issues detected!")
println(dashboard.getFormattedText())
}
// Get performance grade
val grade = dashboard.getPerformanceGrade() // A, B, C, D, or F
// Measure a code block
PerformanceProfiler.measure("myOperation", ProfileCategory.RENDERING) {
// Your code here
processGeometry()
}
// Use a scope for more control
val scope = PerformanceProfiler.startScope("complexOperation", ProfileCategory.GEOMETRY)
try {
doComplexWork()
} finally {
scope.end()
}
Create focused profiling sessions to analyze specific scenarios:
val session = ProfilingHelpers.createSession("LoadingBenchmark")
// Run your scenario
loadAllAssets()
renderTestScene()
// End session and get summary
val summary = session.end()
summary.printSummary()
Wrap your renderer for automatic instrumentation:
val baseRenderer = createRenderer().getOrThrow()
val renderer = baseRenderer.withProfiling()
// All render calls are now profiled automatically
renderer.render(scene, camera)
Profile scene operations:
// Profile scene traversal
scene.traverseProfiled { obj ->
// Process each object
processObject(obj)
}
// Analyze scene complexity
val complexity = scene.getComplexity()
if (complexity.isComplex()) {
println("Scene has ${complexity.totalNodes} nodes at depth ${complexity.maxDepth}")
}
Profile geometry operations:
// Analyze geometry complexity
val complexity = geometry.analyzeComplexity()
println("Geometry has ${complexity.vertexCount} vertices, ${complexity.triangleCount} triangles")
println("Memory usage: ${complexity.getMemoryUsageMB()}MB")
// Get optimization recommendations
complexity.getRecommendations().forEach { recommendation ->
println("- $recommendation")
}
// Profile specific operations
GeometryProfiler.profileNormalCalculation {
geometry.computeVertexNormals()
}
Profile animation systems:
// Profile animation mixer update
AnimationProfiler.profileMixerUpdate(deltaTime, mixer.getActionCount()) {
mixer.update(deltaTime)
}
// Analyze animation complexity
val complexity = AnimationProfiler.analyzeAnimationComplexity(
trackCount = clip.tracks.size,
keyframeCount = totalKeyframes,
duration = clip.duration,
boneCount = skeleton.bones.size
)
if (complexity.isComplex()) {
complexity.getRecommendations().forEach { println(it) }
}
Profile physics simulation:
// Profile physics step
PhysicsProfiler.profilePhysicsStep(deltaTime, world.bodyCount) {
world.step(deltaTime)
}
// Analyze physics complexity
val complexity = PhysicsProfiler.analyzePhysicsComplexity(
bodyCount = world.bodies.size,
constraintCount = world.constraints.size,
contactCount = world.contacts.size
)
println("Physics complexity score: ${complexity.getComplexityScore()}")
println("Estimated CPU usage: ${complexity.estimateCPUUsage()}%")
PerformanceProfiler.configure(ProfilerConfig(
enabled = true, // Enable/disable profiling
trackMemory = true, // Track memory usage
frameHistorySize = 300, // Keep 300 frames in history
memoryHistorySize = 60, // Keep 60 memory snapshots
frameStatsWindow = 60, // Calculate stats over 60 frames
memorySnapshotInterval = 10, // Take memory snapshot every 10 frames
verbosity = ProfileVerbosity.NORMAL // Profiling detail level
))
MINIMAL: Only frame statistics, minimal overheadNORMAL: Frame stats + hotspot detectionDETAILED: Everything including individual measurementsTRACE: Maximum detail with memory trackingdashboard.enable(DashboardConfig(
updateIntervalMs = 1000, // Update every second
showHotspots = true, // Show performance hotspots
showMemory = true, // Show memory statistics
showFrameGraph = true, // Show frame time graph
showRecommendations = true, // Show optimization suggestions
maxHotspots = 10, // Show top 10 hotspots
maxRecommendations = 5, // Show top 5 recommendations
verbosity = ProfileVerbosity.NORMAL
))
val textReport = ProfilingReport.generateTextReport()
println(textReport)
val htmlReport = ProfilingReport.generateHtmlReport()
File("performance-report.html").writeText(htmlReport)
val json = PerformanceProfiler.export(ExportFormat.JSON)
File("profiling-data.json").writeText(json)
val csv = PerformanceProfiler.export(ExportFormat.CSV)
File("profiling-data.csv").writeText(csv)
val trace = PerformanceProfiler.export(ExportFormat.CHROME_TRACE)
File("trace.json").writeText(trace)
// Open in chrome://tracing
Materia has constitutional performance requirements:
val stats = PerformanceProfiler.getFrameStats()
// Check if meeting 60 FPS target
if (stats.meetsTargetFps(60)) {
println("✓ Meeting 60 FPS constitutional requirement")
} else {
println("✗ NOT meeting 60 FPS requirement")
println(" Average FPS: ${stats.averageFps}")
println(" 95th percentile frame time: ${stats.percentile95 / 1_000_000}ms")
}
// Get recommendations
val report = ProfilingReport.generateReport()
report.recommendations
.filter { it.severity == Severity.HIGH }
.forEach { println("⚠ ${it.message}") }
Always profile in release mode with optimizations enabled:
// Only enable profiling in debug builds if needed
if (BuildConfig.DEBUG) {
ProfilingHelpers.enableDevelopmentProfiling()
} else {
// Lightweight profiling in production
ProfilingHelpers.enableProductionProfiling()
}
Group related profiling data into sessions:
val session = ProfilingHelpers.createSession("AssetLoadingTest")
// Run scenario
loadModels()
loadTextures()
compileShaders()
val summary = session.end()
summary.printSummary()
Concentrate optimization efforts on the biggest hotspots:
val hotspots = PerformanceProfiler.getHotspots()
hotspots.filter { it.percentage > 10f }.forEach { hotspot ->
println("⚠ ${hotspot.name} consuming ${hotspot.percentage}% of frame time")
println(" Called ${hotspot.callCount} times")
println(" Average time: ${hotspot.averageTime / 1_000_000}ms")
}
Track memory usage to prevent leaks:
val memoryStats = PerformanceProfiler.getMemoryStats()
memoryStats?.let { memory ->
if (memory.trend > 10 * 1024 * 1024) { // 10MB growth
println("⚠ Memory trending upward: ${memory.trend / (1024 * 1024)}MB")
}
if (memory.gcPressure > 0.5f) {
println("⚠ High GC pressure: ${memory.gcPressure * 100}%")
}
}
Integrate profiling into your CI/CD:
@Test
fun testPerformanceRegression() {
ProfilingHelpers.enableDevelopmentProfiling()
val session = ProfilingHelpers.createSession("PerformanceTest")
// Run test scenario
repeat(100) {
renderComplexScene()
}
val summary = session.end()
// Assert performance requirements
assertTrue(summary.averageFps >= 58.0, "Must maintain near 60 FPS")
val hotspots = summary.hotspots
hotspots.forEach { hotspot ->
assertTrue(
hotspot.percentage < 30f,
"${hotspot.name} consuming too much time: ${hotspot.percentage}%"
)
}
}
fun gameLoop(deltaTime: Float) {
ProfilingHelpers.profileGameLoop(
deltaTime = deltaTime,
updatePhase = {
updatePhysics(deltaTime)
updateAnimations(deltaTime)
updateAI(deltaTime)
},
renderPhase = {
renderer.render(scene, camera)
}
)
}
ProfilingHelpers.profileSceneRender(
sceneName = "MainScene",
culling = { frustumCulling() },
sorting = { depthSort() },
drawCalls = { executeDraw() }
)
GeometryProfiler.profilePrimitiveGeneration("SphereGeometry") {
SphereGeometry(radius = 1f, segments = 32)
}
val geometry = GeometryProfiler.profileBufferUpload(bufferSize) {
uploadGeometryToGPU(geometry)
}
val hotspots = PerformanceProfiler.getHotspots()
hotspots.take(5).forEach { hotspot ->
when (hotspot.category) {
ProfileCategory.RENDERING -> println("Rendering bottleneck: ${hotspot.name}")
ProfileCategory.PHYSICS -> println("Physics bottleneck: ${hotspot.name}")
ProfileCategory.SCENE_GRAPH -> println("Scene graph bottleneck: ${hotspot.name}")
else -> println("Other bottleneck: ${hotspot.name}")
}
}
val memoryStats = PerformanceProfiler.getMemoryStats()
memoryStats?.let { memory ->
println("Current: ${memory.current / (1024 * 1024)}MB")
println("Peak: ${memory.peak / (1024 * 1024)}MB")
println("Allocation rate: ${memory.allocations / (1024 * 1024)}MB/s")
if (memory.gcPressure > 0.3f) {
println("Reduce object allocations:")
println("- Use object pooling")
println("- Reuse buffers")
println("- Avoid temporary objects in hot paths")
}
}
The profiler is designed to have minimal overhead:
Choose the appropriate verbosity level for your needs.
Use IBLConvolutionProfiler to inspect the CPU cost of irradiance and prefilter generation. The profiler records the most recent durations and sample counts:
val metrics = IBLConvolutionProfiler.snapshot()
println("Prefilter: ${metrics.prefilterMs} ms, samples=${metrics.prefilterSamples}")
println("Irradiance: ${metrics.irradianceMs} ms")
These values bubble up into RenderStats (iblCpuMs, iblPrefilterMipCount, iblLastRoughness) so the WebGPU renderer can surface lighting diagnostics without additional instrumentation.