feat: add standard deviation to average in lastest run page
resolves #3
This commit is contained in:
+47
-15
@@ -18,10 +18,14 @@ class StorageManager:
|
||||
self.history: List[Dict] = []
|
||||
self.max_history = 10000
|
||||
self.node_averages: Dict[str, Dict] = defaultdict(lambda: {
|
||||
'total_time': 0.0, 'count': 0, 'vram_usage': 0.0, 'ram_usage': 0.0
|
||||
'total_time': 0.0, 'count': 0, 'vram_usage': 0.0, 'ram_usage': 0.0,
|
||||
'_m2_time': 0.0, '_m2_vram': 0.0, '_m2_ram': 0.0,
|
||||
'std_time': 0.0, 'std_vram': 0.0, 'std_ram': 0.0,
|
||||
})
|
||||
self.workflow_averages = {
|
||||
'total_time': 0.0, 'count': 0, 'vram_peak': 0.0, 'ram_peak': 0.0
|
||||
'total_time': 0.0, 'count': 0, 'vram_peak': 0.0, 'ram_peak': 0.0,
|
||||
'_m2_time': 0.0, '_m2_vram': 0.0, '_m2_ram': 0.0,
|
||||
'std_time': 0.0, 'std_vram': 0.0, 'std_ram': 0.0,
|
||||
}
|
||||
|
||||
self.data_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data")
|
||||
@@ -43,22 +47,38 @@ class StorageManager:
|
||||
|
||||
# --- Averages ---
|
||||
|
||||
@staticmethod
|
||||
def _welford_update(avg, m2, n, new_value):
|
||||
"""Welford's online algorithm: returns (new_avg, new_m2, stddev)."""
|
||||
delta = new_value - avg
|
||||
new_avg = avg + delta / n
|
||||
delta2 = new_value - new_avg
|
||||
new_m2 = m2 + delta * delta2
|
||||
stddev = (new_m2 / n) ** 0.5 if n > 1 else 0.0
|
||||
return new_avg, new_m2, stddev
|
||||
|
||||
def _update_node_average(self, node_type: str, execution_time: float, vram_used: float, ram_used: float) -> Dict:
|
||||
avg = self.node_averages[node_type]
|
||||
avg['count'] += 1
|
||||
n = avg['count']
|
||||
avg['total_time'] = (avg['total_time'] * (n - 1) + execution_time) / n
|
||||
avg['vram_usage'] = (avg['vram_usage'] * (n - 1) + vram_used) / n
|
||||
avg['ram_usage'] = (avg['ram_usage'] * (n - 1) + ram_used) / n
|
||||
avg['total_time'], avg['_m2_time'], avg['std_time'] = self._welford_update(
|
||||
avg['total_time'], avg['_m2_time'], n, execution_time)
|
||||
avg['vram_usage'], avg['_m2_vram'], avg['std_vram'] = self._welford_update(
|
||||
avg['vram_usage'], avg['_m2_vram'], n, vram_used)
|
||||
avg['ram_usage'], avg['_m2_ram'], avg['std_ram'] = self._welford_update(
|
||||
avg['ram_usage'], avg['_m2_ram'], n, ram_used)
|
||||
return avg
|
||||
|
||||
def _update_workflow_average(self, execution_time: float, vram_peak: float, ram_peak: float) -> Dict:
|
||||
wa = self.workflow_averages
|
||||
wa['count'] += 1
|
||||
n = wa['count']
|
||||
wa['total_time'] = (wa['total_time'] * (n - 1) + execution_time) / n
|
||||
wa['vram_peak'] = (wa['vram_peak'] * (n - 1) + vram_peak) / n
|
||||
wa['ram_peak'] = (wa['ram_peak'] * (n - 1) + ram_peak) / n
|
||||
wa['total_time'], wa['_m2_time'], wa['std_time'] = self._welford_update(
|
||||
wa['total_time'], wa['_m2_time'], n, execution_time)
|
||||
wa['vram_peak'], wa['_m2_vram'], wa['std_vram'] = self._welford_update(
|
||||
wa['vram_peak'], wa['_m2_vram'], n, vram_peak)
|
||||
wa['ram_peak'], wa['_m2_ram'], wa['std_ram'] = self._welford_update(
|
||||
wa['ram_peak'], wa['_m2_ram'], n, ram_peak)
|
||||
return wa
|
||||
|
||||
# --- Save / Load ---
|
||||
@@ -101,12 +121,16 @@ class StorageManager:
|
||||
run_data.get('totalRamPeak', 0),
|
||||
)
|
||||
|
||||
# Inject averages into run_data for frontend
|
||||
# Inject averages + stddev into run_data for frontend
|
||||
wa = self.workflow_averages
|
||||
run_data['averages'] = {
|
||||
'execution_time': self.workflow_averages['total_time'],
|
||||
'vram_peak': self.workflow_averages['vram_peak'],
|
||||
'ram_peak': self.workflow_averages['ram_peak'],
|
||||
'count': self.workflow_averages['count'],
|
||||
'execution_time': wa['total_time'],
|
||||
'vram_peak': wa['vram_peak'],
|
||||
'ram_peak': wa['ram_peak'],
|
||||
'count': wa['count'],
|
||||
'std_time': wa['std_time'],
|
||||
'std_vram': wa['std_vram'],
|
||||
'std_ram': wa['std_ram'],
|
||||
}
|
||||
for node in run_data.get('nodes', {}).values():
|
||||
nt = node.get('nodeType', 'unknown')
|
||||
@@ -117,6 +141,9 @@ class StorageManager:
|
||||
'vram_usage': a['vram_usage'],
|
||||
'ram_usage': a['ram_usage'],
|
||||
'count': a['count'],
|
||||
'std_time': a['std_time'],
|
||||
'std_vram': a['std_vram'],
|
||||
'std_ram': a['std_ram'],
|
||||
}
|
||||
|
||||
self.history.append(run_data)
|
||||
@@ -125,13 +152,18 @@ class StorageManager:
|
||||
else:
|
||||
self._save_history()
|
||||
|
||||
@staticmethod
|
||||
def _strip_internal(d: Dict) -> Dict:
|
||||
"""Strip internal _m2_* fields from a dict for API responses."""
|
||||
return {k: v for k, v in d.items() if not k.startswith('_')}
|
||||
|
||||
def get_stats(self) -> Dict:
|
||||
with self._lock:
|
||||
return {
|
||||
'current': {},
|
||||
'latest': self.history[-1] if self.history else None,
|
||||
'node_averages': dict(self.node_averages),
|
||||
'workflow_averages': self.workflow_averages,
|
||||
'node_averages': {k: self._strip_internal(v) for k, v in self.node_averages.items()},
|
||||
'workflow_averages': self._strip_internal(self.workflow_averages),
|
||||
'history': self.history[-10:],
|
||||
}
|
||||
|
||||
|
||||
+34
-24
@@ -5,7 +5,7 @@ export function showNodeAnalytics(element, stats) {
|
||||
function updateContent() {
|
||||
// Filter history by time range
|
||||
const history = filterHistoryByTimeRange(stats.history || [], timeSelect.value);
|
||||
// Calculate node averages from filtered history
|
||||
// Calculate node averages + stddev from filtered history (Welford's algorithm)
|
||||
const nodeAverages = {};
|
||||
history.forEach(run => {
|
||||
const nodes = Object.values(run.nodes);
|
||||
@@ -13,32 +13,40 @@ export function showNodeAnalytics(element, stats) {
|
||||
const nodeType = node.nodeType;
|
||||
if (!nodeAverages[nodeType]) {
|
||||
nodeAverages[nodeType] = {
|
||||
total_time: 0,
|
||||
vram_usage: 0,
|
||||
ram_usage: 0,
|
||||
count: 0,
|
||||
cache_hits: 0
|
||||
total_time: 0, vram_usage: 0, ram_usage: 0,
|
||||
_m2_time: 0, _m2_vram: 0, _m2_ram: 0,
|
||||
std_time: 0, std_vram: 0, std_ram: 0,
|
||||
count: 0, cache_hits: 0
|
||||
};
|
||||
}
|
||||
const stats = nodeAverages[nodeType];
|
||||
stats.count++;
|
||||
stats.total_time = (stats.total_time * (stats.count - 1) + (node.endTime - node.startTime)) / stats.count;
|
||||
stats.vram_usage = (stats.vram_usage * (stats.count - 1) + (node.vramAfter - node.vramBefore)) / stats.count;
|
||||
stats.ram_usage = (stats.ram_usage * (stats.count - 1) + (node.ramAfter - node.ramBefore)) / stats.count;
|
||||
if (node.cacheHit)
|
||||
stats.cache_hits++;
|
||||
const s = nodeAverages[nodeType];
|
||||
s.count++;
|
||||
const n = s.count;
|
||||
const time_val = (node.endTime || 0) - (node.startTime || 0);
|
||||
const vram_val = (node.vramAfter || 0) - (node.vramBefore || 0);
|
||||
const ram_val = (node.ramAfter || 0) - (node.ramBefore || 0);
|
||||
// Welford update for each metric
|
||||
for (const [key, val, m2key, stdkey] of [
|
||||
['total_time', time_val, '_m2_time', 'std_time'],
|
||||
['vram_usage', vram_val, '_m2_vram', 'std_vram'],
|
||||
['ram_usage', ram_val, '_m2_ram', 'std_ram'],
|
||||
]) {
|
||||
const delta = val - s[key];
|
||||
s[key] += delta / n;
|
||||
const delta2 = val - s[key];
|
||||
s[m2key] += delta * delta2;
|
||||
s[stdkey] = n > 1 ? Math.sqrt(s[m2key] / n) : 0;
|
||||
}
|
||||
if (node.cacheHit) s.cache_hits++;
|
||||
});
|
||||
});
|
||||
// Convert to array for sorting
|
||||
const nodeStats = Object.entries(nodeAverages).map(([nodeType, stats]) => ({
|
||||
nodeType,
|
||||
...stats
|
||||
const nodeStats = Object.entries(nodeAverages).map(([nodeType, s]) => ({
|
||||
nodeType, ...s
|
||||
}));
|
||||
// Sort by execution time by default
|
||||
nodeStats.sort((a, b) => b.total_time - a.total_time);
|
||||
const contentDiv = element.querySelector('.node-analytics-content');
|
||||
if (!contentDiv)
|
||||
return;
|
||||
if (!contentDiv) return;
|
||||
const html = [`
|
||||
<div style="margin: 8px 0;">
|
||||
<table>
|
||||
@@ -46,7 +54,9 @@ export function showNodeAnalytics(element, stats) {
|
||||
<tr>
|
||||
<th>Node Type</th>
|
||||
<th class="sort-numeric">Avg Time (s)</th>
|
||||
<th class="sort-numeric">\u00b1 Time (s)</th>
|
||||
<th class="sort-numeric">Avg VRAM (GB)</th>
|
||||
<th class="sort-numeric">\u00b1 VRAM (GB)</th>
|
||||
<th class="sort-numeric">Runs</th>
|
||||
<th class="sort-numeric">Cache Hit %</th>
|
||||
</tr>
|
||||
@@ -55,13 +65,17 @@ export function showNodeAnalytics(element, stats) {
|
||||
`];
|
||||
nodeStats.forEach(node => {
|
||||
const avgTime = (node.total_time / 1000).toFixed(3);
|
||||
const stdTime = (node.std_time / 1000).toFixed(3);
|
||||
const avgVram = (node.vram_usage / 1e9).toFixed(2);
|
||||
const stdVram = (node.std_vram / 1e9).toFixed(2);
|
||||
const cacheHitRate = ((node.cache_hits / node.count) * 100).toFixed(1);
|
||||
html.push(`
|
||||
<tr>
|
||||
<td>${node.nodeType}</td>
|
||||
<td>${avgTime}s</td>
|
||||
<td>${stdTime}s</td>
|
||||
<td>${avgVram}GB</td>
|
||||
<td>${stdVram}GB</td>
|
||||
<td>${node.count}</td>
|
||||
<td>${cacheHitRate}%</td>
|
||||
</tr>
|
||||
@@ -86,12 +100,8 @@ export function showNodeAnalytics(element, stats) {
|
||||
<div class="node-analytics-content"></div>
|
||||
</div>
|
||||
`;
|
||||
// Insert range selector after heading
|
||||
const heading = element.querySelector('h3');
|
||||
if (heading)
|
||||
heading.after(rangeSelector);
|
||||
// Update when time range changes
|
||||
if (heading) heading.after(rangeSelector);
|
||||
timeSelect.addEventListener('change', updateContent);
|
||||
// Initial content update
|
||||
updateContent();
|
||||
}
|
||||
|
||||
+3
-3
@@ -12,9 +12,9 @@ export function showLatestRun(element, stats) {
|
||||
`<h3>Latest Workflow Run</h3>`,
|
||||
`<div style="margin: 8px 0;">`,
|
||||
` <div>Last Run: ${(workflow_time / 1000).toFixed(2)}s</div>`,
|
||||
` <div>Average: ${(workflow_avg.total_time / 1000 || 0).toFixed(2)}s (${workflow_avg.count || 0} runs)</div>`,
|
||||
` <div>VRAM Peak: ${(profile.totalVramPeak / 1e9).toFixed(2)}GB</div>`,
|
||||
` <div>RAM Peak: ${(profile.totalRamPeak / 1e9).toFixed(2)}GB</div>`,
|
||||
` <div>Average: ${(workflow_avg.total_time / 1000 || 0).toFixed(2)}s \u00b1${(workflow_avg.std_time / 1000 || 0).toFixed(2)}s (${workflow_avg.count || 0} runs)</div>`,
|
||||
` <div>VRAM Peak: ${(profile.totalVramPeak / 1e9).toFixed(2)}GB (avg \u00b1${(workflow_avg.std_vram / 1e9 || 0).toFixed(2)}GB)</div>`,
|
||||
` <div>RAM Peak: ${(profile.totalRamPeak / 1e9).toFixed(2)}GB (avg \u00b1${(workflow_avg.std_ram / 1e9 || 0).toFixed(2)}GB)</div>`,
|
||||
` <div>Cache: ${profile.cacheHits} hits / ${profile.cacheMisses} misses</div>`,
|
||||
`</div>`,
|
||||
`<div style="margin: 8px 0;">`,
|
||||
|
||||
Reference in New Issue
Block a user