From c9ba4aa464468ff09c412871dfaa38d90b160124 Mon Sep 17 00:00:00 2001 From: Alexandre Neuwald CTW Date: Thu, 2 May 2024 11:52:25 +0100 Subject: [PATCH] Add performance panel --- packages/studio-base/src/i18n/en/panels.ts | 3 + .../PlaybackPerformance/index.stories.tsx | 32 ++++ .../src/panels/PlaybackPerformance/index.tsx | 163 ++++++++++++++++++ packages/studio-base/src/panels/index.ts | 6 + 4 files changed, 204 insertions(+) create mode 100644 packages/studio-base/src/panels/PlaybackPerformance/index.stories.tsx create mode 100644 packages/studio-base/src/panels/PlaybackPerformance/index.tsx diff --git a/packages/studio-base/src/i18n/en/panels.ts b/packages/studio-base/src/i18n/en/panels.ts index 8d255d16ca..4e7124db62 100644 --- a/packages/studio-base/src/i18n/en/panels.ts +++ b/packages/studio-base/src/i18n/en/panels.ts @@ -34,6 +34,9 @@ export const panels = { ROSDiagnosticSummaryDescription: "Display a summary of all ROS DiagnosticArray messages.", stateTransitions: "State Transitions", stateTransitionsDescription: "Track when values change over time.", + studioPlaybackPerformance: "Studio - Playback Performance", + studioPlaybackPerformanceDescription: + "Display playback and data-streaming performance statistics.", tab: "Tab", tabDescription: "Group panels together in a tabbed interface.", table: "Table", diff --git a/packages/studio-base/src/panels/PlaybackPerformance/index.stories.tsx b/packages/studio-base/src/panels/PlaybackPerformance/index.stories.tsx new file mode 100644 index 0000000000..0659486181 --- /dev/null +++ b/packages/studio-base/src/panels/PlaybackPerformance/index.stories.tsx @@ -0,0 +1,32 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/ +// +// This file incorporates work covered by the following copyright and +// permission notice: +// +// Copyright 2018-2021 Cruise LLC +// +// This source code is licensed under the Apache License, Version 2.0, +// found at http://www.apache.org/licenses/LICENSE-2.0 +// You may not use this file except in compliance with the License. + +import { StoryObj } from "@storybook/react"; + +import PanelSetup from "@foxglove/studio-base/stories/PanelSetup"; + +import PlaybackPerformance from "./index"; + +export default { + title: "panels/PlaybackPerformance", +}; + +export const SimpleExample: StoryObj = { + render: () => { + return ( + + + + ); + }, +}; diff --git a/packages/studio-base/src/panels/PlaybackPerformance/index.tsx b/packages/studio-base/src/panels/PlaybackPerformance/index.tsx new file mode 100644 index 0000000000..7465fb7836 --- /dev/null +++ b/packages/studio-base/src/panels/PlaybackPerformance/index.tsx @@ -0,0 +1,163 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/ +// +// This file incorporates work covered by the following copyright and +// permission notice: +// +// Copyright 2018-2021 Cruise LLC +// +// This source code is licensed under the Apache License, Version 2.0, +// found at http://www.apache.org/licenses/LICENSE-2.0 +// You may not use this file except in compliance with the License. + +import { Typography } from "@mui/material"; +import * as _ from "lodash-es"; +import { ReactElement } from "react"; + +import { subtract as subtractTimes, toSec } from "@foxglove/rostime"; +import { Immutable } from "@foxglove/studio"; +import { useMessagePipeline } from "@foxglove/studio-base/components/MessagePipeline"; +import Panel from "@foxglove/studio-base/components/Panel"; +import PanelToolbar from "@foxglove/studio-base/components/PanelToolbar"; +import { Sparkline, SparklinePoint } from "@foxglove/studio-base/components/Sparkline"; +import Stack from "@foxglove/studio-base/components/Stack"; +import { PlayerStateActiveData } from "@foxglove/studio-base/players/types"; + +const TIME_RANGE = 5000; + +type PlaybackPerformanceItemProps = { + points: SparklinePoint[]; + maximum: number; + decimalPlaces: number; + label: React.ReactNode; +}; + +function PlaybackPerformanceItem(props: PlaybackPerformanceItemProps): ReactElement { + return ( + + + + + {(_.last(props.points) ?? { value: 0 }).value.toFixed(props.decimalPlaces)} + {props.label} + + + {(_.sumBy(props.points, "value") / props.points.length).toFixed(props.decimalPlaces)} avg + + + + ); +} + +type UnconnectedPlaybackPerformanceProps = Immutable<{ + timestamp: number; + activeData?: PlayerStateActiveData; +}>; + +function UnconnectedPlaybackPerformance({ + timestamp, + activeData, +}: UnconnectedPlaybackPerformanceProps): JSX.Element { + const playbackInfo = + React.useRef>(); + const lastPlaybackInfo = playbackInfo.current; + if (activeData && (!playbackInfo.current || playbackInfo.current.activeData !== activeData)) { + playbackInfo.current = { timestamp, activeData }; + } + + const perfPoints = React.useRef<{ + speed: SparklinePoint[]; + framerate: SparklinePoint[]; + bagTimeMs: SparklinePoint[]; + megabitsPerSecond: SparklinePoint[]; + }>({ + speed: [], + framerate: [], + bagTimeMs: [], + megabitsPerSecond: [], + }); + + if ( + activeData && + playbackInfo.current && + lastPlaybackInfo && + lastPlaybackInfo.activeData !== activeData + ) { + const renderTimeMs = timestamp - lastPlaybackInfo.timestamp; + if ( + lastPlaybackInfo.activeData.isPlaying && + activeData.isPlaying && + lastPlaybackInfo.activeData.lastSeekTime === activeData.lastSeekTime && + lastPlaybackInfo.activeData.currentTime !== activeData.currentTime + ) { + const elapsedPlayerTime = + toSec(subtractTimes(activeData.currentTime, lastPlaybackInfo.activeData.currentTime)) * + 1000; + perfPoints.current.speed.push({ value: elapsedPlayerTime / renderTimeMs, timestamp }); + perfPoints.current.framerate.push({ value: 1000 / renderTimeMs, timestamp }); + perfPoints.current.bagTimeMs.push({ value: elapsedPlayerTime, timestamp }); + } + const newBytesReceived = + activeData.totalBytesReceived - lastPlaybackInfo.activeData.totalBytesReceived; + const newMegabitsReceived = (8 * newBytesReceived) / 1e6; + const megabitsPerSecond = newMegabitsReceived / (renderTimeMs / 1000); + perfPoints.current.megabitsPerSecond.push({ value: megabitsPerSecond, timestamp }); + for (const points of Object.values(perfPoints.current)) { + while (points[0] && points[0].timestamp < timestamp - TIME_RANGE) { + points.shift(); + } + } + } + + return ( + + + + × realtime} + /> + + + + + + ); +} + +function PlaybackPerformance() { + const timestamp = Date.now(); + const activeData = useMessagePipeline( + React.useCallback(({ playerState }) => playerState.activeData, []), + ); + return ; +} + +PlaybackPerformance.panelType = "PlaybackPerformance"; +PlaybackPerformance.defaultConfig = {}; + +export default Panel(PlaybackPerformance); diff --git a/packages/studio-base/src/panels/index.ts b/packages/studio-base/src/panels/index.ts index 468f259704..d6cba45104 100644 --- a/packages/studio-base/src/panels/index.ts +++ b/packages/studio-base/src/panels/index.ts @@ -172,4 +172,10 @@ export const getBuiltin: (t: TFunction<"panels">) => PanelInfo[] = (t) => [ module: async () => await import("./Tab"), hasCustomToolbar: true, }, + { + title: t("studioPlaybackPerformance"), + type: "PlaybackPerformance", + description: t("studioPlaybackPerformanceDescription"), + module: async () => await import("./PlaybackPerformance"), + }, ];