debug: add FPS counter (#2558)

Adds an FPS counter to detect when there's a UI slowdown.
(btw, drive-by typo fix for a file)


https://github.com/tldraw/tldraw/assets/469604/b83d4b10-35d9-4584-af46-c63b5cc107ac

### Change Type

- [ ] `patch` — Bug fix
- [x] `minor` — New feature
- [ ] `major` — Breaking change
- [ ] `dependencies` — Changes to package dependencies[^1]
- [ ] `documentation` — Changes to the documentation only[^2]
- [ ] `tests` — Changes to any test code only[^2]
- [ ] `internal` — Any other changes that don't affect the published
package[^2]
- [ ] I don't know

[^1]: publishes a `patch` release, for devDependencies use `internal`
[^2]: will not publish a new version

### Test Plan

1. 

- [ ] Unit Tests
- [ ] End to end tests

### Release Notes

- Adds FPS counter to debug panel.

---------

Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
Mime Čuvalo 2024-01-24 12:23:26 +00:00 committed by GitHub
parent 3e50d66f33
commit 014a95cf51
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 86 additions and 2 deletions

View file

@ -2,7 +2,7 @@ import classNames from 'classnames'
import { ForwardedRef, forwardRef, useEffect, useId, useLayoutEffect, useRef } from 'react' import { ForwardedRef, forwardRef, useEffect, useId, useLayoutEffect, useRef } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { Example } from '../examples' import { Example } from '../examples'
import { useMergedRefs } from '../hooks/useMegedRefs' import { useMergedRefs } from '../hooks/useMergedRefs'
import { StandaloneIcon } from './Icons' import { StandaloneIcon } from './Icons'
import { Markdown } from './Markdown' import { Markdown } from './Markdown'

View file

@ -421,6 +421,7 @@ export const debugFlags: {
pointerCaptureTrackingObject: DebugFlag<Map<Element, number>>; pointerCaptureTrackingObject: DebugFlag<Map<Element, number>>;
elementRemovalLogging: DebugFlag<boolean>; elementRemovalLogging: DebugFlag<boolean>;
debugSvg: DebugFlag<boolean>; debugSvg: DebugFlag<boolean>;
showFps: DebugFlag<boolean>;
throwToBlob: DebugFlag<boolean>; throwToBlob: DebugFlag<boolean>;
logMessages: DebugFlag<any[]>; logMessages: DebugFlag<any[]>;
resetConnectionEveryPing: DebugFlag<boolean>; resetConnectionEveryPing: DebugFlag<boolean>;

View file

@ -38,6 +38,9 @@ export const debugFlags = {
debugSvg: createDebugValue('debugSvg', { debugSvg: createDebugValue('debugSvg', {
defaults: { all: false }, defaults: { all: false },
}), }),
showFps: createDebugValue('showFps', {
defaults: { all: false },
}),
throwToBlob: createDebugValue('throwToBlob', { throwToBlob: createDebugValue('throwToBlob', {
defaults: { all: false }, defaults: { all: false },
}), }),

View file

@ -575,7 +575,7 @@
width: 100%; width: 100%;
display: grid; display: grid;
align-items: center; align-items: center;
grid-template-columns: 1fr auto auto; grid-template-columns: 1fr auto auto auto;
justify-content: space-between; justify-content: space-between;
padding-left: var(--space-4); padding-left: var(--space-4);
border-top: 1px solid var(--color-background); border-top: 1px solid var(--color-background);
@ -589,6 +589,15 @@
white-space: nowrap; white-space: nowrap;
} }
.tlui-debug-panel__fps {
margin-right: 8px;
}
.tlui-debug-panel__fps__slow {
font-weight: bold;
color: var(--color-warn);
}
/* -------------------- Menu Zone ------------------- */ /* -------------------- Menu Zone ------------------- */
.tlui-menu-zone { .tlui-menu-zone {

View file

@ -47,9 +47,12 @@ export const DebugPanel = React.memo(function DebugPanel({
renderDebugMenuItems: (() => React.ReactNode) | null renderDebugMenuItems: (() => React.ReactNode) | null
}) { }) {
const msg = useTranslation() const msg = useTranslation()
const showFps = useValue('show_fps', () => debugFlags.showFps.get(), [debugFlags])
return ( return (
<div className="tlui-debug-panel"> <div className="tlui-debug-panel">
<CurrentState /> <CurrentState />
{showFps && <FPS />}
<ShapeCount /> <ShapeCount />
<DropdownMenu.Root id="debug"> <DropdownMenu.Root id="debug">
<DropdownMenu.Trigger> <DropdownMenu.Trigger>
@ -68,6 +71,73 @@ const CurrentState = track(function CurrentState() {
return <div className="tlui-debug-panel__current-state">{editor.getPath()}</div> return <div className="tlui-debug-panel__current-state">{editor.getPath()}</div>
}) })
function FPS() {
const fpsRef = React.useRef<HTMLDivElement>(null)
React.useEffect(() => {
const TICK_LENGTH = 250
let maxKnownFps = 0
let cancelled = false
let start = performance.now()
let currentTickLength = 0
let framesInCurrentTick = 0
let isSlow = false
// A "tick" is the amount of time between renders. Even though
// we'll loop on every frame, we will only paint when the time
// since the last paint is greater than the tick length.
// When we paint, we'll calculate the FPS based on the number
// of frames that we've seen since the last time we rendered,
// and the actual time since the last render.
function loop() {
if (cancelled) return
// Count the frame
framesInCurrentTick++
// Check if we should render
currentTickLength = performance.now() - start
if (currentTickLength > TICK_LENGTH) {
// Calculate the FPS and paint it
const fps = Math.round(
framesInCurrentTick * (TICK_LENGTH / currentTickLength) * (1000 / TICK_LENGTH)
)
if (fps > maxKnownFps) {
maxKnownFps = fps
}
const slowFps = maxKnownFps * 0.75
if ((fps < slowFps && !isSlow) || (fps >= slowFps && isSlow)) {
isSlow = !isSlow
}
fpsRef.current!.innerHTML = `FPS ${fps.toString()}`
fpsRef.current!.className =
`tlui-debug-panel__fps` + (isSlow ? ` tlui-debug-panel__fps__slow` : ``)
// Reset the values
currentTickLength -= TICK_LENGTH
framesInCurrentTick = 0
start = performance.now()
}
requestAnimationFrame(loop)
}
loop()
return () => {
cancelled = true
}
}, [])
return <div ref={fpsRef} />
}
const ShapeCount = function ShapeCount() { const ShapeCount = function ShapeCount() {
const editor = useEditor() const editor = useEditor()
const count = useValue('rendering shapes count', () => editor.getRenderingShapes().length, [ const count = useValue('rendering shapes count', () => editor.getRenderingShapes().length, [
@ -249,6 +319,7 @@ const DebugMenuContent = track(function DebugMenuContent({
</DropdownMenu.Group> </DropdownMenu.Group>
<DropdownMenu.Group> <DropdownMenu.Group>
<DebugFlagToggle flag={debugFlags.debugSvg} /> <DebugFlagToggle flag={debugFlags.debugSvg} />
<DebugFlagToggle flag={debugFlags.showFps} />
<DebugFlagToggle flag={debugFlags.forceSrgb} /> <DebugFlagToggle flag={debugFlags.forceSrgb} />
<DebugFlagToggle flag={debugFlags.debugGeometry} /> <DebugFlagToggle flag={debugFlags.debugGeometry} />
<DebugFlagToggle flag={debugFlags.hideShapes} /> <DebugFlagToggle flag={debugFlags.hideShapes} />