fix: remove unused imports and format source files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -46,7 +46,15 @@ interface MetricRowData {
|
||||
note?: string;
|
||||
}
|
||||
|
||||
function ResultTable({ title, rows, higherIsBetter }: { title: string; rows: MetricRowData[]; higherIsBetter: boolean }) {
|
||||
function ResultTable({
|
||||
title,
|
||||
rows,
|
||||
higherIsBetter,
|
||||
}: {
|
||||
title: string;
|
||||
rows: MetricRowData[];
|
||||
higherIsBetter: boolean;
|
||||
}) {
|
||||
return (
|
||||
<SectionBox title={title}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '14px' }}>
|
||||
@@ -55,14 +63,24 @@ function ResultTable({ title, rows, higherIsBetter }: { title: string; rows: Met
|
||||
<th style={{ textAlign: 'left', padding: '8px 4px', fontWeight: 600 }}>Metric</th>
|
||||
<th style={{ textAlign: 'right', padding: '8px 4px', fontWeight: 600 }}>Read</th>
|
||||
<th style={{ textAlign: 'right', padding: '8px 4px', fontWeight: 600 }}>Write</th>
|
||||
<th style={{ textAlign: 'left', padding: '8px 4px', fontWeight: 400, color: 'var(--mui-palette-text-secondary)' }}>
|
||||
<th
|
||||
style={{
|
||||
textAlign: 'left',
|
||||
padding: '8px 4px',
|
||||
fontWeight: 400,
|
||||
color: 'var(--mui-palette-text-secondary)',
|
||||
}}
|
||||
>
|
||||
{higherIsBetter ? '↑ higher is better' : '↓ lower is better'}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map(row => (
|
||||
<tr key={row.label} style={{ borderBottom: '1px solid var(--mui-palette-divider, #f0f0f0)' }}>
|
||||
<tr
|
||||
key={row.label}
|
||||
style={{ borderBottom: '1px solid var(--mui-palette-divider, #f0f0f0)' }}
|
||||
>
|
||||
<td style={{ padding: '8px 4px' }}>{row.label}</td>
|
||||
<td style={{ padding: '8px 4px', textAlign: 'right', fontFamily: 'monospace' }}>
|
||||
{row.formatter(row.read)}
|
||||
@@ -83,21 +101,69 @@ function ResultTable({ title, rows, higherIsBetter }: { title: string; rows: Met
|
||||
|
||||
function KbenchResultDisplay({ result }: { result: KbenchResult }) {
|
||||
const iopsRows: MetricRowData[] = [
|
||||
{ label: 'Random', read: result.iops.randomRead, write: result.iops.randomWrite, formatter: formatIops },
|
||||
{ label: 'Sequential', read: result.iops.sequentialRead, write: result.iops.sequentialWrite, formatter: formatIops },
|
||||
{ label: 'CPU Idleness', read: result.iops.cpuIdleness, write: null, formatter: v => `${v}%`, note: result.iops.cpuIdleness < 40 ? '⚠ Low — may indicate CPU-bound results' : '' },
|
||||
{
|
||||
label: 'Random',
|
||||
read: result.iops.randomRead,
|
||||
write: result.iops.randomWrite,
|
||||
formatter: formatIops,
|
||||
},
|
||||
{
|
||||
label: 'Sequential',
|
||||
read: result.iops.sequentialRead,
|
||||
write: result.iops.sequentialWrite,
|
||||
formatter: formatIops,
|
||||
},
|
||||
{
|
||||
label: 'CPU Idleness',
|
||||
read: result.iops.cpuIdleness,
|
||||
write: null,
|
||||
formatter: v => `${v}%`,
|
||||
note: result.iops.cpuIdleness < 40 ? '⚠ Low — may indicate CPU-bound results' : '',
|
||||
},
|
||||
];
|
||||
|
||||
const bwRows: MetricRowData[] = [
|
||||
{ label: 'Random', read: result.bandwidth.randomRead, write: result.bandwidth.randomWrite, formatter: formatBandwidth },
|
||||
{ label: 'Sequential', read: result.bandwidth.sequentialRead, write: result.bandwidth.sequentialWrite, formatter: formatBandwidth },
|
||||
{ label: 'CPU Idleness', read: result.bandwidth.cpuIdleness, write: null, formatter: v => `${v}%` },
|
||||
{
|
||||
label: 'Random',
|
||||
read: result.bandwidth.randomRead,
|
||||
write: result.bandwidth.randomWrite,
|
||||
formatter: formatBandwidth,
|
||||
},
|
||||
{
|
||||
label: 'Sequential',
|
||||
read: result.bandwidth.sequentialRead,
|
||||
write: result.bandwidth.sequentialWrite,
|
||||
formatter: formatBandwidth,
|
||||
},
|
||||
{
|
||||
label: 'CPU Idleness',
|
||||
read: result.bandwidth.cpuIdleness,
|
||||
write: null,
|
||||
formatter: v => `${v}%`,
|
||||
},
|
||||
];
|
||||
|
||||
const latRows: MetricRowData[] = [
|
||||
{ label: 'Random', read: result.latency.randomRead, write: result.latency.randomWrite, formatter: formatLatency },
|
||||
{ label: 'Sequential', read: result.latency.sequentialRead, write: result.latency.sequentialWrite, formatter: formatLatency },
|
||||
{ label: 'CPU Idleness', read: result.latency.cpuIdleness, write: null, formatter: v => `${v}%`, note: result.latency.cpuIdleness < 40 ? '⚠ CPU-starved — latency results may be unreliable' : '' },
|
||||
{
|
||||
label: 'Random',
|
||||
read: result.latency.randomRead,
|
||||
write: result.latency.randomWrite,
|
||||
formatter: formatLatency,
|
||||
},
|
||||
{
|
||||
label: 'Sequential',
|
||||
read: result.latency.sequentialRead,
|
||||
write: result.latency.sequentialWrite,
|
||||
formatter: formatLatency,
|
||||
},
|
||||
{
|
||||
label: 'CPU Idleness',
|
||||
read: result.latency.cpuIdleness,
|
||||
write: null,
|
||||
formatter: v => `${v}%`,
|
||||
note:
|
||||
result.latency.cpuIdleness < 40 ? '⚠ CPU-starved — latency results may be unreliable' : '',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -109,7 +175,12 @@ function KbenchResultDisplay({ result }: { result: KbenchResult }) {
|
||||
{ name: 'Test Size', value: result.metadata.size },
|
||||
{ name: 'Job', value: result.metadata.jobName || '—' },
|
||||
{ name: 'Namespace', value: result.metadata.namespace || '—' },
|
||||
{ name: 'Completed', value: result.metadata.completedAt ? new Date(result.metadata.completedAt).toLocaleString() : '—' },
|
||||
{
|
||||
name: 'Completed',
|
||||
value: result.metadata.completedAt
|
||||
? new Date(result.metadata.completedAt).toLocaleString()
|
||||
: '—',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
@@ -154,32 +225,66 @@ function RunForm({ storageClasses, onRun, disabled }: RunFormProps) {
|
||||
|
||||
return (
|
||||
<SectionBox title="Run New Benchmark">
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '200px 1fr', gap: '12px 16px', alignItems: 'center', maxWidth: '600px' }}>
|
||||
<label htmlFor="kbench-sc" style={{ fontWeight: 500 }}>Storage Class *</label>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '200px 1fr',
|
||||
gap: '12px 16px',
|
||||
alignItems: 'center',
|
||||
maxWidth: '600px',
|
||||
}}
|
||||
>
|
||||
<label htmlFor="kbench-sc" style={{ fontWeight: 500 }}>
|
||||
Storage Class *
|
||||
</label>
|
||||
<select
|
||||
id="kbench-sc"
|
||||
value={storageClass}
|
||||
onChange={e => setStorageClass(e.target.value)}
|
||||
disabled={disabled || storageClasses.length === 0}
|
||||
style={{ padding: '6px 8px', borderRadius: '4px', border: '1px solid var(--mui-palette-divider, #ccc)', fontSize: '14px', backgroundColor: 'var(--mui-palette-background-paper)', color: 'var(--mui-palette-text-primary)' }}
|
||||
style={{
|
||||
padding: '6px 8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid var(--mui-palette-divider, #ccc)',
|
||||
fontSize: '14px',
|
||||
backgroundColor: 'var(--mui-palette-background-paper)',
|
||||
color: 'var(--mui-palette-text-primary)',
|
||||
}}
|
||||
aria-label="Select storage class for benchmark"
|
||||
>
|
||||
{storageClasses.length === 0 && <option value="">No tns-csi storage classes found</option>}
|
||||
{storageClasses.map(sc => <option key={sc} value={sc}>{sc}</option>)}
|
||||
{storageClasses.length === 0 && (
|
||||
<option value="">No tns-csi storage classes found</option>
|
||||
)}
|
||||
{storageClasses.map(sc => (
|
||||
<option key={sc} value={sc}>
|
||||
{sc}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<label htmlFor="kbench-ns" style={{ fontWeight: 500 }}>Namespace</label>
|
||||
<label htmlFor="kbench-ns" style={{ fontWeight: 500 }}>
|
||||
Namespace
|
||||
</label>
|
||||
<input
|
||||
id="kbench-ns"
|
||||
type="text"
|
||||
value={namespace}
|
||||
onChange={e => setNamespace(e.target.value)}
|
||||
disabled={disabled}
|
||||
style={{ padding: '6px 8px', borderRadius: '4px', border: '1px solid var(--mui-palette-divider, #ccc)', fontSize: '14px', backgroundColor: 'var(--mui-palette-background-paper)', color: 'var(--mui-palette-text-primary)' }}
|
||||
style={{
|
||||
padding: '6px 8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid var(--mui-palette-divider, #ccc)',
|
||||
fontSize: '14px',
|
||||
backgroundColor: 'var(--mui-palette-background-paper)',
|
||||
color: 'var(--mui-palette-text-primary)',
|
||||
}}
|
||||
aria-label="Kubernetes namespace for benchmark job"
|
||||
/>
|
||||
|
||||
<label htmlFor="kbench-size" style={{ fontWeight: 500 }}>Test Size</label>
|
||||
<label htmlFor="kbench-size" style={{ fontWeight: 500 }}>
|
||||
Test Size
|
||||
</label>
|
||||
<div>
|
||||
<input
|
||||
id="kbench-size"
|
||||
@@ -187,21 +292,44 @@ function RunForm({ storageClasses, onRun, disabled }: RunFormProps) {
|
||||
value={size}
|
||||
onChange={e => setSize(e.target.value)}
|
||||
disabled={disabled}
|
||||
style={{ padding: '6px 8px', borderRadius: '4px', border: '1px solid var(--mui-palette-divider, #ccc)', fontSize: '14px', width: '120px', backgroundColor: 'var(--mui-palette-background-paper)', color: 'var(--mui-palette-text-primary)' }}
|
||||
style={{
|
||||
padding: '6px 8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid var(--mui-palette-divider, #ccc)',
|
||||
fontSize: '14px',
|
||||
width: '120px',
|
||||
backgroundColor: 'var(--mui-palette-background-paper)',
|
||||
color: 'var(--mui-palette-text-primary)',
|
||||
}}
|
||||
aria-label="FIO test size"
|
||||
/>
|
||||
<span style={{ marginLeft: '8px', fontSize: '12px', color: 'var(--mui-palette-text-secondary)' }}>
|
||||
<span
|
||||
style={{
|
||||
marginLeft: '8px',
|
||||
fontSize: '12px',
|
||||
color: 'var(--mui-palette-text-secondary)',
|
||||
}}
|
||||
>
|
||||
PVC will be ~10% larger (33Gi for 30G)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<label htmlFor="kbench-mode" style={{ fontWeight: 500 }}>Mode</label>
|
||||
<label htmlFor="kbench-mode" style={{ fontWeight: 500 }}>
|
||||
Mode
|
||||
</label>
|
||||
<select
|
||||
id="kbench-mode"
|
||||
value={mode}
|
||||
onChange={e => setMode(e.target.value)}
|
||||
disabled={disabled}
|
||||
style={{ padding: '6px 8px', borderRadius: '4px', border: '1px solid var(--mui-palette-divider, #ccc)', fontSize: '14px', backgroundColor: 'var(--mui-palette-background-paper)', color: 'var(--mui-palette-text-primary)' }}
|
||||
style={{
|
||||
padding: '6px 8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid var(--mui-palette-divider, #ccc)',
|
||||
fontSize: '14px',
|
||||
backgroundColor: 'var(--mui-palette-background-paper)',
|
||||
color: 'var(--mui-palette-text-primary)',
|
||||
}}
|
||||
aria-label="Benchmark mode"
|
||||
>
|
||||
<option value="full">Full (~6 minutes)</option>
|
||||
@@ -216,7 +344,9 @@ function RunForm({ storageClasses, onRun, disabled }: RunFormProps) {
|
||||
aria-label="Start kbench storage benchmark"
|
||||
style={{
|
||||
padding: '8px 20px',
|
||||
backgroundColor: disabled ? 'var(--mui-palette-action-disabled, #ccc)' : 'var(--mui-palette-primary-main, #1976d2)',
|
||||
backgroundColor: disabled
|
||||
? 'var(--mui-palette-action-disabled, #ccc)'
|
||||
: 'var(--mui-palette-primary-main, #1976d2)',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
@@ -231,35 +361,82 @@ function RunForm({ storageClasses, onRun, disabled }: RunFormProps) {
|
||||
|
||||
{showConfirm && (
|
||||
<div
|
||||
style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 2000, backgroundColor: 'rgba(0,0,0,0.5)' }}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 2000,
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
}}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="kbench-confirm-title"
|
||||
>
|
||||
<div style={{ backgroundColor: 'var(--mui-palette-background-paper, #fff)', borderRadius: '8px', padding: '24px', maxWidth: '480px', boxShadow: '0 4px 24px rgba(0,0,0,0.2)', color: 'var(--mui-palette-text-primary)' }}>
|
||||
<h3 id="kbench-confirm-title" style={{ margin: '0 0 16px' }}>Confirm Benchmark</h3>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'var(--mui-palette-background-paper, #fff)',
|
||||
borderRadius: '8px',
|
||||
padding: '24px',
|
||||
maxWidth: '480px',
|
||||
boxShadow: '0 4px 24px rgba(0,0,0,0.2)',
|
||||
color: 'var(--mui-palette-text-primary)',
|
||||
}}
|
||||
>
|
||||
<h3 id="kbench-confirm-title" style={{ margin: '0 0 16px' }}>
|
||||
Confirm Benchmark
|
||||
</h3>
|
||||
<p style={{ margin: '0 0 8px', fontSize: '14px' }}>
|
||||
This will create a <strong>~33Gi PVC</strong> and run an FIO benchmark (
|
||||
<strong>~6 minutes</strong>).
|
||||
</p>
|
||||
<p style={{ margin: '0 0 8px', fontSize: '14px' }}>
|
||||
Storage class: <strong>{storageClass}</strong> · Namespace: <strong>{namespace}</strong>
|
||||
Storage class: <strong>{storageClass}</strong> · Namespace:{' '}
|
||||
<strong>{namespace}</strong>
|
||||
</p>
|
||||
<p style={{ margin: '0 0 16px', fontSize: '14px', color: 'var(--mui-palette-text-secondary)' }}>
|
||||
The Job and PVC will remain until manually deleted. You will be prompted to clean up after completion.
|
||||
<p
|
||||
style={{
|
||||
margin: '0 0 16px',
|
||||
fontSize: '14px',
|
||||
color: 'var(--mui-palette-text-secondary)',
|
||||
}}
|
||||
>
|
||||
The Job and PVC will remain until manually deleted. You will be prompted to clean up
|
||||
after completion.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
|
||||
<button
|
||||
onClick={() => setShowConfirm(false)}
|
||||
aria-label="Cancel benchmark"
|
||||
style={{ padding: '8px 16px', border: '1px solid var(--mui-palette-divider)', borderRadius: '4px', background: 'transparent', cursor: 'pointer', fontSize: '14px', color: 'var(--mui-palette-text-primary)' }}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
border: '1px solid var(--mui-palette-divider)',
|
||||
borderRadius: '4px',
|
||||
background: 'transparent',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
color: 'var(--mui-palette-text-primary)',
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirm}
|
||||
aria-label="Confirm and start benchmark"
|
||||
style={{ padding: '8px 16px', backgroundColor: 'var(--mui-palette-primary-main, #1976d2)', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '14px', fontWeight: 500 }}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: 'var(--mui-palette-primary-main, #1976d2)',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Start Benchmark
|
||||
</button>
|
||||
@@ -305,9 +482,7 @@ function BenchmarkProgress({ state }: { state: BenchmarkState }) {
|
||||
{
|
||||
name: 'Status',
|
||||
value: (
|
||||
<StatusLabel status={statusColor[state.status]}>
|
||||
{labels[state.status]}
|
||||
</StatusLabel>
|
||||
<StatusLabel status={statusColor[state.status]}>{labels[state.status]}</StatusLabel>
|
||||
),
|
||||
},
|
||||
...('jobName' in state && state.jobName ? [{ name: 'Job', value: state.jobName }] : []),
|
||||
@@ -344,7 +519,9 @@ function PastBenchmarks({ namespace }: PastBenchmarksProps) {
|
||||
}
|
||||
}, [namespace]);
|
||||
|
||||
useEffect(() => { void loadJobs(); }, [loadJobs]);
|
||||
useEffect(() => {
|
||||
void loadJobs();
|
||||
}, [loadJobs]);
|
||||
|
||||
async function handleDelete(job: KbenchJobSummary) {
|
||||
if (!window.confirm(`Delete job "${job.jobName}" and its PVC "${job.jobName}-pvc"?`)) return;
|
||||
@@ -372,7 +549,11 @@ function PastBenchmarks({ namespace }: PastBenchmarksProps) {
|
||||
{
|
||||
label: 'Status',
|
||||
getter: (j: KbenchJobSummary) => (
|
||||
<StatusLabel status={j.phase === 'Complete' ? 'success' : j.phase === 'Failed' ? 'error' : 'warning'}>
|
||||
<StatusLabel
|
||||
status={
|
||||
j.phase === 'Complete' ? 'success' : j.phase === 'Failed' ? 'error' : 'warning'
|
||||
}
|
||||
>
|
||||
{j.phase}
|
||||
</StatusLabel>
|
||||
),
|
||||
@@ -385,7 +566,15 @@ function PastBenchmarks({ namespace }: PastBenchmarksProps) {
|
||||
onClick={() => void handleDelete(j)}
|
||||
disabled={deleting === j.jobName}
|
||||
aria-label={`Delete benchmark job ${j.jobName}`}
|
||||
style={{ padding: '4px 10px', border: '1px solid var(--mui-palette-error-main, #d32f2f)', color: 'var(--mui-palette-error-main, #d32f2f)', background: 'transparent', borderRadius: '4px', cursor: 'pointer', fontSize: '12px' }}
|
||||
style={{
|
||||
padding: '4px 10px',
|
||||
border: '1px solid var(--mui-palette-error-main, #d32f2f)',
|
||||
color: 'var(--mui-palette-error-main, #d32f2f)',
|
||||
background: 'transparent',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px',
|
||||
}}
|
||||
>
|
||||
{deleting === j.jobName ? 'Deleting...' : 'Delete'}
|
||||
</button>
|
||||
@@ -422,21 +611,38 @@ export default function BenchmarkPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function runBenchmark(opts: { storageClass: string; namespace: string; size: string; mode: string }) {
|
||||
async function runBenchmark(opts: {
|
||||
storageClass: string;
|
||||
namespace: string;
|
||||
size: string;
|
||||
mode: string;
|
||||
}) {
|
||||
stopPolling();
|
||||
setCurrentResult(null);
|
||||
setLastNamespace(opts.namespace);
|
||||
|
||||
const jobName = generateJobName();
|
||||
const pvcName = generatePvcName(jobName);
|
||||
const jobOpts = { jobName, pvcName, namespace: opts.namespace, storageClass: opts.storageClass, size: opts.size, mode: opts.mode };
|
||||
const jobOpts = {
|
||||
jobName,
|
||||
pvcName,
|
||||
namespace: opts.namespace,
|
||||
storageClass: opts.storageClass,
|
||||
size: opts.size,
|
||||
mode: opts.mode,
|
||||
};
|
||||
|
||||
// Step 1: Create PVC
|
||||
setBenchState({ status: 'creating-pvc' });
|
||||
try {
|
||||
await createPvc(jobOpts);
|
||||
} catch (err: unknown) {
|
||||
setBenchState({ status: 'failed', error: `Failed to create PVC: ${err instanceof Error ? err.message : String(err)}`, jobName, pvcName });
|
||||
setBenchState({
|
||||
status: 'failed',
|
||||
error: `Failed to create PVC: ${err instanceof Error ? err.message : String(err)}`,
|
||||
jobName,
|
||||
pvcName,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -446,13 +652,25 @@ export default function BenchmarkPage() {
|
||||
let pvcBound = false;
|
||||
while (Date.now() < pvcDeadline) {
|
||||
try {
|
||||
const pvc = await ApiProxy.request(`/api/v1/namespaces/${opts.namespace}/persistentvolumeclaims/${pvcName}`) as { status?: { phase?: string } };
|
||||
if (pvc.status?.phase === 'Bound') { pvcBound = true; break; }
|
||||
} catch { /* retry */ }
|
||||
const pvc = (await ApiProxy.request(
|
||||
`/api/v1/namespaces/${opts.namespace}/persistentvolumeclaims/${pvcName}`
|
||||
)) as { status?: { phase?: string } };
|
||||
if (pvc.status?.phase === 'Bound') {
|
||||
pvcBound = true;
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
/* retry */
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 5000));
|
||||
}
|
||||
if (!pvcBound) {
|
||||
setBenchState({ status: 'failed', error: 'PVC did not bind within 2 minutes. Check StorageClass and provisioner.', jobName, pvcName });
|
||||
setBenchState({
|
||||
status: 'failed',
|
||||
error: 'PVC did not bind within 2 minutes. Check StorageClass and provisioner.',
|
||||
jobName,
|
||||
pvcName,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -460,7 +678,12 @@ export default function BenchmarkPage() {
|
||||
try {
|
||||
await createJob(jobOpts);
|
||||
} catch (err: unknown) {
|
||||
setBenchState({ status: 'failed', error: `Failed to create Job: ${err instanceof Error ? err.message : String(err)}`, jobName, pvcName });
|
||||
setBenchState({
|
||||
status: 'failed',
|
||||
error: `Failed to create Job: ${err instanceof Error ? err.message : String(err)}`,
|
||||
jobName,
|
||||
pvcName,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -487,18 +710,38 @@ export default function BenchmarkPage() {
|
||||
setCurrentResult(result);
|
||||
setBenchState({ status: 'complete', result, jobName, pvcName });
|
||||
} else {
|
||||
setBenchState({ status: 'failed', error: 'Could not parse FIO output from pod logs.', jobName, pvcName });
|
||||
setBenchState({
|
||||
status: 'failed',
|
||||
error: 'Could not parse FIO output from pod logs.',
|
||||
jobName,
|
||||
pvcName,
|
||||
});
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
setBenchState({ status: 'failed', error: `Log retrieval failed: ${err instanceof Error ? err.message : String(err)}`, jobName, pvcName });
|
||||
setBenchState({
|
||||
status: 'failed',
|
||||
error: `Log retrieval failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||
jobName,
|
||||
pvcName,
|
||||
});
|
||||
}
|
||||
} else if (phase === 'Failed') {
|
||||
stopPolling();
|
||||
setBenchState({ status: 'failed', error: 'kbench Job failed. Check pod logs for details.', jobName, pvcName });
|
||||
setBenchState({
|
||||
status: 'failed',
|
||||
error: 'kbench Job failed. Check pod logs for details.',
|
||||
jobName,
|
||||
pvcName,
|
||||
});
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
stopPolling();
|
||||
setBenchState({ status: 'failed', error: `Polling error: ${err instanceof Error ? err.message : String(err)}`, jobName, pvcName });
|
||||
setBenchState({
|
||||
status: 'failed',
|
||||
error: `Polling error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
jobName,
|
||||
pvcName,
|
||||
});
|
||||
}
|
||||
}, POLL_INTERVAL_MS);
|
||||
}
|
||||
@@ -506,7 +749,10 @@ export default function BenchmarkPage() {
|
||||
// Clean up polling on unmount
|
||||
useEffect(() => () => stopPolling(), []);
|
||||
|
||||
const isRunning = benchState.status !== 'idle' && benchState.status !== 'complete' && benchState.status !== 'failed';
|
||||
const isRunning =
|
||||
benchState.status !== 'idle' &&
|
||||
benchState.status !== 'complete' &&
|
||||
benchState.status !== 'failed';
|
||||
|
||||
if (loading) return <Loader title="Loading tns-csi data..." />;
|
||||
|
||||
@@ -518,15 +764,35 @@ export default function BenchmarkPage() {
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{ name: 'Duration', value: 'Full benchmark takes ~6 minutes. Do not cancel mid-run.' },
|
||||
{ name: 'Test Size', value: 'SIZE must be at least 10% smaller than PVC capacity (default: 30G in 33Gi PVC).' },
|
||||
{ name: 'Cache Warning', value: 'For accurate results, SIZE should be at least 25× the read/write bandwidth to bypass cache.' },
|
||||
{ name: 'CPU Idleness', value: 'Latency benchmark CPU Idleness should be ≥40%. Lower values indicate CPU-starved results.' },
|
||||
{ name: 'Interpretation', value: 'Lower read latency than local storage is a red flag (likely caching). Better write than local is nearly impossible for distributed storage.' },
|
||||
{
|
||||
name: 'Test Size',
|
||||
value:
|
||||
'SIZE must be at least 10% smaller than PVC capacity (default: 30G in 33Gi PVC).',
|
||||
},
|
||||
{
|
||||
name: 'Cache Warning',
|
||||
value:
|
||||
'For accurate results, SIZE should be at least 25× the read/write bandwidth to bypass cache.',
|
||||
},
|
||||
{
|
||||
name: 'CPU Idleness',
|
||||
value:
|
||||
'Latency benchmark CPU Idleness should be ≥40%. Lower values indicate CPU-starved results.',
|
||||
},
|
||||
{
|
||||
name: 'Interpretation',
|
||||
value:
|
||||
'Lower read latency than local storage is a red flag (likely caching). Better write than local is nearly impossible for distributed storage.',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
|
||||
<RunForm storageClasses={scNames} onRun={opts => void runBenchmark(opts)} disabled={isRunning} />
|
||||
<RunForm
|
||||
storageClasses={scNames}
|
||||
onRun={opts => void runBenchmark(opts)}
|
||||
disabled={isRunning}
|
||||
/>
|
||||
|
||||
<BenchmarkProgress state={benchState} />
|
||||
|
||||
@@ -535,30 +801,47 @@ export default function BenchmarkPage() {
|
||||
<KbenchResultDisplay result={currentResult} />
|
||||
<SectionBox title="Cleanup">
|
||||
<NameValueTable
|
||||
rows={[{
|
||||
name: 'Resources',
|
||||
value: (
|
||||
<button
|
||||
onClick={async () => {
|
||||
const state = benchState;
|
||||
if (state.status !== 'complete') return;
|
||||
if (!window.confirm(`Delete job "${state.jobName}" and PVC "${state.pvcName}"?`)) return;
|
||||
try {
|
||||
await deleteJob(state.jobName, lastNamespace);
|
||||
await deletePvc(state.pvcName, lastNamespace);
|
||||
setBenchState({ status: 'idle' });
|
||||
setCurrentResult(null);
|
||||
} catch (err: unknown) {
|
||||
alert(`Cleanup error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}}
|
||||
aria-label="Delete benchmark job and PVC"
|
||||
style={{ padding: '6px 14px', border: '1px solid var(--mui-palette-error-main, #d32f2f)', color: 'var(--mui-palette-error-main, #d32f2f)', background: 'transparent', borderRadius: '4px', cursor: 'pointer', fontSize: '13px' }}
|
||||
>
|
||||
Delete Job + PVC
|
||||
</button>
|
||||
),
|
||||
}]}
|
||||
rows={[
|
||||
{
|
||||
name: 'Resources',
|
||||
value: (
|
||||
<button
|
||||
onClick={async () => {
|
||||
const state = benchState;
|
||||
if (state.status !== 'complete') return;
|
||||
if (
|
||||
!window.confirm(
|
||||
`Delete job "${state.jobName}" and PVC "${state.pvcName}"?`
|
||||
)
|
||||
)
|
||||
return;
|
||||
try {
|
||||
await deleteJob(state.jobName, lastNamespace);
|
||||
await deletePvc(state.pvcName, lastNamespace);
|
||||
setBenchState({ status: 'idle' });
|
||||
setCurrentResult(null);
|
||||
} catch (err: unknown) {
|
||||
alert(
|
||||
`Cleanup error: ${err instanceof Error ? err.message : String(err)}`
|
||||
);
|
||||
}
|
||||
}}
|
||||
aria-label="Delete benchmark job and PVC"
|
||||
style={{
|
||||
padding: '6px 14px',
|
||||
border: '1px solid var(--mui-palette-error-main, #d32f2f)',
|
||||
color: 'var(--mui-palette-error-main, #d32f2f)',
|
||||
background: 'transparent',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '13px',
|
||||
}}
|
||||
>
|
||||
Delete Job + PVC
|
||||
</button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user