|
|
@@ -79,10 +79,12 @@ const CsvGraph = ({ rawData }) => {
|
|
|
complete: (results) => {
|
|
|
if (results.data.length === 0) throw new Error("No data found in CSV");
|
|
|
|
|
|
- // Clean keys (case insensitive, trim)
|
|
|
- const cleanedData = results.data.map(row => {
|
|
|
- const newRow = {};
|
|
|
+ // Clean keys and add timing index
|
|
|
+ const cleanedData = results.data.map((row, idx) => {
|
|
|
+ const newRow = { _index: idx };
|
|
|
Object.keys(row).forEach(k => {
|
|
|
+ // Remove non-ascii or special chars that might break recharts if needed,
|
|
|
+ // but trimmed is usually fine.
|
|
|
newRow[k.trim()] = row[k];
|
|
|
});
|
|
|
return newRow;
|
|
|
@@ -115,16 +117,76 @@ const CsvGraph = ({ rawData }) => {
|
|
|
},
|
|
|
cpu: {
|
|
|
total: findCol(/^cpu\(%\)$/i) || findCol(/^cpu usage$/i),
|
|
|
- cores: columns.filter(c => /^cpu\d+\(%\)$/i.test(c))
|
|
|
+ cores: columns.filter(c => /^cpu\d+\(%\)$/i.test(c)),
|
|
|
+ clocks: columns.filter(c => /^cpu\d+\(mhz\)$/i.test(c))
|
|
|
},
|
|
|
power: {
|
|
|
current: findCol(/^current\(ma\)$/i),
|
|
|
power: findCol(/^power\(mw\)$/i),
|
|
|
- temp: findCol(/^battery temp/i) || findCol(/^temp/i)
|
|
|
+ // Handle both Battery(℃) and Battery(°C) and generic Temp
|
|
|
+ temp: findCol(/battery.*temp/i) || findCol(/battery.*℃/i) || findCol(/battery.*°C/i) || findCol(/^temp/i),
|
|
|
+ level: findCol(/^battery\(%\)$/i) || findCol(/^battery$/i)
|
|
|
}
|
|
|
};
|
|
|
}, [columns]);
|
|
|
|
|
|
+ const stats = useMemo(() => {
|
|
|
+ if (!data.length || !groups) return null;
|
|
|
+
|
|
|
+ const fpsKey = groups.fps.fps;
|
|
|
+ const fpsValues = fpsKey ? data.map(d => d[fpsKey]).filter(v => typeof v === 'number') : [];
|
|
|
+
|
|
|
+ const avg = (key) => {
|
|
|
+ if (!key) return null;
|
|
|
+ const values = data.map(d => d[key]).filter(v => typeof v === 'number');
|
|
|
+ return values.length ? values.reduce((a, b) => a + b, 0) / values.length : null;
|
|
|
+ };
|
|
|
+
|
|
|
+ const max = (key) => {
|
|
|
+ if (!key) return null;
|
|
|
+ const values = data.map(d => d[key]).filter(v => typeof v === 'number');
|
|
|
+ return values.length ? Math.max(...values) : null;
|
|
|
+ };
|
|
|
+
|
|
|
+ let minFps = null;
|
|
|
+ let fps5PercentLows = null;
|
|
|
+ let above45Percent = null;
|
|
|
+
|
|
|
+ if (fpsValues.length > 0) {
|
|
|
+ minFps = Math.min(...fpsValues);
|
|
|
+ const sortedFps = [...fpsValues].sort((a, b) => a - b);
|
|
|
+ const fivePercentCount = Math.max(1, Math.floor(sortedFps.length * 0.05));
|
|
|
+ fps5PercentLows = sortedFps.slice(0, fivePercentCount).reduce((a, b) => a + b, 0) / fivePercentCount;
|
|
|
+ above45Percent = (fpsValues.filter(v => v >= 45).length / fpsValues.length) * 100;
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ avgFps: avg(fpsKey),
|
|
|
+ minFps,
|
|
|
+ fps5PercentLows,
|
|
|
+ above45Percent,
|
|
|
+ avgPower: avg(groups.power.power),
|
|
|
+ maxTemp: max(groups.power.temp)
|
|
|
+ };
|
|
|
+ }, [data, groups]);
|
|
|
+
|
|
|
+ const renderXAxis = (hide = false) => (
|
|
|
+ <XAxis
|
|
|
+ dataKey="_index"
|
|
|
+ hide={hide}
|
|
|
+ tickFormatter={(val) => {
|
|
|
+ const totalSeconds = parseInt(val, 10);
|
|
|
+ const minutes = Math.floor(totalSeconds / 60);
|
|
|
+ const seconds = totalSeconds % 60;
|
|
|
+ return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
|
|
+ }}
|
|
|
+ tick={{ fontSize: 9 }}
|
|
|
+ stroke="#9ca3af"
|
|
|
+ interval="preserveStartEnd"
|
|
|
+ minTickGap={40}
|
|
|
+ />
|
|
|
+ );
|
|
|
+
|
|
|
if (loading) return (
|
|
|
<div className="w-full h-64 flex flex-col items-center justify-center space-y-4">
|
|
|
<div className="w-12 h-12 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
|
|
|
@@ -144,13 +206,43 @@ const CsvGraph = ({ rawData }) => {
|
|
|
const isGeneric = !groups.fps.fps && !groups.cpu.total && !groups.power.power;
|
|
|
|
|
|
return (
|
|
|
- <div className="w-full space-y-12">
|
|
|
+ <div className="w-full space-y-6">
|
|
|
+ {/* Summary Stats */}
|
|
|
+ {stats && (
|
|
|
+ <div className="grid grid-cols-3 md:grid-cols-6 bg-gray-100 dark:bg-gray-800/40 rounded-lg divide-x divide-y md:divide-y-0 divide-gray-200 dark:divide-gray-700 border theme-border overflow-hidden">
|
|
|
+ <div className="text-center py-2 px-1">
|
|
|
+ <p className="text-[9px] font-bold text-gray-500 uppercase tracking-tighter opacity-70">Avg FPS</p>
|
|
|
+ <p className="text-lg font-black theme-text leading-none mt-1">{stats.avgFps !== null ? stats.avgFps.toFixed(1) : '-'}</p>
|
|
|
+ </div>
|
|
|
+ <div className="text-center py-2 px-1">
|
|
|
+ <p className="text-[9px] font-bold text-gray-500 uppercase tracking-tighter opacity-70">Min FPS</p>
|
|
|
+ <p className="text-lg font-black theme-text leading-none mt-1">{stats.minFps !== null ? stats.minFps.toFixed(1) : '-'}</p>
|
|
|
+ </div>
|
|
|
+ <div className="text-center py-2 px-1">
|
|
|
+ <p className="text-[9px] font-bold text-gray-500 uppercase tracking-tighter opacity-70">5% Lows</p>
|
|
|
+ <p className="text-lg font-black theme-text leading-none mt-1">{stats.fps5PercentLows !== null ? stats.fps5PercentLows.toFixed(1) : '-'}</p>
|
|
|
+ </div>
|
|
|
+ <div className="text-center py-2 px-1">
|
|
|
+ <p className="text-[9px] font-bold text-gray-500 uppercase tracking-tighter opacity-70">≥45 FPS %</p>
|
|
|
+ <p className="text-lg font-black theme-text leading-none mt-1">{stats.above45Percent !== null ? `${stats.above45Percent.toFixed(0)}%` : '-'}</p>
|
|
|
+ </div>
|
|
|
+ <div className="text-center py-2 px-1">
|
|
|
+ <p className="text-[9px] font-bold text-gray-500 uppercase tracking-tighter opacity-70">Avg Power</p>
|
|
|
+ <p className="text-lg font-black theme-text leading-none mt-1">{stats.avgPower !== null ? `${stats.avgPower.toFixed(0)} mW` : '-'}</p>
|
|
|
+ </div>
|
|
|
+ <div className="text-center py-2 px-1">
|
|
|
+ <p className="text-[9px] font-bold text-gray-500 uppercase tracking-tighter opacity-70">Max Temp</p>
|
|
|
+ <p className="text-lg font-black text-red-500 leading-none mt-1">{stats.maxTemp !== null ? `${stats.maxTemp.toFixed(1)}°` : '-'}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
{/* 1. FPS & Stutter */}
|
|
|
{(groups.fps.fps || groups.fps.jank) && (
|
|
|
<PerformanceCard title="FPS and Stutter (Jank)">
|
|
|
<ComposedChart data={data} margin={{ top: 10, right: 10, left: 10, bottom: 0 }}>
|
|
|
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e5e7eb" />
|
|
|
- <XAxis dataKey={null} tick={false} hide />
|
|
|
+ {renderXAxis()}
|
|
|
<YAxis domain={[0, 'auto']} tick={{ fontSize: 10 }} stroke="#9ca3af" />
|
|
|
<Tooltip content={<CustomTooltip />} />
|
|
|
<Legend verticalAlign="top" height={36} />
|
|
|
@@ -168,28 +260,47 @@ const CsvGraph = ({ rawData }) => {
|
|
|
</PerformanceCard>
|
|
|
)}
|
|
|
|
|
|
- {/* 2. CPU Usage */}
|
|
|
- {groups.cpu.total && (
|
|
|
- <PerformanceCard title="CPU Usage">
|
|
|
- <LineChart data={data} margin={{ top: 10, right: 10, left: 10, bottom: 0 }}>
|
|
|
+ {/* 2. CPU Usage & Clocks */}
|
|
|
+ {(groups.cpu.total || groups.cpu.clocks.length > 0) && (
|
|
|
+ <PerformanceCard title="CPU Usage and Clock Speeds">
|
|
|
+ <ComposedChart data={data} margin={{ top: 10, right: 10, left: 10, bottom: 0 }}>
|
|
|
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e5e7eb" />
|
|
|
- <XAxis dataKey={null} tick={false} hide />
|
|
|
- <YAxis domain={[0, 100]} tick={{ fontSize: 10 }} stroke="#9ca3af" />
|
|
|
+ {renderXAxis()}
|
|
|
+ <YAxis yAxisId="left" domain={[0, 100]} tick={{ fontSize: 10 }} stroke="#10b981" name="Usage %" />
|
|
|
+ <YAxis yAxisId="right" orientation="right" tick={{ fontSize: 10 }} stroke="#9ca3af" name="Clock (MHz)" />
|
|
|
<Tooltip content={<CustomTooltip />} />
|
|
|
- <Legend verticalAlign="top" height={36} />
|
|
|
- <Line type="monotone" dataKey={groups.cpu.total} stroke="#10b981" strokeWidth={1.5} dot={false} name="Total CPU %" isAnimationActive={false} />
|
|
|
- </LineChart>
|
|
|
+ <Legend verticalAlign="top" height={36} wrapperStyle={{ fontSize: '10px' }} />
|
|
|
+
|
|
|
+ {groups.cpu.total && (
|
|
|
+ <Line yAxisId="left" type="monotone" dataKey={groups.cpu.total} stroke="#10b981" strokeWidth={2.5} dot={false} name="Total CPU %" isAnimationActive={false} />
|
|
|
+ )}
|
|
|
+
|
|
|
+ {groups.cpu.clocks.map((clock, idx) => (
|
|
|
+ <Line
|
|
|
+ key={clock}
|
|
|
+ yAxisId="right"
|
|
|
+ type="monotone"
|
|
|
+ dataKey={clock}
|
|
|
+ stroke={`hsl(${idx * 40}, 60%, 50%)`}
|
|
|
+ strokeWidth={0.8}
|
|
|
+ dot={false}
|
|
|
+ name={clock}
|
|
|
+ opacity={0.25}
|
|
|
+ isAnimationActive={false}
|
|
|
+ />
|
|
|
+ ))}
|
|
|
+ </ComposedChart>
|
|
|
</PerformanceCard>
|
|
|
)}
|
|
|
|
|
|
- {/* 3. Power & Temp */}
|
|
|
- {(groups.power.power || groups.power.temp) && (
|
|
|
- <PerformanceCard title="Power Consumption and Battery Temperature">
|
|
|
+ {/* 3. Power & Temp & Battery */}
|
|
|
+ {(groups.power.power || groups.power.temp || groups.power.level) && (
|
|
|
+ <PerformanceCard title="Power, Thermal and Battery Level">
|
|
|
<ComposedChart data={data} margin={{ top: 10, right: 10, left: 10, bottom: 0 }}>
|
|
|
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e5e7eb" />
|
|
|
- <XAxis dataKey={null} tick={false} hide />
|
|
|
+ {renderXAxis()}
|
|
|
<YAxis yAxisId="left" tick={{ fontSize: 10 }} stroke="#8b5cf6" name="Power (mW)" />
|
|
|
- <YAxis yAxisId="right" orientation="right" tick={{ fontSize: 10 }} stroke="#ef4444" name="Temp (°C)" domain={['dataMin - 1', 'dataMax + 1']} />
|
|
|
+ <YAxis yAxisId="right" orientation="right" tick={{ fontSize: 10 }} stroke="#ef4444" name="Temp/Level" domain={['auto', 'auto']} />
|
|
|
<Tooltip content={<CustomTooltip />} />
|
|
|
<Legend verticalAlign="top" height={36} />
|
|
|
|
|
|
@@ -197,7 +308,10 @@ const CsvGraph = ({ rawData }) => {
|
|
|
<Line yAxisId="left" type="monotone" dataKey={groups.power.power} stroke="#8b5cf6" strokeWidth={1.5} dot={false} name="Power (mW)" isAnimationActive={false} />
|
|
|
)}
|
|
|
{groups.power.temp && (
|
|
|
- <Line yAxisId="right" type="stepAfter" dataKey={groups.power.temp} stroke="#ef4444" strokeWidth={2} strokeDasharray="4 4" dot={false} name="Battery Temp (°C)" isAnimationActive={false} />
|
|
|
+ <Line yAxisId="right" type="monotone" dataKey={groups.power.temp} stroke="#ef4444" strokeWidth={2} strokeDasharray="4 4" dot={false} name="Battery Temp (°C)" isAnimationActive={false} />
|
|
|
+ )}
|
|
|
+ {groups.power.level && (
|
|
|
+ <Line yAxisId="right" type="stepAfter" dataKey={groups.power.level} stroke="#3b82f6" strokeWidth={2} dot={false} name="Battery Level (%)" isAnimationActive={false} />
|
|
|
)}
|
|
|
</ComposedChart>
|
|
|
</PerformanceCard>
|
|
|
@@ -218,6 +332,17 @@ const CsvGraph = ({ rawData }) => {
|
|
|
</LineChart>
|
|
|
</PerformanceCard>
|
|
|
)}
|
|
|
+
|
|
|
+ {/* Shared Brush for scrolling */}
|
|
|
+ {data.length > 0 && (
|
|
|
+ <div className="h-12 w-full mt-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg p-2">
|
|
|
+ <ResponsiveContainer width="100%" height="100%">
|
|
|
+ <LineChart data={data}>
|
|
|
+ <Brush height={20} stroke="#3b82f6" fill="transparent" />
|
|
|
+ </LineChart>
|
|
|
+ </ResponsiveContainer>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
</div>
|
|
|
);
|
|
|
};
|