Skip to content
Closed
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
26 changes: 16 additions & 10 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ function App() {
keyword: 'norm:',
regex: 'grad[\\s_]norm:\\s*([\\d.eE+-]+)'
}
]
],
stepKeyword: 'step:',
useStepKeyword: false
});

const [compareMode, setCompareMode] = useState('normal');
Expand Down Expand Up @@ -117,16 +119,18 @@ function App() {

// 全局解析配置变更处理
const handleGlobalParsingConfigChange = useCallback((newConfig) => {
setGlobalParsingConfig(newConfig);
setGlobalParsingConfig(prev => ({ ...prev, ...newConfig }));

// 同步所有文件的解析配置
setUploadedFiles(prev => prev.map(file => ({
...file,
config: {
...file.config,
metrics: newConfig.metrics.map(m => ({ ...m }))
}
})));
// 如果更新了指标配置,同步到所有文件
if (newConfig.metrics) {
setUploadedFiles(prev => prev.map(file => ({
...file,
config: {
...file.config,
metrics: newConfig.metrics.map(m => ({ ...m }))
}
})));
}
}, []);

// 全局拖拽事件处理
Expand Down Expand Up @@ -414,6 +418,8 @@ function App() {
xRange={xRange}
onXRangeChange={setXRange}
onMaxStepChange={setMaxStep}
stepKeyword={globalParsingConfig.stepKeyword}
useStepKeyword={globalParsingConfig.useStepKeyword}
/>
</section>
</main>
Expand Down
127 changes: 80 additions & 47 deletions src/components/ChartContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ const ChartWrapper = ({ data, options, chartId, onRegisterChart, onSyncHover })
...options,
onHover: (event, activeElements) => {
if (activeElements.length > 0) {
const step = activeElements[0].index;
const el = activeElements[0].element;
const step = el?.$context?.parsed?.x ?? activeElements[0].index;
onSyncHover(step, chartId);
} else {
onSyncHover(null, chartId);
Expand Down Expand Up @@ -68,7 +69,9 @@ export default function ChartContainer({
absoluteBaseline = 0.005,
xRange = { min: undefined, max: undefined },
onXRangeChange,
onMaxStepChange
onMaxStepChange,
stepKeyword = 'step:',
useStepKeyword = false
}) {
const chartRefs = useRef(new Map());
const registerChart = useCallback((id, inst) => {
Expand All @@ -85,8 +88,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 idx = dataset.data.findIndex(p => p.x === step);
if (idx !== -1) {
activeElements.push({ datasetIndex, index: idx });
}
});
chart.setActiveElements(activeElements);
Expand All @@ -100,62 +104,73 @@ export default function ChartContainer({
const enabled = files.filter(f => f.enabled !== false);
return enabled.map(file => {
if (!file.content) return { ...file, metricsData: {} };

const lines = file.content.split('\n');
const metricsData = {};
metrics.forEach(metric => {
metricsData[metric.name || metric.keyword] = [];
});

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);
}
}
});
return results;
};
const escapeRegex = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const stepReg = useStepKeyword && stepKeyword
? new RegExp(`${escapeRegex(stepKeyword)}\\s*\\[?\\s*(\\d+)`, 'i')
: null;
const numberRegex = /[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/;

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 => {
reg.lastIndex = 0;
lines.forEach(line => {
const stepMatch = stepReg ? stepReg.exec(line) : null;
const stepVal = stepMatch ? parseInt(stepMatch[1]) : null;

metrics.forEach(metric => {
let value;
if (metric.mode === 'keyword' && metric.keyword) {
const idx = line.toLowerCase().indexOf(metric.keyword.toLowerCase());
if (idx !== -1) {
const after = line.substring(idx + metric.keyword.length);
const match = after.match(numberRegex);
if (match) {
const v = parseFloat(match[0]);
if (!isNaN(v)) value = v;
}
}
} else if (metric.regex) {
const reg = new RegExp(metric.regex);
const m = reg.exec(line);
if (m && m[1]) {
const v = parseFloat(m[1]);
if (!isNaN(v)) values.push(v);
if (!isNaN(v)) value = v;
}
});
}
metricsData[metric.name || metric.keyword] = values.map((v, i) => ({ x: i, y: v }));
}

if (value !== undefined) {
const arr = metricsData[metric.name || metric.keyword];
if (useStepKeyword) {
if (stepVal !== null) arr.push({ x: stepVal, y: value });
} else {
arr.push({ x: arr.length, y: value });
}
}
});
});

const range = file.config?.dataRange;
if (range && (range.start > 0 || range.end !== undefined)) {
const applyRange = data => {
if (data.length === 0) return data;
Object.keys(metricsData).forEach(k => {
const data = metricsData[k];
if (data.length === 0) return;
const start = Math.max(0, parseInt(range.start) || 0);
const end = range.end !== undefined ? parseInt(range.end) : data.length;
const endIndex = Math.min(data.length, end);
return data.slice(start, endIndex);
};
const reindex = data => data.map((p, idx) => ({ x: idx, y: p.y }));
Object.keys(metricsData).forEach(k => {
metricsData[k] = reindex(applyRange(metricsData[k]));
const sliced = data.slice(start, endIndex);
metricsData[k] = useStepKeyword
? sliced
: sliced.map((p, idx) => ({ x: idx, y: p.y }));
});
}

return { ...file, metricsData };
});
}, [files, metrics]);
}, [files, metrics, stepKeyword, useStepKeyword]);

