Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 53455063b9 | |||
| 4602f97836 | |||
| c65d3ae3b1 |
@@ -1068,17 +1068,23 @@ func renderValidate(opts HandlerOptions) string {
|
|||||||
`</div>
|
`</div>
|
||||||
<div style="height:1px;background:var(--border);margin:16px 0"></div>
|
<div style="height:1px;background:var(--border);margin:16px 0"></div>
|
||||||
<div class="grid3">
|
<div class="grid3">
|
||||||
` + renderSATCard("nvidia", "NVIDIA GPU", "runNvidiaValidateSet('nvidia')", "", renderValidateCardBody(
|
` + renderSATCard("nvidia-selection", "NVIDIA GPU Selection", "", "", renderValidateCardBody(
|
||||||
|
inv.NVIDIA,
|
||||||
|
`Select which NVIDIA GPUs to include in Validate. The same selection is used by both NVIDIA GPU cards below and by Validate one by one.`,
|
||||||
|
`<code>nvidia-smi --query-gpu=index,name,memory.total</code>`,
|
||||||
|
`<div id="sat-gpu-list"><p style="color:var(--muted);font-size:13px">Loading NVIDIA GPUs…</p></div><div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:8px"><button type="button" class="btn btn-sm btn-secondary" onclick="satSelectAllGPUs()">Select all</button><button type="button" class="btn btn-sm btn-secondary" onclick="satSelectNoGPUs()">Clear</button></div><div id="sat-gpu-selection-note" style="font-size:12px;color:var(--muted);margin-top:8px"></div>`,
|
||||||
|
)) +
|
||||||
|
renderSATCard("nvidia", "NVIDIA GPU", "runNvidiaValidateSet('nvidia')", "", renderValidateCardBody(
|
||||||
inv.NVIDIA,
|
inv.NVIDIA,
|
||||||
`Runs NVIDIA diagnostics and board inventory checks.`,
|
`Runs NVIDIA diagnostics and board inventory checks.`,
|
||||||
`<code>nvidia-smi</code>, <code>dmidecode</code>, <code>dcgmi diag</code>`,
|
`<code>nvidia-smi</code>, <code>dmidecode</code>, <code>dcgmi diag</code>`,
|
||||||
`Runs one GPU at a time. Diag level is taken from Validate Profile.`,
|
`Runs one GPU at a time on the selected NVIDIA GPUs. Diag level is taken from Validate Profile.`,
|
||||||
)) +
|
)) +
|
||||||
renderSATCard("nvidia-targeted-stress", "NVIDIA GPU Targeted Stress", "runNvidiaValidateSet('nvidia-targeted-stress')", "", renderValidateCardBody(
|
renderSATCard("nvidia-targeted-stress", "NVIDIA GPU Targeted Stress", "runNvidiaValidateSet('nvidia-targeted-stress')", "", renderValidateCardBody(
|
||||||
inv.NVIDIA,
|
inv.NVIDIA,
|
||||||
`Runs a controlled NVIDIA DCGM load in Validate to check stability under moderate stress.`,
|
`Runs a controlled NVIDIA DCGM load in Validate to check stability under moderate stress.`,
|
||||||
`<code>dcgmi diag targeted_stress</code>`,
|
`<code>dcgmi diag targeted_stress</code>`,
|
||||||
`Runs one GPU at a time with the fixed DCGM targeted stress recipe.`,
|
`Runs one GPU at a time on the selected NVIDIA GPUs with the fixed DCGM targeted stress recipe.`,
|
||||||
)) +
|
)) +
|
||||||
`</div>
|
`</div>
|
||||||
<div class="grid3" style="margin-top:16px">
|
<div class="grid3" style="margin-top:16px">
|
||||||
@@ -1100,6 +1106,8 @@ func renderValidate(opts HandlerOptions) string {
|
|||||||
.validate-card-body { padding:0; }
|
.validate-card-body { padding:0; }
|
||||||
.validate-card-section { padding:12px 16px 0; }
|
.validate-card-section { padding:12px 16px 0; }
|
||||||
.validate-card-section:last-child { padding-bottom:16px; }
|
.validate-card-section:last-child { padding-bottom:16px; }
|
||||||
|
.sat-gpu-row { display:flex; align-items:flex-start; gap:8px; padding:6px 0; cursor:pointer; font-size:13px; }
|
||||||
|
.sat-gpu-row input[type=checkbox] { width:16px; height:16px; margin-top:2px; flex-shrink:0; }
|
||||||
@media(max-width:900px){ .validate-profile-body { grid-template-columns:1fr; } }
|
@media(max-width:900px){ .validate-profile-body { grid-template-columns:1fr; } }
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
@@ -1128,6 +1136,59 @@ function loadSatNvidiaGPUs() {
|
|||||||
}
|
}
|
||||||
return satNvidiaGPUsPromise;
|
return satNvidiaGPUsPromise;
|
||||||
}
|
}
|
||||||
|
function satSelectedGPUIndices() {
|
||||||
|
return Array.from(document.querySelectorAll('.sat-nvidia-checkbox'))
|
||||||
|
.filter(function(el) { return el.checked && !el.disabled; })
|
||||||
|
.map(function(el) { return parseInt(el.value, 10); })
|
||||||
|
.filter(function(v) { return !Number.isNaN(v); })
|
||||||
|
.sort(function(a, b) { return a - b; });
|
||||||
|
}
|
||||||
|
function satUpdateGPUSelectionNote() {
|
||||||
|
const note = document.getElementById('sat-gpu-selection-note');
|
||||||
|
if (!note) return;
|
||||||
|
const selected = satSelectedGPUIndices();
|
||||||
|
if (!selected.length) {
|
||||||
|
note.textContent = 'Select at least one NVIDIA GPU to enable NVIDIA validate tasks.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
note.textContent = 'Selected NVIDIA GPUs: ' + selected.join(', ') + '.';
|
||||||
|
}
|
||||||
|
function satRenderGPUList(gpus) {
|
||||||
|
const root = document.getElementById('sat-gpu-list');
|
||||||
|
if (!root) return;
|
||||||
|
if (!gpus || !gpus.length) {
|
||||||
|
root.innerHTML = '<p style="color:var(--muted);font-size:13px">No NVIDIA GPUs detected.</p>';
|
||||||
|
satUpdateGPUSelectionNote();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.innerHTML = gpus.map(function(gpu) {
|
||||||
|
const mem = gpu.memory_mb > 0 ? ' · ' + gpu.memory_mb + ' MiB' : '';
|
||||||
|
return '<label class="sat-gpu-row">'
|
||||||
|
+ '<input class="sat-nvidia-checkbox" type="checkbox" value="' + gpu.index + '" checked onchange="satUpdateGPUSelectionNote()">'
|
||||||
|
+ '<span><strong>GPU ' + gpu.index + '</strong> — ' + gpu.name + mem + '</span>'
|
||||||
|
+ '</label>';
|
||||||
|
}).join('');
|
||||||
|
satUpdateGPUSelectionNote();
|
||||||
|
}
|
||||||
|
function satSelectAllGPUs() {
|
||||||
|
document.querySelectorAll('.sat-nvidia-checkbox').forEach(function(el) { el.checked = true; });
|
||||||
|
satUpdateGPUSelectionNote();
|
||||||
|
}
|
||||||
|
function satSelectNoGPUs() {
|
||||||
|
document.querySelectorAll('.sat-nvidia-checkbox').forEach(function(el) { el.checked = false; });
|
||||||
|
satUpdateGPUSelectionNote();
|
||||||
|
}
|
||||||
|
function satLoadGPUs() {
|
||||||
|
loadSatNvidiaGPUs().then(function(gpus) {
|
||||||
|
satRenderGPUList(gpus);
|
||||||
|
}).catch(function(err) {
|
||||||
|
const root = document.getElementById('sat-gpu-list');
|
||||||
|
if (root) {
|
||||||
|
root.innerHTML = '<p style="color:var(--crit-fg);font-size:13px">Error: ' + err.message + '</p>';
|
||||||
|
}
|
||||||
|
satUpdateGPUSelectionNote();
|
||||||
|
});
|
||||||
|
}
|
||||||
function satGPUDisplayName(gpu) {
|
function satGPUDisplayName(gpu) {
|
||||||
const idx = (gpu && Number.isFinite(Number(gpu.index))) ? Number(gpu.index) : 0;
|
const idx = (gpu && Number.isFinite(Number(gpu.index))) ? Number(gpu.index) : 0;
|
||||||
const name = gpu && gpu.name ? gpu.name : ('GPU ' + idx);
|
const name = gpu && gpu.name ? gpu.name : ('GPU ' + idx);
|
||||||
@@ -1149,6 +1210,36 @@ function enqueueSATTarget(target, overrides) {
|
|||||||
return fetch('/api/sat/'+target+'/run', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(satRequestBody(target, overrides))})
|
return fetch('/api/sat/'+target+'/run', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(satRequestBody(target, overrides))})
|
||||||
.then(r => r.json());
|
.then(r => r.json());
|
||||||
}
|
}
|
||||||
|
function streamSATTask(taskId, title, resetTerminal) {
|
||||||
|
if (satES) { satES.close(); satES = null; }
|
||||||
|
document.getElementById('sat-output').style.display='block';
|
||||||
|
document.getElementById('sat-title').textContent = '— ' + title;
|
||||||
|
const term = document.getElementById('sat-terminal');
|
||||||
|
if (resetTerminal) {
|
||||||
|
term.textContent = '';
|
||||||
|
}
|
||||||
|
term.textContent += 'Task ' + taskId + ' queued. Streaming log...\n';
|
||||||
|
return new Promise(function(resolve) {
|
||||||
|
satES = new EventSource('/api/tasks/' + taskId + '/stream');
|
||||||
|
satES.onmessage = function(e) { term.textContent += e.data + '\n'; term.scrollTop = term.scrollHeight; };
|
||||||
|
satES.addEventListener('done', function(e) {
|
||||||
|
satES.close();
|
||||||
|
satES = null;
|
||||||
|
term.textContent += (e.data ? '\nERROR: ' + e.data : '\nCompleted.') + '\n';
|
||||||
|
term.scrollTop = term.scrollHeight;
|
||||||
|
resolve({ok: !e.data, error: e.data || ''});
|
||||||
|
});
|
||||||
|
satES.onerror = function() {
|
||||||
|
if (satES) {
|
||||||
|
satES.close();
|
||||||
|
satES = null;
|
||||||
|
}
|
||||||
|
term.textContent += '\nERROR: stream disconnected.\n';
|
||||||
|
term.scrollTop = term.scrollHeight;
|
||||||
|
resolve({ok: false, error: 'stream disconnected'});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
function selectedAMDValidateTargets() {
|
function selectedAMDValidateTargets() {
|
||||||
const targets = [];
|
const targets = [];
|
||||||
const gpu = document.getElementById('sat-amd-target');
|
const gpu = document.getElementById('sat-amd-target');
|
||||||
@@ -1163,24 +1254,23 @@ function runSAT(target) {
|
|||||||
return runSATWithOverrides(target, null);
|
return runSATWithOverrides(target, null);
|
||||||
}
|
}
|
||||||
function runSATWithOverrides(target, overrides) {
|
function runSATWithOverrides(target, overrides) {
|
||||||
if (satES) { satES.close(); satES = null; }
|
const title = (overrides && overrides.display_name) || target;
|
||||||
document.getElementById('sat-output').style.display='block';
|
|
||||||
document.getElementById('sat-title').textContent = '— ' + target;
|
|
||||||
const term = document.getElementById('sat-terminal');
|
const term = document.getElementById('sat-terminal');
|
||||||
term.textContent = 'Enqueuing ' + target + ' test...\n';
|
document.getElementById('sat-output').style.display='block';
|
||||||
|
document.getElementById('sat-title').textContent = '— ' + title;
|
||||||
|
term.textContent = 'Enqueuing ' + title + ' test...\n';
|
||||||
return enqueueSATTarget(target, overrides)
|
return enqueueSATTarget(target, overrides)
|
||||||
.then(d => {
|
.then(d => streamSATTask(d.task_id, title, false));
|
||||||
term.textContent += 'Task ' + d.task_id + ' queued. Streaming log...\n';
|
|
||||||
satES = new EventSource('/api/tasks/'+d.task_id+'/stream');
|
|
||||||
satES.onmessage = e => { term.textContent += e.data+'\n'; term.scrollTop=term.scrollHeight; };
|
|
||||||
satES.addEventListener('done', e => { satES.close(); satES=null; term.textContent += (e.data ? '\nERROR: '+e.data : '\nCompleted.')+'\n'; });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
function expandSATTarget(target) {
|
function expandSATTarget(target) {
|
||||||
if (target !== 'nvidia' && target !== 'nvidia-targeted-stress') {
|
if (target !== 'nvidia' && target !== 'nvidia-targeted-stress') {
|
||||||
return Promise.resolve([{target: target}]);
|
return Promise.resolve([{target: target}]);
|
||||||
}
|
}
|
||||||
return loadSatNvidiaGPUs().then(gpus => gpus.map(gpu => ({
|
const selected = satSelectedGPUIndices();
|
||||||
|
if (!selected.length) {
|
||||||
|
return Promise.reject(new Error('Select at least one NVIDIA GPU.'));
|
||||||
|
}
|
||||||
|
return loadSatNvidiaGPUs().then(gpus => gpus.filter(gpu => selected.indexOf(Number(gpu.index)) >= 0).map(gpu => ({
|
||||||
target: target,
|
target: target,
|
||||||
overrides: {
|
overrides: {
|
||||||
gpu_indices: [Number(gpu.index)],
|
gpu_indices: [Number(gpu.index)],
|
||||||
@@ -1191,65 +1281,61 @@ function expandSATTarget(target) {
|
|||||||
}
|
}
|
||||||
function runNvidiaValidateSet(target) {
|
function runNvidiaValidateSet(target) {
|
||||||
return loadSatNvidiaGPUs().then(gpus => {
|
return loadSatNvidiaGPUs().then(gpus => {
|
||||||
if (!gpus.length) return;
|
const selected = satSelectedGPUIndices();
|
||||||
if (gpus.length === 1) {
|
const picked = gpus.filter(gpu => selected.indexOf(Number(gpu.index)) >= 0);
|
||||||
const gpu = gpus[0];
|
if (!picked.length) {
|
||||||
|
throw new Error('Select at least one NVIDIA GPU.');
|
||||||
|
}
|
||||||
|
if (picked.length === 1) {
|
||||||
|
const gpu = picked[0];
|
||||||
return runSATWithOverrides(target, {
|
return runSATWithOverrides(target, {
|
||||||
gpu_indices: [Number(gpu.index)],
|
gpu_indices: [Number(gpu.index)],
|
||||||
display_name: (satLabels()[target] || ('Validate ' + target)) + ' (' + satGPUDisplayName(gpu) + ')'
|
display_name: (satLabels()[target] || ('Validate ' + target)) + ' (' + satGPUDisplayName(gpu) + ')'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (satES) { satES.close(); satES = null; }
|
|
||||||
document.getElementById('sat-output').style.display='block';
|
document.getElementById('sat-output').style.display='block';
|
||||||
document.getElementById('sat-title').textContent = '— ' + target;
|
document.getElementById('sat-title').textContent = '— ' + target;
|
||||||
const term = document.getElementById('sat-terminal');
|
const term = document.getElementById('sat-terminal');
|
||||||
term.textContent = 'Enqueuing ' + target + ' tests one GPU at a time...\n';
|
term.textContent = 'Running ' + target + ' one GPU at a time...\n';
|
||||||
const labelBase = satLabels()[target] || ('Validate ' + target);
|
const labelBase = satLabels()[target] || ('Validate ' + target);
|
||||||
const enqueueNext = (idx) => {
|
const runNext = (idx) => {
|
||||||
if (idx >= gpus.length) return;
|
if (idx >= picked.length) return Promise.resolve();
|
||||||
const gpu = gpus[idx];
|
const gpu = picked[idx];
|
||||||
const gpuLabel = satGPUDisplayName(gpu);
|
const gpuLabel = satGPUDisplayName(gpu);
|
||||||
enqueueSATTarget(target, {
|
term.textContent += '\n[' + (idx + 1) + '/' + picked.length + '] ' + gpuLabel + '\n';
|
||||||
|
return enqueueSATTarget(target, {
|
||||||
gpu_indices: [Number(gpu.index)],
|
gpu_indices: [Number(gpu.index)],
|
||||||
display_name: labelBase + ' (' + gpuLabel + ')'
|
display_name: labelBase + ' (' + gpuLabel + ')'
|
||||||
}).then(d => {
|
}).then(d => {
|
||||||
term.textContent += 'Task ' + d.task_id + ' queued for ' + gpuLabel + '.\n';
|
return streamSATTask(d.task_id, labelBase + ' (' + gpuLabel + ')', false);
|
||||||
if (idx === gpus.length - 1) {
|
}).then(function() {
|
||||||
satES = new EventSource('/api/tasks/' + d.task_id + '/stream');
|
return runNext(idx + 1);
|
||||||
satES.onmessage = e => { term.textContent += e.data+'\n'; term.scrollTop=term.scrollHeight; };
|
|
||||||
satES.addEventListener('done', e => { satES.close(); satES=null; term.textContent += (e.data ? '\nERROR: '+e.data : '\nCompleted.')+'\n'; });
|
|
||||||
}
|
|
||||||
enqueueNext(idx + 1);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
enqueueNext(0);
|
return runNext(0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function runAMDValidateSet() {
|
function runAMDValidateSet() {
|
||||||
const targets = selectedAMDValidateTargets();
|
const targets = selectedAMDValidateTargets();
|
||||||
if (!targets.length) return;
|
if (!targets.length) return;
|
||||||
if (targets.length === 1) return runSAT(targets[0]);
|
if (targets.length === 1) return runSAT(targets[0]);
|
||||||
if (satES) { satES.close(); satES = null; }
|
|
||||||
document.getElementById('sat-output').style.display='block';
|
document.getElementById('sat-output').style.display='block';
|
||||||
document.getElementById('sat-title').textContent = '— amd';
|
document.getElementById('sat-title').textContent = '— amd';
|
||||||
const term = document.getElementById('sat-terminal');
|
const term = document.getElementById('sat-terminal');
|
||||||
term.textContent = 'Enqueuing AMD validate set...\n';
|
term.textContent = 'Running AMD validate set one by one...\n';
|
||||||
const labels = satLabels();
|
const labels = satLabels();
|
||||||
const enqueueNext = (idx) => {
|
const runNext = (idx) => {
|
||||||
if (idx >= targets.length) return;
|
if (idx >= targets.length) return Promise.resolve();
|
||||||
const target = targets[idx];
|
const target = targets[idx];
|
||||||
enqueueSATTarget(target)
|
term.textContent += '\n[' + (idx + 1) + '/' + targets.length + '] ' + labels[target] + '\n';
|
||||||
|
return enqueueSATTarget(target)
|
||||||
.then(d => {
|
.then(d => {
|
||||||
term.textContent += 'Task ' + d.task_id + ' queued for ' + labels[target] + '.\n';
|
return streamSATTask(d.task_id, labels[target], false);
|
||||||
if (idx === targets.length - 1) {
|
}).then(function() {
|
||||||
satES = new EventSource('/api/tasks/'+d.task_id+'/stream');
|
return runNext(idx + 1);
|
||||||
satES.onmessage = e => { term.textContent += e.data+'\n'; term.scrollTop=term.scrollHeight; };
|
|
||||||
satES.addEventListener('done', e => { satES.close(); satES=null; term.textContent += (e.data ? '\nERROR: '+e.data : '\nCompleted.')+'\n'; });
|
|
||||||
}
|
|
||||||
enqueueNext(idx + 1);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
enqueueNext(0);
|
return runNext(0);
|
||||||
}
|
}
|
||||||
function runAllSAT() {
|
function runAllSAT() {
|
||||||
const cycles = Math.max(1, parseInt(document.getElementById('sat-cycles').value)||1);
|
const cycles = Math.max(1, parseInt(document.getElementById('sat-cycles').value)||1);
|
||||||
@@ -1271,17 +1357,17 @@ function runAllSAT() {
|
|||||||
status.textContent = 'No tasks selected.';
|
status.textContent = 'No tasks selected.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const enqueueNext = (idx) => {
|
const runNext = (idx) => {
|
||||||
if (idx >= expanded.length) { status.textContent = 'Enqueued ' + total + ' tasks.'; return; }
|
if (idx >= expanded.length) { status.textContent = 'Completed ' + total + ' task(s).'; return Promise.resolve(); }
|
||||||
const item = expanded[idx];
|
const item = expanded[idx];
|
||||||
enqueueSATTarget(item.target, item.overrides)
|
status.textContent = 'Running ' + (idx + 1) + '/' + total + '...';
|
||||||
|
return enqueueSATTarget(item.target, item.overrides)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
enqueued++;
|
enqueued++;
|
||||||
status.textContent = 'Enqueued ' + enqueued + '/' + total + '...';
|
return runNext(idx + 1);
|
||||||
enqueueNext(idx + 1);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
enqueueNext(0);
|
return runNext(0);
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
status.textContent = 'Error: ' + err.message;
|
status.textContent = 'Error: ' + err.message;
|
||||||
});
|
});
|
||||||
@@ -1294,6 +1380,7 @@ fetch('/api/gpu/presence').then(r=>r.json()).then(gp => {
|
|||||||
if (!gp.amd) disableSATCard('amd', 'No AMD GPU detected');
|
if (!gp.amd) disableSATCard('amd', 'No AMD GPU detected');
|
||||||
if (!gp.amd) disableSATAMDOptions('No AMD GPU detected');
|
if (!gp.amd) disableSATAMDOptions('No AMD GPU detected');
|
||||||
});
|
});
|
||||||
|
satLoadGPUs();
|
||||||
function disableSATAMDOptions(reason) {
|
function disableSATAMDOptions(reason) {
|
||||||
['sat-amd-target','sat-amd-mem-target','sat-amd-bandwidth-target'].forEach(function(id) {
|
['sat-amd-target','sat-amd-mem-target','sat-amd-bandwidth-target'].forEach(function(id) {
|
||||||
const cb = document.getElementById(id);
|
const cb = document.getElementById(id);
|
||||||
@@ -1886,6 +1973,36 @@ function streamTask(taskId, label) {
|
|||||||
term.scrollTop = term.scrollHeight;
|
term.scrollTop = term.scrollHeight;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
function streamBurnTask(taskId, label, resetTerminal) {
|
||||||
|
if (biES) { biES.close(); biES = null; }
|
||||||
|
document.getElementById('bi-output').style.display = 'block';
|
||||||
|
document.getElementById('bi-title').textContent = '— ' + label + ' [' + burnProfile() + ']';
|
||||||
|
const term = document.getElementById('bi-terminal');
|
||||||
|
if (resetTerminal) {
|
||||||
|
term.textContent = '';
|
||||||
|
}
|
||||||
|
term.textContent += 'Task ' + taskId + ' queued. Streaming...\n';
|
||||||
|
return new Promise(function(resolve) {
|
||||||
|
biES = new EventSource('/api/tasks/' + taskId + '/stream');
|
||||||
|
biES.onmessage = function(e) { term.textContent += e.data + '\n'; term.scrollTop = term.scrollHeight; };
|
||||||
|
biES.addEventListener('done', function(e) {
|
||||||
|
biES.close();
|
||||||
|
biES = null;
|
||||||
|
term.textContent += (e.data ? '\nERROR: ' + e.data : '\nCompleted.') + '\n';
|
||||||
|
term.scrollTop = term.scrollHeight;
|
||||||
|
resolve({ok: !e.data, error: e.data || ''});
|
||||||
|
});
|
||||||
|
biES.onerror = function() {
|
||||||
|
if (biES) {
|
||||||
|
biES.close();
|
||||||
|
biES = null;
|
||||||
|
}
|
||||||
|
term.textContent += '\nERROR: stream disconnected.\n';
|
||||||
|
term.scrollTop = term.scrollHeight;
|
||||||
|
resolve({ok: false, error: 'stream disconnected'});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function runBurnTaskSet(tasks, statusElId) {
|
function runBurnTaskSet(tasks, statusElId) {
|
||||||
const enabled = tasks.filter(function(t) {
|
const enabled = tasks.filter(function(t) {
|
||||||
@@ -1898,19 +2015,33 @@ function runBurnTaskSet(tasks, statusElId) {
|
|||||||
if (status) status.textContent = 'No tasks selected.';
|
if (status) status.textContent = 'No tasks selected.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
enabled.forEach(function(t) {
|
const term = document.getElementById('bi-terminal');
|
||||||
enqueueBurnTask(t.target, t.label, t.extra, !!t.nvidia)
|
document.getElementById('bi-output').style.display = 'block';
|
||||||
|
document.getElementById('bi-title').textContent = '— Burn one by one [' + burnProfile() + ']';
|
||||||
|
term.textContent = '';
|
||||||
|
const runNext = function(idx) {
|
||||||
|
if (idx >= enabled.length) {
|
||||||
|
if (status) status.textContent = 'Completed ' + enabled.length + ' task(s).';
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
const t = enabled[idx];
|
||||||
|
term.textContent += '\n[' + (idx + 1) + '/' + enabled.length + '] ' + t.label + '\n';
|
||||||
|
if (status) status.textContent = 'Running ' + (idx + 1) + '/' + enabled.length + '...';
|
||||||
|
return enqueueBurnTask(t.target, t.label, t.extra, !!t.nvidia)
|
||||||
.then(function(d) {
|
.then(function(d) {
|
||||||
if (status) status.textContent = enabled.length + ' task(s) queued.';
|
return streamBurnTask(d.task_id, t.label, false);
|
||||||
streamTask(d.task_id, t.label);
|
})
|
||||||
|
.then(function() {
|
||||||
|
return runNext(idx + 1);
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
if (status) status.textContent = 'Error: ' + err.message;
|
if (status) status.textContent = 'Error: ' + err.message;
|
||||||
const term = document.getElementById('bi-terminal');
|
|
||||||
document.getElementById('bi-output').style.display = 'block';
|
document.getElementById('bi-output').style.display = 'block';
|
||||||
term.textContent += 'ERROR: ' + err.message + '\n';
|
term.textContent += 'ERROR: ' + err.message + '\n';
|
||||||
|
return Promise.reject(err);
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
return runNext(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function runPlatformStress() {
|
function runPlatformStress() {
|
||||||
|
|||||||
@@ -601,8 +601,8 @@ func TestToolsPageRendersRestartGPUDriversButton(t *testing.T) {
|
|||||||
if !strings.Contains(body, `Restart GPU Drivers`) {
|
if !strings.Contains(body, `Restart GPU Drivers`) {
|
||||||
t.Fatalf("tools page missing restart gpu drivers button: %s", body)
|
t.Fatalf("tools page missing restart gpu drivers button: %s", body)
|
||||||
}
|
}
|
||||||
if !strings.Contains(body, `svcAction('bee-nvidia', 'restart')`) {
|
if !strings.Contains(body, `restartGPUDrivers()`) {
|
||||||
t.Fatalf("tools page missing bee-nvidia restart action: %s", body)
|
t.Fatalf("tools page missing restartGPUDrivers action: %s", body)
|
||||||
}
|
}
|
||||||
if !strings.Contains(body, `id="boot-source-text"`) {
|
if !strings.Contains(body, `id="boot-source-text"`) {
|
||||||
t.Fatalf("tools page missing boot source field: %s", body)
|
t.Fatalf("tools page missing boot source field: %s", body)
|
||||||
@@ -649,6 +649,8 @@ func TestValidatePageRendersNvidiaTargetedStressCard(t *testing.T) {
|
|||||||
`nvidia-targeted-stress`,
|
`nvidia-targeted-stress`,
|
||||||
`controlled NVIDIA DCGM load`,
|
`controlled NVIDIA DCGM load`,
|
||||||
`<code>dcgmi diag targeted_stress</code>`,
|
`<code>dcgmi diag targeted_stress</code>`,
|
||||||
|
`NVIDIA GPU Selection`,
|
||||||
|
`id="sat-gpu-list"`,
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(body, needle) {
|
if !strings.Contains(body, needle) {
|
||||||
t.Fatalf("validate page missing %q: %s", needle, body)
|
t.Fatalf("validate page missing %q: %s", needle, body)
|
||||||
|
|||||||
@@ -106,23 +106,61 @@ func renderTaskDetailPage(opts HandlerOptions, task Task) string {
|
|||||||
body.WriteString(`</div></div>`)
|
body.WriteString(`</div></div>`)
|
||||||
body.WriteString(`<script>
|
body.WriteString(`<script>
|
||||||
function cancelTaskDetail(id) {
|
function cancelTaskDetail(id) {
|
||||||
fetch('/api/tasks/' + id + '/cancel', {method:'POST'}).then(function(){ window.location.reload(); });
|
fetch('/api/tasks/' + id + '/cancel', {method:'POST'}).then(function(){
|
||||||
|
var term = document.getElementById('task-live-log');
|
||||||
|
if (term) {
|
||||||
|
term.textContent += '\nCancel requested.\n';
|
||||||
|
term.scrollTop = term.scrollHeight;
|
||||||
}
|
}
|
||||||
function loadTaskLiveCharts(taskId) {
|
});
|
||||||
fetch('/api/tasks/' + taskId + '/charts').then(function(r){ return r.json(); }).then(function(charts){
|
}
|
||||||
|
function renderTaskLiveCharts(taskId, charts) {
|
||||||
const host = document.getElementById('task-live-charts');
|
const host = document.getElementById('task-live-charts');
|
||||||
if (!host) return;
|
if (!host) return;
|
||||||
if (!Array.isArray(charts) || charts.length === 0) {
|
if (!Array.isArray(charts) || charts.length === 0) {
|
||||||
host.innerHTML = 'Waiting for metric samples...';
|
host.innerHTML = 'Waiting for metric samples...';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
host.innerHTML = charts.map(function(chart) {
|
const seen = {};
|
||||||
return '<div class="card" style="margin:0">' +
|
charts.forEach(function(chart) {
|
||||||
'<div class="card-head">' + chart.title + '</div>' +
|
seen[chart.file] = true;
|
||||||
'<div class="card-body" style="padding:12px">' +
|
let img = host.querySelector('img[data-chart-file="' + chart.file + '"]');
|
||||||
'<img data-task-chart="1" data-base-src="/api/tasks/' + taskId + '/chart/' + chart.file + '" src="/api/tasks/' + taskId + '/chart/' + chart.file + '?t=' + Date.now() + '" style="width:100%;display:block;border-radius:6px" alt="' + chart.title + '">' +
|
if (img) {
|
||||||
'</div></div>';
|
const card = img.closest('.card');
|
||||||
}).join('');
|
if (card) {
|
||||||
|
const title = card.querySelector('.card-head');
|
||||||
|
if (title) title.textContent = chart.title;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'card';
|
||||||
|
card.style.margin = '0';
|
||||||
|
card.innerHTML = '<div class="card-head"></div><div class="card-body" style="padding:12px"></div>';
|
||||||
|
card.querySelector('.card-head').textContent = chart.title;
|
||||||
|
const body = card.querySelector('.card-body');
|
||||||
|
img = document.createElement('img');
|
||||||
|
img.setAttribute('data-task-chart', '1');
|
||||||
|
img.setAttribute('data-chart-file', chart.file);
|
||||||
|
img.setAttribute('data-base-src', '/api/tasks/' + taskId + '/chart/' + chart.file);
|
||||||
|
img.src = '/api/tasks/' + taskId + '/chart/' + chart.file + '?t=' + Date.now();
|
||||||
|
img.style.width = '100%';
|
||||||
|
img.style.display = 'block';
|
||||||
|
img.style.borderRadius = '6px';
|
||||||
|
img.alt = chart.title;
|
||||||
|
body.appendChild(img);
|
||||||
|
host.appendChild(card);
|
||||||
|
});
|
||||||
|
Array.from(host.querySelectorAll('img[data-task-chart="1"]')).forEach(function(img) {
|
||||||
|
const file = img.getAttribute('data-chart-file') || '';
|
||||||
|
if (seen[file]) return;
|
||||||
|
const card = img.closest('.card');
|
||||||
|
if (card) card.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function loadTaskLiveCharts(taskId) {
|
||||||
|
fetch('/api/tasks/' + taskId + '/charts').then(function(r){ return r.json(); }).then(function(charts){
|
||||||
|
renderTaskLiveCharts(taskId, charts);
|
||||||
}).catch(function(){
|
}).catch(function(){
|
||||||
const host = document.getElementById('task-live-charts');
|
const host = document.getElementById('task-live-charts');
|
||||||
if (host) host.innerHTML = 'Task charts are unavailable.';
|
if (host) host.innerHTML = 'Task charts are unavailable.';
|
||||||
@@ -138,12 +176,31 @@ function refreshTaskLiveCharts() {
|
|||||||
var _taskDetailES = new EventSource('/api/tasks/` + html.EscapeString(task.ID) + `/stream');
|
var _taskDetailES = new EventSource('/api/tasks/` + html.EscapeString(task.ID) + `/stream');
|
||||||
var _taskDetailTerm = document.getElementById('task-live-log');
|
var _taskDetailTerm = document.getElementById('task-live-log');
|
||||||
var _taskChartTimer = null;
|
var _taskChartTimer = null;
|
||||||
|
var _taskChartsFrozen = false;
|
||||||
_taskDetailES.onopen = function(){ _taskDetailTerm.textContent = ''; };
|
_taskDetailES.onopen = function(){ _taskDetailTerm.textContent = ''; };
|
||||||
_taskDetailES.onmessage = function(e){ _taskDetailTerm.textContent += e.data + "\n"; _taskDetailTerm.scrollTop = _taskDetailTerm.scrollHeight; };
|
_taskDetailES.onmessage = function(e){ _taskDetailTerm.textContent += e.data + "\n"; _taskDetailTerm.scrollTop = _taskDetailTerm.scrollHeight; };
|
||||||
_taskDetailES.addEventListener('done', function(){ if (_taskChartTimer) clearInterval(_taskChartTimer); _taskDetailES.close(); setTimeout(function(){ window.location.reload(); }, 1000); });
|
_taskDetailES.addEventListener('done', function(e){
|
||||||
_taskDetailES.onerror = function(){ if (_taskChartTimer) clearInterval(_taskChartTimer); _taskDetailES.close(); };
|
if (_taskChartTimer) clearInterval(_taskChartTimer);
|
||||||
|
_taskDetailES.close();
|
||||||
|
_taskDetailES = null;
|
||||||
|
_taskChartsFrozen = true;
|
||||||
|
_taskDetailTerm.textContent += (e.data ? '\nTask finished with error.\n' : '\nTask finished.\n');
|
||||||
|
_taskDetailTerm.scrollTop = _taskDetailTerm.scrollHeight;
|
||||||
|
refreshTaskLiveCharts();
|
||||||
|
});
|
||||||
|
_taskDetailES.onerror = function(){
|
||||||
|
if (_taskChartTimer) clearInterval(_taskChartTimer);
|
||||||
|
if (_taskDetailES) {
|
||||||
|
_taskDetailES.close();
|
||||||
|
_taskDetailES = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
loadTaskLiveCharts('` + html.EscapeString(task.ID) + `');
|
loadTaskLiveCharts('` + html.EscapeString(task.ID) + `');
|
||||||
_taskChartTimer = setInterval(function(){ refreshTaskLiveCharts(); loadTaskLiveCharts('` + html.EscapeString(task.ID) + `'); }, 2000);
|
_taskChartTimer = setInterval(function(){
|
||||||
|
if (_taskChartsFrozen) return;
|
||||||
|
loadTaskLiveCharts('` + html.EscapeString(task.ID) + `');
|
||||||
|
refreshTaskLiveCharts();
|
||||||
|
}, 2000);
|
||||||
</script>`)
|
</script>`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -423,13 +423,14 @@ func (q *taskQueue) worker() {
|
|||||||
setCPUGovernor("performance")
|
setCPUGovernor("performance")
|
||||||
defer setCPUGovernor("powersave")
|
defer setCPUGovernor("powersave")
|
||||||
|
|
||||||
// Drain all pending tasks and start them in parallel.
|
|
||||||
q.mu.Lock()
|
|
||||||
var batch []*Task
|
|
||||||
for {
|
for {
|
||||||
|
q.mu.Lock()
|
||||||
t := q.nextPending()
|
t := q.nextPending()
|
||||||
if t == nil {
|
if t == nil {
|
||||||
break
|
q.prune()
|
||||||
|
q.persistLocked()
|
||||||
|
q.mu.Unlock()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
t.Status = TaskRunning
|
t.Status = TaskRunning
|
||||||
@@ -438,29 +439,14 @@ func (q *taskQueue) worker() {
|
|||||||
t.ErrMsg = ""
|
t.ErrMsg = ""
|
||||||
j := newTaskJobState(t.LogPath, taskSerialPrefix(t))
|
j := newTaskJobState(t.LogPath, taskSerialPrefix(t))
|
||||||
t.job = j
|
t.job = j
|
||||||
batch = append(batch, t)
|
|
||||||
}
|
|
||||||
if len(batch) > 0 {
|
|
||||||
q.persistLocked()
|
q.persistLocked()
|
||||||
}
|
|
||||||
q.mu.Unlock()
|
q.mu.Unlock()
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
for _, t := range batch {
|
|
||||||
t := t
|
|
||||||
j := t.job
|
|
||||||
taskCtx, taskCancel := context.WithCancel(context.Background())
|
taskCtx, taskCancel := context.WithCancel(context.Background())
|
||||||
j.cancel = taskCancel
|
j.cancel = taskCancel
|
||||||
wg.Add(1)
|
|
||||||
goRecoverOnce("task "+t.Target, func() {
|
|
||||||
defer wg.Done()
|
|
||||||
defer taskCancel()
|
|
||||||
q.executeTask(t, j, taskCtx)
|
q.executeTask(t, j, taskCtx)
|
||||||
})
|
taskCancel()
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
if len(batch) > 0 {
|
|
||||||
q.mu.Lock()
|
q.mu.Lock()
|
||||||
q.prune()
|
q.prune()
|
||||||
q.persistLocked()
|
q.persistLocked()
|
||||||
|
|||||||
@@ -11,18 +11,18 @@ echo " Hardware Audit LiveCD"
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
menuentry "EASY-BEE" {
|
menuentry "EASY-BEE" {
|
||||||
linux @KERNEL_LIVE@ @APPEND_LIVE@ bee.nvidia.mode=normal net.ifnames=0 biosdevname=0 mitigations=off transparent_hugepage=always numa_balancing=disable nowatchdog nosoftlockup
|
linux @KERNEL_LIVE@ @APPEND_LIVE@ nomodeset bee.nvidia.mode=normal net.ifnames=0 biosdevname=0 mitigations=off transparent_hugepage=always numa_balancing=disable nowatchdog nosoftlockup
|
||||||
initrd @INITRD_LIVE@
|
initrd @INITRD_LIVE@
|
||||||
}
|
}
|
||||||
|
|
||||||
submenu "EASY-BEE (advanced options) -->" {
|
submenu "EASY-BEE (advanced options) -->" {
|
||||||
menuentry "EASY-BEE — GSP=off" {
|
menuentry "EASY-BEE — GSP=off" {
|
||||||
linux @KERNEL_LIVE@ @APPEND_LIVE@ bee.nvidia.mode=gsp-off net.ifnames=0 biosdevname=0 mitigations=off transparent_hugepage=always numa_balancing=disable nowatchdog nosoftlockup
|
linux @KERNEL_LIVE@ @APPEND_LIVE@ nomodeset bee.nvidia.mode=gsp-off net.ifnames=0 biosdevname=0 mitigations=off transparent_hugepage=always numa_balancing=disable nowatchdog nosoftlockup
|
||||||
initrd @INITRD_LIVE@
|
initrd @INITRD_LIVE@
|
||||||
}
|
}
|
||||||
|
|
||||||
menuentry "EASY-BEE — nomodeset" {
|
menuentry "EASY-BEE — KMS (no nomodeset)" {
|
||||||
linux @KERNEL_LIVE@ @APPEND_LIVE@ nomodeset bee.nvidia.mode=normal net.ifnames=0 biosdevname=0 mitigations=off transparent_hugepage=always numa_balancing=disable nowatchdog nosoftlockup
|
linux @KERNEL_LIVE@ @APPEND_LIVE@ bee.nvidia.mode=normal net.ifnames=0 biosdevname=0 mitigations=off transparent_hugepage=always numa_balancing=disable nowatchdog nosoftlockup
|
||||||
initrd @INITRD_LIVE@
|
initrd @INITRD_LIVE@
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,23 +44,27 @@ else:
|
|||||||
img = Image.new('RGB', (W, H), (0, 0, 0))
|
img = Image.new('RGB', (W, H), (0, 0, 0))
|
||||||
draw = ImageDraw.Draw(img)
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
# Measure logo block
|
# Measure logo block line by line to avoid font ascender offset
|
||||||
lines = LOGO.split('\n')
|
lines = LOGO.split('\n')
|
||||||
bbox = draw.textbbox((0, 0), LOGO, font=font_logo)
|
|
||||||
text_w = bbox[2] - bbox[0]
|
|
||||||
text_h = bbox[3] - bbox[1]
|
|
||||||
|
|
||||||
x = (W - text_w) // 2
|
|
||||||
y = (H - text_h) // 2
|
|
||||||
|
|
||||||
# Draw logo lines: first 6 in amber, last line (subtitle) dimmer
|
|
||||||
logo_lines = lines[:6]
|
logo_lines = lines[:6]
|
||||||
sub_line = lines[6] if len(lines) > 6 else ''
|
sub_line = lines[6] if len(lines) > 6 else ''
|
||||||
|
|
||||||
|
line_h = SIZE + 2
|
||||||
|
block_h = len(logo_lines) * line_h + 8 + (SIZE if sub_line else 0)
|
||||||
|
|
||||||
|
# Width: measure the widest logo line
|
||||||
|
max_w = 0
|
||||||
|
for line in logo_lines:
|
||||||
|
bb = draw.textbbox((0, 0), line, font=font_logo)
|
||||||
|
max_w = max(max_w, bb[2] - bb[0])
|
||||||
|
|
||||||
|
x = (W - max_w) // 2
|
||||||
|
y = (H - block_h) // 2
|
||||||
|
|
||||||
cy = y
|
cy = y
|
||||||
for line in logo_lines:
|
for line in logo_lines:
|
||||||
draw.text((x, cy), line, font=font_logo, fill=(0xf6, 0xc9, 0x0e))
|
draw.text((x, cy), line, font=font_logo, fill=(0xf6, 0xc9, 0x0e))
|
||||||
cy += SIZE + 2
|
cy += line_h
|
||||||
cy += 8
|
cy += 8
|
||||||
if sub_line:
|
if sub_line:
|
||||||
draw.text((x, cy), sub_line, font=font_sub, fill=(0x80, 0x68, 0x18))
|
draw.text((x, cy), sub_line, font=font_sub, fill=(0x80, 0x68, 0x18))
|
||||||
|
|||||||
Reference in New Issue
Block a user