Adam Jafarov 3 долоо хоног өмнө
parent
commit
69f6b8d03b

+ 1 - 1
backend/config.json

@@ -1,4 +1,4 @@
 {
-  "activeTheme": "ocean",
+  "activeTheme": "dark",
   "postWidth": "max-w-6xl"
 }

+ 1 - 1
public/posts

@@ -1 +1 @@
-Subproject commit c044583471bad933b17f65372dfaa23dfc1011ed
+Subproject commit 39ec8fb8d003a0abab3adfd84769590cb20c3dd7

+ 147 - 22
src/components/CsvGraph.jsx

@@ -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>
     );
 };