├── .codesandbox └── ci.json ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── demo ├── index.html ├── public │ └── caveman.png ├── server.mjs └── src │ ├── App.jsx │ ├── entry-client.jsx │ ├── entry-server.jsx │ ├── fire.jsx │ ├── firefly.jsx │ ├── index.css │ ├── patrick.jpeg │ └── styles.module.css ├── package.json ├── pnpm-lock.yaml ├── src ├── components │ ├── Graph.tsx │ ├── HtmlMinimal.tsx │ ├── Perf.tsx │ ├── PerfHeadless.tsx │ ├── Program.tsx │ └── TextsHighHZ.tsx ├── globals.d.ts ├── helpers │ ├── countGeoDrawCalls.ts │ └── estimateBytesUsed.ts ├── index.ts ├── internal.ts ├── roboto.woff ├── store.ts ├── styles.tsx └── types.ts ├── tsconfig.json └── vite.config.js /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "sandboxes": ["/demo/src/sandboxes/perf-minimal"] 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2 14 | 15 | - name: Install dependencies 16 | run: yarn --silent 17 | 18 | - name: Build 19 | run: yarn build 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules/ 5 | .yarn/* 6 | !.yarn/releases 7 | !.yarn/plugins 8 | !.yarn/sdks 9 | !.yarn/versions 10 | .pnp.* 11 | demo/.yarn 12 | example/.yarn 13 | 14 | # testing 15 | /coverage 16 | 17 | # production 18 | build/ 19 | dist/ 20 | .cache/ 21 | .parcel-cache/ 22 | 23 | # misc 24 | .DS_Store 25 | .env.local 26 | .env.development.local 27 | .env.test.local 28 | .env.production.local 29 | 30 | npm-debug.log* 31 | yarn-debug.log* 32 | yarn-error.log* 33 | 34 | storybook-static/ 35 | .idea 36 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "jsxBracketSameLine": true, 6 | "tabWidth": 2, 7 | "printWidth": 120 8 | } 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # R3F-Perf 2 | 3 | ## 7.2.2: 4 | 5 | - feat: Add offline support @Alkebsi 6 | 7 | ## 7.2.1: 8 | 9 | - fix: Fix TS 10 | 11 | ## 7.1.2: 12 | 13 | - feat: name html group for easier debugging (#41) @AlaricBaraou 14 | 15 | ## 7.1.1: 16 | 17 | - feat: `getReport()` now returns additional infos such as the vendor, WebGL renderer and version 18 | 19 | ## 7.1.0: 20 | 21 | - feat: New `logsPerSecond` parameter to specify the refresh rate of the logs 22 | - improvement: [GUI] draw texts only when needed (before it was based on the Raf). 23 | - fix: deleteQuery when the GPU Query Result is available 24 | 25 | ## 7.0.1: 26 | 27 | - fix: `getReport()` average log was not updated 28 | 29 | ## 7.0.0: 30 | 31 | _Complete refactor for headless mode, new CPU metric fot the loop duration in ms, switches to Vite for bundling and uses Rollup's preserveModules option to create subentry targets. Also publishes sourcemaps for debug and browser profiling._ 32 | 33 | - refactor: use Vite, preserveModules subentries #39 @CodyJasonBennett 34 | - feat: Treeshaking of the library 35 | - feat: Introduce new `Cpu` value which translates to the duration the r3f render loop takes each frame. 36 | - feat: Introducing `` which is the new lighter headless mode usable in production. See the #PerfHeadless section in the README.md 37 | - feat: A new `getReport()` method, available through the store `const getReport = usePerf(s=> s.getReport)` will give you a complete report of the average performances since when the `` or `` was mounted. 38 | - feat: `getPerf`, `setPerf` and `usePerf` are now shallowed exposed shortcuts of the internal store. 39 | - feat: New report of the current session of the user accessible via the Perf store `const { perf} = getPerf()` 40 | - feat: customData has a new `round` property define a change the float precision (0.00 by default). 41 | - deprecated: [GUI] Removed `Memory` as the value was only available on Chrome and not relevant and replaced it with `Cpu`. 42 | - deprecated: Removed unused `trackCPU` parameter 43 | - deprecated: `usePerf()` is now a `zustand` store and does not return an object anymore. 44 | - fix: texture informations in `deepAnalyze` mode where showing wrong values. 45 | 46 | ## 6.6.3: 47 | 48 | - Blend default material to NormalBlending 49 | - Max Hz is now dynamic and more solid (calculated on a 2 seconds range) 50 | 51 | ## 6.6.2: 52 | 53 | - Fixed over clocked graph fps displaying wrong value 54 | 55 | ## 6.6.1: 56 | 57 | - Made the over clocked fps monitor more precise 58 | - Improved the UI regarding over clocked UI 59 | - `overClock = false` Made the over clocked fps monitor optional, disabled by default 60 | - `overClock` and `chart` setting are now reactive 61 | 62 | ## 6.6.0: 63 | 64 | - The Fps metric is not limited anymore by the framerate of the monitor on Chrome and Firefox. 65 | 66 | ## 6.5.0: 67 | 68 | - Improved the names of the functions for debugging in the profiler (updated some anonymous functions) 69 | 70 | ## 6.4.4: 71 | 72 | - Manually update matrixworld in r3f-perf 73 | 74 | ## 6.4.3: 75 | 76 | - Allow r3f-perf to be rendered when `scene.autoUpdate = false` 77 | 78 | ## 6.4.2: 79 | 80 | - Added the ability to override the style 81 | - Fix issue GPU Monitor was losing context sometimes 82 | 83 | ## 6.0.1: 84 | 85 | - Enable jsx runtime #31 86 | - Simplify logic in (remove babel generated code + move React root creation inside effect) 87 | 88 | Thanks @alexandernanberg 89 | 90 | ## 6.0.0: 91 | 92 | - Update r3f-perf to React v18 and R3F v8 93 | - Fix an issue where the numbers were not getting displayed sometimes. 94 | 95 | ## 5.4.0: 96 | 97 | - New `minimal` option. Useful for smartphone and smaller viewports. 98 | - New `customData` option (See [README](https://github.com/utsuboco/r3f-perf)). Introducing custom data log. Can be useful for logging your physic fps for instance. 99 | - New getter setter `setCustomData()` and `getCustomData()` methods to update the customData information. 100 | - Added `/` before maxMemory. 101 | 102 | ## 5.3.2: 103 | 104 | - Fix memory leak createQuery stacking in WebGL2 context 105 | 106 | ## 5.3.1: 107 | 108 | - Fix potential memory leak when gl context is lost. 109 | 110 | ## 5.3.0: 111 | 112 | - New parameter "antialiasing", enabled by default. 113 | - Tool is now slightly transparent 114 | 115 | ## 5.2.0: 116 | 117 | - Removed the CPU monitoring as it is not relevant enough. 118 | - Fix an issue where the graphs would disappear on HMR #23. 119 | - Added new `maxMemory` information which represent the `jsHeapSizeLimit`. Requires `window.performance.memory`. 120 | - Added memory graph monitor, it represents the real-time memory usage divided by the max memory. 121 | - Replaced the dom text with 3D text using [troika-text](https://github.com/protectwise/troika/tree/master/packages/troika-3d-text) 122 | - Replaced React-Icons by [Radix-Icons](https://icons.modulz.app/). related: #26 123 | - Removed candygraph (regl), rafz, lerp dependencies 124 | - Graphs are now drawn with threejs and custom buffer attributes. 125 | - Added new parameter `deepAnalyze` in order to show further information about programs. Set to false by default. 126 | - Increased refresh rate of the logs. 127 | - The logs and the graphs are always shown even in the programs tab. 128 | - Dev: updated package to the latest dependencies. related: #27 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2023 Renaud ROHLINGER 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![npm](https://img.shields.io/npm/v/r3f-perf) ![npm](https://img.shields.io/npm/dw/r3f-perf) 2 | 3 | # R3F-Perf 4 | 5 | **[Changelog](https://github.com/utsuboco/r3f-perf/blob/main/CHANGELOG.md)** 6 | 7 | Easily monitor the performances of your @react-three/fiber application. 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 |
Add the Perf component anywhere in your Canvas. 13 | 14 |
18 | 19 | ## Installation 20 | 21 | ```bash 22 | yarn add --dev r3f-perf 23 | ``` 24 | 25 | ## Options 26 | 27 | ```jsx 28 | logsPerSecond?: 10, // Refresh rate of the logs 29 | antialias?: true, // Take a bit more performances but render the text with antialiasing 30 | overClock?: false, // Disable the limitation of the monitor refresh rate for the fps 31 | deepAnalyze?: false, // More detailed informations about gl programs 32 | showGraph?: true // show the graphs 33 | minimal?: false // condensed version with the most important informations (gpu/memory/fps/custom data) 34 | customData?: { 35 | value: 0, // initial value, 36 | name: '', // name to show 37 | round: 2, // precision of the float 38 | info: '', // additional information about the data (fps/ms for instance) 39 | } 40 | matrixUpdate?: false // count the number of time matrixWorldUpdate is called per frame 41 | chart?: { 42 | hz: 60, // graphs refresh frequency parameter 43 | length: 120, // number of values shown on the monitor 44 | } 45 | colorBlind?: false // Color blind colors for accessibility 46 | className?: '' // override CSS class 47 | style?: {} // override style 48 | position?: 'top-right'|'top-left'|'bottom-right'|'bottom-left' // quickly set the position, default is top-right 49 | ``` 50 | 51 | ## Usage 52 | 53 | ```jsx 54 | import { Canvas } from '@react-three/fiber' 55 | import { Perf } from 'r3f-perf' 56 | 57 | function App() { 58 | return ( 59 | 60 | 61 | 62 | ) 63 | } 64 | ``` 65 | 66 | #### Usage without interface : PerfHeadless 67 | 68 | [Codesandbox Example](https://codesandbox.io/s/perlin-cubes-r3f-perf-headless-mh1jl7?file=/src/App.js) 69 | 70 | ```jsx 71 | import { Canvas } from '@react-three/fiber' 72 | import { PerfHeadless, usePerf } from 'r3f-perf' 73 | 74 | const PerfHook = () => { 75 | // getPerf() is also available for non-reactive way 76 | const [gl, log, getReport] = usePerf((s) => s[(s.gl, s.log, s.getReport)]) 77 | console.log(gl, log, getReport()) 78 | return 79 | } 80 | 81 | function App() { 82 | return ( 83 | 84 | 85 | 86 | ) 87 | } 88 | ``` 89 | 90 | ## Custom Data 91 | 92 | ```jsx 93 | import { setCustomData, getCustomData } from 'r3f-perf' 94 | 95 | const UpdateCustomData = () => { 96 | // recommended to throttle to 1sec for readability 97 | useFrame(() => { 98 | setCustomData(55 + Math.random() * 5) // will update the panel with the current information 99 | }) 100 | return null 101 | } 102 | ``` 103 | 104 | ## SSR 105 | 106 | The tool work with any server side rendering framework. You can try with Next and @react-three/fiber using this starter : 107 | https://github.com/pmndrs/react-three-next 108 | 109 | ### Maintainers : 110 | 111 | - [`twitter 🐈‍⬛ @onirenaud`](https://twitter.com/onirenaud) 112 | - [`twitter @utsuboco`](https://twitter.com/utsuboco) 113 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vite App 7 | 8 | 59 | 60 |
61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /demo/public/caveman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsuboco/r3f-perf/752adc19edbcabc43fc917519c5366718fa0b9d0/demo/public/caveman.png -------------------------------------------------------------------------------- /demo/server.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import { fileURLToPath } from 'url' 4 | import express from 'express' 5 | import { createServer as createViteServer } from 'vite' 6 | 7 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 8 | 9 | async function createServer() { 10 | const app = express() 11 | 12 | // Create Vite server in middleware mode and configure the app type as 13 | // 'custom', disabling Vite's own HTML serving logic so parent server 14 | // can take control 15 | const vite = await createViteServer({ 16 | server: { middlewareMode: true }, 17 | appType: 'custom' 18 | }) 19 | 20 | // use vite's connect instance as middleware 21 | // if you use your own express router (express.Router()), you should use router.use 22 | app.use(vite.middlewares) 23 | 24 | app.use('*', async (req, res, next) => { 25 | const url = req.originalUrl 26 | 27 | try { 28 | // 1. Read index.html 29 | let template = fs.readFileSync( 30 | path.resolve(__dirname, 'index.html'), 31 | 'utf-8', 32 | ) 33 | 34 | // 2. Apply Vite HTML transforms. This injects the Vite HMR client, and 35 | // also applies HTML transforms from Vite plugins, e.g. global preambles 36 | // from @vitejs/plugin-react 37 | template = await vite.transformIndexHtml(url, template) 38 | 39 | // 3. Load the server entry. vite.ssrLoadModule automatically transforms 40 | // your ESM source code to be usable in Node.js! There is no bundling 41 | // required, and provides efficient invalidation similar to HMR. 42 | const { render } = await vite.ssrLoadModule('./src/entry-server.jsx') 43 | 44 | // 4. render the app HTML. This assumes entry-server.js's exported `render` 45 | // function calls appropriate framework SSR APIs, 46 | // e.g. ReactDOMServer.renderToString() 47 | 48 | const appHtml = await render(url) 49 | // 5. Inject the app-rendered HTML into the template. 50 | const html = template.replace(``, appHtml) 51 | 52 | // 6. Send the rendered HTML back. 53 | res.status(200).set({ 'Content-Type': 'text/html' }).end(html) 54 | } catch (e) { 55 | // If an error is caught, let Vite fix the stack trace so it maps back to 56 | // your actual source code. 57 | vite.ssrFixStacktrace(e) 58 | next(e) 59 | } 60 | }) 61 | 62 | app.listen(4000) 63 | } 64 | 65 | createServer() -------------------------------------------------------------------------------- /demo/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useEffect, useRef, useState } from 'react' 2 | import './index.css' 3 | import { Canvas, useFrame, useThree } from '@react-three/fiber' 4 | import * as THREE from 'three' 5 | import { useControls } from 'leva' 6 | 7 | import { Box, useTexture, Instances, Instance, OrbitControls } from '@react-three/drei' 8 | import { PerfHeadless, Perf, usePerf, setCustomData } from 'r3f-perf' 9 | 10 | const vertexShader = /* glsl */ ` 11 | varying vec2 vUv; 12 | uniform vec2 offset; // { "value": [0.1, 0.0], "max": [10., 10.0], "min": [-5., -5.0]} 13 | 14 | void main() { 15 | vUv = uv; 16 | vec3 pos = position; 17 | pos.xy += + offset; 18 | gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); 19 | } 20 | ` 21 | const fragmentShader = /* glsl */ ` 22 | uniform float amount; 23 | uniform sampler2D albedo; 24 | varying vec2 vUv; 25 | out vec4 glFrag; 26 | void main() { 27 | glFrag = vec4(vUv * amount, 0., 1.); 28 | } 29 | ` 30 | 31 | const MyMaterial = new THREE.ShaderMaterial({ 32 | uniforms: { 33 | amount: { value: 0.5 }, 34 | offset: { value: [0, -0.2] }, 35 | offset2: { value: new THREE.Vector4(2, 1, 2, 2) }, 36 | albedo: { value: null }, 37 | }, 38 | fragmentShader, 39 | vertexShader, 40 | glslVersion: THREE.GLSL3, 41 | }) 42 | 43 | const Bob = () => { 44 | const bob = useTexture('../caveman.png') 45 | return ( 46 | <> 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ) 55 | } 56 | 57 | const UpdateCustomData = () => { 58 | // recommended to throttle to 1sec for readability 59 | const { width } = useThree((s) => s.size) 60 | const { noUI } = useControls({ noUI: false }) 61 | 62 | const [getReport] = usePerf((s) => [s.getReport]) 63 | console.log(getReport()) 64 | 65 | useFrame(() => { 66 | setCustomData(30 + Math.random() * 5) 67 | }) 68 | 69 | return noUI ? ( 70 | 71 | ) : ( 72 | 93 | ) 94 | } 95 | 96 | const color = new THREE.Color() 97 | const randomVector = (r) => [r / 2 - Math.random() * r, r / 2 - Math.random() * r, r / 2 - Math.random() * r] 98 | const randomEuler = () => [Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI] 99 | const randomData = Array.from({ length: 2000 }, (r = 10) => ({ 100 | random: Math.random(), 101 | position: randomVector(r), 102 | rotation: randomEuler(), 103 | })) 104 | 105 | function Shoes() { 106 | const scene = useThree((s) => s.scene) 107 | const { range, autoUpdate } = useControls({ autoUpdate: false, range: { value: 800, min: 0, max: 2000, step: 10 } }) 108 | 109 | useEffect(() => { 110 | scene.matrixWorldAutoUpdate = autoUpdate 111 | scene.updateMatrixWorld() 112 | // // basically what it does 113 | // if (autoUpdate) { 114 | // scene.traverse((obj) => (obj.matrixAutoUpdate = true)) 115 | // } else { 116 | // scene.traverse((obj) => (obj.matrixAutoUpdate = false)) 117 | // } 118 | }, [scene, autoUpdate]) 119 | 120 | return ( 121 | 122 | 123 | 124 | {randomData.map((props, i) => ( 125 | 126 | ))} 127 | 128 | ) 129 | } 130 | 131 | function Shoe({ random, ...props }) { 132 | const ref = useRef() 133 | const [hovered, setHover] = useState(false) 134 | useFrame((state) => { 135 | const t = state.clock.getElapsedTime() + random * 10000 136 | ref.current.rotation.set(Math.cos(t / 4) / 2, Math.sin(t / 4) / 2, Math.cos(t / 1.5) / 2) 137 | ref.current.position.y = Math.sin(t / 1.5) / 2 138 | ref.current.scale.x = 139 | ref.current.scale.y = 140 | ref.current.scale.z = 141 | THREE.MathUtils.lerp(ref.current.scale.z, hovered ? 1.4 : 1, 0.1) 142 | ref.current.color.lerp(color.set(hovered ? 'red' : 'white'), hovered ? 1 : 0.1) 143 | }) 144 | return ( 145 | 146 | 147 | 148 | ) 149 | } 150 | 151 | export function App() { 152 | const { otherboxes, mountCanvas, aa, boxes } = useControls({ 153 | enable: true, 154 | mountCanvas: true, 155 | minimal: true, 156 | boxes: true, 157 | otherboxes: false, 158 | aa: false, 159 | }) 160 | const mat = useMemo(() => new THREE.MeshBasicMaterial({ color: 'blue' })) 161 | 162 | // const { average } = usePerf(); 163 | 164 | // average = { 165 | // fps: 0, 166 | // loop: 0, 167 | // cpu: 0, 168 | // gpu: 0, 169 | // } 170 | 171 | return true ? ( 172 | <> 173 | {/* frameloop={'demand'} */} 174 | (scene.matrixWorldAutoUpdate = false)} 181 | performance={{ min: 0.2 }} 182 | orthographic 183 | camera={{ position: [0, 0, 10], near: 1, far: 15, zoom: 50 }}> 184 | 185 | {/* 186 | 187 | 188 | 203 | Some 3D Text 204 | 205 | */} 206 | 207 | {/* */} 208 | {/* 209 | {/* 210 | 211 | */} 212 | {/* 213 | 214 | */} 215 | 216 | {/* {otherboxes && } */} 217 | {boxes && } 218 | {otherboxes && ( 219 | <> 220 | 221 | 222 | 223 | 224 | 225 | 226 | )} 227 | 228 | {/* {enable && } */} 229 | 230 | 231 | {/* */} 232 | 233 | ) : null 234 | } 235 | -------------------------------------------------------------------------------- /demo/src/entry-client.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { App } from './App'; 3 | import * as ReactDOM from 'react-dom/client'; 4 | 5 | const container = document.getElementById('root'); 6 | const root = ReactDOM.createRoot(container); 7 | 8 | root.render(); 9 | -------------------------------------------------------------------------------- /demo/src/entry-server.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOMServer from 'react-dom/server'; 3 | import { App } from './App'; 4 | 5 | export function render(url) { 6 | return ReactDOMServer.renderToString(); 7 | } 8 | -------------------------------------------------------------------------------- /demo/src/fire.jsx: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import React, { useRef, useMemo } from 'react'; 3 | import { extend, useFrame } from '@react-three/fiber'; 4 | import FireflyMaterial from './firefly'; 5 | 6 | extend({ FireflyMaterial }); 7 | 8 | export default function Fireflies({ count = 40 }) { 9 | const shader = useRef(); 10 | const [positionArray, scaleArray] = useMemo(() => { 11 | const positionArray = new Float32Array(count * 3); 12 | const scaleArray = new Float32Array(count); 13 | for (let i = 0; i < count; i++) { 14 | new THREE.Vector3( 15 | (Math.random() - 0.5) * 7, 16 | 1 + Math.random() * 1.5, 17 | (Math.random() - 0.5) * 7 18 | ).toArray(positionArray, i * 3); 19 | scaleArray[i] = Math.random(); 20 | } 21 | return [positionArray, scaleArray]; 22 | }, [count]); 23 | useFrame((state, delta) => (shader.current.time += delta / 4)); 24 | return ( 25 | 26 | 27 | 33 | 39 | 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /demo/src/firefly.jsx: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { extend } from '@react-three/fiber'; 3 | 4 | export default class FireflyMaterial extends THREE.ShaderMaterial { 5 | constructor() { 6 | super({ 7 | uniforms: { 8 | uTime: { value: 0 }, 9 | uPixelRatio: { value: 2 }, 10 | uSize: { value: 150 }, 11 | }, 12 | vertexShader: ` 13 | uniform float uPixelRatio; 14 | uniform float uSize; 15 | uniform float uTime; 16 | attribute float aScale; 17 | void main() { 18 | vec4 modelPosition = modelMatrix * vec4(position, 1.0); 19 | modelPosition.y += sin(uTime + modelPosition.x * 100.0) * aScale * 0.6; 20 | modelPosition.z += cos(uTime + modelPosition.x * 100.0) * aScale * 0.6; 21 | modelPosition.x += cos(uTime + modelPosition.x * 100.0) * aScale * 0.6; 22 | vec4 viewPosition = viewMatrix * modelPosition; 23 | vec4 projectionPostion = projectionMatrix * viewPosition; 24 | gl_Position = projectionPostion; 25 | gl_PointSize = uSize * aScale * uPixelRatio; 26 | gl_PointSize *= (1.0 / - viewPosition.z); 27 | }`, 28 | fragmentShader: ` 29 | void main() { 30 | float distanceToCenter = distance(gl_PointCoord, vec2(0.5)); 31 | float strength = 0.05 / distanceToCenter - 0.1; 32 | gl_FragColor = vec4(1.0, 0.5, 0.2, strength); 33 | }`, 34 | }); 35 | } 36 | 37 | get time() { 38 | return this.uniforms.uTime.value; 39 | } 40 | 41 | set time(v) { 42 | this.uniforms.uTime.value = v; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /demo/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | color: white; 5 | } 6 | 7 | body, 8 | #root, 9 | canvas { 10 | width: 100%; 11 | height: 100vh; 12 | } 13 | -------------------------------------------------------------------------------- /demo/src/patrick.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsuboco/r3f-perf/752adc19edbcabc43fc917519c5366718fa0b9d0/demo/src/patrick.jpeg -------------------------------------------------------------------------------- /demo/src/styles.module.css: -------------------------------------------------------------------------------- 1 | .back { 2 | position: fixed; 3 | left: 10px; 4 | bottom: 10px; 5 | z-index: 100; 6 | padding: 10px; 7 | background: #000; 8 | color: #fff; 9 | font-weight: 500; 10 | font-size: 14px; 11 | text-decoration: none; 12 | border-radius: 2px; 13 | border: 1px solid #333; 14 | } 15 | 16 | .link { 17 | color: inherit; 18 | } 19 | 20 | .linkList { 21 | display: grid; 22 | gap: 10px; 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "r3f-perf", 3 | "version": "7.2.3", 4 | "description": "Easily monitor your ThreeJS performances.", 5 | "keywords": [ 6 | "react", 7 | "three", 8 | "benchmark", 9 | "performance", 10 | "monitor", 11 | "analysis", 12 | "profiling", 13 | "r3f" 14 | ], 15 | "author": "Renaud ROHLINGER (https://github.com/RenaudRohlinger)", 16 | "homepage": "https://github.com/utsuboco/r3f-perf", 17 | "repository": "https://github.com/utsuboco/r3f-perf", 18 | "license": "MIT", 19 | "files": [ 20 | "dist/*", 21 | "src/*" 22 | ], 23 | "types": "./dist/index.d.ts", 24 | "main": "./dist/index.js", 25 | "module": "./dist/index.mjs", 26 | "exports": { 27 | ".": { 28 | "types": "./dist/index.d.ts", 29 | "require": "./dist/index.js", 30 | "import": "./dist/index.mjs" 31 | }, 32 | "./headless": { 33 | "types": "./dist/headless.d.ts", 34 | "require": "./dist/headless.js", 35 | "import": "./dist/headless.mjs" 36 | } 37 | }, 38 | "sideEffects": false, 39 | "scripts": { 40 | "dev": "vite", 41 | "dev:server": "node demo/server.mjs", 42 | "build": "vite build && tsc", 43 | "serve": "vite serve" 44 | }, 45 | "devDependencies": { 46 | "@react-three/fiber": "^8.16.1", 47 | "@types/node": "^18.19.28", 48 | "@types/react": "^17.0.80", 49 | "@types/react-dom": "^17.0.25", 50 | "@types/three": "^0.148.1", 51 | "@vitejs/plugin-react": "^3.1.0", 52 | "express": "^4.19.2", 53 | "fs": "0.0.1-security", 54 | "leva": "^0.9.35", 55 | "path": "^0.12.7", 56 | "react": "^18.2.0", 57 | "react-dom": "^18.2.0", 58 | "rollup-plugin-visualizer": "^5.12.0", 59 | "three": "^0.149.0", 60 | "typescript": "^4.9.5", 61 | "url": "^0.11.3", 62 | "vite": "^5.4.8" 63 | }, 64 | "dependencies": { 65 | "@radix-ui/react-icons": "^1.3.0", 66 | "@react-three/drei": "^9.103.0", 67 | "@stitches/react": "^1.2.8", 68 | "@utsubo/events": "^0.1.7", 69 | "zustand": "~4.5.2" 70 | }, 71 | "peerDependencies": { 72 | "@react-three/fiber": ">=8.0", 73 | "react": ">=18.0", 74 | "react-dom": ">=18.0", 75 | "three": ">=0.133" 76 | }, 77 | "peerDependenciesMeta": { 78 | "@react-three/fiber": { 79 | "optional": true 80 | }, 81 | "dom": { 82 | "optional": true 83 | }, 84 | "react-dom": { 85 | "optional": true 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/components/Graph.tsx: -------------------------------------------------------------------------------- 1 | import { type FC, useMemo, useRef } from 'react'; 2 | import { matriceCount, matriceWorldCount } from './PerfHeadless'; 3 | import { Graph, Graphpc } from '../styles'; 4 | import { PauseIcon } from '@radix-ui/react-icons'; 5 | import { Canvas, useFrame, type Viewport } from '@react-three/fiber'; 6 | import { getPerf, usePerf } from '../store'; 7 | import { colorsGraph } from './Perf'; 8 | import * as THREE from 'three'; 9 | import type { PerfUIProps } from '../types'; 10 | import { TextsHighHZ } from './TextsHighHZ'; 11 | 12 | export interface graphData { 13 | curve: THREE.SplineCurve; 14 | maxVal: number; 15 | element: string; 16 | } 17 | 18 | 19 | const ChartCurve:FC = ({colorBlind, minimal, chart= {length: 120, hz: 60}}) => { 20 | 21 | const curves: any = useMemo(() => { 22 | return { 23 | fps: new Float32Array(chart.length * 3), 24 | cpu: new Float32Array(chart.length * 3), 25 | // mem: new Float32Array(chart.length * 3), 26 | gpu: new Float32Array(chart.length * 3) 27 | } 28 | }, [chart]) 29 | 30 | const fpsRef= useRef(null) 31 | const fpsMatRef= useRef(null) 32 | const gpuRef= useRef(null) 33 | const cpuRef= useRef(null) 34 | 35 | const dummyVec3 = useMemo(() => new THREE.Vector3(0,0,0), []) 36 | const updatePoints = (element: string, factor: number = 1, ref: any, viewport: Viewport) => { 37 | let maxVal = 0; 38 | const {width: w, height: h} = viewport 39 | 40 | const chart = getPerf().chart.data[element]; 41 | if (!chart || chart.length === 0) { 42 | return 43 | } 44 | const padding = minimal ? 2 : 6 45 | const paddingTop = minimal ? 12 : 50 46 | let len = chart.length; 47 | for (let i = 0; i < len; i++) { 48 | let id = (getPerf().chart.circularId + i + 1) % len; 49 | if (chart[id] !== undefined) { 50 | if (chart[id] > maxVal) { 51 | maxVal = chart[id] * factor; 52 | } 53 | dummyVec3.set(padding + i / (len - 1) * (w - padding * 2) - w / 2, (Math.min(100, chart[id]) * factor) / 100 * (h - padding * 2 - paddingTop) - h / 2, 0) 54 | 55 | dummyVec3.toArray(ref.attributes.position.array, i * 3) 56 | } 57 | } 58 | 59 | ref.attributes.position.needsUpdate = true; 60 | }; 61 | 62 | // const [supportMemory] = useState(window.performance.memory) 63 | useFrame(function updateChartCurve({viewport}) { 64 | 65 | updatePoints('fps', 1, fpsRef.current, viewport) 66 | if (fpsMatRef.current) { 67 | fpsMatRef.current.color.set(getPerf().overclockingFps ? colorsGraph(colorBlind).overClock.toString() : `rgb(${colorsGraph(colorBlind).fps.toString()})`) 68 | } 69 | updatePoints('gpu', 5, gpuRef.current, viewport) 70 | // if (supportMemory) { 71 | updatePoints('cpu', 5, cpuRef.current, viewport) 72 | // } 73 | }) 74 | return ( 75 | <> 76 | {/* @ts-ignore */} 77 | { 78 | self.updateMatrix() 79 | matriceCount.value -= 1 80 | self.matrixWorld.copy(self.matrix) 81 | }}> 82 | 83 | 91 | 92 | 93 | 94 | {/* @ts-ignore */} 95 | { 96 | self.updateMatrix() 97 | matriceCount.value -= 1 98 | self.matrixWorld.copy(self.matrix) 99 | }}> 100 | 101 | 109 | 110 | 111 | 112 | {/* @ts-ignore */} 113 | { 114 | self.updateMatrix() 115 | matriceCount.value -= 1 116 | self.matrixWorld.copy(self.matrix) 117 | }}> 118 | 119 | 127 | 128 | 129 | 130 | 131 | ); 132 | }; 133 | 134 | export const ChartUI: FC = ({ 135 | colorBlind, 136 | chart, 137 | customData, 138 | matrixUpdate, 139 | showGraph= true, 140 | antialias= true, 141 | minimal, 142 | }) => { 143 | const canvas = useRef(undefined); 144 | 145 | const paused = usePerf((state) => state.paused); 146 | return ( 147 | 155 | { 167 | scene.traverse((obj: THREE.Object3D)=>{ 168 | //@ts-ignore 169 | obj.matrixWorldAutoUpdate=false 170 | obj.matrixAutoUpdate=false 171 | }) 172 | }} 173 | flat={true} 174 | style={{ 175 | marginBottom: `-42px`, 176 | position: 'relative', 177 | pointerEvents: 'none', 178 | background: 'transparent !important', 179 | height: `${minimal ? 37 : showGraph ? 100 : 60 }px` 180 | }} 181 | > 182 | {!paused ? ( 183 | <> 184 | 185 | 186 | {showGraph && } 191 | 192 | ) : null} 193 | 194 | {paused && ( 195 | 196 | PAUSED 197 | 198 | )} 199 | 200 | ); 201 | }; 202 | 203 | const Renderer = () =>{ 204 | 205 | useFrame(function updateR3FPerf({ gl, scene, camera }) { 206 | camera.updateMatrix() 207 | matriceCount.value -= 1 208 | camera.matrixWorld.copy(camera.matrix) 209 | camera.matrixWorldInverse.copy(camera.matrixWorld).invert(); 210 | gl.render(scene,camera) 211 | matriceWorldCount.value = 0 212 | matriceCount.value = 0 213 | }, Infinity) 214 | 215 | 216 | return null 217 | } -------------------------------------------------------------------------------- /src/components/HtmlMinimal.tsx: -------------------------------------------------------------------------------- 1 | import { useThree } from '@react-three/fiber' 2 | import React, { forwardRef, type ReactNode, useLayoutEffect, useRef, useEffect } from 'react' 3 | // @ts-ignore 4 | import { createRoot, Root } from 'react-dom/client' 5 | 6 | interface HtmlProps { 7 | portal?: React.MutableRefObject 8 | className?: string, 9 | name?: string, 10 | children?: ReactNode 11 | } 12 | 13 | const HtmlMinimal = forwardRef(({ portal, className, children, name, ...props }, ref) => { 14 | const gl = useThree(state => state.gl) 15 | const group = useRef(null) 16 | const rootRef = useRef(null) 17 | 18 | const target = portal?.current != null ? portal.current : gl.domElement.parentNode 19 | 20 | useLayoutEffect(() => { 21 | if (!group.current || !target) return 22 | 23 | const el = document.createElement('div') 24 | const root = (rootRef.current = createRoot(el)) 25 | 26 | target.appendChild(el) 27 | 28 | return () => { 29 | root.unmount() 30 | rootRef.current = null 31 | target.removeChild(el) 32 | } 33 | }, [target]) 34 | 35 | useLayoutEffect(() => { 36 | const root = rootRef.current 37 | if (!root) return 38 | root.render( 39 |
40 | {children} 41 |
42 | ) 43 | }) 44 | 45 | return 46 | }) 47 | 48 | export { HtmlMinimal } 49 | -------------------------------------------------------------------------------- /src/components/Perf.tsx: -------------------------------------------------------------------------------- 1 | import React, { type FC, useRef } from 'react' 2 | import { ChartUI } from './Graph' 3 | import { 4 | ActivityLogIcon, 5 | BarChartIcon, 6 | DotIcon, 7 | DropdownMenuIcon, 8 | ImageIcon, 9 | LapTimerIcon, 10 | LightningBoltIcon, 11 | MarginIcon, 12 | MinusIcon, 13 | RulerHorizontalIcon, 14 | TextAlignJustifyIcon, 15 | TriangleDownIcon, 16 | TriangleUpIcon, 17 | VercelLogoIcon, 18 | } from '@radix-ui/react-icons' 19 | 20 | import { HtmlMinimal } from './HtmlMinimal' 21 | import { PerfHeadless } from './PerfHeadless' 22 | 23 | import { Toggle, PerfS, PerfIContainer, PerfI, PerfB, ToggleContainer, ContainerScroll, PerfSmallI } from '../styles' 24 | import { ProgramsUI } from './Program' 25 | import { setPerf, usePerf } from '../store' 26 | import type { PerfPropsGui } from '../types' 27 | 28 | interface colors { 29 | [index: string]: string 30 | } 31 | 32 | export const colorsGraph = (colorBlind: boolean | undefined) => { 33 | const colors: colors = { 34 | overClock: `#ff6eff`, 35 | fps: colorBlind ? '100, 143, 255' : '238,38,110', 36 | cpu: colorBlind ? '254, 254, 98' : '66,226,46', 37 | gpu: colorBlind ? '254,254,254' : '253,151,31', 38 | custom: colorBlind ? '86,180,233' : '40,255,255', 39 | } 40 | return colors 41 | } 42 | 43 | const DynamicUIPerf: FC = ({ showGraph, colorBlind }) => { 44 | const overclockingFps = usePerf((s) => s.overclockingFps) 45 | const fpsLimit = usePerf((s) => s.fpsLimit) 46 | 47 | return ( 48 | 58 | FPS {overclockingFps ? `${fpsLimit}🚀` : ''} 59 | 60 | ) 61 | } 62 | 63 | const DynamicUI: FC = ({ showGraph, colorBlind, customData, minimal }) => { 64 | const gl = usePerf((state) => state.gl) 65 | 66 | return gl ? ( 67 | 68 | 69 | 70 | 78 | GPU 79 | 80 | ms 81 | 82 | 83 | 84 | 92 | CPU 93 | 94 | ms 95 | 96 | {/* 97 | 98 | Memory 105 | mb 106 | */} 107 | 108 | 109 | 110 | 111 | {!minimal && gl && ( 112 | 113 | 114 | {/* @ts-ignore */} 115 | {gl.info.render.calls === 1 ? 'call' : 'calls'} 116 | 117 | )} 118 | {!minimal && gl && ( 119 | 120 | 121 | Triangles 122 | 123 | )} 124 | {customData && ( 125 | 126 | 127 | {customData.name} 128 | {customData.info && {customData.info}} 129 | 130 | )} 131 | 132 | ) : null 133 | } 134 | 135 | const PerfUI: FC = ({ 136 | showGraph, 137 | colorBlind, 138 | deepAnalyze, 139 | customData, 140 | matrixUpdate, 141 | openByDefault, 142 | minimal, 143 | }) => { 144 | return ( 145 | <> 146 | 147 | {!minimal && ( 148 | 154 | )} 155 | 156 | ) 157 | } 158 | 159 | const InfoUI: FC = ({ matrixUpdate }) => { 160 | return ( 161 |
162 | 163 | 164 | Geometries 165 | 166 | 167 | 168 | Textures 169 | 170 | 171 | 172 | shaders 173 | 174 | 175 | 176 | Lines 177 | 178 | 179 | 180 | Points 181 | 182 | {matrixUpdate && ( 183 | 184 | 185 | Matrices 186 | 187 | )} 188 |
189 | ) 190 | } 191 | 192 | const ToggleEl = ({ tab, title, set }: any) => { 193 | const tabStore = usePerf((s: { tab: any }) => s.tab) 194 | return ( 195 | { 198 | set(true) 199 | setPerf({ tab: tab }) 200 | }}> 201 | {title} 202 | 203 | ) 204 | } 205 | const PerfThree: FC = ({ openByDefault, showGraph, deepAnalyze, matrixUpdate }) => { 206 | const [show, set] = React.useState(openByDefault) 207 | 208 | return ( 209 | 210 | 211 | {openByDefault && !deepAnalyze ? null : ( 212 | 213 | {/* */} 214 | {deepAnalyze && } 215 | {deepAnalyze && } 216 | { 218 | set(!show) 219 | }}> 220 | {show ? ( 221 | 222 | Minimize 223 | 224 | ) : ( 225 | 226 | More 227 | 228 | )} 229 | 230 | 231 | )} 232 | 233 | ) 234 | } 235 | 236 | const TabContainers = ({ show, showGraph, matrixUpdate }: any) => { 237 | const tab = usePerf((state) => state.tab) 238 | 239 | return ( 240 | <> 241 | 242 | {show && ( 243 |
244 | 245 | {tab === 'programs' && } 246 | 247 |
248 | )} 249 | 250 | ) 251 | } 252 | /** 253 | * Performance profiler component 254 | */ 255 | export const Perf: FC = ({ 256 | showGraph = true, 257 | colorBlind = false, 258 | openByDefault = true, 259 | className, 260 | overClock = false, 261 | style, 262 | position = 'top-right', 263 | chart, 264 | logsPerSecond, 265 | deepAnalyze = false, 266 | antialias = true, 267 | customData, 268 | matrixUpdate, 269 | minimal, 270 | }) => { 271 | const perfContainerRef = useRef(null) 272 | 273 | return ( 274 | <> 275 | 282 | 283 | 289 | 299 | 308 | 309 | 310 | 311 | ) 312 | } 313 | -------------------------------------------------------------------------------- /src/components/PerfHeadless.tsx: -------------------------------------------------------------------------------- 1 | import { type FC, type HTMLAttributes, useEffect, useMemo } from 'react' 2 | import { addEffect, addAfterEffect, useThree, addTail } from '@react-three/fiber' 3 | import { overLimitFps, GLPerf } from '../internal' 4 | 5 | import * as THREE from 'three' 6 | import { countGeoDrawCalls } from '../helpers/countGeoDrawCalls' 7 | import { getPerf, type ProgramsPerfs, setPerf } from '../store' 8 | import type { PerfProps } from '../types' 9 | import { emitEvent } from '@utsubo/events' 10 | 11 | // cameras from r3f-perf scene 12 | 13 | // @ts-ignore 14 | const updateMatrixWorldTemp = THREE.Object3D.prototype.updateMatrixWorld 15 | const updateWorldMatrixTemp = THREE.Object3D.prototype.updateWorldMatrix 16 | const updateMatrixTemp = THREE.Object3D.prototype.updateMatrix 17 | 18 | const maxGl = ['calls', 'triangles', 'points', 'lines'] 19 | const maxLog = ['gpu', 'cpu', 'mem', 'fps'] 20 | 21 | export let matriceWorldCount = { 22 | value: 0, 23 | } 24 | export let matriceCount = { 25 | value: 0, 26 | } 27 | 28 | const isUUID = (uuid: string) => { 29 | let s: any = '' + uuid 30 | 31 | 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}$') 32 | if (s === null) { 33 | return false 34 | } 35 | return true 36 | } 37 | 38 | const addMuiPerfID = (material: THREE.Material, currentObjectWithMaterials: any) => { 39 | if (!material.defines) { 40 | material.defines = {} 41 | } 42 | 43 | if (material.defines && !material.defines.muiPerf) { 44 | material.defines = Object.assign(material.defines || {}, { 45 | muiPerf: material.uuid, 46 | }) 47 | } 48 | 49 | const uuid = material.uuid 50 | 51 | if (!currentObjectWithMaterials[uuid]) { 52 | currentObjectWithMaterials[uuid] = { 53 | meshes: {}, 54 | material: material, 55 | } 56 | material.needsUpdate = true 57 | } 58 | material.needsUpdate = false 59 | return uuid 60 | } 61 | 62 | type Chart = { 63 | data: { 64 | [index: string]: number[] 65 | } 66 | id: number 67 | circularId: number 68 | } 69 | 70 | const getMUIIndex = (muid: string) => muid === 'muiPerf' 71 | 72 | export interface Props extends HTMLAttributes {} 73 | 74 | /** 75 | * Performance profiler component 76 | */ 77 | export const PerfHeadless: FC = ({ overClock, logsPerSecond, chart, deepAnalyze, matrixUpdate }) => { 78 | const { gl, scene } = useThree() 79 | setPerf({ gl, scene }) 80 | 81 | const PerfLib = useMemo(() => { 82 | const PerfLib = new GLPerf({ 83 | trackGPU: true, 84 | overClock: overClock, 85 | chartLen: chart ? chart.length : 120, 86 | chartHz: chart ? chart.hz : 60, 87 | logsPerSecond: logsPerSecond || 10, 88 | gl: gl.getContext(), 89 | chartLogger: (chart: Chart) => { 90 | setPerf({ chart }) 91 | }, 92 | paramLogger: (logger: any) => { 93 | const log = { 94 | maxMemory: logger.maxMemory, 95 | gpu: logger.gpu, 96 | cpu: logger.cpu, 97 | mem: logger.mem, 98 | fps: logger.fps, 99 | totalTime: logger.duration, 100 | frameCount: logger.frameCount, 101 | } 102 | setPerf({ 103 | log, 104 | }) 105 | const { accumulated }: any = getPerf() 106 | const glRender: any = gl.info.render 107 | 108 | accumulated.totalFrames++ 109 | accumulated.gl.calls += glRender.calls 110 | accumulated.gl.triangles += glRender.triangles 111 | accumulated.gl.points += glRender.points 112 | accumulated.gl.lines += glRender.lines 113 | 114 | accumulated.log.gpu += logger.gpu 115 | accumulated.log.cpu += logger.cpu 116 | accumulated.log.mem += logger.mem 117 | accumulated.log.fps += logger.fps 118 | // calculate max 119 | for (let i = 0; i < maxGl.length; i++) { 120 | const key = maxGl[i] 121 | const value = glRender[key] 122 | if (value > accumulated.max.gl[key]) { 123 | accumulated.max.gl[key] = value 124 | } 125 | } 126 | 127 | for (let i = 0; i < maxLog.length; i++) { 128 | const key = maxLog[i] 129 | const value = logger[key] 130 | if (value > accumulated.max.log[key]) { 131 | accumulated.max.log[key] = value 132 | } 133 | } 134 | 135 | // TODO CONVERT TO OBJECT AND VALUE ALWAYS 0 THIS IS NOT CALL 136 | setPerf({ accumulated }) 137 | 138 | emitEvent('log', [log, gl]) 139 | }, 140 | }) 141 | 142 | // Infos 143 | 144 | const ctx = gl.getContext() 145 | let glRenderer = null 146 | let glVendor = null 147 | 148 | const rendererInfo: any = ctx.getExtension('WEBGL_debug_renderer_info') 149 | const glVersion = ctx.getParameter(ctx.VERSION) 150 | 151 | if (rendererInfo != null) { 152 | glRenderer = ctx.getParameter(rendererInfo.UNMASKED_RENDERER_WEBGL) 153 | glVendor = ctx.getParameter(rendererInfo.UNMASKED_VENDOR_WEBGL) 154 | } 155 | 156 | if (!glVendor) { 157 | glVendor = 'Unknown vendor' 158 | } 159 | 160 | if (!glRenderer) { 161 | glRenderer = ctx.getParameter(ctx.RENDERER) 162 | } 163 | 164 | setPerf({ 165 | startTime: window.performance.now(), 166 | infos: { 167 | version: glVersion, 168 | renderer: glRenderer, 169 | vendor: glVendor, 170 | }, 171 | }) 172 | 173 | const callbacks = new Map() 174 | const callbacksAfter = new Map() 175 | Object.defineProperty(THREE.Scene.prototype, 'onBeforeRender', { 176 | get() { 177 | return (...args: any) => { 178 | if (PerfLib) { 179 | PerfLib.begin('profiler') 180 | } 181 | callbacks.get(this)?.(...args) 182 | } 183 | }, 184 | set(callback) { 185 | callbacks.set(this, callback) 186 | }, 187 | configurable: true, 188 | }) 189 | 190 | Object.defineProperty(THREE.Scene.prototype, 'onAfterRender', { 191 | get() { 192 | return (...args: any) => { 193 | if (PerfLib) { 194 | PerfLib.end('profiler') 195 | } 196 | callbacksAfter.get(this)?.(...args) 197 | } 198 | }, 199 | set(callback) { 200 | callbacksAfter.set(this, callback) 201 | }, 202 | configurable: true, 203 | }) 204 | 205 | return PerfLib 206 | }, []) 207 | 208 | useEffect(() => { 209 | if (PerfLib) { 210 | PerfLib.overClock = overClock || false 211 | if (overClock === false) { 212 | setPerf({ overclockingFps: false }) 213 | overLimitFps.value = 0 214 | overLimitFps.isOverLimit = 0 215 | } 216 | PerfLib.chartHz = chart?.hz || 60 217 | PerfLib.chartLen = chart?.length || 120 218 | } 219 | }, [overClock, PerfLib, chart?.length, chart?.hz]) 220 | 221 | useEffect(() => { 222 | if (matrixUpdate) { 223 | THREE.Object3D.prototype.updateMatrixWorld = function () { 224 | if (this.matrixWorldNeedsUpdate || arguments[0] /*force*/) { 225 | matriceWorldCount.value++ 226 | } 227 | // @ts-ignore 228 | updateMatrixWorldTemp.apply(this, arguments) 229 | } 230 | THREE.Object3D.prototype.updateWorldMatrix = function () { 231 | matriceWorldCount.value++ 232 | // @ts-ignore 233 | updateWorldMatrixTemp.apply(this, arguments) 234 | } 235 | THREE.Object3D.prototype.updateMatrix = function () { 236 | matriceCount.value++ 237 | // @ts-ignore 238 | updateMatrixTemp.apply(this, arguments) 239 | } 240 | } 241 | 242 | gl.info.autoReset = false 243 | let effectSub: any = null 244 | let afterEffectSub: any = null 245 | if (!gl.info) return 246 | 247 | effectSub = addEffect(function preRafR3FPerf() { 248 | if (getPerf().paused) { 249 | setPerf({ paused: false }) 250 | } 251 | 252 | if (window.performance) { 253 | window.performance.mark('cpu-started') 254 | PerfLib.startCpuProfiling = true 255 | } 256 | 257 | matriceCount.value -= 1 258 | matriceWorldCount.value = 0 259 | matriceCount.value = 0 260 | 261 | if (gl.info) { 262 | gl.info.reset() 263 | } 264 | }) 265 | 266 | afterEffectSub = addAfterEffect(function postRafR3FPerf() { 267 | if (PerfLib && !PerfLib.paused) { 268 | PerfLib.nextFrame(window.performance.now()) 269 | 270 | if (overClock && typeof window.requestIdleCallback !== 'undefined') { 271 | PerfLib.idleCbId = requestIdleCallback(PerfLib.nextFps) 272 | } 273 | } 274 | if (deepAnalyze) { 275 | const currentObjectWithMaterials: any = {} 276 | const programs: ProgramsPerfs = new Map() 277 | 278 | scene.traverse(function deepAnalyzeR3FPerf(object) { 279 | if (object instanceof THREE.Mesh || object instanceof THREE.Points) { 280 | if (object.material) { 281 | let uuid = object.material.uuid 282 | // troika generate and attach 2 materials 283 | const isTroika = Array.isArray(object.material) && object.material.length > 1 284 | if (isTroika) { 285 | uuid = addMuiPerfID(object.material[1], currentObjectWithMaterials) 286 | } else { 287 | uuid = addMuiPerfID(object.material, currentObjectWithMaterials) 288 | } 289 | 290 | currentObjectWithMaterials[uuid].meshes[object.uuid] = object 291 | } 292 | } 293 | }) 294 | 295 | gl?.info?.programs?.forEach((program: any) => { 296 | const cacheKeySplited = program.cacheKey.split(',') 297 | const muiPerfTracker = cacheKeySplited[cacheKeySplited.findIndex(getMUIIndex) + 1] 298 | if (isUUID(muiPerfTracker) && currentObjectWithMaterials[muiPerfTracker]) { 299 | const { material, meshes } = currentObjectWithMaterials[muiPerfTracker] 300 | programs.set(muiPerfTracker, { 301 | program, 302 | material, 303 | meshes, 304 | drawCounts: { 305 | total: 0, 306 | type: 'triangle', 307 | data: [], 308 | }, 309 | expand: false, 310 | visible: true, 311 | }) 312 | } 313 | }) 314 | 315 | if (programs.size !== getPerf().programs.size) { 316 | countGeoDrawCalls(programs) 317 | setPerf({ 318 | programs: programs, 319 | triggerProgramsUpdate: getPerf().triggerProgramsUpdate++, 320 | }) 321 | } 322 | } 323 | }) 324 | 325 | return () => { 326 | if (PerfLib) { 327 | if (typeof window.cancelIdleCallback !== 'undefined') { 328 | window.cancelIdleCallback(PerfLib.idleCbId) 329 | } 330 | window.cancelAnimationFrame(PerfLib.rafId) 331 | window.cancelAnimationFrame(PerfLib.checkQueryId) 332 | } 333 | 334 | if (matrixUpdate) { 335 | THREE.Object3D.prototype.updateMatrixWorld = updateMatrixTemp 336 | } 337 | 338 | effectSub() 339 | afterEffectSub() 340 | } 341 | }, [PerfLib, gl, chart, matrixUpdate]) 342 | 343 | useEffect(() => { 344 | const unsub = addTail(function postRafTailR3FPerf() { 345 | if (PerfLib) { 346 | PerfLib.paused = true 347 | matriceCount.value = 0 348 | matriceWorldCount.value = 0 349 | setPerf({ 350 | paused: true, 351 | log: { 352 | maxMemory: 0, 353 | gpu: 0, 354 | mem: 0, 355 | cpu: 0, 356 | fps: 0, 357 | totalTime: 0, 358 | frameCount: 0, 359 | }, 360 | }) 361 | } 362 | return false 363 | }) 364 | 365 | return () => { 366 | unsub() 367 | } 368 | }, []) 369 | 370 | return null 371 | } 372 | -------------------------------------------------------------------------------- /src/components/Program.tsx: -------------------------------------------------------------------------------- 1 | import { type FC, useEffect, useState } from 'react'; 2 | 3 | import { 4 | ProgramGeo, 5 | ProgramHeader, 6 | ProgramTitle, 7 | ToggleVisible, 8 | ProgramConsole, 9 | ProgramsUL, 10 | ProgramsULHeader, 11 | Toggle, 12 | PerfI, 13 | PerfB, 14 | ProgramsGeoLi, 15 | ProgramsContainer, 16 | } from '../styles'; 17 | import { ActivityLogIcon, ButtonIcon, CubeIcon, EyeNoneIcon, EyeOpenIcon, ImageIcon, LayersIcon, RocketIcon, TriangleDownIcon, TriangleUpIcon, VercelLogoIcon } from '@radix-ui/react-icons'; 18 | import { usePerf, type ProgramsPerf } from '../store'; 19 | import type { PerfProps } from '../types'; 20 | import { estimateBytesUsed } from '../helpers/estimateBytesUsed'; 21 | 22 | const addTextureUniforms = (id: string, texture: any) => { 23 | const repeatType = (wrap: number) => { 24 | switch (wrap) { 25 | case 1000: 26 | return 'RepeatWrapping'; 27 | case 1001: 28 | return 'ClampToEdgeWrapping'; 29 | case 1002: 30 | return 'MirroredRepeatWrapping'; 31 | default: 32 | return 'ClampToEdgeWrapping'; 33 | } 34 | }; 35 | 36 | const encodingType = (encoding: number) => { 37 | switch (encoding) { 38 | case 3000: 39 | return 'LinearEncoding'; 40 | case 3001: 41 | return 'sRGBEncoding'; 42 | case 3002: 43 | return 'RGBEEncoding'; 44 | case 3003: 45 | return 'LogLuvEncoding'; 46 | case 3004: 47 | return 'RGBM7Encoding'; 48 | case 3005: 49 | return 'RGBM16Encoding'; 50 | case 3006: 51 | return 'RGBDEncoding'; 52 | case 3007: 53 | return 'GammaEncoding'; 54 | default: 55 | return 'ClampToEdgeWrapping'; 56 | } 57 | }; 58 | return { 59 | name: id, 60 | url: texture.image.currentSrc, 61 | encoding: encodingType(texture.encoding), 62 | wrapT: repeatType(texture.wrapT), 63 | flipY: texture.flipY.toString(), 64 | }; 65 | }; 66 | 67 | const UniformsGL = ({ program, material, setTexNumber }: any) => { 68 | const gl = usePerf((state) => state.gl); 69 | const [uniforms, set] = useState(null); 70 | 71 | useEffect(() => { 72 | if (gl) { 73 | const data: any = program?.getUniforms(); 74 | let TexCount = 0; 75 | const format: any = new Map(); 76 | 77 | data.seq.forEach((e: any) => { 78 | if ( 79 | !e.id.includes('uTroika') && 80 | e.id !== 'isOrthographic' && 81 | e.id !== 'uvTransform' && 82 | e.id !== 'lightProbe' && 83 | e.id !== 'projectionMatrix' && 84 | e.id !== 'viewMatrix' && 85 | e.id !== 'normalMatrix' && 86 | e.id !== 'modelMatrix' && 87 | e.id !== 'modelViewMatrix' 88 | ) { 89 | let values: any = []; 90 | let data: any = { 91 | name: e.id, 92 | }; 93 | if (e.cache) { 94 | e.cache.forEach((v: any) => { 95 | if (typeof v !== 'undefined') { 96 | values.push(v.toString().substring(0, 4)); 97 | } 98 | }); 99 | data.value = values.join(); 100 | if (material[e.id] && material[e.id].image) { 101 | if (material[e.id].image) { 102 | TexCount++; 103 | data.value = addTextureUniforms(e.id, material[e.id]); 104 | } 105 | } 106 | if (!data.value) { 107 | data.value = 'empty'; 108 | } 109 | format.set(e.id, data); 110 | } 111 | } 112 | }); 113 | 114 | if (material.uniforms) { 115 | Object.keys(material.uniforms).forEach((key: any) => { 116 | const uniform = material.uniforms[key]; 117 | if (uniform.value) { 118 | const { value } = uniform; 119 | let data: any = { 120 | name: key, 121 | }; 122 | if (key.includes('uTroika')) { 123 | return; 124 | } 125 | if (value.isTexture) { 126 | TexCount++; 127 | data.value = addTextureUniforms(key, value); 128 | } else { 129 | let sb = JSON.stringify(value); 130 | try { 131 | sb = JSON.stringify(value); 132 | } catch (_err) { 133 | sb = value.toString(); 134 | } 135 | data.value = sb; 136 | } 137 | format.set(key, data); 138 | } 139 | }); 140 | } 141 | 142 | if (TexCount > 0) { 143 | setTexNumber(TexCount); 144 | } 145 | set(format); 146 | } 147 | }, []); 148 | 149 | return ( 150 | 151 | {uniforms && 152 | Array.from(uniforms.values()).map((uniform: any) => { 153 | return ( 154 | 155 | {typeof uniform.value === 'string' ? ( 156 |
  • 157 | 158 | {uniform.name} :{' '} 159 | 160 | {uniform.value.substring(0, 30)} 161 | {uniform.value.length > 30 ? '...' : ''} 162 | 163 | 164 |
  • 165 | ) : ( 166 | <> 167 |
  • 168 | {uniform.value.name}: 169 |
  • 170 |
    171 | {Object.keys(uniform.value).map((key) => { 172 | return key !== 'name' ? ( 173 |
    174 | {key === 'url' ? ( 175 | 176 | 177 | 178 | ) : ( 179 |
  • 180 | {key}: {uniform.value[key]} 181 |
  • 182 | )} 183 |
    184 | ) : null; 185 | })} 186 | { 188 | console.info( 189 | material[uniform.value.name] || 190 | material?.uniforms[uniform.value.name]?.value 191 | ); 192 | }} 193 | > 194 | console.info({uniform.value.name}); 195 | 196 |
    197 | 198 | )} 199 |
    200 | ); 201 | })} 202 |
    203 | ); 204 | }; 205 | type ProgramUIProps = { 206 | el: ProgramsPerf; 207 | }; 208 | 209 | const DynamicDrawCallInfo = ({ el }: any) => { 210 | usePerf((state) => state.log); 211 | const gl: any = usePerf((state) => state.gl); 212 | 213 | const getVal = (el: any) => { 214 | if (!gl) return 0; 215 | 216 | const res = 217 | Math.round( 218 | (el.drawCounts.total / 219 | (gl.info.render.triangles + 220 | gl.info.render.lines + 221 | gl.info.render.points)) * 222 | 100 * 223 | 10 224 | ) / 10; 225 | return (isFinite(res) && res) || 0; 226 | }; 227 | return ( 228 | <> 229 | {el.drawCounts.total > 0 && ( 230 | 231 | {el.drawCounts.type === 'Triangle' ? ( 232 | 233 | ) : ( 234 | 235 | )} 236 | {el.drawCounts.total} 237 | {el.drawCounts.type}s 238 | {gl && ( 239 | 242 | {el.visible && !el.material.wireframe ? getVal(el) : 0}% 243 | 244 | )} 245 | 246 | )} 247 | 248 | ); 249 | }; 250 | const ProgramUI: FC = ({ el }) => { 251 | const [showProgram, setShowProgram] = useState(el.visible); 252 | 253 | const [toggleProgram, set] = useState(el.expand); 254 | const [texNumber, setTexNumber] = useState(0); 255 | const { meshes, program, material }: any = el; 256 | 257 | return ( 258 | 259 | { 261 | el.expand = !toggleProgram; 262 | 263 | Object.keys(meshes).forEach((key) => { 264 | const mesh = meshes[key]; 265 | 266 | mesh.material.wireframe = false; 267 | }); 268 | 269 | set(!toggleProgram); 270 | }} 271 | > 272 | 273 | {toggleProgram ? ( 274 | 275 | 276 | 277 | ) : ( 278 | 279 | 280 | 281 | )} 282 | 283 | {program && ( 284 | 285 | {program.name} 286 | 287 | 288 | 289 | {Object.keys(meshes).length} 290 | {Object.keys(meshes).length > 1 ? 'users' : 'user'} 291 | 292 | {texNumber > 0 && ( 293 | 294 | {texNumber > 1 ? ( 295 | 296 | ) : ( 297 | 298 | )} 299 | {texNumber} 300 | tex 301 | 302 | )} 303 | 304 | {material.glslVersion === '300 es' && ( 305 | 306 | 307 | 300 308 | es 309 | glsl 310 | 311 | )} 312 | 313 | )} 314 | { 316 | Object.keys(meshes).forEach((key) => { 317 | const mesh = meshes[key]; 318 | mesh.material.wireframe = true; 319 | }); 320 | }} 321 | onPointerLeave={() => { 322 | Object.keys(meshes).forEach((key) => { 323 | const mesh = meshes[key]; 324 | mesh.material.wireframe = false; 325 | }); 326 | }} 327 | onClick={(e: any) => { 328 | e.stopPropagation(); 329 | 330 | Object.keys(meshes).forEach((key) => { 331 | const mesh = meshes[key]; 332 | const invert = !showProgram; 333 | mesh.visible = invert; 334 | el.visible = invert; 335 | setShowProgram(invert); 336 | }); 337 | }} 338 | > 339 | {showProgram ? : } 340 | 341 | 342 |
    345 | 346 | Uniforms: 347 | 348 | 353 | 354 | Geometries: 355 | 356 | 357 | 358 | {meshes && 359 | Object.keys(meshes).map( 360 | (key) => 361 | meshes[key] && 362 | meshes[key].geometry && ( 363 | 364 | {meshes[key].geometry.type}: 365 | {meshes[key].userData && meshes[key].userData.drawCount && ( 366 | 367 |
    368 | {meshes[key].userData.drawCount.count} 369 | {meshes[key].userData.drawCount.type}s 370 |
    371 |
    372 |
    373 | {Math.round( 374 | (estimateBytesUsed(meshes[key].geometry) / 1024) * 375 | 1000 376 | ) / 1000} 377 | Kb 378 | memory used 379 |
    380 |
    381 | )} 382 |
    383 | ) 384 | )} 385 |
    386 | { 388 | console.info(material); 389 | }} 390 | > 391 | console.info({material.type}) 392 | 393 |
    394 |
    395 | ); 396 | }; 397 | 398 | export const ProgramsUI: FC = () => { 399 | usePerf((state) => state.triggerProgramsUpdate); 400 | const programs:any = usePerf((state) => state.programs); 401 | return ( 402 | 403 | {programs && 404 | Array.from(programs.values()).map((el: any) => { 405 | if (!el) { 406 | return null; 407 | } 408 | return el ? : null; 409 | })} 410 | 411 | ); 412 | }; 413 | -------------------------------------------------------------------------------- /src/components/TextsHighHZ.tsx: -------------------------------------------------------------------------------- 1 | import { type FC, memo, Suspense, useRef } from 'react' 2 | import { matriceCount, matriceWorldCount } from './PerfHeadless' 3 | import { useThree } from '@react-three/fiber' 4 | import { Text } from '@react-three/drei' 5 | import { getPerf } from '..' 6 | import { colorsGraph } from './Perf' 7 | import * as THREE from 'three' 8 | import type { customData, PerfUIProps } from '../types' 9 | import { useEvent } from '@utsubo/events' 10 | import localFont from '../roboto.woff' 11 | 12 | interface TextHighHZProps { 13 | metric?: string 14 | colorBlind?: boolean 15 | isPerf?: boolean 16 | hasInstance?: boolean 17 | isMemory?: boolean 18 | isShadersInfo?: boolean 19 | fontSize: number 20 | round: number 21 | color?: string 22 | offsetX: number 23 | offsetY?: number 24 | customData?: customData 25 | minimal?: boolean 26 | } 27 | 28 | const TextHighHZ: FC = memo( 29 | ({ 30 | isPerf, 31 | color, 32 | colorBlind, 33 | customData, 34 | isMemory, 35 | isShadersInfo, 36 | metric, 37 | fontSize, 38 | offsetY = 0, 39 | offsetX, 40 | round, 41 | hasInstance, 42 | }) => { 43 | const { width: w, height: h } = useThree((s) => s.viewport) 44 | const fpsRef = useRef(null) 45 | const fpsInstanceRef = useRef(null) 46 | 47 | useEvent('log', function updateR3FPerfText([log, gl]) { 48 | if (!log || !fpsRef.current) return 49 | 50 | if (customData) { 51 | fpsRef.current.text = (Math.round(getPerf().customData * Math.pow(10, round)) / Math.pow(10, round)).toFixed( 52 | round 53 | ) 54 | } 55 | 56 | if (!metric) return 57 | 58 | let info = log[metric] 59 | if (isShadersInfo) { 60 | info = gl.info.programs?.length 61 | } else if (metric === 'matriceCount') { 62 | info = matriceCount.value 63 | } else if (!isPerf && gl.info.render) { 64 | const infos: any = isMemory ? gl.info.memory : gl.info.render 65 | info = infos[metric] 66 | } 67 | 68 | if (metric === 'fps') { 69 | fpsRef.current.color = getPerf().overclockingFps 70 | ? colorsGraph(colorBlind).overClock.toString() 71 | : `rgb(${colorsGraph(colorBlind).fps.toString()})` 72 | } 73 | fpsRef.current.text = (Math.round(info * Math.pow(10, round)) / Math.pow(10, round)).toFixed(round) 74 | 75 | if (hasInstance) { 76 | const infosInstance: any = gl.info.instance 77 | 78 | if (typeof infosInstance === 'undefined' && metric !== 'matriceCount') { 79 | return 80 | } 81 | 82 | let infoInstance 83 | if (metric === 'matriceCount') { 84 | infoInstance = matriceWorldCount.value 85 | } else { 86 | infoInstance = infosInstance[metric] 87 | } 88 | 89 | if (infoInstance > 0) { 90 | fpsRef.current.fontSize = fontSize / 1.15 91 | fpsInstanceRef.current.fontSize = info > 0 ? fontSize / 1.4 : fontSize 92 | 93 | fpsRef.current.position.y = h / 2 - offsetY - fontSize / 1.9 94 | fpsInstanceRef.current.text = 95 | ' ± ' + (Math.round(infoInstance * Math.pow(10, round)) / Math.pow(10, round)).toFixed(round) 96 | } else { 97 | if (fpsInstanceRef.current.text) fpsInstanceRef.current.text = '' 98 | 99 | fpsRef.current.position.y = h / 2 - offsetY - fontSize 100 | fpsRef.current.fontSize = fontSize 101 | } 102 | } 103 | matriceCount.value -= 1 104 | fpsRef.current.updateMatrix() 105 | fpsRef.current.matrixWorld.copy(fpsRef.current.matrix) 106 | }) 107 | return ( 108 | 109 | { 119 | self.updateMatrix() 120 | matriceCount.value -= 1 121 | self.matrixWorld.copy(self.matrix) 122 | }}> 123 | 0 124 | 125 | {hasInstance && ( 126 | { 136 | self.updateMatrix() 137 | matriceCount.value -= 1 138 | self.matrixWorld.copy(self.matrix) 139 | }}> 140 | 141 | 142 | )} 143 | 144 | ) 145 | } 146 | ) 147 | 148 | export const TextsHighHZ: FC = ({ colorBlind, customData, minimal, matrixUpdate }) => { 149 | // const [supportMemory] = useState(window.performance.memory) 150 | // const supportMemory = false 151 | 152 | const fontSize: number = 14 153 | return ( 154 | <> 155 | 164 | 172 | {/* */} 173 | 181 | {!minimal ? ( 182 | <> 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | {matrixUpdate && ( 191 | 200 | )} 201 | 202 | ) : null} 203 | 204 | {customData && ( 205 | 213 | )} 214 | 215 | ) 216 | } 217 | -------------------------------------------------------------------------------- /src/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.woff'; 2 | 3 | declare module '*.css' { 4 | const content: { [className: string]: string } 5 | export default content 6 | } -------------------------------------------------------------------------------- /src/helpers/countGeoDrawCalls.ts: -------------------------------------------------------------------------------- 1 | import type { drawCounts, ProgramsPerfs } from '../store' 2 | 3 | export const countGeoDrawCalls = (programs: ProgramsPerfs) => { 4 | programs.forEach((program, _pkey) => { 5 | const { meshes } = program 6 | if (!meshes) { 7 | return 8 | } 9 | let drawCounts: drawCounts = { 10 | total: 0, 11 | type: 'Triangle', 12 | data: [], 13 | } 14 | Object.keys(meshes).forEach((key) => { 15 | const mesh: any = meshes[key] 16 | const { geometry, material } = mesh 17 | 18 | let index = geometry.index 19 | const position = geometry.attributes.position 20 | 21 | if (!position) return 22 | 23 | let rangeFactor = 1 24 | 25 | if (material.wireframe === true) { 26 | rangeFactor = 0 27 | } 28 | 29 | const dataCount = index !== null ? index.count : position.count 30 | const rangeStart = geometry.drawRange.start * rangeFactor 31 | const rangeCount = geometry.drawRange.count * rangeFactor 32 | const drawStart = rangeStart 33 | const drawEnd = Math.min(dataCount, rangeStart + rangeCount) - 1 34 | let countInstanceRatio = 1 35 | const instanceCount = mesh.count || 1 36 | let type = 'Triangle' 37 | let mostDrawCalls = 0 38 | if (mesh.isMesh) { 39 | if (material.wireframe === true) { 40 | type = 'Line' 41 | countInstanceRatio = countInstanceRatio / 2 42 | } else { 43 | type = 'Triangle' 44 | countInstanceRatio = countInstanceRatio / 3 45 | } 46 | } else if (mesh.isLine) { 47 | type = 'Line' 48 | if (mesh.isLineSegments) { 49 | countInstanceRatio = countInstanceRatio / 2 50 | } else if (mesh.isLineLoop) { 51 | countInstanceRatio = countInstanceRatio 52 | } else { 53 | countInstanceRatio = countInstanceRatio - 1 54 | } 55 | } else if (mesh.isPoints) { 56 | type = 'Point' 57 | countInstanceRatio = countInstanceRatio 58 | } else if (mesh.isSprite) { 59 | type = 'Triangle' 60 | countInstanceRatio = countInstanceRatio / 3 61 | } 62 | 63 | const drawCount = Math.round(Math.max(0, drawEnd - drawStart + 1) * (countInstanceRatio * instanceCount)) 64 | 65 | if (drawCount > mostDrawCalls) { 66 | mostDrawCalls = drawCount 67 | drawCounts.type = type 68 | } 69 | drawCounts.total += drawCount 70 | drawCounts.data.push({ drawCount, type }) 71 | mesh.userData.drawCount = { 72 | type, 73 | count: drawCount, 74 | } 75 | }) 76 | program.drawCounts = drawCounts 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /src/helpers/estimateBytesUsed.ts: -------------------------------------------------------------------------------- 1 | import { BufferGeometry } from "three" 2 | 3 | /** 4 | * @param {Array} geometry 5 | * @return {number} 6 | */ 7 | export function estimateBytesUsed(geometry: BufferGeometry): number { 8 | // Return the estimated memory used by this geometry in bytes 9 | // Calculate using itemSize, count, and BYTES_PER_ELEMENT to account 10 | // for InterleavedBufferAttributes. 11 | let mem = 0 12 | for (let name in geometry.attributes) { 13 | const attr = geometry.getAttribute(name) 14 | mem += attr.count * attr.itemSize * (attr.array as any).BYTES_PER_ELEMENT 15 | } 16 | 17 | const indices = geometry.getIndex() 18 | mem += indices ? indices.count * indices.itemSize * (indices.array as any).BYTES_PER_ELEMENT : 0 19 | return mem 20 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { usePerf, getPerf, setPerf, setCustomData, getCustomData } from './store'; 2 | export { Perf } from './components/Perf'; 3 | export { PerfHeadless } from './components/PerfHeadless'; -------------------------------------------------------------------------------- /src/internal.ts: -------------------------------------------------------------------------------- 1 | import { MathUtils } from 'three' 2 | import { getPerf, setPerf } from './store' 3 | 4 | declare global { 5 | interface Window { 6 | GLPerf: any 7 | } 8 | interface Performance { 9 | memory: any 10 | } 11 | } 12 | 13 | export const overLimitFps = { 14 | value: 0, 15 | fpsLimit: 60, 16 | isOverLimit: 0, 17 | } 18 | 19 | interface LogsAccums { 20 | mem: number[] 21 | gpu: number[] 22 | cpu: number[] 23 | fps: number[] 24 | fpsFixed: number[] 25 | } 26 | 27 | const average = (arr: number[]) => arr?.reduce((a: number, b: number) => a + b, 0) / arr.length 28 | 29 | export class GLPerf { 30 | names: string[] = [''] 31 | finished: any[] = [] 32 | gl: any 33 | extension: any 34 | query: any 35 | paused: boolean = false 36 | overClock: boolean = false 37 | queryHasResult: boolean = false 38 | queryCreated: boolean = false 39 | isWebGL2: boolean = true 40 | memAccums: number[] = [] 41 | gpuAccums: number[] = [] 42 | activeAccums: boolean[] = [] 43 | logsAccums: LogsAccums = { 44 | mem: [], 45 | gpu: [], 46 | cpu: [], 47 | fps: [], 48 | fpsFixed: [], 49 | } 50 | fpsChart: number[] = [] 51 | gpuChart: number[] = [] 52 | cpuChart: number[] = [] 53 | memChart: number[] = [] 54 | paramLogger: any = () => {} 55 | glFinish: any = () => {} 56 | chartLogger: any = () => {} 57 | chartLen: number = 60 58 | logsPerSecond: number = 10 59 | maxMemory: number = 1500 60 | chartHz: number = 10 61 | startCpuProfiling: boolean = false 62 | lastCalculateFixed: number = 0 63 | chartFrame: number = 0 64 | gpuTimeProcess: number = 0 65 | chartTime: number = 0 66 | activeQueries: number = 0 67 | circularId: number = 0 68 | detected: number = 0 69 | frameId: number = 0 70 | rafId: number = 0 71 | idleCbId: number = 0 72 | checkQueryId: number = 0 73 | uuid: string | undefined = undefined 74 | currentCpu: number = 0 75 | currentMem: number = 0 76 | paramFrame: number = 0 77 | paramTime: number = 0 78 | now: any = () => {} 79 | t0: number = 0 80 | 81 | constructor(settings: object = {}) { 82 | window.GLPerf = window.GLPerf || {} 83 | 84 | Object.assign(this, settings) 85 | 86 | this.fpsChart = new Array(this.chartLen).fill(0) 87 | this.gpuChart = new Array(this.chartLen).fill(0) 88 | this.cpuChart = new Array(this.chartLen).fill(0) 89 | this.memChart = new Array(this.chartLen).fill(0) 90 | this.now = () => (window.performance && window.performance.now ? window.performance.now() : Date.now()) 91 | this.initGpu() 92 | this.is120hz() 93 | } 94 | initGpu() { 95 | this.uuid = MathUtils.generateUUID() 96 | if (this.gl) { 97 | this.isWebGL2 = true 98 | if (!this.extension) { 99 | this.extension = this.gl.getExtension('EXT_disjoint_timer_query_webgl2') 100 | } 101 | if (this.extension === null) { 102 | this.isWebGL2 = false 103 | } 104 | } 105 | } 106 | /** 107 | * 120hz device detection 108 | */ 109 | is120hz() { 110 | let n = 0 111 | const loop = (t: number) => { 112 | if (++n < 20) { 113 | this.rafId = window.requestAnimationFrame(loop) 114 | } else { 115 | this.detected = Math.ceil((1e3 * n) / (t - this.t0) / 70) 116 | window.cancelAnimationFrame(this.rafId) 117 | } 118 | if (!this.t0) this.t0 = t 119 | } 120 | this.rafId = window.requestAnimationFrame(loop) 121 | } 122 | 123 | /** 124 | * Explicit UI add 125 | * @param { string | undefined } name 126 | */ 127 | addUI(name: string) { 128 | if (this.names.indexOf(name) === -1) { 129 | this.names.push(name) 130 | this.gpuAccums.push(0) 131 | this.activeAccums.push(false) 132 | } 133 | } 134 | 135 | nextFps(d: any) { 136 | const goal = 1000 / 60 137 | const elapsed = goal - d.timeRemaining() 138 | const fps = (goal * overLimitFps.fpsLimit) / 10 / elapsed 139 | if (fps < 0) return 140 | 141 | overLimitFps.value = fps 142 | if (overLimitFps.isOverLimit < 25) { 143 | overLimitFps.isOverLimit++ 144 | } else { 145 | setPerf({ overclockingFps: true }) 146 | } 147 | } 148 | /** 149 | * Increase frameID 150 | * @param { any | undefined } now 151 | */ 152 | nextFrame(now: any) { 153 | this.frameId++ 154 | const t = now || this.now() 155 | let duration = t - this.paramTime 156 | let gpu = 0 157 | // params 158 | if (this.frameId <= 1) { 159 | this.paramFrame = this.frameId 160 | this.paramTime = t 161 | } else { 162 | if (t >= this.paramTime) { 163 | this.maxMemory = window.performance.memory ? window.performance.memory.jsHeapSizeLimit / 1048576 : 0 164 | const frameCount = this.frameId - this.paramFrame 165 | const fpsFixed = (frameCount * 1000) / duration 166 | const fps = getPerf().overclockingFps ? overLimitFps.value : fpsFixed 167 | 168 | gpu = this.isWebGL2 ? this.gpuAccums[0] : this.gpuAccums[0] / duration 169 | 170 | if (this.isWebGL2) { 171 | this.gpuAccums[0] = 0 172 | } else { 173 | Promise.all(this.finished).then(() => { 174 | this.gpuAccums[0] = 0 175 | this.finished = [] 176 | }) 177 | } 178 | 179 | this.currentMem = Math.round( 180 | window.performance && window.performance.memory ? window.performance.memory.usedJSHeapSize / 1048576 : 0 181 | ) 182 | 183 | if (window.performance && this.startCpuProfiling) { 184 | window.performance.mark('cpu-finished') 185 | const cpuMeasure = performance.measure('cpu-duration', 'cpu-started', 'cpu-finished') 186 | // fix cpuMeasure return null in ios 187 | this.currentCpu = cpuMeasure?.duration || 0; 188 | 189 | this.logsAccums.cpu.push(this.currentCpu) 190 | // make sure the measure has started and ended 191 | this.startCpuProfiling = false 192 | } 193 | 194 | this.logsAccums.mem.push(this.currentMem) 195 | this.logsAccums.fpsFixed.push(fpsFixed) 196 | this.logsAccums.fps.push(fps) 197 | this.logsAccums.gpu.push(gpu) 198 | 199 | if (this.overClock && typeof window.requestIdleCallback !== 'undefined') { 200 | if (overLimitFps.isOverLimit > 0 && fps > fpsFixed) { 201 | overLimitFps.isOverLimit-- 202 | } else if (getPerf().overclockingFps) { 203 | setPerf({ overclockingFps: false }) 204 | } 205 | } 206 | // TODO 200 to settings 207 | if (t >= this.paramTime + 1000 / this.logsPerSecond) { 208 | this.paramLogger({ 209 | cpu: average(this.logsAccums.cpu), 210 | gpu: average(this.logsAccums.gpu), 211 | mem: average(this.logsAccums.mem), 212 | fps: average(this.logsAccums.fps), 213 | duration: Math.round(duration), 214 | maxMemory: this.maxMemory, 215 | frameCount, 216 | }) 217 | 218 | this.logsAccums.mem = [] 219 | this.logsAccums.fps = [] 220 | this.logsAccums.gpu = [] 221 | this.logsAccums.cpu = [] 222 | 223 | this.paramFrame = this.frameId 224 | this.paramTime = t 225 | } 226 | 227 | if (this.overClock) { 228 | // calculate the max framerate every two seconds 229 | if (t - this.lastCalculateFixed >= 2 * 1000) { 230 | this.lastCalculateFixed = now 231 | overLimitFps.fpsLimit = Math.round(average(this.logsAccums.fpsFixed) / 10) * 100 232 | setPerf({ fpsLimit: overLimitFps.fpsLimit / 10 }) 233 | this.logsAccums.fpsFixed = [] 234 | 235 | this.paramFrame = this.frameId 236 | this.paramTime = t 237 | } 238 | } 239 | } 240 | } 241 | 242 | // chart 243 | if (!this.detected || !this.chartFrame) { 244 | this.chartFrame = this.frameId 245 | this.chartTime = t 246 | this.circularId = 0 247 | } else { 248 | const timespan = t - this.chartTime 249 | let hz = (this.chartHz * timespan) / 1e3 250 | while (--hz > 0 && this.detected) { 251 | const frameCount = this.frameId - this.chartFrame 252 | const fpsFixed = (frameCount / timespan) * 1e3 253 | const fps = getPerf().overclockingFps ? overLimitFps.value : fpsFixed 254 | this.fpsChart[this.circularId % this.chartLen] = fps 255 | // this.fpsChart[this.circularId % this.chartLen] = ((overLimitFps.isOverLimit > 0 ? overLimitFps.value : fps) / overLimitFps.fpsLimit) * 60; 256 | const memS = 1000 / this.currentMem 257 | const cpuS = this.currentCpu 258 | const gpuS = (this.isWebGL2 ? this.gpuAccums[1] * 2 : Math.round((this.gpuAccums[1] / duration) * 100)) + 4 259 | if (gpuS > 0) { 260 | this.gpuChart[this.circularId % this.chartLen] = gpuS 261 | } 262 | if (cpuS > 0) { 263 | this.cpuChart[this.circularId % this.chartLen] = cpuS 264 | } 265 | if (memS > 0) { 266 | this.memChart[this.circularId % this.chartLen] = memS 267 | } 268 | for (let i = 0; i < this.names.length; i++) { 269 | this.chartLogger({ 270 | i, 271 | data: { 272 | fps: this.fpsChart, 273 | gpu: this.gpuChart, 274 | cpu: this.cpuChart, 275 | mem: this.memChart, 276 | }, 277 | circularId: this.circularId, 278 | }) 279 | } 280 | this.circularId++ 281 | this.chartFrame = this.frameId 282 | this.chartTime = t 283 | } 284 | } 285 | } 286 | 287 | startGpu() { 288 | const gl = this.gl 289 | const ext = this.extension 290 | 291 | if (!gl || !ext) return 292 | if (this.isWebGL2) { 293 | let available = false 294 | let disjoint: any, ns: any 295 | 296 | if (this.query) { 297 | this.queryHasResult = false 298 | let query = this.query 299 | // console.log(gl.getParameter(ext.TIMESTAMP_EXT)) 300 | available = gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE) 301 | disjoint = gl.getParameter(ext.GPU_DISJOINT_EXT) 302 | 303 | if (available && !disjoint) { 304 | ns = gl.getQueryParameter(this.query, gl.QUERY_RESULT) 305 | const ms = ns * 1e-6 306 | 307 | if (available || disjoint) { 308 | // Clean up the query object. 309 | gl.deleteQuery(this.query) 310 | // Don't re-enter this polling loop. 311 | query = null 312 | } 313 | 314 | if (available && ms > 0) { 315 | // update the display if it is valid 316 | if (!disjoint) { 317 | this.activeAccums.forEach((_active: any, i: any) => { 318 | this.gpuAccums[i] = ms 319 | }) 320 | } 321 | } 322 | } 323 | } 324 | 325 | if (available || !this.query) { 326 | this.queryCreated = true 327 | this.query = gl.createQuery() 328 | 329 | gl.beginQuery(ext.TIME_ELAPSED_EXT, this.query) 330 | } 331 | } 332 | } 333 | 334 | endGpu() { 335 | // finish the query measurement 336 | const ext = this.extension 337 | const gl = this.gl 338 | 339 | if (this.isWebGL2 && this.queryCreated && gl.getQuery(ext.TIME_ELAPSED_EXT, gl.CURRENT_QUERY)) { 340 | gl.endQuery(ext.TIME_ELAPSED_EXT) 341 | } 342 | } 343 | 344 | /** 345 | * Begin named measurement 346 | * @param { string | undefined } name 347 | */ 348 | begin(name: string) { 349 | this.startGpu() 350 | this.updateAccums(name) 351 | } 352 | 353 | /** 354 | * End named measure 355 | * @param { string | undefined } name 356 | */ 357 | end(name: string) { 358 | this.endGpu() 359 | this.updateAccums(name) 360 | } 361 | 362 | updateAccums(name: string) { 363 | let nameId = this.names.indexOf(name) 364 | if (nameId === -1) { 365 | nameId = this.names.length 366 | this.addUI(name) 367 | } 368 | 369 | const t = this.now() 370 | 371 | this.activeAccums[nameId] = !this.activeAccums[nameId] 372 | this.t0 = t 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /src/roboto.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsuboco/r3f-perf/752adc19edbcabc43fc917519c5366718fa0b9d0/src/roboto.woff -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import { createWithEqualityFn } from 'zustand/traditional' 2 | import { shallow } from 'zustand/shallow' 3 | import * as THREE from 'three' 4 | 5 | type drawCount = { 6 | type: string 7 | drawCount: number 8 | } 9 | export type drawCounts = { 10 | total: number 11 | type: string 12 | data: drawCount[] 13 | } 14 | 15 | export type ProgramsPerf = { 16 | meshes?: { 17 | [index: string]: THREE.Mesh[] 18 | } 19 | material: THREE.Material 20 | program?: WebGLProgram 21 | visible: boolean 22 | drawCounts: drawCounts 23 | expand: boolean 24 | } 25 | 26 | type Logger = { 27 | i: number 28 | maxMemory: number 29 | gpu: number 30 | mem: number 31 | cpu: number 32 | fps: number 33 | duration: number 34 | frameCount: number 35 | } 36 | 37 | type GLLogger = { 38 | calls: number 39 | triangles: number 40 | points: number 41 | lines: number 42 | counts: number 43 | } 44 | 45 | export type State = { 46 | getReport: () => any 47 | log: any 48 | paused: boolean 49 | overclockingFps: boolean 50 | fpsLimit: number 51 | startTime: number 52 | triggerProgramsUpdate: number 53 | customData: number 54 | accumulated: { 55 | totalFrames: number 56 | log: Logger 57 | gl: GLLogger 58 | max: { 59 | log: Logger 60 | gl: GLLogger 61 | } 62 | } 63 | chart: { 64 | data: { 65 | [index: string]: number[] 66 | } 67 | circularId: number 68 | } 69 | infos: { 70 | version: string 71 | renderer: string 72 | vendor: string 73 | } 74 | gl: THREE.WebGLRenderer | undefined 75 | scene: THREE.Scene | undefined 76 | programs: ProgramsPerfs 77 | objectWithMaterials: THREE.Mesh[] | null 78 | tab: 'infos' | 'programs' | 'data' 79 | } 80 | 81 | export type ProgramsPerfs = Map 82 | 83 | const setCustomData = (customData: number) => { 84 | setPerf({ customData }) 85 | } 86 | const getCustomData = () => { 87 | return getPerf().customData 88 | } 89 | 90 | export const usePerfImpl = createWithEqualityFn((set, get): any => { 91 | function getReport() { 92 | const { accumulated, startTime, infos } = get() 93 | const maxMemory = get().log?.maxMemory 94 | const { totalFrames, log, gl, max } = accumulated 95 | 96 | const glAverage = { 97 | calls: gl.calls / totalFrames, 98 | triangles: gl.triangles / totalFrames, 99 | points: gl.points / totalFrames, 100 | lines: gl.lines / totalFrames, 101 | } 102 | 103 | const logAverage = { 104 | gpu: log.gpu / totalFrames, 105 | cpu: log.cpu / totalFrames, 106 | mem: log.mem / totalFrames, 107 | fps: log.fps / totalFrames, 108 | } 109 | 110 | const sessionTime = (window.performance.now() - startTime) / 1000 111 | 112 | return { 113 | sessionTime, 114 | infos, 115 | log: logAverage, 116 | gl: glAverage, 117 | max, 118 | maxMemory, 119 | totalFrames, 120 | } 121 | } 122 | 123 | return { 124 | log: null, 125 | paused: false, 126 | triggerProgramsUpdate: 0, 127 | startTime: 0, 128 | customData: 0, 129 | fpsLimit: 60, 130 | overclockingFps: false, 131 | accumulated: { 132 | totalFrames: 0, 133 | gl: { 134 | calls: 0, 135 | triangles: 0, 136 | points: 0, 137 | lines: 0, 138 | counts: 0, 139 | }, 140 | log: { 141 | gpu: 0, 142 | cpu: 0, 143 | mem: 0, 144 | fps: 0, 145 | }, 146 | max: { 147 | gl: { 148 | calls: 0, 149 | triangles: 0, 150 | points: 0, 151 | lines: 0, 152 | counts: 0, 153 | }, 154 | log: { 155 | gpu: 0, 156 | cpu: 0, 157 | mem: 0, 158 | fps: 0, 159 | }, 160 | }, 161 | }, 162 | chart: { 163 | data: { 164 | fps: [], 165 | cpu: [], 166 | gpu: [], 167 | mem: [], 168 | }, 169 | circularId: 0, 170 | }, 171 | gl: undefined, 172 | objectWithMaterials: null, 173 | scene: undefined, 174 | programs: new Map(), 175 | sceneLength: undefined, 176 | tab: 'infos', 177 | getReport, 178 | } 179 | }) 180 | 181 | const usePerf = (sel: (state: State) => S) => usePerfImpl(sel, shallow) 182 | Object.assign(usePerf, usePerfImpl) 183 | const { getState: getPerf, setState: setPerf } = usePerfImpl 184 | 185 | export { usePerf, getPerf, setPerf, setCustomData, getCustomData } 186 | -------------------------------------------------------------------------------- /src/styles.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@stitches/react'; 2 | 3 | export const PerfS = styled('div', { 4 | position: 'fixed', 5 | top: 0, 6 | right: 0, 7 | zIndex: 9999, 8 | fontFamily: `-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 9 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 10 | sans-serif`, 11 | backgroundColor: 'rgba(36, 36, 36, .9)', 12 | color: '#fff', 13 | margin: 0, 14 | minHeight: '100px', 15 | padding: '4px 0', 16 | '-webkit-font-smoothing': 'antialiased', 17 | '-moz-osx-font-smoothing': 'grayscale', 18 | userSelect: 'none', 19 | '&.top-left': { 20 | right: 'initial', 21 | left: 0, 22 | }, 23 | '&.bottom-left': { 24 | right: 'initial', 25 | top: 'initial', 26 | bottom: 0, 27 | left: 0, 28 | '.__perf_toggle': { 29 | top: '-20px', 30 | bottom: 'initial', 31 | }, 32 | }, 33 | '&.bottom-right': { 34 | top: 'initial', 35 | bottom: 0, 36 | '.__perf_toggle': { 37 | top: '-20px', 38 | bottom: 'initial', 39 | }, 40 | }, 41 | '&.minimal': { 42 | backgroundColor: 'rgba(36, 36, 36, .75)', 43 | }, 44 | '*': { 45 | margin: '0', 46 | padding: '0', 47 | border: '0', 48 | fontSize: '100%', 49 | lineHeight: '1', 50 | verticalAlign: 'baseline', 51 | }, 52 | }); 53 | 54 | export const PerfSmallI = styled('small', { 55 | position: 'absolute', 56 | right: 0, 57 | fontSize: '10px' 58 | }) 59 | 60 | export const PerfI = styled('div', { 61 | display: 'inline-flex', 62 | fontStyle: 'normal', 63 | padding: 0, 64 | lineHeight: '13px', 65 | fontSize: '14px', 66 | width: '62px', 67 | position: 'relative', 68 | pointerEvents: 'auto', 69 | cursor: 'default', 70 | fontWeight: 500, 71 | letterSpacing: '0px', 72 | textAlign: 'left', 73 | height: '29px', 74 | whiteSpace: 'nowrap', 75 | justifyContent: 'space-evenly', 76 | fontVariantNumeric: 'tabular-nums', 77 | small: { 78 | paddingLeft: '12px', 79 | }, 80 | svg: { 81 | padding: 0, 82 | color: 'rgba(145, 145, 145, 0.3)', 83 | fontSize: '40px', 84 | position: 'absolute', 85 | zIndex: 1, 86 | maxHeight: '20px', 87 | left: ' 50%', 88 | marginLeft: '-23px', 89 | top: '4px', 90 | }, 91 | }); 92 | 93 | export const PerfB = styled('span', { 94 | verticalAlign: 'bottom', 95 | position: 'absolute', 96 | bottom: '5px', 97 | color: 'rgba(101, 197, 188, 1)', 98 | textAlign: 'right', 99 | letterSpacing: '1px', 100 | fontSize: '8px', 101 | fontWeight: '500', 102 | width: '60px', 103 | }); 104 | 105 | export const PerfIContainer = styled('div', { 106 | display: 'flex', 107 | // justifyContent: 'space-between', 108 | }); 109 | 110 | export const ProgramHeader = styled('div', { 111 | backgroundColor: '#404040', 112 | padding: '6px', 113 | display: 'block', 114 | fontSize: '12px', 115 | marginBottom: '6px', 116 | cursor: 'pointer', 117 | '*': { 118 | cursor: 'pointer !important', 119 | }, 120 | '> span': {}, 121 | small: { 122 | fontSize: '9px', 123 | }, 124 | '> b': { 125 | marginRight: '4px', 126 | cursor: 'pointer', 127 | }, 128 | }); 129 | 130 | export const Graph = styled('div', { 131 | height: '66px', 132 | overflow: 'hidden', 133 | position: 'absolute', 134 | pointerEvents: 'none', 135 | display: 'flex', 136 | top: '0px', 137 | justifyContent: 'center', 138 | width: '100%', 139 | minWidth: '310px', 140 | margin: '0 auto', 141 | canvas: { 142 | background: 'transparent !important', 143 | position: 'absolute !important', 144 | } 145 | }); 146 | 147 | export const Graphpc = styled('div', { 148 | textAlign: 'center', 149 | fontWeight: 700, 150 | fontSize: '12px', 151 | lineHeight: '12px', 152 | display: 'flex', 153 | justifyContent: 'center', 154 | alignItems: 'center', 155 | verticalAlign: 'middle', 156 | color: '#f1f1f1', 157 | padding: '7px', 158 | width: '100%', 159 | backgroundColor: 'rgba(36, 36, 37, 0.8)', 160 | zIndex: 1, 161 | position: 'absolute', 162 | height: '100%', 163 | }); 164 | 165 | export const Toggle = styled('div', { 166 | pointerEvents: 'auto', 167 | justifyContent: 'center', 168 | cursor: 'pointer', 169 | fontSize: '12px', 170 | backgroundColor: 'rgb(41, 43, 45)', 171 | marginTop: '6px', 172 | width: 'auto', 173 | margin: '0', 174 | color: 'rgba(145, 145, 145, 1)', 175 | textAlign: 'center', 176 | display: 'inline-block', 177 | verticalAlign: 'middle', 178 | padding: '4px 6px', 179 | '&.__perf_toggle_tab_active': { 180 | backgroundColor: 'rgb(31 31 31)', 181 | }, 182 | svg: { 183 | width: '12px', 184 | height: '12px', 185 | float: 'left', 186 | }, 187 | }); 188 | 189 | export const ToggleVisible = styled('div', { 190 | pointerEvents: 'auto', 191 | justifyContent: 'center', 192 | cursor: 'pointer', 193 | fontSize: '12px', 194 | float: 'right', 195 | backgroundColor: 'rgb(41, 43, 45)', 196 | width: 'auto', 197 | margin: '0', 198 | color: 'rgba(145, 145, 145, 1)', 199 | textAlign: 'center', 200 | display: 'inline-block', 201 | verticalAlign: 'middle', 202 | padding: '4px 6px', 203 | '&.__perf_toggle_tab_active': { 204 | backgroundColor: 'rgb(31 31 31)', 205 | }, 206 | svg: { 207 | width: '12px', 208 | height: '12px', 209 | float: 'left', 210 | }, 211 | }); 212 | 213 | export const ProgramGeo = styled('div', { 214 | padding: '4px 6px', 215 | fontSize: '12px', 216 | pointerEvents: 'auto', 217 | }); 218 | 219 | export const ProgramTitle = styled('span', { 220 | fontWeight: 'bold', 221 | letterSpacing: '0.08em', 222 | maxWidth: '145px', 223 | overflow: 'hidden', 224 | textOverflow: 'ellipsis', 225 | display: 'inline-block', 226 | verticalAlign: 'middle', 227 | fontSize: '11px', 228 | marginRight: '10px', 229 | }); 230 | 231 | export const ContainerScroll = styled('div', { 232 | maxHeight: '50vh', 233 | overflowY: 'auto', 234 | marginTop: '38px' 235 | }); 236 | export const ProgramsContainer = styled('div', { 237 | marginTop: '0' 238 | }); 239 | 240 | export const ProgramsULHeader = styled('div', { 241 | display: 'flex', 242 | position: 'relative', 243 | fontWeight: 'bold', 244 | color: '#fff', 245 | lineHeight: '14px', 246 | svg: { 247 | marginRight: '4px', 248 | display: 'inline-block', 249 | }, 250 | }); 251 | 252 | export const ProgramsUL = styled('ul', { 253 | display: 'block', 254 | position: 'relative', 255 | paddingLeft: '10px', 256 | margin: '6px 6px', 257 | img: { 258 | maxHeight: '60px', 259 | maxWidth: '100%', 260 | margin: '6px auto', 261 | display: 'block', 262 | }, 263 | '&:after': { 264 | content: '', 265 | position: 'absolute', 266 | left: '0px', 267 | top: '0px', 268 | width: '1px', 269 | height: '100%', 270 | backgroundColor: 'grey', 271 | transform: 'translateX(-50%)', 272 | maxHeight: '50vh', 273 | overflowY: 'auto', 274 | }, 275 | li: { 276 | borderBottom: '1px solid #313131', 277 | display: 'block', 278 | padding: '4px', 279 | margin: 0, 280 | lineHeight: 1, 281 | verticalAlign: 'middle', 282 | height: '24px', 283 | }, 284 | b: { 285 | fontWeight: 'bold', 286 | }, 287 | small: { 288 | textAlign: 'revert', 289 | letterSpacing: '1px', 290 | fontSize: '10px', 291 | fontWeight: '500', 292 | marginLeft: '2px', 293 | color: 'rgb(101, 197, 188)', 294 | }, 295 | }); 296 | 297 | export const ProgramConsole = styled('button', { 298 | fontWeight: 'bold', 299 | letterSpacing: '0.02em', 300 | backgroundColor: 'rgb(41, 43, 45)', 301 | color: 'rgb(211, 211, 211)', 302 | overflow: 'hidden', 303 | textOverflow: 'ellipsis', 304 | cursor: 'pointer', 305 | display: 'block', 306 | verticalAlign: 'middle', 307 | fontSize: '11px', 308 | padding: '5px', 309 | margin: '4px auto', 310 | }); 311 | export const ToggleContainer = styled('div', { 312 | display: 'flex', 313 | justifyContent: 'center', 314 | cursor: 'pointer', 315 | fontSize: '12px', 316 | backgroundColor: 'rgb(41, 43, 45)', 317 | marginTop: '6px', 318 | width: 'auto', 319 | margin: '0 auto', 320 | color: 'rgba(145, 145, 145, 1)', 321 | textAlign: 'center', 322 | position: 'absolute', 323 | right: 0, 324 | bottom: ' -20px', 325 | svg: { 326 | width: '12px', 327 | height: '12px', 328 | float: 'left', 329 | }, 330 | }); 331 | 332 | export const ProgramsGeoLi = styled('li', { 333 | display: 'flex !important', 334 | height: 'auto !important', 335 | span: { 336 | height: '40px', 337 | display: 'block', 338 | position: 'relative', 339 | }, 340 | b: { 341 | paddingLeft: '12px', 342 | }, 343 | }); 344 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { HTMLAttributes } from 'react' 2 | 3 | export type chart = { 4 | length: number 5 | hz: number 6 | } 7 | 8 | export type customData = { 9 | name: number 10 | info: number 11 | value: number 12 | round: number 13 | } 14 | export interface PerfProps { 15 | logsPerSecond?: number 16 | overClock?: boolean 17 | matrixUpdate?: boolean 18 | customData?: customData 19 | chart?: chart 20 | deepAnalyze?: boolean 21 | } 22 | 23 | export interface PerfPropsGui extends PerfProps { 24 | showGraph?: boolean 25 | colorBlind?: boolean 26 | antialias?: boolean 27 | openByDefault?: boolean 28 | position?: string 29 | minimal?: boolean 30 | className?: string 31 | style?: object 32 | } 33 | 34 | export interface PerfUIProps extends HTMLAttributes { 35 | perfContainerRef?: any 36 | colorBlind?: boolean 37 | showGraph?: boolean 38 | antialias?: boolean 39 | chart?: chart 40 | customData?: customData 41 | minimal?: boolean 42 | matrixUpdate?: boolean 43 | } 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "target": "es6", 5 | "module": "ESNext", 6 | "lib": ["ESNext", "dom"], 7 | "moduleResolution": "node", 8 | "esModuleInterop": true, 9 | "jsx": "react-jsx", 10 | "pretty": true, 11 | "strict": true, 12 | "skipLibCheck": true, 13 | "declaration": true, 14 | "emitDeclarationOnly": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "paths": { 17 | "r3f-perf": ["./src"], 18 | "r3f-perf/*": ["./src/*"] 19 | } 20 | }, 21 | "include": ["src/**/*"] 22 | } 23 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import * as path from 'node:path' 3 | import react from '@vitejs/plugin-react' 4 | // import visualizer from 'rollup-plugin-visualizer' 5 | 6 | const entries = ['./src/index.ts'] 7 | 8 | export default defineConfig({ 9 | root: process.argv[2] ? undefined : 'demo', 10 | resolve: { 11 | alias: { 12 | 'r3f-perf': path.resolve(__dirname, './src'), 13 | }, 14 | }, 15 | server: { 16 | port: 4000, 17 | open: true 18 | }, 19 | build: { 20 | minify: false, 21 | sourcemap: true, 22 | target: 'es2018', 23 | lib: { 24 | formats: ['es', 'cjs'], 25 | entry: entries[0], 26 | fileName: '[name]', 27 | }, 28 | rollupOptions: { 29 | // plugins: [ 30 | // visualizer({ 31 | // emitFile: false, 32 | // open: true, 33 | // sourcemap: true 34 | // }), 35 | // ], 36 | external: (id) => !id.startsWith('.') && !path.isAbsolute(id), 37 | treeshake: false, 38 | input: entries, 39 | output: { 40 | preserveModules: true, 41 | preserveModulesRoot: 'src', 42 | sourcemapExcludeSources: true, 43 | }, 44 | }, 45 | }, 46 | plugins: [react()], 47 | }) 48 | --------------------------------------------------------------------------------