Skip to content

[Performance] Raw Graph Performance Improvement #388

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: graph_performance
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion gui_dev/scripts/wdyr.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ if (
}
}

if (import.meta.env.DEV && import.meta.env.VITE_ENABLE_REACT_SCAN === "true") {
if (
import.meta.env.DEV &&
import.meta.env.VITE_ENABLE_REACT_SCAN === "true"
) {
try {
const { scan } = await import("react-scan");
scan({
Expand Down
2 changes: 0 additions & 2 deletions gui_dev/src/components/BandPowerGraph.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ export const BandPowerGraph = () => {

// Create a subscription to socket data updates
useEffect(() => {
console.log("subscribe!");
const unsubscribe = useSocketStore.subscribe((state, prevState) => {
const newData = state.getData(selectedChannel, usedChannels);

Expand All @@ -56,7 +55,6 @@ export const BandPowerGraph = () => {
});
});
return () => {
console.log("unsubscribe!");
unsubscribe();
};
}, []);
Expand Down
202 changes: 139 additions & 63 deletions gui_dev/src/components/RawDataGraph.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState, useMemo } from "react";
import { useEffect, useRef, useState } from "react";
import { useSocketStore } from "@/stores";
import { useSessionStore } from "@/stores/sessionStore";
import Plotly from "plotly.js-basic-dist-min";
Expand All @@ -13,7 +13,6 @@ import {
Slider,
} from "@mui/material";
import { CollapsibleBox } from "./CollapsibleBox";
import { getChannelAndFeature } from "./utils";
import { shallow } from "zustand/shallow";

// TODO redundant and might be candidate for refactor
Expand All @@ -32,28 +31,32 @@ export const RawDataGraph = ({
yAxisTitle = "Value",
}) => {
//const graphData = useSocketStore((state) => state.graphData);
const graphRawData = useSocketStore((state) => state.graphRawData);
const getAccuRawData = useSocketStore((state) => state.getAccuRawData);
const maxDataPoints = useSocketStore((state) => state.maxDataPoints);
const setMaxDataPoints = useSocketStore((state) => state.setMaxDataPoints);

// const currentXLength = useSocketStore((state) => state.currentXLength);
// const setCurrentXLength = useSocketStore((state) => state.setCurrentXLength);

const currentXLength = useRef(0);

const channels = useSessionStore((state) => state.channels, shallow);
const samplingRate = useSessionStore((state) => state.streamParameters.samplingRate);

const usedChannels = useMemo(
() => channels.filter((channel) => channel.used === 1),
[channels]
);

const availableChannels = useMemo(
() => usedChannels.map((channel) => channel.name),
[usedChannels]
);
const usedChannels = channels.filter((channel) => channel.used === 1);
const availableChannels = usedChannels.map((channel) => channel.name);

const [selectedChannels, setSelectedChannels] = useState([]);
const selectedChannelsRef = useRef(selectedChannels);

const hasInitialized = useRef(false);
const [rawData, setRawData] = useState({});
const graphRef = useRef(null);
const plotlyRef = useRef(null);
const prevDataRef = useRef(null);
const [yAxisMaxValue, setYAxisMaxValue] = useState("Auto");
const [maxDataPoints, setMaxDataPoints] = useState(10000);

const ganzLayout = useRef({})

const layoutRef = useRef({
// title: {
Expand Down Expand Up @@ -110,64 +113,116 @@ export const RawDataGraph = ({
setMaxDataPoints(newValue * samplingRate); // Convert seconds to samples
};

// Updates reference for selectedChannels to access in subscription
useEffect(() => {
if (usedChannels.length > 0 && !hasInitialized.current) {
const availableChannelNames = usedChannels.map((channel) => channel.name);
setSelectedChannels(availableChannelNames);
hasInitialized.current = true;
}
}, [usedChannels]);
selectedChannelsRef.current = selectedChannels;
}, [selectedChannels]);

// Process incoming graphData to extract raw data for each channel -> TODO: Check later if this fits here better than socketStore
// Creates a subscription to socket data updates
useEffect(() => {
// if (!graphData || Object.keys(graphData).length === 0) return;
if (!graphRawData || Object.keys(graphRawData).length === 0) return;

//const latestData = graphData;
const latestData = graphRawData;

setRawData((prevRawData) => {
const updatedRawData = { ...prevRawData };

Object.entries(latestData).forEach(([key, value]) => {
//const { channelName = "", featureName = "" } = getChannelAndFeature(
// availableChannels,
// key
//);

//if (!channelName) return;

//if (featureName !== "raw") return;

const channelName = key;
const colors = generateColors(selectedChannels.length);

if (!selectedChannels.includes(key)) return;
const unsubscribe = useSocketStore.subscribe((state, prevState) => {
const currentSelectedChannels = selectedChannelsRef.current;
const newData = state.getAccuRawData(currentSelectedChannels);

const traces = currentSelectedChannels.map((channelName, idx) => {
const yData = newData[channelName] || [];
const y = yData.slice().reverse();
const x = Array.from({ length: y.length }, (_, i) => i / samplingRate); // Convert samples to negative seconds

return {
x,
y,
type: "scattergl",
mode: "lines",
name: channelName,
hoverinfo: 'skip',
line: { simplify: false, color: colors[idx] },
yaxis: idx === 0 ? "y" : `y${idx + 1}`,
};
});

if (!updatedRawData[channelName]) {
updatedRawData[channelName] = [];
}
console.time('[Performance] React Plot Update: ');
Plotly
.react(plotlyRef.current, traces, ganzLayout.current)
.then(
() => console.timeEnd('[Performance] React Plot Update: ')
);


// let newDataLength = (newData[currentSelectedChannels[0]] || []).slice().length;

// console.log("currentXLength: ", currentXLength.current);
// console.log("newDataLength: ", newDataLength);
// if (currentXLength.current !== newDataLength){
// console.log("[DEBUG][IF] HERE")
// currentXLength.current = newDataLength;

// const traces = currentSelectedChannels.reduce((acc, channelName) => {
// const yData = newData[channelName] || [];
// const y = yData.slice().reverse();
// const x = Array.from({ length: y.length }, (_, i) => i / samplingRate); // Convert samples to negative seconds

// let newAcc = acc;
// newAcc['x'].push(x);
// newAcc['y'].push(y);

// return newAcc;
// }, {x: [], y: []});

// try {
// console.time('[Performance] FULL Restyle Plot Update: ');

// // { x: [xTraces], y: [yTraces] }
// Plotly.restyle(plotlyRef.current, traces).then(
// () => console.timeEnd('[Performance] FULL Restyle Plot Update: ')
// );
// } catch (error) {
// }


// } else {
// console.log("[DEBUG][ELSE] HERE")
// const yTraces = currentSelectedChannels.map((channelName) => {
// const yData = newData[channelName] || [];
// const y = yData.slice().reverse();

// return y;
// });

// try {
// console.time('[Performance] Restyle Plot Update: ');

// Plotly.restyle(plotlyRef.current, { y: yTraces }).then(
// () => console.timeEnd('[Performance] Restyle Plot Update: ')
// );
// } catch (error) {
// }
// }

updatedRawData[channelName].push(...value);

if (updatedRawData[channelName].length > maxDataPoints) {
updatedRawData[channelName] = updatedRawData[channelName].slice(
-maxDataPoints
);
}
});

return updatedRawData;
});
}, [graphRawData, availableChannels, maxDataPoints]);

return () => {
unsubscribe();
};

}, []);

// Initial render of the graph
useEffect(() => {
if (!graphRef.current) return;


if (selectedChannels.length === 0) {
Plotly.purge(graphRef.current);
return;
}

const initialData = getAccuRawData(selectedChannels);
if (!initialData) return;

const colors = generateColors(selectedChannels.length);

const totalChannels = selectedChannels.length;
Expand Down Expand Up @@ -205,8 +260,12 @@ export const RawDataGraph = ({
}
});


// let newDataLength = (initialData[selectedChannels[0]] || []).slice().length;
// console.log("[initial] newDataLength: ", newDataLength);
// setCurrentXLength(newDataLength);
const traces = selectedChannels.map((channelName, idx) => {
const yData = rawData[channelName] || [];
const yData = initialData[channelName] || [];
const y = yData.slice().reverse();
const x = Array.from({ length: y.length }, (_, i) => i / samplingRate); // Convert samples to negative seconds

Expand Down Expand Up @@ -236,17 +295,34 @@ export const RawDataGraph = ({
hovermode: false, // Add this line to disable hovermode in the trace
};

Plotly.react(graphRef.current, traces, layout, {
ganzLayout.current = layout;

Plotly.newPlot(graphRef.current, traces, layout, {
responsive: true,
displayModeBar: false,
}).then((gd) => {
plotlyRef.current = gd;
prevDataRef.current = initialData;
})
.then((gd) => {
plotlyRef.current = gd;
})
.catch((error) => {
console.error("Plotly error:", error);
});
}, [rawData, selectedChannels, yAxisMaxValue, maxDataPoints]);

return () => {
if (plotlyRef.current) {
Plotly.purge(plotlyRef.current);
}
};

}, [selectedChannels, yAxisMaxValue, maxDataPoints]);

// Initialize selected channels
useEffect(() => {

if (usedChannels.length > 0 && !hasInitialized.current) {
const availableChannelNames = usedChannels.map((channel) => channel.name);
setSelectedChannels(availableChannelNames);
hasInitialized.current = true;
}

}, [usedChannels]);

return (
<Box>
Expand Down
44 changes: 44 additions & 0 deletions gui_dev/src/stores/socketStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,18 @@ export const useSocketStore = createStore("socket", (set, get) => ({
error: null,
graphData: [],
graphRawData: [],
accuRawData: [],
graphDecodingData: [],
availableDecodingOutputs: [],
infoMessages: [],
reconnectTimer: null,
intentionalDisconnect: false,
messageCount: 0,
maxDataPoints: 10000,
currentXLength: 0,

setMaxDataPoints: (maxDataPoints) => set({ maxDataPoints }),
setCurrentXLength: (currentXLength) => set({ currentXLength }),

setSocket: (socket) => set({ socket }),

Expand Down Expand Up @@ -84,6 +90,7 @@ export const useSocketStore = createStore("socket", (set, get) => ({
const decodedData = CBOR.decode(event.data);
if (Object.keys(decodedData)[0] == "raw_data") {
set({ graphRawData: decodedData.raw_data });
get().setAccuRawData();
} else {
// check here if there are values in decodedData that start with "decoding"
// if so, set graphDecodingData to the value of those keys
Expand All @@ -104,6 +111,12 @@ export const useSocketStore = createStore("socket", (set, get) => ({
graphData: dataNonDecodingFeatures,
});
}

let currentMessageCount = get().messageCount;
if (currentMessageCount % 500 == 0){
console.log("[DEBUG] MessageCount: ", currentMessageCount);
}

set({
messageCount: get().messageCount + 1,
});
Expand Down Expand Up @@ -217,4 +230,35 @@ export const useSocketStore = createStore("socket", (set, get) => ({
};
}
},

setAccuRawData: () => {
const { graphRawData, maxDataPoints } = get();
const updatedAccuRawData = { ...get().accuRawData };

Object.entries(graphRawData).forEach(([key, value]) => {

if (!updatedAccuRawData[key]) {
updatedAccuRawData[key] = [];
}

updatedAccuRawData[key].push(...value);

// Trim data to `maxDataPoints`
if (updatedAccuRawData[key].length > maxDataPoints) {
updatedAccuRawData[key] = updatedAccuRawData[key].slice(-maxDataPoints);
}
});

set({ accuRawData: updatedAccuRawData });
},

getAccuRawData: (selectedChannels) => {
let rawData = get().accuRawData;

let filteredRawData = Object.fromEntries(
Object.entries(rawData).filter( ([key,valye]) => selectedChannels.includes(key) )
);

return filteredRawData;
}
}));
Loading