From 31df1db74fd6c9109fb883e5f3e1a43355bd5870 Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Mon, 27 Jan 2020 07:32:34 -0800 Subject: [PATCH] introduce FPS graph to visualize slow UIs Summary: This diff creates a small FPS graph to be able to see where we slow down the app. This visualizes two things 1. The amount of FPS we render at (from tracking.fps). 2. If we were not able to render at all (due to the main thread being blocked fully), we interpolate the graph and draw it in red. Reviewed By: nikoant Differential Revision: D19579115 fbshipit-source-id: 2421d724c6d514986759bc9d68b92a5e4f51e401 --- src/chrome/FpsGraph.tsx | 90 +++++++++++++++++++++++++++++++++++++ src/chrome/TitleBar.tsx | 4 ++ src/dispatcher/tracking.tsx | 7 ++- 3 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 src/chrome/FpsGraph.tsx diff --git a/src/chrome/FpsGraph.tsx b/src/chrome/FpsGraph.tsx new file mode 100644 index 000000000..ccdc711f8 --- /dev/null +++ b/src/chrome/FpsGraph.tsx @@ -0,0 +1,90 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +import React, {useEffect, useRef} from 'react'; +import {fpsEmitter} from '../dispatcher/tracking'; + +export default function FpsGraph({ + width, + height, + sampleRate = 200, +}: { + width: number; + height: number; + sampleRate?: number; +}) { + const canvasRef = useRef(null); + + useEffect(() => { + const graphWidth = width - 20; + const fps: number[] = new Array(graphWidth).fill(0, 0, graphWidth); + let lastFps = 0; + let lastDraw = Date.now(); + + const handler = (xfps: number) => { + // at any interval, take the lowest to better show slow downs + lastFps = Math.min(lastFps, xfps); + }; + + const interval = setInterval(() => { + const ctx = canvasRef.current!.getContext('2d')!; + ctx.clearRect(0, 0, width, height); + ctx.strokeStyle = '#ccc'; + + const now = Date.now(); + let missedFrames = 0; + // check if we missed some measurements, in that case the CPU was fully choked! + for (let i = 0; i < Math.floor((now - lastDraw) / sampleRate) - 1; i++) { + fps.push(0); + fps.shift(); + missedFrames++; + } + lastDraw = now; + + // latest measurement + fps.push(lastFps); + fps.shift(); + + ctx.strokeText( + '' + + (missedFrames + ? // if we were chocked, show FPS based on frames missed + Math.floor((1000 / sampleRate) * missedFrames) + : lastFps), + width - 15, + 5 + height / 2, + ); + + ctx.beginPath(); + ctx.moveTo(0, height); + ctx.lineWidth = 1; + fps.forEach((num, idx) => { + ctx.lineTo(idx, height - (Math.min(60, num) / 60) * height); + }); + + ctx.strokeStyle = missedFrames ? '#ff0000' : '#ccc'; + + ctx.stroke(); + lastFps = 60; + }, sampleRate); + + fpsEmitter.on('fps', handler); + + return () => { + clearInterval(interval); + fpsEmitter.off('fps', handler); + }; + }, []); + + return ( +
+ +
+ ); +} diff --git a/src/chrome/TitleBar.tsx b/src/chrome/TitleBar.tsx index 5f1fd35b3..03fa2a625 100644 --- a/src/chrome/TitleBar.tsx +++ b/src/chrome/TitleBar.tsx @@ -43,6 +43,7 @@ import {clipboard} from 'electron'; import React from 'react'; import {State} from 'src/reducers'; import {reportUsage} from '../utils/metrics'; +import FpsGraph from './FpsGraph'; const AppTitleBar = styled(FlexRow)<{focused?: boolean}>(({focused}) => ({ background: focused @@ -160,6 +161,9 @@ class TitleBar extends React.Component { share != null ? share.statusComponent : undefined, )} + + {!isProduction() && } + {config.showFlipperRating ? : null} {this.props.version + (isProduction() ? '' : '-dev')} diff --git a/src/dispatcher/tracking.tsx b/src/dispatcher/tracking.tsx index 6ae71f831..27545363f 100644 --- a/src/dispatcher/tracking.tsx +++ b/src/dispatcher/tracking.tsx @@ -9,6 +9,7 @@ import {ipcRenderer} from 'electron'; import {performance} from 'perf_hooks'; +import EventEmitter from 'events'; import {Store} from '../reducers/index'; import {Logger} from '../fb-interfaces/Logger'; @@ -37,6 +38,8 @@ export type UsageSummary = { [pluginName: string]: {focusedTime: number; unfocusedTime: number}; }; +export const fpsEmitter = new EventEmitter(); + export default (store: Store, logger: Logger) => { let droppedFrames: number = 0; let largeFrameDrops: number = 0; @@ -46,7 +49,9 @@ export default (store: Store, logger: Logger) => { ) { const now = performance.now(); requestAnimationFrame(() => droppedFrameDetection(now, isWindowFocused)); - const dropped = Math.round((now - past) / (1000 / 60) - 1); + const delta = now - past; + const dropped = Math.round(delta / (1000 / 60) - 1); + fpsEmitter.emit('fps', delta > 1000 ? 0 : Math.round(1000 / (now - past))); if (!isWindowFocused() || dropped < 1) { return; }