Compare commits

...
Author SHA1 Message Date
Oshgnacknak b8a83d2cc1 Increase laser cache 2025-10-31 19:13:10 +01:00
kraemr 212cf50f0b write to gpu_info.json instead 2025-10-31 19:04:21 +01:00
kraemr e95fa23c34 added gpu debugging 2025-10-31 19:04:20 +01:00
4 changed files with 347 additions and 5 deletions

No files matched your search

+17 -1
View File
@@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import React, { useEffect,Profiler } from "react";
import { createRoot } from "react-dom/client";
import { syncThemeWithLocal } from "./helpers/theme_helpers";
import { useTranslation } from "react-i18next";
@@ -10,6 +10,18 @@ import { Toaster } from "./components/ui/sonner";
import { enableMapSet } from "immer";
import { useGlobalLogStreaming } from "./hooks/useGlobalLogStreaming";
/*
function onRender(id, phase, actualDuration, baseDuration, startTime, commitTime) {
console.log(id,phase,actualDuration,baseDuration,startTime,commitTime)
}*/
function onRender(id, phase, actualDuration) {
if (actualDuration > 10) { // threshold in ms
console.warn(`${id} ${phase} took ${actualDuration.toFixed(1)}ms`);
}
}
export default function App() {
const { i18n } = useTranslation();
@@ -28,9 +40,13 @@ export default function App() {
enableMapSet();
const root = createRoot(document.getElementById("app")!);
root.render(
<React.StrictMode>
<Profiler id="App" onRender={onRender}>
<App />
</Profiler>
<Toaster />
</React.StrictMode>,
);
@@ -0,0 +1,312 @@
import React, { useRef, useState, useCallback, useEffect } from "react";
import uPlot from "uplot";
import "uplot/dist/uPlot.min.css";
import { renderUnitSymbol, getUnitIcon } from "@/control/units";
import { Icon } from "@/components/Icon";
import { BigGraphProps, HandlerRefs, SeriesData } from "./types";
import { GraphExportData } from "./excelExport";
import { DEFAULT_COLORS } from "./constants";
import {
useAnimationRefs,
stopAnimations,
normalizeDataSeries,
getPrimarySeries,
formatDisplayValue,
} from "./animation";
import { useLiveMode } from "./liveMode";
import { useHistoricalMode } from "./historicalMode";
import { useBigGraphEffects } from "./useBigGraphEffects";
import { TouchButton } from "../touch/TouchButton";
import { ControlCard } from "@/control/ControlCard";
// Utility: lazy filter series to only points in window
function sliceSeriesForWindow(
series: SeriesData,
startTime: number,
endTime: number
): SeriesData {
if (!series.newData?.long) return series;
return {
...series,
newData: {
...series.newData,
long: series.newData.long.filter(
([ts]) => ts >= startTime && ts <= endTime
),
},
};
}
// Merge config with visible lines
const mergeConfigWithVisibleLines = (config: any, data: any, visible: boolean[]) => {
const configLines = config.lines || [];
const seriesLines: any[] = [];
const seriesArray = Array.isArray(data) ? data : [data];
seriesArray.forEach((series, i) => {
if (visible[i] && series.lines) {
series.lines.forEach((line: any) => {
seriesLines.push({
...line,
color: line.color || series.color,
seriesIndex: i,
seriesTitle: series.title,
});
});
}
});
return {
...config,
lines: [...configLines, ...seriesLines],
};
};
export function BigGraphLazy({
newData,
unit,
renderValue,
config,
graphId,
syncGraph,
onRegisterForExport,
onUnregisterFromExport,
}: BigGraphProps & {
onRegisterForExport?: (graphId: string, getDataFn: () => GraphExportData | null) => void;
onUnregisterFromExport?: (graphId: string) => void;
}) {
const containerRef = useRef<HTMLDivElement | null>(null);
const uplotRef = useRef<uPlot | null>(null);
const chartCreatedRef = useRef(false);
const animationRefs = useAnimationRefs();
const normalizedSeries = normalizeDataSeries(newData);
const [visibleSeries, setVisibleSeries] = useState(
new Array(normalizedSeries.length).fill(true)
);
const [viewMode, setViewMode] = useState<"default" | "all" | "manual">(syncGraph?.viewMode ?? "default");
const [isLiveMode, setIsLiveMode] = useState(syncGraph?.isLiveMode ?? true);
const [selectedTimeWindow, setSelectedTimeWindow] = useState<number | "all">(
syncGraph?.timeWindow ?? config.defaultTimeWindow ?? 30 * 60 * 1000
);
const [cursorValue, setCursorValue] = useState<number | null>(null);
const [cursorValues, setCursorValues] = useState<(number | null)[]>(
new Array(normalizedSeries.length).fill(null)
);
const startTimeRef = useRef<number | null>(null);
const manualScaleRef = useRef<{ x: { min: number; max: number }; y: { min: number; max: number } } | null>(null);
const lastProcessedCountRef = useRef(0);
const handlerRefs: HandlerRefs = {
isUserZoomingRef: useRef(false),
isDraggingRef: useRef(false),
lastDragXRef: useRef<number | null>(null),
isPinchingRef: useRef(false),
lastPinchDistanceRef: useRef<number | null>(null),
pinchCenterRef: useRef<{ x: number; y: number } | null>(null),
touchStartRef: useRef<{ x: number; y: number; time: number } | null>(null),
touchDirectionRef: useRef<"horizontal" | "vertical" | "unknown">("unknown"),
};
const colors = {
primary: config.colors?.primary ?? DEFAULT_COLORS.primary,
grid: config.colors?.grid ?? DEFAULT_COLORS.grid,
axis: config.colors?.axis ?? DEFAULT_COLORS.axis,
background: config.colors?.background ?? DEFAULT_COLORS.background,
};
// Lazy slice series for window
const lazyData = React.useMemo(() => {
const startTime = startTimeRef.current ?? Date.now() - (selectedTimeWindow === "all" ? Infinity : selectedTimeWindow);
const endTime = startTime + (selectedTimeWindow === "all" ? Infinity : selectedTimeWindow);
return normalizedSeries.map((series) =>
sliceSeriesForWindow(series, startTime, endTime)
);
}, [normalizedSeries, selectedTimeWindow]);
const enhancedConfig = React.useMemo(() => mergeConfigWithVisibleLines(config, lazyData, visibleSeries), [
config,
lazyData,
visibleSeries,
]);
// Update cursor values when series length changes
useEffect(() => {
setCursorValues(new Array(normalizedSeries.length).fill(null));
}, [normalizedSeries.length]);
// Toggle series visibility
const toggleSeries = useCallback((index: number) => {
setVisibleSeries((prev) => {
const newVisibility = [...prev];
const currentlyVisible = newVisibility.filter((v) => v);
const wouldHideAll = newVisibility[index] && currentlyVisible.length === 1;
if (wouldHideAll) return prev;
newVisibility[index] = !newVisibility[index];
return newVisibility;
});
}, []);
// Live & historical modes
const liveMode = useLiveMode({
newData: lazyData,
uplotRef,
config: enhancedConfig,
animationRefs,
viewMode,
selectedTimeWindow,
startTimeRef,
updateYAxisScale: () => {}, // handled by uPlot internal lazy updates
lastProcessedCountRef,
chartCreatedRef,
});
const historicalMode = useHistoricalMode({
newData: lazyData,
uplotRef,
animationRefs,
getCurrentLiveEndTimestamp: liveMode.getCurrentLiveEndTimestamp,
updateYAxisScale: () => {},
lastProcessedCountRef,
manualScaleRef,
});
const handleTimeWindowChange = useCallback(
(newWindow: number | "all") => {
stopAnimations(animationRefs);
setSelectedTimeWindow(newWindow);
if (newWindow === "all") {
setViewMode("all");
if (!isLiveMode) {
setIsLiveMode(true);
historicalMode.switchToLiveMode();
}
liveMode.handleLiveTimeWindow(newWindow);
} else {
setViewMode("default");
if (isLiveMode) {
liveMode.handleLiveTimeWindow(newWindow);
} else {
historicalMode.handleHistoricalTimeWindow(newWindow);
}
}
},
[animationRefs, isLiveMode, liveMode, historicalMode]
);
// Effects for chart creation and updates
useBigGraphEffects({
containerRef,
uplotRef,
startTimeRef,
manualScaleRef,
lastProcessedCountRef,
animationRefs,
handlerRefs,
chartCreatedRef,
newData: lazyData,
unit,
renderValue,
config: enhancedConfig,
graphId,
syncGraph,
viewMode,
isLiveMode,
selectedTimeWindow,
visibleSeries,
setSelectedTimeWindow,
setViewMode,
setIsLiveMode,
setCursorValue,
setCursorValues,
liveMode,
historicalMode,
colors,
handleTimeWindowChangeInternal: handleTimeWindowChange,
});
// Get series display value
const getSeriesDisplayValue = useCallback(
(index: number) => {
const series = lazyData[index];
if (!series) return null;
return lazyData.length > 1
? cursorValues[index] ?? series.newData?.current?.value
: cursorValue ?? series.newData?.current?.value;
},
[lazyData, cursorValues, cursorValue]
);
const displayValue = getSeriesDisplayValue(0);
const defaultColors = ["#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6"];
return (
<div className="h-[50vh] w-full">
<div className="flex h-full w-full flex-col overflow-hidden rounded-3xl border border-gray-200 bg-white shadow">
{/* Header */}
<div className="flex items-center justify-between pt-4 pr-5 pb-6 pl-6">
<div className="mt-1 flex items-center gap-4">
<Icon name={unit ? getUnitIcon(unit) : "lu:TrendingUp"} className="size-6 text-gray-600" />
<h2 className="text-2xl leading-none font-bold text-gray-900">{enhancedConfig.title}</h2>
</div>
{/* Series values / toggles */}
<div className="flex items-center gap-4">
{lazyData.length === 1 && (
<ControlCard className="rounded-md px-4 py-3">
<div className="flex items-center gap-2 text-base text-gray-600">
<span className="font-mono leading-none font-bold text-gray-900">
{formatDisplayValue(displayValue, renderValue)}
</span>
<span className="leading-none text-gray-500">{renderUnitSymbol(unit)}</span>
</div>
</ControlCard>
)}
{lazyData.length > 1 && (
<div className="flex items-center gap-2">
{lazyData.map((series, index) => {
const seriesColor = series.color ?? defaultColors[index % defaultColors.length];
const val = getSeriesDisplayValue(index);
const formatted = formatDisplayValue(val, renderValue);
const isLastVisible = visibleSeries[index] && visibleSeries.filter(Boolean).length === 1;
return (
<TouchButton
key={index}
onClick={() => !isLastVisible && toggleSeries(index)}
className={`rounded-md px-4 py-1.5 text-sm transition-all ${
visibleSeries[index] ? "text-white shadow-md" : "bg-gray-100 text-gray-500 hover:bg-gray-200"
} ${isLastVisible ? "cursor-not-allowed" : ""}`}
style={{ backgroundColor: visibleSeries[index] ? seriesColor : undefined }}
title={`${series.title || `S${index + 1}`}: ${formatted} ${renderUnitSymbol(unit)}`}
>
<div className="flex flex-col items-center">
<span className="text-xs font-medium">{series.title || `S${index + 1}`}</span>
<span className={`font-mono leading-none font-bold ${visibleSeries[index] ? "text-white" : "text-gray-500"}`}>
{formatted} {renderUnitSymbol(unit)}
</span>
</div>
</TouchButton>
);
})}
</div>
)}
</div>
</div>
<div className="-mt-2 px-6">
<div className="h-px bg-gray-200"></div>
</div>
<div className="flex-1 overflow-hidden rounded-b-3xl pt-4">
<div ref={containerRef} className="h-full w-full overflow-hidden" style={{ backgroundColor: colors.background }} />
</div>
</div>
</div>
);
}
+16 -2
View File
@@ -7,10 +7,24 @@ import {
installExtension,
REACT_DEVELOPER_TOOLS,
} from "electron-devtools-installer";
// Set consistent app ID for Windows taskbar and GNOME dock integration
const fs = require('fs');
const path = require('path');
// Set consistent app ID for Windows
//taskbar and GNOME dock integration
app.setAppUserModelId("de.qitech.control-electron");
app.getGPUInfo('complete').then((data) => {
const filePath = path.join('/tmp', 'gpu_info.json');
fs.writeFile(filePath, JSON.stringify(data, null, 2), (err) => {
if (err) {
console.error('Failed to write GPU info:', err);
} else {
console.log('GPU info written to', filePath);
}
});
});
// Ensure single instance
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
+2 -2
View File
@@ -73,11 +73,11 @@ impl CacheableEvents<Self> for LaserEvents {
}
fn event_cache_fn(&self) -> CacheFn {
let cache_one_hour = cache_duration(Duration::from_secs(60 * 60), Duration::from_secs(1));
let cache_one_day = cache_duration(Duration::from_secs(24 * 60 * 60), Duration::from_secs(1));
let cache_first_and_last = cache_first_and_last_event();
match self {
Self::LiveValues(_) => cache_one_hour,
Self::LiveValues(_) => cache_one_day,
Self::State(_) => cache_first_and_last,
}
}