import { type FC, type HTMLAttributes, useEffect, useMemo } from 'react' import { addEffect, addAfterEffect, useThree, addTail } from '@react-three/fiber' import { overLimitFps, GLPerf } from '../internal' import * as THREE from 'three' import { countGeoDrawCalls } from '../helpers/countGeoDrawCalls' import { getPerf, type ProgramsPerfs, setPerf } from '../store' import type { PerfProps } from '../types' import { emitEvent } from '@utsubo/events' // cameras from r3f-perf scene // @ts-ignore const updateMatrixWorldTemp = THREE.Object3D.prototype.updateMatrixWorld const updateWorldMatrixTemp = THREE.Object3D.prototype.updateWorldMatrix const updateMatrixTemp = THREE.Object3D.prototype.updateMatrix const maxGl = ['calls', 'triangles', 'points', 'lines'] const maxLog = ['gpu', 'cpu', 'mem', 'fps'] export let matriceWorldCount = { value: 0, } export let matriceCount = { value: 0, } const isUUID = (uuid: string) => { let s: any = '' + uuid s = s.match('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') if (s === null) { return false } return true } const addMuiPerfID = (material: THREE.Material, currentObjectWithMaterials: any) => { if (!material.defines) { material.defines = {} } if (material.defines && !material.defines.muiPerf) { material.defines = Object.assign(material.defines || {}, { muiPerf: material.uuid, }) } const uuid = material.uuid if (!currentObjectWithMaterials[uuid]) { currentObjectWithMaterials[uuid] = { meshes: {}, material: material, } material.needsUpdate = true } material.needsUpdate = false return uuid } type Chart = { data: { [index: string]: number[] } id: number circularId: number } const getMUIIndex = (muid: string) => muid === 'muiPerf' export interface Props extends HTMLAttributes {} /** * Performance profiler component */ export const PerfHeadless: FC = ({ overClock, logsPerSecond, chart, deepAnalyze, matrixUpdate }) => { const { gl, scene } = useThree() setPerf({ gl, scene }) const PerfLib = useMemo(() => { const PerfLib = new GLPerf({ trackGPU: true, overClock: overClock, chartLen: chart ? chart.length : 120, chartHz: chart ? chart.hz : 60, logsPerSecond: logsPerSecond || 10, gl: gl.getContext(), chartLogger: (chart: Chart) => { setPerf({ chart }) }, paramLogger: (logger: any) => { const log = { maxMemory: logger.maxMemory, gpu: logger.gpu, cpu: logger.cpu, mem: logger.mem, fps: logger.fps, totalTime: logger.duration, frameCount: logger.frameCount, } setPerf({ log, }) const { accumulated }: any = getPerf() const glRender: any = gl.info.render accumulated.totalFrames++ accumulated.gl.calls += glRender.calls accumulated.gl.triangles += glRender.triangles accumulated.gl.points += glRender.points accumulated.gl.lines += glRender.lines accumulated.log.gpu += logger.gpu accumulated.log.cpu += logger.cpu accumulated.log.mem += logger.mem accumulated.log.fps += logger.fps // calculate max for (let i = 0; i < maxGl.length; i++) { const key = maxGl[i] const value = glRender[key] if (value > accumulated.max.gl[key]) { accumulated.max.gl[key] = value } } for (let i = 0; i < maxLog.length; i++) { const key = maxLog[i] const value = logger[key] if (value > accumulated.max.log[key]) { accumulated.max.log[key] = value } } // TODO CONVERT TO OBJECT AND VALUE ALWAYS 0 THIS IS NOT CALL setPerf({ accumulated }) emitEvent('log', [log, gl]) }, }) // Infos const ctx = gl.getContext() let glRenderer = null let glVendor = null const rendererInfo: any = ctx.getExtension('WEBGL_debug_renderer_info') const glVersion = ctx.getParameter(ctx.VERSION) if (rendererInfo != null) { glRenderer = ctx.getParameter(rendererInfo.UNMASKED_RENDERER_WEBGL) glVendor = ctx.getParameter(rendererInfo.UNMASKED_VENDOR_WEBGL) } if (!glVendor) { glVendor = 'Unknown vendor' } if (!glRenderer) { glRenderer = ctx.getParameter(ctx.RENDERER) } setPerf({ startTime: window.performance.now(), infos: { version: glVersion, renderer: glRenderer, vendor: glVendor, }, }) const callbacks = new Map() const callbacksAfter = new Map() Object.defineProperty(THREE.Scene.prototype, 'onBeforeRender', { get() { return (...args: any) => { if (PerfLib) { PerfLib.begin('profiler') } callbacks.get(this)?.(...args) } }, set(callback) { callbacks.set(this, callback) }, configurable: true, }) Object.defineProperty(THREE.Scene.prototype, 'onAfterRender', { get() { return (...args: any) => { if (PerfLib) { PerfLib.end('profiler') } callbacksAfter.get(this)?.(...args) } }, set(callback) { callbacksAfter.set(this, callback) }, configurable: true, }) return PerfLib }, []) useEffect(() => { if (PerfLib) { PerfLib.overClock = overClock || false if (overClock === false) { setPerf({ overclockingFps: false }) overLimitFps.value = 0 overLimitFps.isOverLimit = 0 } PerfLib.chartHz = chart?.hz || 60 PerfLib.chartLen = chart?.length || 120 } }, [overClock, PerfLib, chart?.length, chart?.hz]) useEffect(() => { if (matrixUpdate) { THREE.Object3D.prototype.updateMatrixWorld = function () { if (this.matrixWorldNeedsUpdate || arguments[0] /*force*/) { matriceWorldCount.value++ } // @ts-ignore updateMatrixWorldTemp.apply(this, arguments) } THREE.Object3D.prototype.updateWorldMatrix = function () { matriceWorldCount.value++ // @ts-ignore updateWorldMatrixTemp.apply(this, arguments) } THREE.Object3D.prototype.updateMatrix = function () { matriceCount.value++ // @ts-ignore updateMatrixTemp.apply(this, arguments) } } gl.info.autoReset = false let effectSub: any = null let afterEffectSub: any = null if (!gl.info) return effectSub = addEffect(function preRafR3FPerf() { if (getPerf().paused) { setPerf({ paused: false }) } if (window.performance) { window.performance.mark('cpu-started') PerfLib.startCpuProfiling = true } matriceCount.value -= 1 matriceWorldCount.value = 0 matriceCount.value = 0 if (gl.info) { gl.info.reset() } }) afterEffectSub = addAfterEffect(function postRafR3FPerf() { if (PerfLib && !PerfLib.paused) { PerfLib.nextFrame(window.performance.now()) if (overClock && typeof window.requestIdleCallback !== 'undefined') { PerfLib.idleCbId = requestIdleCallback(PerfLib.nextFps) } } if (deepAnalyze) { const currentObjectWithMaterials: any = {} const programs: ProgramsPerfs = new Map() scene.traverse(function deepAnalyzeR3FPerf(object) { if (object instanceof THREE.Mesh || object instanceof THREE.Points) { if (object.material) { let uuid = object.material.uuid // troika generate and attach 2 materials const isTroika = Array.isArray(object.material) && object.material.length > 1 if (isTroika) { uuid = addMuiPerfID(object.material[1], currentObjectWithMaterials) } else { uuid = addMuiPerfID(object.material, currentObjectWithMaterials) } currentObjectWithMaterials[uuid].meshes[object.uuid] = object } } }) gl?.info?.programs?.forEach((program: any) => { const cacheKeySplited = program.cacheKey.split(',') const muiPerfTracker = cacheKeySplited[cacheKeySplited.findIndex(getMUIIndex) + 1] if (isUUID(muiPerfTracker) && currentObjectWithMaterials[muiPerfTracker]) { const { material, meshes } = currentObjectWithMaterials[muiPerfTracker] programs.set(muiPerfTracker, { program, material, meshes, drawCounts: { total: 0, type: 'triangle', data: [], }, expand: false, visible: true, }) } }) if (programs.size !== getPerf().programs.size) { countGeoDrawCalls(programs) setPerf({ programs: programs, triggerProgramsUpdate: getPerf().triggerProgramsUpdate++, }) } } }) return () => { if (PerfLib) { if (typeof window.cancelIdleCallback !== 'undefined') { window.cancelIdleCallback(PerfLib.idleCbId) } window.cancelAnimationFrame(PerfLib.rafId) window.cancelAnimationFrame(PerfLib.checkQueryId) } if (matrixUpdate) { THREE.Object3D.prototype.updateMatrixWorld = updateMatrixTemp } effectSub() afterEffectSub() } }, [PerfLib, gl, chart, matrixUpdate]) useEffect(() => { const unsub = addTail(function postRafTailR3FPerf() { if (PerfLib) { PerfLib.paused = true matriceCount.value = 0 matriceWorldCount.value = 0 setPerf({ paused: true, log: { maxMemory: 0, gpu: 0, mem: 0, cpu: 0, fps: 0, totalTime: 0, frameCount: 0, }, }) } return false }) return () => { unsub() } }, []) return null }