├── .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 |  
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 | Add the Perf component anywhere in your Canvas. |
12 |
13 |
14 |  |
15 |
16 |
17 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------