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
This commit is contained in:
Michel Weststrate
2020-01-27 07:32:34 -08:00
committed by Facebook Github Bot
parent 33ad41c98c
commit 31df1db74f
3 changed files with 100 additions and 1 deletions

90
src/chrome/FpsGraph.tsx Normal file
View File

@@ -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<HTMLCanvasElement>(null);
useEffect(() => {
const graphWidth = width - 20;
const fps: number[] = new Array<number>(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 (
<div>
<canvas ref={canvasRef} width={width} height={height} />
</div>
);
}

View File

@@ -43,6 +43,7 @@ import {clipboard} from 'electron';
import React from 'react'; import React from 'react';
import {State} from 'src/reducers'; import {State} from 'src/reducers';
import {reportUsage} from '../utils/metrics'; import {reportUsage} from '../utils/metrics';
import FpsGraph from './FpsGraph';
const AppTitleBar = styled(FlexRow)<{focused?: boolean}>(({focused}) => ({ const AppTitleBar = styled(FlexRow)<{focused?: boolean}>(({focused}) => ({
background: focused background: focused
@@ -160,6 +161,9 @@ class TitleBar extends React.Component<Props, StateFromProps> {
share != null ? share.statusComponent : undefined, share != null ? share.statusComponent : undefined,
)} )}
<Spacer /> <Spacer />
{!isProduction() && <FpsGraph height={20} width={60} />}
{config.showFlipperRating ? <RatingButton /> : null} {config.showFlipperRating ? <RatingButton /> : null}
<Version>{this.props.version + (isProduction() ? '' : '-dev')}</Version> <Version>{this.props.version + (isProduction() ? '' : '-dev')}</Version>

View File

@@ -9,6 +9,7 @@
import {ipcRenderer} from 'electron'; import {ipcRenderer} from 'electron';
import {performance} from 'perf_hooks'; import {performance} from 'perf_hooks';
import EventEmitter from 'events';
import {Store} from '../reducers/index'; import {Store} from '../reducers/index';
import {Logger} from '../fb-interfaces/Logger'; import {Logger} from '../fb-interfaces/Logger';
@@ -37,6 +38,8 @@ export type UsageSummary = {
[pluginName: string]: {focusedTime: number; unfocusedTime: number}; [pluginName: string]: {focusedTime: number; unfocusedTime: number};
}; };
export const fpsEmitter = new EventEmitter();
export default (store: Store, logger: Logger) => { export default (store: Store, logger: Logger) => {
let droppedFrames: number = 0; let droppedFrames: number = 0;
let largeFrameDrops: number = 0; let largeFrameDrops: number = 0;
@@ -46,7 +49,9 @@ export default (store: Store, logger: Logger) => {
) { ) {
const now = performance.now(); const now = performance.now();
requestAnimationFrame(() => droppedFrameDetection(now, isWindowFocused)); 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) { if (!isWindowFocused() || dropped < 1) {
return; return;
} }