useEffect(() => {
const maxStep = parsedData.reduce((m, f) => {
Expand All @@ -166,15 +181,33 @@ 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 ranges = [];
parsedData.forEach(f => {
Object.values(f.metricsData).forEach(d => {
if (d.length > 0) ranges.push({ min: d[0].x, max: d[d.length - 1].x });
});
});
if (ranges.length > 0) {
const globalMin = Math.min(...ranges.map(r => r.min));
const globalMax = Math.max(...ranges.map(r => r.max));
onXRangeChange(prev => {
const next = { min: globalMin, max: globalMax };
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 => ({
Expand Down
22 changes: 21 additions & 1 deletion src/components/RegexControls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -447,8 +447,28 @@ export function RegexControls({
</button>
</div>
</div>

<div className="space-y-4">
<div className="border rounded-lg p-3">
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={globalParsingConfig.useStepKeyword || false}
onChange={(e) => onGlobalParsingConfigChange({ useStepKeyword: e.target.checked })}
/>
根据关键词确定步数
</label>
{globalParsingConfig.useStepKeyword && (
<input
type="text"
className="mt-2 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"
value={globalParsingConfig.stepKeyword || ''}
onChange={(e) => onGlobalParsingConfigChange({ stepKeyword: e.target.value })}
placeholder="step:"
/>
)}
</div>

{globalParsingConfig.metrics.map((cfg, idx) => (
<div key={idx} className="border rounded-lg p-3 relative">
<button
Expand Down
26 changes: 26 additions & 0 deletions src/components/__tests__/ChartContainer.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,30 @@ describe('ChartContainer', () => {
opts.plugins.zoom.pan.onPanComplete({ chart: { scales: { x: { min: 0, max: 10 } } } });
opts.plugins.zoom.zoom.onZoomComplete({ chart: { scales: { x: { min: 2, max: 4 } } } });
});

it('uses step keyword to place data points', () => {
const onXRangeChange = vi.fn(fn => fn({}));
const onMaxStepChange = vi.fn();
const files = [
{ name: 'a.log', enabled: true, content: 'step:[2/350], loss: 1\nstep:[3/350], loss: 2' }
];
render(
<ChartContainer
files={files}
metrics={[{ name: 'loss', keyword: 'loss', mode: 'keyword' }]}
compareMode="normal"
onXRangeChange={onXRangeChange}
onMaxStepChange={onMaxStepChange}
useStepKeyword
stepKeyword="step:"
/>
);

const data = __lineProps[__lineProps.length - 1].data.datasets[0].data;
expect(data[0].x).toBe(2);
expect(data[1].x).toBe(3);
expect(onMaxStepChange).toHaveBeenCalledWith(3);
const lastCall = onXRangeChange.mock.calls[onXRangeChange.mock.calls.length - 1][0];
expect(lastCall({})).toEqual({ min: 2, max: 3 });
});
});