From 50ae9ba9aade13032466bcb53681caa17e191eb2 Mon Sep 17 00:00:00 2001 From: JavaZero <71128095+JavaZeroo@users.noreply.github.com> Date: Tue, 19 Aug 2025 17:44:07 +0800 Subject: [PATCH] feat: align logs using step keyword --- src/App.jsx | 34 ++++++++ src/components/ChartContainer.jsx | 124 +++++++++++++++++++----------- 2 files changed, 114 insertions(+), 44 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index fce2a15..6e46e27 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -40,6 +40,8 @@ function App() { const [xRange, setXRange] = useState({ min: undefined, max: undefined }); const [maxStep, setMaxStep] = useState(0); const [sidebarVisible, setSidebarVisible] = useState(true); + const [useStepKeyword, setUseStepKeyword] = useState(false); + const [stepKeyword, setStepKeyword] = useState('step:'); const handleFilesUploaded = useCallback((files) => { const filesWithDefaults = files.map(file => ({ @@ -395,6 +397,36 @@ function App() { +
+

Step 关键词对齐

+
+ setUseStepKeyword(e.target.checked)} + className="h-4 w-4 text-blue-600 border-gray-300 rounded" + /> + +
+ {useStepKeyword && ( +
+ + setStepKeyword(e.target.value)} + className="w-full px-2 py-1 text-xs border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none" + placeholder="step:" + /> +
+ )} +
@@ -414,6 +446,8 @@ function App() { xRange={xRange} onXRangeChange={setXRange} onMaxStepChange={setMaxStep} + useStepKeyword={useStepKeyword} + stepKeyword={stepKeyword} /> diff --git a/src/components/ChartContainer.jsx b/src/components/ChartContainer.jsx index b10f0f5..1d38cdd 100644 --- a/src/components/ChartContainer.jsx +++ b/src/components/ChartContainer.jsx @@ -40,8 +40,10 @@ const ChartWrapper = ({ data, options, chartId, onRegisterChart, onSyncHover }) ...options, onHover: (event, activeElements) => { if (activeElements.length > 0) { - const step = activeElements[0].index; - onSyncHover(step, chartId); + const { datasetIndex, index } = activeElements[0]; + const point = chartRef.current?.data?.datasets?.[datasetIndex]?.data?.[index]; + const step = point?.x; + onSyncHover(step ?? null, chartId); } else { onSyncHover(null, chartId); } @@ -68,7 +70,9 @@ export default function ChartContainer({ absoluteBaseline = 0.005, xRange = { min: undefined, max: undefined }, onXRangeChange, - onMaxStepChange + onMaxStepChange, + useStepKeyword = false, + stepKeyword = 'step:' }) { const chartRefs = useRef(new Map()); const registerChart = useCallback((id, inst) => { @@ -85,8 +89,9 @@ export default function ChartContainer({ } else if (id !== sourceId) { const activeElements = []; chart.data.datasets.forEach((dataset, datasetIndex) => { - if (dataset.data && dataset.data.length > step) { - activeElements.push({ datasetIndex, index: step }); + const index = dataset.data.findIndex(p => p.x === step); + if (index !== -1) { + activeElements.push({ datasetIndex, index }); } }); chart.setActiveElements(activeElements); @@ -103,39 +108,46 @@ export default function ChartContainer({ const lines = file.content.split('\n'); const metricsData = {}; - const extractByKeyword = (content, keyword) => { - const results = []; - const numberRegex = /[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/; - content.split('\n').forEach(line => { - const idx = line.toLowerCase().indexOf(keyword.toLowerCase()); - if (idx !== -1) { - const after = line.substring(idx + keyword.length); - const match = after.match(numberRegex); - if (match) { - const v = parseFloat(match[0]); - if (!isNaN(v)) results.push(v); - } + metrics.forEach(metric => { + metricsData[metric.name || metric.keyword] = []; + }); + + const numberRegex = /[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/; + const stepKeyLower = stepKeyword.toLowerCase(); + + lines.forEach(line => { + let stepVal; + if (useStepKeyword) { + const idxStep = line.toLowerCase().indexOf(stepKeyLower); + if (idxStep !== -1) { + const afterStep = line.substring(idxStep + stepKeyword.length); + const mStep = afterStep.match(/[+-]?\d+/); + if (mStep) stepVal = parseInt(mStep[0]); } - }); - return results; - }; + if (stepVal === undefined) return; + } - metrics.forEach(metric => { - let values = []; - if (metric.mode === 'keyword') { - values = extractByKeyword(file.content, metric.keyword); - } else if (metric.regex) { - const reg = new RegExp(metric.regex); - lines.forEach(line => { + metrics.forEach(metric => { + let value; + if (metric.mode === 'keyword') { + const idx = line.toLowerCase().indexOf(metric.keyword.toLowerCase()); + if (idx !== -1) { + const after = line.substring(idx + metric.keyword.length); + const m = after.match(numberRegex); + if (m) value = parseFloat(m[0]); + } + } else if (metric.regex) { + const reg = new RegExp(metric.regex); reg.lastIndex = 0; const m = reg.exec(line); - if (m && m[1]) { - const v = parseFloat(m[1]); - if (!isNaN(v)) values.push(v); - } - }); - } - metricsData[metric.name || metric.keyword] = values.map((v, i) => ({ x: i, y: v })); + if (m && m[1]) value = parseFloat(m[1]); + } + if (value !== undefined && !isNaN(value)) { + const arr = metricsData[metric.name || metric.keyword]; + const x = useStepKeyword ? stepVal : arr.length; + arr.push({ x, y: value }); + } + }); }); const range = file.config?.dataRange; @@ -147,7 +159,7 @@ export default function ChartContainer({ const endIndex = Math.min(data.length, end); return data.slice(start, endIndex); }; - const reindex = data => data.map((p, idx) => ({ x: idx, y: p.y })); + const reindex = data => data.map((p, idx) => (useStepKeyword ? { x: p.x, y: p.y } : { x: idx, y: p.y })); Object.keys(metricsData).forEach(k => { metricsData[k] = reindex(applyRange(metricsData[k])); }); @@ -155,7 +167,7 @@ export default function ChartContainer({ return { ...file, metricsData }; }); - }, [files, metrics]); + }, [files, metrics, useStepKeyword, stepKeyword]); useEffect(() => { const maxStep = parsedData.reduce((m, f) => { @@ -166,15 +178,39 @@ export default function ChartContainer({ }, [parsedData, onMaxStepChange]); useEffect(() => { - const minSteps = getMinSteps(parsedData); - if (minSteps > 0) { - onXRangeChange(prev => { - const next = { min: 0, max: minSteps - 1 }; - if (prev.min === next.min && prev.max === next.max) return prev; - return next; - }); + if (useStepKeyword) { + const minStart = parsedData.reduce((m, f) => { + const localMin = Object.values(f.metricsData).reduce( + (mm, d) => Math.min(mm, d.length > 0 ? d[0].x : Infinity), + Infinity + ); + return Math.min(m, localMin); + }, Infinity); + const maxEnd = parsedData.reduce((m, f) => { + const localMax = Object.values(f.metricsData).reduce( + (mm, d) => Math.max(mm, d.length > 0 ? d[d.length - 1].x : -Infinity), + -Infinity + ); + return Math.max(m, localMax); + }, -Infinity); + if (minStart !== Infinity && maxEnd !== -Infinity) { + onXRangeChange(prev => { + const next = { min: minStart, max: maxEnd }; + if (prev.min === next.min && prev.max === next.max) return prev; + return next; + }); + } + } else { + const minSteps = getMinSteps(parsedData); + if (minSteps > 0) { + onXRangeChange(prev => { + const next = { min: 0, max: minSteps - 1 }; + if (prev.min === next.min && prev.max === next.max) return prev; + return next; + }); + } } - }, [parsedData, onXRangeChange]); + }, [parsedData, onXRangeChange, useStepKeyword]); const colors = ['#ef4444', '#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#f97316']; const createChartData = dataArray => ({