diff --git a/packages/commands/measure/src/__tests__/__snapshots__/measure.test.tsx.snap b/packages/commands/measure/src/__tests__/__snapshots__/measure.test.tsx.snap index 6e2f8699..cbf25089 100644 --- a/packages/commands/measure/src/__tests__/__snapshots__/measure.test.tsx.snap +++ b/packages/commands/measure/src/__tests__/__snapshots__/measure.test.tsx.snap @@ -12,7 +12,8 @@ Time taken to run the test. Can be helpful to measure Time To Interactive of your app, if the test is checking app start for instance. Average FPS 60 FPS -Frame Per Second. Your app should display 60 Frames Per Second to give an impression of fluidity. This number should be close to 60, otherwise it will seem laggy. +Frame Per Second. Your app should display 60 Frames Per Second to give an impression of fluidity. This number should be close to 60, otherwise it will seem laggy. + See this video @@ -20,7 +21,7 @@ this video for more details Average CPU usage 83 % -An app might run at 60FPS but might be using too much processing power, so it's important to check CPU usage. +An app might run at high frame rates, such as 60 FPS or higher, but might be using too much processing power, so it's important to check CPU usage. Depending on the device, this value can go up to 100% x number of cores . For instance, a Samsung A10s has 4 cores, so the max value would be 400%. @@ -855,7 +856,7 @@ exports[`flashlight measure interactive it displays measures: Web app with measu
- An app might run at 60FPS but might be using too much processing power, so it's important to check CPU usage. + An app might run at high frame rates, such as 60 FPS or higher, but might be using too much processing power, so it's important to check CPU usage.
Depending on the device, this value can go up to @@ -3812,7 +3813,8 @@ Time taken to run the test. Can be helpful to measure Time To Interactive of your app, if the test is checking app start for instance. Average FPS - -Frame Per Second. Your app should display 60 Frames Per Second to give an impression of fluidity. This number should be close to 60, otherwise it will seem laggy. +Frame Per Second. Your app should display 60 Frames Per Second to give an impression of fluidity. This number should be close to 60, otherwise it will seem laggy. + See this video @@ -3820,7 +3822,7 @@ this video for more details Average CPU usage - -An app might run at 60FPS but might be using too much processing power, so it's important to check CPU usage. +An app might run at high frame rates, such as 60 FPS or higher, but might be using too much processing power, so it's important to check CPU usage. Depending on the device, this value can go up to 100% x number of cores . For instance, a Samsung A10s has 4 cores, so the max value would be 400%. @@ -4218,7 +4220,7 @@ exports[`flashlight measure interactive it displays measures: Web app with no me
- An app might run at 60FPS but might be using too much processing power, so it's important to check CPU usage. + An app might run at high frame rates, such as 60 FPS or higher, but might be using too much processing power, so it's important to check CPU usage.
Depending on the device, this value can go up to diff --git a/packages/commands/measure/src/server/ServerSocketConnectionApp.tsx b/packages/commands/measure/src/server/ServerSocketConnectionApp.tsx index 0b459c79..ee48be93 100644 --- a/packages/commands/measure/src/server/ServerSocketConnectionApp.tsx +++ b/packages/commands/measure/src/server/ServerSocketConnectionApp.tsx @@ -1,9 +1,10 @@ import { PerformanceMeasurer } from "@perf-profiler/e2e"; import { Logger } from "@perf-profiler/logger"; +import { profiler } from "@perf-profiler/profiler"; import { Measure } from "@perf-profiler/types"; import React, { useCallback, useEffect } from "react"; import { HostAndPortInfo } from "./components/HostAndPortInfo"; -import { SocketType } from "./socket/socketInterface"; +import { SocketType, SocketEvents } from "./socket/socketInterface"; import { useSocketState, updateMeasuresReducer, addNewResultReducer } from "./socket/socketState"; import { useBundleIdControls } from "./useBundleIdControls"; import { useLogSocketEvents } from "../common/useLogSocketEvents"; @@ -33,9 +34,11 @@ export const ServerSocketConnectionApp = ({ socket, url }: { socket: SocketType; ) ); - socket.on("start", async () => { + socket.on(SocketEvents.START, async () => { + const refreshRate = profiler.detectDeviceRefreshRate(); setState({ isMeasuring: true, + refreshRate, }); if (!state.bundleId) { @@ -55,9 +58,9 @@ export const ServerSocketConnectionApp = ({ socket, url }: { socket: SocketType; ); }); - socket.on("stop", stop); + socket.on(SocketEvents.STOP, stop); - socket.on("reset", () => { + socket.on(SocketEvents.RESET, () => { stop(); setState({ results: [], @@ -65,9 +68,9 @@ export const ServerSocketConnectionApp = ({ socket, url }: { socket: SocketType; }); return () => { - socket.removeAllListeners("start"); - socket.removeAllListeners("stop"); - socket.removeAllListeners("reset"); + socket.removeAllListeners(SocketEvents.START); + socket.removeAllListeners(SocketEvents.STOP); + socket.removeAllListeners(SocketEvents.RESET); }; }, [setState, socket, state.bundleId, stop]); diff --git a/packages/commands/measure/src/server/socket/socketInterface.ts b/packages/commands/measure/src/server/socket/socketInterface.ts index 86f3dcb5..adaee861 100644 --- a/packages/commands/measure/src/server/socket/socketInterface.ts +++ b/packages/commands/measure/src/server/socket/socketInterface.ts @@ -5,6 +5,7 @@ export interface SocketData { isMeasuring: boolean; bundleId: string | null; results: TestCaseResult[]; + refreshRate: number; } export interface ServerToClientEvents { @@ -18,6 +19,7 @@ export interface ClientToServerEvents { reset: () => void; autodetectBundleId: () => void; setBundleId: (bundleId: string) => void; + autodetectRefreshRate: () => void; } interface InterServerEvents { @@ -37,3 +39,17 @@ export type SocketType = Socket< InterServerEvents, SocketData >; + +export enum SocketEvents { + START = "start", + STOP = "stop", + RESET = "reset", + AUTODETECT_BUNDLE_ID = "autodetectBundleId", + SET_BUNDLE_ID = "setBundleId", + AUTODETECT_REFRESH_RATE = "autodetectRefreshRate", + UPDATE_STATE = "updateState", + SEND_ERROR = "sendError", + PING = "ping", + CONNECT = "connect", + DISCONNECT = "disconnect", +} diff --git a/packages/commands/measure/src/server/socket/socketState.ts b/packages/commands/measure/src/server/socket/socketState.ts index 92d99cb0..945c633c 100644 --- a/packages/commands/measure/src/server/socket/socketState.ts +++ b/packages/commands/measure/src/server/socket/socketState.ts @@ -1,12 +1,13 @@ import { Measure, POLLING_INTERVAL } from "@perf-profiler/types"; import { useState, useEffect } from "react"; -import { SocketType, SocketData } from "./socketInterface"; +import { SocketType, SocketData, SocketEvents } from "./socketInterface"; export const useSocketState = (socket: SocketType) => { const [state, _setState] = useState({ isMeasuring: false, bundleId: null, results: [], + refreshRate: 60, }); const setState = ( @@ -23,7 +24,7 @@ export const useSocketState = (socket: SocketType) => { }; useEffect(() => { - socket.emit("updateState", state); + socket.emit(SocketEvents.UPDATE_STATE, state); }, [state, socket]); return [state, setState] as const; @@ -54,6 +55,9 @@ export const addNewResultReducer = (state: SocketData, name: string): SocketData name, iterations: [], status: "SUCCESS", + specs: { + refreshRate: state.refreshRate, + }, }, ], }); diff --git a/packages/commands/measure/src/server/useBundleIdControls.ts b/packages/commands/measure/src/server/useBundleIdControls.ts index c06cf2f9..fba9b315 100644 --- a/packages/commands/measure/src/server/useBundleIdControls.ts +++ b/packages/commands/measure/src/server/useBundleIdControls.ts @@ -1,6 +1,6 @@ import { profiler } from "@perf-profiler/profiler"; import { useEffect } from "react"; -import { SocketType, SocketData } from "./socket/socketInterface"; +import { SocketType, SocketData, SocketEvents } from "./socket/socketInterface"; export const useBundleIdControls = ( socket: SocketType, @@ -8,13 +8,13 @@ export const useBundleIdControls = ( stop: () => void ) => { useEffect(() => { - socket.on("setBundleId", (bundleId) => { + socket.on(SocketEvents.SET_BUNDLE_ID, (bundleId) => { setState({ bundleId, }); }); - socket.on("autodetectBundleId", () => { + socket.on(SocketEvents.AUTODETECT_BUNDLE_ID, () => { stop(); try { @@ -23,13 +23,26 @@ export const useBundleIdControls = ( bundleId, }); } catch (error) { - socket.emit("sendError", error instanceof Error ? error.message : "unknown error"); + socket.emit( + SocketEvents.SEND_ERROR, + error instanceof Error ? error.message : "unknown error" + ); } }); + socket.on(SocketEvents.AUTODETECT_REFRESH_RATE, () => { + stop(); + + const refreshRate = profiler.detectDeviceRefreshRate(); + setState({ + refreshRate, + }); + }); + return () => { - socket.removeAllListeners("setBundleId"); - socket.removeAllListeners("autodetectBundleId"); + socket.removeAllListeners(SocketEvents.SET_BUNDLE_ID); + socket.removeAllListeners(SocketEvents.AUTODETECT_BUNDLE_ID); + socket.removeAllListeners(SocketEvents.AUTODETECT_REFRESH_RATE); }; }, [setState, socket, stop]); }; diff --git a/packages/commands/measure/src/webapp/components/SocketState.tsx b/packages/commands/measure/src/webapp/components/SocketState.tsx index 89b4f98e..5b29e01a 100644 --- a/packages/commands/measure/src/webapp/components/SocketState.tsx +++ b/packages/commands/measure/src/webapp/components/SocketState.tsx @@ -8,6 +8,7 @@ import Button from "@mui/material/Button"; import { Logger } from "@perf-profiler/logger"; import { socket } from "../socket"; import { useLogSocketEvents } from "../../common/useLogSocketEvents"; +import { SocketEvents } from "../../server/socket/socketInterface"; const useSocketState = (onError: (error: string) => void) => { useLogSocketEvents(socket); @@ -28,14 +29,14 @@ const useSocketState = (onError: (error: string) => void) => { } } - socket.on("connect", onConnect); - socket.on("disconnect", onDisconnect); - socket.on("sendError", onError); + socket.on(SocketEvents.CONNECT, onConnect); + socket.on(SocketEvents.DISCONNECT, onDisconnect); + socket.on(SocketEvents.SEND_ERROR, onError); return () => { - socket.off("connect", onConnect); - socket.off("disconnect", onDisconnect); - socket.off("sendError", onError); + socket.off(SocketEvents.CONNECT, onConnect); + socket.off(SocketEvents.DISCONNECT, onDisconnect); + socket.off(SocketEvents.SEND_ERROR, onError); }; }, [onError]); diff --git a/packages/commands/measure/src/webapp/socket.ts b/packages/commands/measure/src/webapp/socket.ts index 6a303f53..8126a885 100644 --- a/packages/commands/measure/src/webapp/socket.ts +++ b/packages/commands/measure/src/webapp/socket.ts @@ -1,8 +1,12 @@ import { io, Socket } from "socket.io-client"; -import { ServerToClientEvents, ClientToServerEvents } from "../server/socket/socketInterface"; +import { + ServerToClientEvents, + ClientToServerEvents, + SocketEvents, +} from "../server/socket/socketInterface"; export const socket: Socket = io( window.__FLASHLIGHT_DATA__.socketServerUrl ); -socket.on("disconnect", () => socket.close()); +socket.on(SocketEvents.DISCONNECT, () => socket.close()); diff --git a/packages/commands/measure/src/webapp/useMeasures.ts b/packages/commands/measure/src/webapp/useMeasures.ts index c73ed798..f4d5b7e3 100644 --- a/packages/commands/measure/src/webapp/useMeasures.ts +++ b/packages/commands/measure/src/webapp/useMeasures.ts @@ -1,36 +1,38 @@ import { useEffect, useState } from "react"; -import type { SocketData } from "../server/socket/socketInterface"; +import { SocketData, SocketEvents } from "../server/socket/socketInterface"; import { socket } from "./socket"; export const useMeasures = () => { const [state, setState] = useState(); useEffect(() => { - socket.on("updateState", setState); + socket.on(SocketEvents.UPDATE_STATE, setState); return () => { - socket.off("updateState", setState); + socket.off(SocketEvents.UPDATE_STATE, setState); }; }, []); return { bundleId: state?.bundleId ?? null, + refreshRate: state?.refreshRate ?? 60, autodetect: () => { - socket.emit("autodetectBundleId"); + socket.emit(SocketEvents.AUTODETECT_BUNDLE_ID); + socket.emit(SocketEvents.AUTODETECT_REFRESH_RATE); }, setBundleId: (bundleId: string) => { - socket.emit("setBundleId", bundleId); + socket.emit(SocketEvents.SET_BUNDLE_ID, bundleId); }, results: state?.results ?? [], isMeasuring: state?.isMeasuring ?? false, start: () => { - socket.emit("start"); + socket.emit(SocketEvents.START); }, stop: () => { - socket.emit("stop"); + socket.emit(SocketEvents.STOP); }, reset: () => { - socket.emit("reset"); + socket.emit(SocketEvents.RESET); }, }; }; diff --git a/packages/core/reporter/src/reporting/Report.ts b/packages/core/reporter/src/reporting/Report.ts index 018e7fa5..2445c18e 100644 --- a/packages/core/reporter/src/reporting/Report.ts +++ b/packages/core/reporter/src/reporting/Report.ts @@ -117,4 +117,8 @@ export class Report { threads: getThreadsStats(iterations), }; } + + public getRefreshRate() { + return this.result.specs?.refreshRate ?? 60; + } } diff --git a/packages/core/reporter/src/reporting/getScore.ts b/packages/core/reporter/src/reporting/getScore.ts index 1d0be12b..644c8e01 100644 --- a/packages/core/reporter/src/reporting/getScore.ts +++ b/packages/core/reporter/src/reporting/getScore.ts @@ -26,7 +26,7 @@ export const getScore = (result: AveragedTestCaseResult) => { const scores = [cpuScore]; if (averageUIFPS !== undefined) { - const fpsScore = (averageUIFPS * 100) / 60; + const fpsScore = (averageUIFPS * 100) / (result?.specs?.refreshRate ?? 60); scores.push(fpsScore); } diff --git a/packages/core/types/index.ts b/packages/core/types/index.ts index 66a75dc6..d9f8a5df 100644 --- a/packages/core/types/index.ts +++ b/packages/core/types/index.ts @@ -39,6 +39,7 @@ export interface TestCaseResult { status: TestCaseResultStatus; iterations: TestCaseIterationResult[]; type?: TestCaseResultType; + specs?: DeviceSpecs; } export interface AveragedTestCaseResult { @@ -49,6 +50,7 @@ export interface AveragedTestCaseResult { average: TestCaseIterationResult; averageHighCpuUsage: { [processName: string]: number }; type?: TestCaseResultType; + specs?: DeviceSpecs; } // Shouldn't really be here but @perf-profiler/types is imported by everyone and doesn't contain any logic @@ -97,4 +99,9 @@ export interface Profiler { cleanup: () => void; getScreenRecorder: (videoPath: string) => ScreenRecorder | undefined; stopApp: (bundleId: string) => Promise; + detectDeviceRefreshRate: () => number; +} + +export interface DeviceSpecs { + refreshRate: number; } diff --git a/packages/core/web-reporter-ui/__tests__/__snapshots__/ReporterView.test.tsx.snap b/packages/core/web-reporter-ui/__tests__/__snapshots__/ReporterView.test.tsx.snap index 5b29d576..cef377c0 100644 --- a/packages/core/web-reporter-ui/__tests__/__snapshots__/ReporterView.test.tsx.snap +++ b/packages/core/web-reporter-ui/__tests__/__snapshots__/ReporterView.test.tsx.snap @@ -31,7 +31,8 @@ Coefficient of variation : % Average FPS 59.7 FPS -Frame Per Second. Your app should display 60 Frames Per Second to give an impression of fluidity. This number should be close to 60, otherwise it will seem laggy. +Frame Per Second. Your app should display 60 Frames Per Second to give an impression of fluidity. This number should be close to 60, otherwise it will seem laggy. + See this video @@ -61,7 +62,7 @@ Coefficient of variation : % Average CPU usage 58.6 % -An app might run at 60FPS but might be using too much processing power, so it's important to check CPU usage. +An app might run at high frame rates, such as 60 FPS or higher, but might be using too much processing power, so it's important to check CPU usage. Depending on the device, this value can go up to 100% x number of cores . For instance, a Samsung A10s has 4 cores, so the max value would be 400%. @@ -206,7 +207,8 @@ Coefficient of variation : Average FPS 59.8 FPS (+0%) -Frame Per Second. Your app should display 60 Frames Per Second to give an impression of fluidity. This number should be close to 60, otherwise it will seem laggy. +Frame Per Second. Your app should display 60 Frames Per Second to give an impression of fluidity. This number should be close to 60, otherwise it will seem laggy. + See this video @@ -237,7 +239,7 @@ Coefficient of variation : Average CPU usage 30.8 % (-47%) -An app might run at 60FPS but might be using too much processing power, so it's important to check CPU usage. +An app might run at high frame rates, such as 60 FPS or higher, but might be using too much processing power, so it's important to check CPU usage. Depending on the device, this value can go up to 100% x number of cores . For instance, a Samsung A10s has 4 cores, so the max value would be 400%. @@ -2213,7 +2215,7 @@ exports[` renders the comparison view 2`] = `
- An app might run at 60FPS but might be using too much processing power, so it's important to check CPU usage. + An app might run at high frame rates, such as 60 FPS or higher, but might be using too much processing power, so it's important to check CPU usage.
Depending on the device, this value can go up to @@ -2889,7 +2891,7 @@ exports[` renders the comparison view 2`] = `
- An app might run at 60FPS but might be using too much processing power, so it's important to check CPU usage. + An app might run at high frame rates, such as 60 FPS or higher, but might be using too much processing power, so it's important to check CPU usage.
Depending on the device, this value can go up to @@ -14017,7 +14019,8 @@ Time taken to run the test. Can be helpful to measure Time To Interactive of your app, if the test is checking app start for instance. Average FPS 59.6 FPS -Frame Per Second. Your app should display 60 Frames Per Second to give an impression of fluidity. This number should be close to 60, otherwise it will seem laggy. +Frame Per Second. Your app should display 60 Frames Per Second to give an impression of fluidity. This number should be close to 60, otherwise it will seem laggy. + See this video @@ -14025,7 +14028,7 @@ this video for more details Average CPU usage 66.1 % -An app might run at 60FPS but might be using too much processing power, so it's important to check CPU usage. +An app might run at high frame rates, such as 60 FPS or higher, but might be using too much processing power, so it's important to check CPU usage. Depending on the device, this value can go up to 100% x number of cores . For instance, a Samsung A10s has 4 cores, so the max value would be 400%. @@ -14056,7 +14059,8 @@ Can be helpful to measure Time To Interactive of your app, if the test is checki Average FPS 59.9 FPS (+1%) -Frame Per Second. Your app should display 60 Frames Per Second to give an impression of fluidity. This number should be close to 60, otherwise it will seem laggy. +Frame Per Second. Your app should display 60 Frames Per Second to give an impression of fluidity. This number should be close to 60, otherwise it will seem laggy. + See this video @@ -14065,7 +14069,7 @@ for more details Average CPU usage 30.9 % (-53%) -An app might run at 60FPS but might be using too much processing power, so it's important to check CPU usage. +An app might run at high frame rates, such as 60 FPS or higher, but might be using too much processing power, so it's important to check CPU usage. Depending on the device, this value can go up to 100% x number of cores . For instance, a Samsung A10s has 4 cores, so the max value would be 400%. @@ -15581,7 +15585,7 @@ exports[` renders the comparison view 4`] = `
- An app might run at 60FPS but might be using too much processing power, so it's important to check CPU usage. + An app might run at high frame rates, such as 60 FPS or higher, but might be using too much processing power, so it's important to check CPU usage.
Depending on the device, this value can go up to @@ -16014,7 +16018,7 @@ exports[` renders the comparison view 4`] = `
- An app might run at 60FPS but might be using too much processing power, so it's important to check CPU usage. + An app might run at high frame rates, such as 60 FPS or higher, but might be using too much processing power, so it's important to check CPU usage.
Depending on the device, this value can go up to @@ -22002,7 +22006,8 @@ Coefficient of variation : % Average FPS 45.3 FPS -Frame Per Second. Your app should display 60 Frames Per Second to give an impression of fluidity. This number should be close to 60, otherwise it will seem laggy. +Frame Per Second. Your app should display 60 Frames Per Second to give an impression of fluidity. This number should be close to 60, otherwise it will seem laggy. + See this video @@ -22032,7 +22037,7 @@ Coefficient of variation : % Average CPU usage 79.9 % -An app might run at 60FPS but might be using too much processing power, so it's important to check CPU usage. +An app might run at high frame rates, such as 60 FPS or higher, but might be using too much processing power, so it's important to check CPU usage. Depending on the device, this value can go up to 100% x number of cores . For instance, a Samsung A10s has 4 cores, so the max value would be 400%. @@ -22208,7 +22213,8 @@ Coefficient of variation : Average FPS 38.2 FPS (-16%) -Frame Per Second. Your app should display 60 Frames Per Second to give an impression of fluidity. This number should be close to 60, otherwise it will seem laggy. +Frame Per Second. Your app should display 60 Frames Per Second to give an impression of fluidity. This number should be close to 60, otherwise it will seem laggy. + See this video @@ -22239,7 +22245,7 @@ Coefficient of variation : Average CPU usage 92.8 % (+16%) -An app might run at 60FPS but might be using too much processing power, so it's important to check CPU usage. +An app might run at high frame rates, such as 60 FPS or higher, but might be using too much processing power, so it's important to check CPU usage. Depending on the device, this value can go up to 100% x number of cores . For instance, a Samsung A10s has 4 cores, so the max value would be 400%. @@ -23818,7 +23824,7 @@ exports[` renders the comparison view with videos 2`] = `
- An app might run at 60FPS but might be using too much processing power, so it's important to check CPU usage. + An app might run at high frame rates, such as 60 FPS or higher, but might be using too much processing power, so it's important to check CPU usage.
Depending on the device, this value can go up to @@ -24537,7 +24543,7 @@ exports[` renders the comparison view with videos 2`] = `
- An app might run at 60FPS but might be using too much processing power, so it's important to check CPU usage. + An app might run at high frame rates, such as 60 FPS or higher, but might be using too much processing power, so it's important to check CPU usage.
Depending on the device, this value can go up to diff --git a/packages/core/web-reporter-ui/src/sections/ReportSummary/Explanations.tsx b/packages/core/web-reporter-ui/src/sections/ReportSummary/Explanations.tsx index d5623dfa..4872a793 100644 --- a/packages/core/web-reporter-ui/src/sections/ReportSummary/Explanations.tsx +++ b/packages/core/web-reporter-ui/src/sections/ReportSummary/Explanations.tsx @@ -12,10 +12,10 @@ const AverageTestRuntimeExplanation = () => ( ); -const AverageFPSExplanation = () => ( +const AverageFPSExplanation = ({ refreshRate }: { refreshRate: number }) => ( <> - Frame Per Second. Your app should display 60 Frames Per Second to give an impression of - fluidity. This number should be close to 60, otherwise it will seem laggy.
+ {`Frame Per Second. Your app should display ${refreshRate} Frames Per Second to give an impression of fluidity. This number should be close to ${refreshRate}, otherwise it will seem laggy.`}{" "} +
See{" "} this video @@ -26,8 +26,8 @@ const AverageFPSExplanation = () => ( const AverageCPUUsageExplanation = () => ( <> - An app might run at 60FPS but might be using too much processing power, so it's important - to check CPU usage. + An app might run at high frame rates, such as 60 FPS or higher, but might be using too much + processing power, so it's important to check CPU usage.
Depending on the device, this value can go up to 100% x number of cores. For instance, a Samsung A10s has 4 cores, so the max value would be 400%. @@ -51,7 +51,13 @@ const AverageRAMUsageExplanation = () => ( ); -const HighCPUUsageExplanation = ({ result }: { result: AveragedTestCaseResult }) => ( +const HighCPUUsageExplanation = ({ + result, + refreshRate, +}: { + result: AveragedTestCaseResult; + refreshRate: number; +}) => ( <>

Impacted threads:

@@ -66,9 +72,7 @@ const HighCPUUsageExplanation = ({ result }: { result: AveragedTestCaseResult })

))}
- High CPU usage by a single process can cause app unresponsiveness, even with low overall CPU - usage. For instance, an overworked JS thread in a React Native app may lead to unresponsiveness - despite maintaining 60 FPS. + {`High CPU usage by a single process can cause app unresponsiveness, even with low overall CPU usage. For instance, an overworked JS thread in a React Native app may lead to unresponsiveness despite maintaining ${refreshRate} FPS.`} ); diff --git a/packages/core/web-reporter-ui/src/sections/ReportSummary/ReportSummaryCard.tsx b/packages/core/web-reporter-ui/src/sections/ReportSummary/ReportSummaryCard.tsx index b4486cdf..e9c0883b 100644 --- a/packages/core/web-reporter-ui/src/sections/ReportSummary/ReportSummaryCard.tsx +++ b/packages/core/web-reporter-ui/src/sections/ReportSummary/ReportSummaryCard.tsx @@ -59,7 +59,9 @@ export const ReportSummaryCard: FunctionComponent = ({ report, baselineRe hasValueImproved={isDifferencePositive} /> } - explanation={} + explanation={ + + } statistics={ reportStats?.fps ? : undefined } @@ -95,7 +97,12 @@ export const ReportSummaryCard: FunctionComponent = ({ report, baselineRe baseline={baselineMetrics?.totalHighCpuTime} /> } - explanation={} + explanation={ + + } statistics={ reportStats ? ( <> diff --git a/packages/platforms/android/src/commands/__tests__/detectCurrentDeviceRefreshRate.test.ts b/packages/platforms/android/src/commands/__tests__/detectCurrentDeviceRefreshRate.test.ts new file mode 100644 index 00000000..20f6b69c --- /dev/null +++ b/packages/platforms/android/src/commands/__tests__/detectCurrentDeviceRefreshRate.test.ts @@ -0,0 +1,38 @@ +import { detectCurrentDeviceRefreshRate } from "../detectCurrentDeviceRefreshRate"; +import fs from "fs"; + +const sampleOutput = fs.readFileSync(`${__dirname}/dumpsys-display.txt`, "utf-8"); +const sampleOutput2 = fs.readFileSync(`${__dirname}/dumpsys-display120.txt`, "utf-8"); + +const executeCommandSpy = jest.spyOn(require("../shell"), "executeCommand"); + +describe("detectCurrentDeviceRefreshRate", () => { + it("retrieves correctly device refresh rate of a basic 60fps device", () => { + executeCommandSpy.mockImplementation((command) => { + expect(command).toEqual( + 'adb shell dumpsys display | grep -E "mRefreshRate|DisplayDeviceInfo"' + ); + + return sampleOutput; + }); + + expect(detectCurrentDeviceRefreshRate()).toEqual(60); + }); + + it("retrieves correctly device refresh rate of a 120fps pixel device", () => { + executeCommandSpy.mockImplementation((command) => { + expect(command).toEqual( + 'adb shell dumpsys display | grep -E "mRefreshRate|DisplayDeviceInfo"' + ); + + return sampleOutput2; + }); + + expect(detectCurrentDeviceRefreshRate()).toEqual(120); + }); + + it("throws an error in case it couldn't find it", () => { + executeCommandSpy.mockImplementation(() => ""); + expect(detectCurrentDeviceRefreshRate).toThrow(); + }); +}); diff --git a/packages/platforms/android/src/commands/__tests__/dumpsys-display.txt b/packages/platforms/android/src/commands/__tests__/dumpsys-display.txt new file mode 100644 index 00000000..8fa56ae6 --- /dev/null +++ b/packages/platforms/android/src/commands/__tests__/dumpsys-display.txt @@ -0,0 +1,2 @@ +DisplayDeviceInfo{"Built-in Screen": uniqueId="local:8141241408256768", 480 x 854, modeId 1, defaultModeId 1, supportedModes [{id=1, width=480, height=854, fps=60.000004}], colorMode 0, supportedColorModes [0], HdrCapabilities HdrCapabilities{mSupportedHdrTypes=[], mMaxLuminance=500.0, mMaxAverageLuminance=500.0, mMinLuminance=0.0}, allmSupported false, gameContentTypeSupported false, density 160, 160.0 x 160.0 dpi, appVsyncOff 1000000, presDeadline 16666666, touch INTERNAL, rotation 0, type INTERNAL, address {port=0, model=0x1cec6a7a2b7b}, deviceProductInfo DeviceProductInfo{name=EMU_display_0, manufacturerPnpId=GGL, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, relativeAddress=null}, state ON, FLAG_DEFAULT_DISPLAY, FLAG_ROTATES_WITH_CONTENT, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS} + mRefreshRateInZone: 0 \ No newline at end of file diff --git a/packages/platforms/android/src/commands/__tests__/dumpsys-display120.txt b/packages/platforms/android/src/commands/__tests__/dumpsys-display120.txt new file mode 100644 index 00000000..e87aea5a --- /dev/null +++ b/packages/platforms/android/src/commands/__tests__/dumpsys-display120.txt @@ -0,0 +1,10 @@ +DisplayDeviceInfo{"Built-in Screen": uniqueId="local:4619827535830457088", 1008 x 2244, modeId 3, renderFrameRate 120.00001, defaultModeId 3, userPreferredModeId 3, supportedModes [{id=1, width=1344, height=2992, fps=60.0, vsync=60.0, alternativeRefreshRates=[120.00001], supportedHdrTypes=[2, 3, 4]}, {id=2, width=1344, height=2992, fps=120.00001, vsync=120.00001, alternativeRefreshRates=[60.0], supportedHdrTypes=[2, 3, 4]}, {id=3, width=1008, height=2244, fps=120.00001, vsync=120.00001, alternativeRefreshRates=[60.0], supportedHdrTypes=[2, 3, 4]}, {id=4, width=1008, height=2244, fps=60.0, vsync=60.0, alternativeRefreshRates=[120.00001], supportedHdrTypes=[2, 3, 4]}], colorMode 0, supportedColorModes [0, 7, 9], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[2, 3, 4], mMaxLuminance=1000.0, mMaxAverageLuminance=120.0, mMinLuminance=5.0E-4}, allmSupported false, gameContentTypeSupported false, density 360, 365.76 x 367.726 dpi, appVsyncOff 6233332, presDeadline 11500000, cutout DisplayCutout{insets=Rect(0, 113 - 0, 0) waterfall=Insets{left=0, top=0, right=0, bottom=0} boundingRect={Bounds=[Rect(0, 0 - 0, 0), Rect(462, 0 - 545, 113), Rect(0, 0 - 0, 0), Rect(0, 0 - 0, 0)]} cutoutPathParserInfo={CutoutPathParserInfo{displayWidth=1008 displayHeight=2244 physicalDisplayWidth=1344 physicalDisplayHeight=2992 density={3.0} cutoutSpec={M 628.75,75 a 43.25,43.25 0 1 0 86.5,0 a 43.25,43.25 0 1 0 -86.5,0 Z @left} rotation={0} scale={1.0} physicalPixelDisplaySizeRatio={0.75}}} sideOverrides={}}, touch INTERNAL, rotation 0, type INTERNAL, address {port=0, model=0x401cecaabcbe1b}, deviceProductInfo DeviceProductInfo{name=HK3-A0B-00, manufacturerPnpId=GGL, productId=0, modelYear=null, manufactureDate=ManufactureDate{week=1, year=1990}, connectionToSinkType=0}, state ON, committedState ON, frameRateOverride , brightnessMinimum 0.0, brightnessMaximum 1.0, brightnessDefault 0.08711423, hdrSdrRatio 1.0, roundedCorners RoundedCorners{[RoundedCorner{position=TopLeft, radius=68, center=Point(68, 68)}, RoundedCorner{position=TopRight, radius=68, center=Point(940, 68)}, RoundedCorner{position=BottomRight, radius=68, center=Point(940, 2176)}, RoundedCorner{position=BottomLeft, radius=68, center=Point(68, 2176)}]}, FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY, FLAG_ROTATES_WITH_CONTENT, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, FLAG_TRUSTED, installOrientation 0, displayShape DisplayShape{ spec=1208095170 displayWidth=1008 displayHeight=2244 physicalPixelDisplaySizeRatio=0.75 rotation=0 offsetX=0 offsetY=0 scale=1.0}} + mAmbientLightSensor=SensorData{type= com.google.sensor.auto_brightness, name= , refreshRateRange: [0.0, Infinity], supportedModes=[]}, mScreenOffBrightnessSensor=SensorData{type= null, name= null, refreshRateRange: [0.0, Infinity], supportedModes=[]}, mProximitySensor=SensorData{type= null, name= null, refreshRateRange: [0.0, Infinity], supportedModes=[]}, mTempSensor=SensorData{type= SKIN, name= null, refreshRateRange: [0.0, Infinity], supportedModes=[]}, mRefreshRateLimitations= [], mDensityMapping= DensityMapping{mDensityMappingEntries=[DensityMappingEntry{squaredDiagonal=6051600, density=360}, DensityMappingEntry{squaredDiagonal=10758400, density=480}]}, mAutoBrightnessBrighteningLightDebounce= 1000, mAutoBrightnessDarkeningLightDebounce= 4000, mAutoBrightnessBrighteningLightDebounceIdle= 1000, mAutoBrightnessDarkeningLightDebounceIdle= 4000, mDisplayBrightnessMapping= mBrightnessLevelsNits= [5.139055, 9.962019, 18.34823, 21.550682, 24.016779, 32.5, 46.0, 53.26923, 54.615383, 58.115383, 62.1394, 67.13133, 79.67614, 98.04727, 125.1222, 161.68752, 208.48856, 264.82214, 328.58694, 627.4315, 826.85846, 867.85583, 1041.3966, 1227.5264, 1331.2893, 1414.5757, 1553.2283, 1614.0104], mBrightnessLevelsLuxMap= {default_normal=[0.0, 1.0, 2.0, 3.0, 4.0, 8.0, 12.0, 15.0, 20.0, 33.0, 55.0, 90.0, 148.0, 245.0, 403.0, 665.0, 1097.0, 1808.0, 3000.0, 6000.0, 9000.0, 10000.0, 14000.0, 20000.0, 25000.0, 31000.0, 51000.0, 81000.0]}, mBrightnessLevelsMap= {default_normal=[]}, mDdcAutoBrightnessAvailable= true, mAutoBrightnessAvailable= true + mDefaultLowBlockingZoneRefreshRate= 120, mDefaultHighBlockingZoneRefreshRate= 0, mDefaultPeakRefreshRate= 120, mDefaultRefreshRate= 0, mRefreshRateZoneProfiles= {}, mDefaultRefreshRateInHbmHdr= 0, mDefaultRefreshRateInHbmSunlight= 0, mRefreshRateThrottlingMap= {}, mLowBlockingZoneThermalMapId= null, mHighBlockingZoneThermalMapId= null + mCurrentLayout=[{dispId: 0(ON), displayGroupName: , addr: {port=0, model=0x401cecaabcbe1b}, mThermalBrightnessThrottlingMapId: null, mRefreshRateZoneId: null, mLeadDisplayId: -1, mLeadDisplayAddress: null, mThermalRefreshRateThrottlingMapId: null, mPowerThrottlingMapId: null}] + state(-1): [{dispId: 0(ON), displayGroupName: , addr: {port=0, model=0x401cecaabcbe1b}, mThermalBrightnessThrottlingMapId: null, mRefreshRateZoneId: null, mLeadDisplayId: -1, mLeadDisplayAddress: null, mThermalRefreshRateThrottlingMapId: null, mPowerThrottlingMapId: null}] + mRefreshRateChangeable: true + mRefreshRateInLowZone: 120 + mRefreshRateInHighZone: 0 + mRefreshRateInHbmSunlight: 0 + mRefreshRateInHbmHdr: 0 \ No newline at end of file diff --git a/packages/platforms/android/src/commands/atrace/pollFpsUsage.ts b/packages/platforms/android/src/commands/atrace/pollFpsUsage.ts index 14b5a386..3f9a201e 100644 --- a/packages/platforms/android/src/commands/atrace/pollFpsUsage.ts +++ b/packages/platforms/android/src/commands/atrace/pollFpsUsage.ts @@ -1,5 +1,6 @@ import { Logger } from "@perf-profiler/logger"; import { POLLING_INTERVAL } from "@perf-profiler/types"; +import { refreshRateManager } from "../detectCurrentDeviceRefreshRate"; export const parseLine = ( line: string @@ -27,9 +28,9 @@ export const parseLine = ( }; }; -// At some point we might want to change this to adapt to 90fps or 120fps devices -const TARGET_FRAME_RATE = 60; +const TARGET_FRAME_RATE = refreshRateManager.getRefreshRate(); const TARGET_FRAME_TIME = 1000 / TARGET_FRAME_RATE; +Logger.info(`Target frame rate: ${TARGET_FRAME_RATE} Hz`); export class FrameTimeParser { private methodStartedCount = 0; diff --git a/packages/platforms/android/src/commands/detectCurrentDeviceRefreshRate.ts b/packages/platforms/android/src/commands/detectCurrentDeviceRefreshRate.ts new file mode 100644 index 00000000..6936828d --- /dev/null +++ b/packages/platforms/android/src/commands/detectCurrentDeviceRefreshRate.ts @@ -0,0 +1,52 @@ +import { executeCommand } from "./shell"; +import { Logger } from "@perf-profiler/logger"; + +function deviceRefreshRateManager() { + let refreshRate = 60; // Default to 60 fps + + return { + getRefreshRate: () => refreshRate, + setRefreshRate: () => { + try { + refreshRate = detectCurrentDeviceRefreshRate(); + } catch (e) { + Logger.error(`Could not detect device refresh rate: ${e}`); + } + }, + }; +} + +export const detectCurrentDeviceRefreshRate = () => { + const command = 'adb shell dumpsys display | grep -E "mRefreshRate|DisplayDeviceInfo"'; + const commandOutput = executeCommand(command); + + const renderFrameRateMatch = commandOutput.match(/renderFrameRate\s+(\d+\.?\d*)/); + + if (renderFrameRateMatch) { + Logger.debug(`Detected device refresh rate: ${renderFrameRateMatch[1]} Hz`); + return Math.floor(parseFloat(renderFrameRateMatch[1])); + } + + const matches = commandOutput.matchAll(/fps=(\d+\.?\d*)/g); + const refreshRates = Array.from(matches, (match) => parseFloat(match[1])); + refreshRates.sort((a, b) => b - a); + + if (refreshRates.length === 0) { + throw new Error( + `Could not detect device refresh rate, ${ + commandOutput + ? `output of ${command} was ${commandOutput}` + : "do you have an Android device connected and unlocked?" + }` + ); + } + + Logger.debug(`Detected device refresh rate: ${refreshRates[0]} Hz`); + + return Math.floor(refreshRates[0]); +}; + +const refreshRateManager = deviceRefreshRateManager(); +refreshRateManager.setRefreshRate(); + +export { refreshRateManager }; diff --git a/packages/platforms/android/src/commands/platforms/AndroidProfiler.ts b/packages/platforms/android/src/commands/platforms/AndroidProfiler.ts index 6b87298a..2ae87fd5 100644 --- a/packages/platforms/android/src/commands/platforms/AndroidProfiler.ts +++ b/packages/platforms/android/src/commands/platforms/AndroidProfiler.ts @@ -5,6 +5,7 @@ import { getAbi } from "../getAbi"; import { detectCurrentAppBundleId } from "../detectCurrentAppBundleId"; import { CppProfilerName, UnixProfiler } from "./UnixProfiler"; import { ScreenRecorder } from "../ScreenRecorder"; +import { refreshRateManager } from "../detectCurrentDeviceRefreshRate"; export class AndroidProfiler extends UnixProfiler { private aTraceProcess: ChildProcess | null = null; @@ -82,4 +83,8 @@ export class AndroidProfiler extends UnixProfiler { execSync(`adb shell am force-stop ${bundleId}`); await new Promise((resolve) => setTimeout(resolve, 3000)); } + + public detectDeviceRefreshRate(): number { + return refreshRateManager.getRefreshRate(); + } } diff --git a/packages/platforms/android/src/commands/platforms/FlashlightSelfProfiler.ts b/packages/platforms/android/src/commands/platforms/FlashlightSelfProfiler.ts index cb4abbd5..72da6c31 100644 --- a/packages/platforms/android/src/commands/platforms/FlashlightSelfProfiler.ts +++ b/packages/platforms/android/src/commands/platforms/FlashlightSelfProfiler.ts @@ -14,6 +14,10 @@ export class FlashlightSelfProfiler extends AndroidProfiler { return CppProfilerName; } + public detectDeviceRefreshRate(): number { + return 60; + } + /** * If we don't override this we end up in a situation where we have: * diff --git a/packages/platforms/android/src/commands/platforms/UnixProfiler.ts b/packages/platforms/android/src/commands/platforms/UnixProfiler.ts index be26fe66..67bbb3bf 100644 --- a/packages/platforms/android/src/commands/platforms/UnixProfiler.ts +++ b/packages/platforms/android/src/commands/platforms/UnixProfiler.ts @@ -255,4 +255,5 @@ export abstract class UnixProfiler implements Profiler { public abstract getDeviceProfilerPath(): string; public abstract detectCurrentBundleId(): string; public abstract supportFPS(): boolean; + public abstract detectDeviceRefreshRate(): number; } diff --git a/packages/platforms/android/src/index.ts b/packages/platforms/android/src/index.ts index f4acd9ae..5ec93d3f 100644 --- a/packages/platforms/android/src/index.ts +++ b/packages/platforms/android/src/index.ts @@ -1,6 +1,7 @@ export { Measure } from "@perf-profiler/types"; export { Measure as GfxInfoMeasure } from "./commands/gfxInfo/parseGfxInfo"; export { waitFor } from "./utils/waitFor"; +export { refreshRateManager } from "./commands/detectCurrentDeviceRefreshRate"; export { executeAsync, executeCommand } from "./commands/shell"; export { AndroidProfiler } from "./commands/platforms/AndroidProfiler"; export { FlashlightSelfProfiler } from "./commands/platforms/FlashlightSelfProfiler"; diff --git a/packages/platforms/ios/src/index.ts b/packages/platforms/ios/src/index.ts index adadcbb0..a7fc5161 100644 --- a/packages/platforms/ios/src/index.ts +++ b/packages/platforms/ios/src/index.ts @@ -104,4 +104,9 @@ export class IOSProfiler implements Profiler { killApp(bundleId); return new Promise((resolve) => resolve()); } + + // This is a placeholder for the method that will be implemented in the future + detectDeviceRefreshRate() { + return 60; + } }