@appkit/llamacpp-cli 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/MONITORING-ACCURACY-FIX.md +199 -0
  3. package/PER-PROCESS-METRICS.md +190 -0
  4. package/README.md +124 -9
  5. package/dist/cli.js +32 -7
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/config.d.ts.map +1 -1
  8. package/dist/commands/config.js +15 -1
  9. package/dist/commands/config.js.map +1 -1
  10. package/dist/commands/create.d.ts.map +1 -1
  11. package/dist/commands/create.js +12 -4
  12. package/dist/commands/create.js.map +1 -1
  13. package/dist/commands/delete.js +12 -10
  14. package/dist/commands/delete.js.map +1 -1
  15. package/dist/commands/logs-all.d.ts +9 -0
  16. package/dist/commands/logs-all.d.ts.map +1 -0
  17. package/dist/commands/logs-all.js +209 -0
  18. package/dist/commands/logs-all.js.map +1 -0
  19. package/dist/commands/logs.d.ts +4 -0
  20. package/dist/commands/logs.d.ts.map +1 -1
  21. package/dist/commands/logs.js +108 -2
  22. package/dist/commands/logs.js.map +1 -1
  23. package/dist/commands/monitor.d.ts.map +1 -1
  24. package/dist/commands/monitor.js +51 -1
  25. package/dist/commands/monitor.js.map +1 -1
  26. package/dist/commands/ps.d.ts +3 -1
  27. package/dist/commands/ps.d.ts.map +1 -1
  28. package/dist/commands/ps.js +75 -5
  29. package/dist/commands/ps.js.map +1 -1
  30. package/dist/commands/rm.d.ts.map +1 -1
  31. package/dist/commands/rm.js +5 -12
  32. package/dist/commands/rm.js.map +1 -1
  33. package/dist/commands/server-show.d.ts.map +1 -1
  34. package/dist/commands/server-show.js +30 -3
  35. package/dist/commands/server-show.js.map +1 -1
  36. package/dist/commands/start.d.ts.map +1 -1
  37. package/dist/commands/start.js +34 -7
  38. package/dist/commands/start.js.map +1 -1
  39. package/dist/commands/stop.js +3 -3
  40. package/dist/commands/stop.js.map +1 -1
  41. package/dist/lib/history-manager.d.ts +46 -0
  42. package/dist/lib/history-manager.d.ts.map +1 -0
  43. package/dist/lib/history-manager.js +157 -0
  44. package/dist/lib/history-manager.js.map +1 -0
  45. package/dist/lib/metrics-aggregator.d.ts +2 -1
  46. package/dist/lib/metrics-aggregator.d.ts.map +1 -1
  47. package/dist/lib/metrics-aggregator.js +15 -4
  48. package/dist/lib/metrics-aggregator.js.map +1 -1
  49. package/dist/lib/system-collector.d.ts +9 -4
  50. package/dist/lib/system-collector.d.ts.map +1 -1
  51. package/dist/lib/system-collector.js +29 -28
  52. package/dist/lib/system-collector.js.map +1 -1
  53. package/dist/tui/HistoricalMonitorApp.d.ts +5 -0
  54. package/dist/tui/HistoricalMonitorApp.d.ts.map +1 -0
  55. package/dist/tui/HistoricalMonitorApp.js +490 -0
  56. package/dist/tui/HistoricalMonitorApp.js.map +1 -0
  57. package/dist/tui/MonitorApp.d.ts.map +1 -1
  58. package/dist/tui/MonitorApp.js +84 -62
  59. package/dist/tui/MonitorApp.js.map +1 -1
  60. package/dist/tui/MultiServerMonitorApp.d.ts +1 -1
  61. package/dist/tui/MultiServerMonitorApp.d.ts.map +1 -1
  62. package/dist/tui/MultiServerMonitorApp.js +293 -77
  63. package/dist/tui/MultiServerMonitorApp.js.map +1 -1
  64. package/dist/types/history-types.d.ts +30 -0
  65. package/dist/types/history-types.d.ts.map +1 -0
  66. package/dist/types/history-types.js +11 -0
  67. package/dist/types/history-types.js.map +1 -0
  68. package/dist/types/monitor-types.d.ts +1 -0
  69. package/dist/types/monitor-types.d.ts.map +1 -1
  70. package/dist/types/server-config.d.ts +1 -0
  71. package/dist/types/server-config.d.ts.map +1 -1
  72. package/dist/types/server-config.js.map +1 -1
  73. package/dist/utils/downsample-utils.d.ts +35 -0
  74. package/dist/utils/downsample-utils.d.ts.map +1 -0
  75. package/dist/utils/downsample-utils.js +107 -0
  76. package/dist/utils/downsample-utils.js.map +1 -0
  77. package/dist/utils/file-utils.d.ts +6 -0
  78. package/dist/utils/file-utils.d.ts.map +1 -1
  79. package/dist/utils/file-utils.js +38 -0
  80. package/dist/utils/file-utils.js.map +1 -1
  81. package/dist/utils/log-utils.d.ts +43 -0
  82. package/dist/utils/log-utils.d.ts.map +1 -0
  83. package/dist/utils/log-utils.js +190 -0
  84. package/dist/utils/log-utils.js.map +1 -0
  85. package/dist/utils/process-utils.d.ts +19 -1
  86. package/dist/utils/process-utils.d.ts.map +1 -1
  87. package/dist/utils/process-utils.js +79 -1
  88. package/dist/utils/process-utils.js.map +1 -1
  89. package/docs/images/.gitkeep +1 -0
  90. package/package.json +3 -1
  91. package/src/cli.ts +32 -7
  92. package/src/commands/config.ts +15 -1
  93. package/src/commands/create.ts +14 -5
  94. package/src/commands/delete.ts +10 -10
  95. package/src/commands/logs-all.ts +251 -0
  96. package/src/commands/logs.ts +138 -2
  97. package/src/commands/monitor.ts +21 -1
  98. package/src/commands/ps.ts +88 -5
  99. package/src/commands/rm.ts +5 -12
  100. package/src/commands/server-show.ts +35 -3
  101. package/src/commands/start.ts +35 -7
  102. package/src/commands/stop.ts +3 -3
  103. package/src/lib/history-manager.ts +172 -0
  104. package/src/lib/metrics-aggregator.ts +18 -5
  105. package/src/lib/system-collector.ts +31 -28
  106. package/src/tui/HistoricalMonitorApp.ts +548 -0
  107. package/src/tui/MonitorApp.ts +89 -64
  108. package/src/tui/MultiServerMonitorApp.ts +348 -103
  109. package/src/types/history-types.ts +39 -0
  110. package/src/types/monitor-types.ts +1 -0
  111. package/src/types/server-config.ts +1 -0
  112. package/src/utils/downsample-utils.ts +128 -0
  113. package/src/utils/file-utils.ts +40 -0
  114. package/src/utils/log-utils.ts +178 -0
  115. package/src/utils/process-utils.ts +85 -1
  116. package/test-load.sh +100 -0
  117. package/dist/tui/components/ErrorState.d.ts +0 -8
  118. package/dist/tui/components/ErrorState.d.ts.map +0 -1
  119. package/dist/tui/components/ErrorState.js +0 -22
  120. package/dist/tui/components/ErrorState.js.map +0 -1
  121. package/dist/tui/components/LoadingState.d.ts +0 -8
  122. package/dist/tui/components/LoadingState.d.ts.map +0 -1
  123. package/dist/tui/components/LoadingState.js +0 -21
  124. package/dist/tui/components/LoadingState.js.map +0 -1
@@ -0,0 +1,548 @@
1
+ import blessed from 'blessed';
2
+ import * as asciichart from 'asciichart';
3
+ import { ServerConfig } from '../types/server-config.js';
4
+ import { HistoryManager } from '../lib/history-manager.js';
5
+ import { HistorySnapshot } from '../types/history-types.js';
6
+ import {
7
+ downsampleMaxTimeWithFullHour,
8
+ downsampleMeanTimeWithFullHour,
9
+ getDownsampleRatio,
10
+ TimeSeriesPoint
11
+ } from '../utils/downsample-utils.js';
12
+
13
+ type ViewMode = 'recent' | 'hour';
14
+
15
+ interface ChartStats {
16
+ avg: number;
17
+ max: number;
18
+ min: number;
19
+ stddev: number;
20
+ }
21
+
22
+ interface ChartConfig {
23
+ title: string;
24
+ color: typeof asciichart.cyan;
25
+ formatValue: (x: number) => string;
26
+ isPercentage: boolean;
27
+ noDataMessage: string;
28
+ }
29
+
30
+ /**
31
+ * Calculate statistics for a set of values.
32
+ */
33
+ function calculateStats(values: number[]): ChartStats {
34
+ if (values.length === 0) {
35
+ return { avg: 0, max: 0, min: 0, stddev: 0 };
36
+ }
37
+
38
+ const avg = values.reduce((a, b) => a + b, 0) / values.length;
39
+ const max = Math.max(...values);
40
+ const min = Math.min(...values);
41
+ const variance = values.reduce((sum, val) => sum + Math.pow(val - avg, 2), 0) / values.length;
42
+ const stddev = Math.sqrt(variance);
43
+
44
+ return { avg, max, min, stddev };
45
+ }
46
+
47
+ /**
48
+ * Calculate expanded range for chart y-axis to prevent duplicate labels.
49
+ */
50
+ function getExpandedRange(data: number[], isPercentage: boolean): { min: number; max: number } {
51
+ if (isPercentage) return { min: 0, max: 100 };
52
+ if (data.length === 0) return { min: 0, max: 10 };
53
+
54
+ const dataMin = Math.min(...data);
55
+ const dataMax = Math.max(...data);
56
+ const range = dataMax - dataMin;
57
+ const padding = Math.max(range * 0.3, 5);
58
+
59
+ return {
60
+ min: Math.max(0, Math.floor(dataMin - padding)),
61
+ max: Math.ceil(dataMax + padding)
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Create a scrollable content box for historical charts.
67
+ */
68
+ function createContentBox(): blessed.Widgets.BoxElement {
69
+ return blessed.box({
70
+ top: 0,
71
+ left: 0,
72
+ width: '100%',
73
+ height: '100%',
74
+ tags: true,
75
+ scrollable: true,
76
+ alwaysScroll: true,
77
+ keys: true,
78
+ vi: true,
79
+ mouse: true,
80
+ scrollbar: {
81
+ ch: '\u2588',
82
+ style: { fg: 'blue' },
83
+ },
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Render a single chart with statistics.
89
+ */
90
+ function renderChart(
91
+ values: number[],
92
+ rawValues: TimeSeriesPoint[],
93
+ config: ChartConfig,
94
+ chartHeight: number
95
+ ): string {
96
+ let content = `{bold}${config.title}{/bold}\n`;
97
+ const validValues = values.filter(v => !isNaN(v) && v > 0);
98
+ const plotData = values.map(v => isNaN(v) ? 0 : v);
99
+
100
+ try {
101
+ if (validValues.length >= 2) {
102
+ const range = getExpandedRange(validValues, config.isPercentage);
103
+ content += asciichart.plot(plotData, {
104
+ height: chartHeight,
105
+ colors: [config.color],
106
+ format: config.formatValue,
107
+ min: range.min,
108
+ max: range.max,
109
+ });
110
+ content += '\n';
111
+
112
+ const stats = calculateStats(validValues);
113
+ const lastValue = rawValues[rawValues.length - 1]?.value ?? 0;
114
+
115
+ if (config.title.includes('GB')) {
116
+ content += ` Avg: ${stats.avg.toFixed(2)} GB (\u00b1${stats.stddev.toFixed(2)}) `;
117
+ content += `Max: ${stats.max.toFixed(2)} GB `;
118
+ content += `Min: ${stats.min.toFixed(2)} GB `;
119
+ content += `Last: ${lastValue.toFixed(2)} GB\n\n`;
120
+ } else if (config.isPercentage) {
121
+ content += ` Avg: ${stats.avg.toFixed(1)}% (\u00b1${stats.stddev.toFixed(1)}) `;
122
+ content += `Max: ${stats.max.toFixed(1)}% `;
123
+ content += `Min: ${stats.min.toFixed(1)}% `;
124
+ content += `Last: ${lastValue.toFixed(1)}%\n\n`;
125
+ } else {
126
+ content += ` Avg: ${stats.avg.toFixed(1)} tok/s (\u00b1${stats.stddev.toFixed(1)}) `;
127
+ content += `Max: ${stats.max.toFixed(1)} tok/s `;
128
+ content += `Last: ${lastValue.toFixed(1)} tok/s\n\n`;
129
+ }
130
+ } else {
131
+ const defaultRange = config.isPercentage ? { min: 0, max: 100 } : { min: 0, max: 10 };
132
+ content += asciichart.plot(plotData, {
133
+ height: chartHeight,
134
+ colors: [config.color],
135
+ format: config.formatValue,
136
+ min: defaultRange.min,
137
+ max: defaultRange.max,
138
+ });
139
+ content += `\n{gray-fg} ${config.noDataMessage}{/gray-fg}\n\n`;
140
+ }
141
+ } catch {
142
+ content += '{red-fg} Error rendering chart{/red-fg}\n\n';
143
+ }
144
+
145
+ return content;
146
+ }
147
+
148
+ export async function createHistoricalUI(
149
+ screen: blessed.Widgets.Screen,
150
+ server: ServerConfig,
151
+ onBack: () => void
152
+ ): Promise<void> {
153
+ const historyManager = new HistoryManager(server.id);
154
+ let refreshIntervalId: NodeJS.Timeout | null = null;
155
+ const REFRESH_INTERVAL = 1000;
156
+ let lastGoodRender: string | null = null;
157
+ let consecutiveErrors = 0;
158
+ let viewMode: ViewMode = 'recent';
159
+
160
+ const contentBox = createContentBox();
161
+ screen.append(contentBox);
162
+
163
+ // Chart configurations
164
+ const chartConfigs: Record<string, ChartConfig> = {
165
+ tokenSpeed: {
166
+ title: 'Model Token Generation Speed (tok/s)',
167
+ color: asciichart.cyan,
168
+ formatValue: (x: number) => Math.round(x).toFixed(0).padStart(6, ' '),
169
+ isPercentage: false,
170
+ noDataMessage: 'No generation activity in this time window',
171
+ },
172
+ cpu: {
173
+ title: 'Model CPU Usage (%)',
174
+ color: asciichart.blue,
175
+ formatValue: (x: number) => Math.round(x).toFixed(0).padStart(6, ' '),
176
+ isPercentage: true,
177
+ noDataMessage: 'No CPU data in this time window',
178
+ },
179
+ memory: {
180
+ title: 'Model Memory Usage (GB)',
181
+ color: asciichart.magenta,
182
+ formatValue: (x: number) => x.toFixed(2).padStart(6, ' '),
183
+ isPercentage: false,
184
+ noDataMessage: 'No memory data in this time window',
185
+ },
186
+ gpu: {
187
+ title: 'System GPU Usage (%)',
188
+ color: asciichart.green,
189
+ formatValue: (x: number) => Math.round(x).toFixed(0).padStart(6, ' '),
190
+ isPercentage: true,
191
+ noDataMessage: 'No GPU data in this time window',
192
+ },
193
+ };
194
+
195
+ async function render(): Promise<void> {
196
+ try {
197
+ const termWidth = (screen.width as number) || 80;
198
+ const divider = '\u2500'.repeat(termWidth - 2);
199
+ const chartHeight = 5;
200
+ const chartWidth = Math.min(Math.max(termWidth - 20, 40), 80);
201
+
202
+ if (chartWidth <= 0 || !Number.isFinite(chartWidth)) {
203
+ contentBox.setContent('{red-fg}Error: Invalid chart width{/red-fg}\n');
204
+ screen.render();
205
+ return;
206
+ }
207
+
208
+ // Header
209
+ const modeLabel = viewMode === 'recent' ? 'Minute' : 'Hour';
210
+ const modeColor = viewMode === 'recent' ? 'cyan' : 'magenta';
211
+ let content = `{bold}{blue-fg}\u2550\u2550\u2550 ${server.modelName} (${server.port}) {/blue-fg} `;
212
+ content += `{${modeColor}-fg}[${modeLabel}]{/${modeColor}-fg}{/bold}\n\n`;
213
+
214
+ const snapshots = await historyManager.loadHistoryByWindow('1h');
215
+
216
+ if (snapshots.length === 0) {
217
+ content += '{yellow-fg}No historical data available.{/yellow-fg}\n\n';
218
+ content += 'Historical data is collected when you run the monitor command.\n';
219
+ content += 'Start monitoring to begin collecting history.\n\n';
220
+ content += divider + '\n';
221
+ content += '{gray-fg}ESC = Back Q = Quit{/gray-fg}';
222
+ contentBox.setContent(content);
223
+ screen.render();
224
+ return;
225
+ }
226
+
227
+ const maxChartPoints = Math.min(chartWidth, 80);
228
+ const displaySnapshots = viewMode === 'recent' && snapshots.length > maxChartPoints
229
+ ? snapshots.slice(-maxChartPoints)
230
+ : snapshots;
231
+
232
+ // Extract time-series data
233
+ const rawData = {
234
+ tokenSpeed: [] as TimeSeriesPoint[],
235
+ gpu: [] as TimeSeriesPoint[],
236
+ cpu: [] as TimeSeriesPoint[],
237
+ memory: [] as TimeSeriesPoint[],
238
+ };
239
+
240
+ for (const snapshot of displaySnapshots) {
241
+ const ts = snapshot.timestamp;
242
+ rawData.tokenSpeed.push({ timestamp: ts, value: snapshot.server.avgGenerateSpeed || 0 });
243
+ rawData.gpu.push({ timestamp: ts, value: snapshot.system?.gpuUsage || 0 });
244
+ rawData.cpu.push({ timestamp: ts, value: snapshot.server.processCpuUsage || 0 });
245
+ rawData.memory.push({
246
+ timestamp: ts,
247
+ value: snapshot.server.processMemory ? snapshot.server.processMemory / (1024 ** 3) : 0
248
+ });
249
+ }
250
+
251
+ // Apply downsampling based on view mode
252
+ const useDownsampling = viewMode === 'hour';
253
+ const values = {
254
+ tokenSpeed: useDownsampling
255
+ ? downsampleMaxTimeWithFullHour(rawData.tokenSpeed, maxChartPoints)
256
+ : rawData.tokenSpeed.map(p => p.value),
257
+ cpu: useDownsampling
258
+ ? downsampleMaxTimeWithFullHour(rawData.cpu, maxChartPoints)
259
+ : rawData.cpu.map(p => p.value),
260
+ memory: useDownsampling
261
+ ? downsampleMeanTimeWithFullHour(rawData.memory, maxChartPoints)
262
+ : rawData.memory.map(p => p.value),
263
+ gpu: useDownsampling
264
+ ? downsampleMaxTimeWithFullHour(rawData.gpu, maxChartPoints)
265
+ : rawData.gpu.map(p => p.value),
266
+ };
267
+
268
+ // Render all charts
269
+ content += renderChart(values.tokenSpeed, rawData.tokenSpeed, chartConfigs.tokenSpeed, chartHeight);
270
+ content += renderChart(values.cpu, rawData.cpu, chartConfigs.cpu, chartHeight);
271
+ content += renderChart(values.memory, rawData.memory, chartConfigs.memory, chartHeight);
272
+ content += renderChart(values.gpu, rawData.gpu, chartConfigs.gpu, chartHeight);
273
+
274
+ // Footer
275
+ content += divider + '\n';
276
+ content += `{gray-fg}Updated: ${new Date().toLocaleTimeString()} | H = Toggle Hour View ESC = Back Q = Quit{/gray-fg}`;
277
+
278
+ contentBox.setContent(content);
279
+ screen.render();
280
+
281
+ lastGoodRender = content;
282
+ consecutiveErrors = 0;
283
+ } catch (error) {
284
+ consecutiveErrors++;
285
+ if (lastGoodRender && consecutiveErrors < 5) {
286
+ contentBox.setContent(lastGoodRender);
287
+ } else {
288
+ const errorMsg = error instanceof Error ? error.message : String(error);
289
+ contentBox.setContent(
290
+ '{bold}{red-fg}Render Error{/red-fg}{/bold}\n\n' +
291
+ `{red-fg}${errorMsg}{/red-fg}\n\n` +
292
+ `Consecutive errors: ${consecutiveErrors}\n\n` +
293
+ '{gray-fg}ESC = Back Q = Quit{/gray-fg}'
294
+ );
295
+ }
296
+ screen.render();
297
+ }
298
+ }
299
+
300
+ function cleanup(): void {
301
+ if (refreshIntervalId) {
302
+ clearInterval(refreshIntervalId);
303
+ refreshIntervalId = null;
304
+ }
305
+ }
306
+
307
+ screen.key(['h', 'H'], () => {
308
+ viewMode = viewMode === 'recent' ? 'hour' : 'recent';
309
+ render();
310
+ });
311
+
312
+ screen.key(['escape'], () => {
313
+ cleanup();
314
+ screen.remove(contentBox);
315
+ onBack();
316
+ });
317
+
318
+ screen.key(['q', 'Q', 'C-c'], () => {
319
+ cleanup();
320
+ screen.destroy();
321
+ process.exit(0);
322
+ });
323
+
324
+ contentBox.setContent('{cyan-fg}\u23f3 Loading historical data...{/cyan-fg}');
325
+ screen.render();
326
+ await render();
327
+
328
+ refreshIntervalId = setInterval(render, REFRESH_INTERVAL);
329
+ }
330
+
331
+ // Multi-server historical view
332
+ export async function createMultiServerHistoricalUI(
333
+ screen: blessed.Widgets.Screen,
334
+ servers: ServerConfig[],
335
+ _selectedIndex: number,
336
+ onBack: () => void
337
+ ): Promise<void> {
338
+ let refreshIntervalId: NodeJS.Timeout | null = null;
339
+ const REFRESH_INTERVAL = 3000;
340
+ let lastGoodRender: string | null = null;
341
+ let consecutiveErrors = 0;
342
+ let viewMode: ViewMode = 'recent';
343
+
344
+ const contentBox = createContentBox();
345
+ screen.append(contentBox);
346
+
347
+ // Chart configurations for multi-server view
348
+ const chartConfigs: Record<string, ChartConfig> = {
349
+ tokenSpeed: {
350
+ title: 'Total Model Token Generation Speed (tok/s)',
351
+ color: asciichart.cyan,
352
+ formatValue: (x: number) => Math.round(x).toFixed(0).padStart(6, ' '),
353
+ isPercentage: false,
354
+ noDataMessage: 'No generation activity in this time window',
355
+ },
356
+ cpu: {
357
+ title: 'Total Model CPU Usage (%)',
358
+ color: asciichart.blue,
359
+ formatValue: (x: number) => Math.round(x).toFixed(0).padStart(6, ' '),
360
+ isPercentage: true,
361
+ noDataMessage: 'No CPU data in this time window',
362
+ },
363
+ memory: {
364
+ title: 'Total Model Memory Usage (GB)',
365
+ color: asciichart.magenta,
366
+ formatValue: (x: number) => x.toFixed(2).padStart(6, ' '),
367
+ isPercentage: false,
368
+ noDataMessage: 'No memory data in this time window',
369
+ },
370
+ gpu: {
371
+ title: 'System GPU Usage (%)',
372
+ color: asciichart.green,
373
+ formatValue: (x: number) => Math.round(x).toFixed(0).padStart(6, ' '),
374
+ isPercentage: true,
375
+ noDataMessage: 'No GPU data in this time window',
376
+ },
377
+ };
378
+
379
+ async function render(): Promise<void> {
380
+ try {
381
+ const termWidth = (screen.width as number) || 80;
382
+ const divider = '\u2500'.repeat(termWidth - 2);
383
+ const chartWidth = Math.min(Math.max(40, termWidth - 20), 80);
384
+ const chartHeight = 5;
385
+
386
+ // Header
387
+ const modeLabel = viewMode === 'recent' ? 'Minute' : 'Hour';
388
+ const modeColor = viewMode === 'recent' ? 'cyan' : 'magenta';
389
+ let content = `{bold}{blue-fg}\u2550\u2550\u2550 All servers (${servers.length}){/blue-fg} `;
390
+ content += `{${modeColor}-fg}[${modeLabel}]{/${modeColor}-fg}{/bold}\n\n`;
391
+
392
+ // Load and aggregate history for all servers
393
+ const serverHistories = await Promise.all(
394
+ servers.map(async (server) => {
395
+ const manager = new HistoryManager(server.id);
396
+ return manager.loadHistoryByWindow('1h');
397
+ })
398
+ );
399
+
400
+ // Aggregate data across all servers at each timestamp
401
+ const ALIGNMENT_INTERVAL = 2000;
402
+ const timestampMap = new Map<number, {
403
+ tokensPerSec: number[];
404
+ gpuUsage: number[];
405
+ cpuUsage: number[];
406
+ memoryGB: number[];
407
+ }>();
408
+
409
+ for (const snapshots of serverHistories) {
410
+ for (const snapshot of snapshots) {
411
+ const timestamp = Math.round(snapshot.timestamp / ALIGNMENT_INTERVAL) * ALIGNMENT_INTERVAL;
412
+
413
+ if (!timestampMap.has(timestamp)) {
414
+ timestampMap.set(timestamp, {
415
+ tokensPerSec: [],
416
+ gpuUsage: [],
417
+ cpuUsage: [],
418
+ memoryGB: [],
419
+ });
420
+ }
421
+
422
+ const data = timestampMap.get(timestamp)!;
423
+
424
+ if (snapshot.server.avgGenerateSpeed && snapshot.server.avgGenerateSpeed > 0) {
425
+ data.tokensPerSec.push(snapshot.server.avgGenerateSpeed);
426
+ }
427
+ if (snapshot.system?.gpuUsage !== undefined) {
428
+ data.gpuUsage.push(snapshot.system.gpuUsage);
429
+ }
430
+ if (snapshot.server.processCpuUsage !== undefined) {
431
+ data.cpuUsage.push(snapshot.server.processCpuUsage);
432
+ }
433
+ if (snapshot.server.processMemory) {
434
+ data.memoryGB.push(snapshot.server.processMemory / (1024 ** 3));
435
+ }
436
+ }
437
+ }
438
+
439
+ // Sort timestamps and aggregate
440
+ const timestamps = Array.from(timestampMap.keys()).sort((a, b) => a - b);
441
+ const aggregatedData = timestamps.map(ts => {
442
+ const data = timestampMap.get(ts)!;
443
+ return {
444
+ timestamp: ts,
445
+ totalTokS: data.tokensPerSec.reduce((a, b) => a + b, 0),
446
+ avgGpu: data.gpuUsage.length > 0
447
+ ? data.gpuUsage.reduce((a, b) => a + b, 0) / data.gpuUsage.length
448
+ : 0,
449
+ totalCpu: data.cpuUsage.reduce((a, b) => a + b, 0),
450
+ totalMemoryGB: data.memoryGB.reduce((a, b) => a + b, 0),
451
+ };
452
+ });
453
+
454
+ if (aggregatedData.length > 0) {
455
+ const maxPoints = Math.min(chartWidth, 80);
456
+ const displayData = viewMode === 'recent' && aggregatedData.length > maxPoints
457
+ ? aggregatedData.slice(-maxPoints)
458
+ : aggregatedData;
459
+
460
+ const useDownsampling = viewMode === 'hour';
461
+
462
+ // Extract time-series data
463
+ const rawData = {
464
+ tokenSpeed: displayData.map(d => ({ timestamp: d.timestamp, value: d.totalTokS })),
465
+ cpu: displayData.map(d => ({ timestamp: d.timestamp, value: d.totalCpu })),
466
+ memory: displayData.map(d => ({ timestamp: d.timestamp, value: d.totalMemoryGB })),
467
+ gpu: displayData.map(d => ({ timestamp: d.timestamp, value: d.avgGpu })),
468
+ };
469
+
470
+ // Apply downsampling
471
+ const values = {
472
+ tokenSpeed: useDownsampling
473
+ ? downsampleMaxTimeWithFullHour(rawData.tokenSpeed, chartWidth)
474
+ : rawData.tokenSpeed.map(d => d.value),
475
+ cpu: useDownsampling
476
+ ? downsampleMaxTimeWithFullHour(rawData.cpu, chartWidth)
477
+ : rawData.cpu.map(d => d.value),
478
+ memory: useDownsampling
479
+ ? downsampleMeanTimeWithFullHour(rawData.memory, chartWidth)
480
+ : rawData.memory.map(d => d.value),
481
+ gpu: useDownsampling
482
+ ? downsampleMaxTimeWithFullHour(rawData.gpu, chartWidth)
483
+ : rawData.gpu.map(d => d.value),
484
+ };
485
+
486
+ // Render all charts
487
+ content += renderChart(values.tokenSpeed, rawData.tokenSpeed, chartConfigs.tokenSpeed, chartHeight);
488
+ content += renderChart(values.cpu, rawData.cpu, chartConfigs.cpu, chartHeight);
489
+ content += renderChart(values.memory, rawData.memory, chartConfigs.memory, chartHeight);
490
+ content += renderChart(values.gpu, rawData.gpu, chartConfigs.gpu, chartHeight);
491
+ }
492
+
493
+ // Footer
494
+ content += divider + '\n';
495
+ content += `{gray-fg}Updated: ${new Date().toLocaleTimeString()} | H = Toggle Hour View ESC = Back Q = Quit{/gray-fg}`;
496
+
497
+ contentBox.setContent(content);
498
+ screen.render();
499
+
500
+ lastGoodRender = content;
501
+ consecutiveErrors = 0;
502
+ } catch (error) {
503
+ consecutiveErrors++;
504
+ if (lastGoodRender && consecutiveErrors < 5) {
505
+ contentBox.setContent(lastGoodRender);
506
+ } else {
507
+ const errorMsg = error instanceof Error ? error.message : String(error);
508
+ contentBox.setContent(
509
+ '{bold}{red-fg}Render Error{/red-fg}{/bold}\n\n' +
510
+ `{red-fg}${errorMsg}{/red-fg}\n\n` +
511
+ `Consecutive errors: ${consecutiveErrors}\n\n` +
512
+ '{gray-fg}ESC = Back Q = Quit{/gray-fg}'
513
+ );
514
+ }
515
+ screen.render();
516
+ }
517
+ }
518
+
519
+ function cleanup(): void {
520
+ if (refreshIntervalId) {
521
+ clearInterval(refreshIntervalId);
522
+ refreshIntervalId = null;
523
+ }
524
+ }
525
+
526
+ screen.key(['h', 'H'], () => {
527
+ viewMode = viewMode === 'recent' ? 'hour' : 'recent';
528
+ render();
529
+ });
530
+
531
+ screen.key(['escape'], () => {
532
+ cleanup();
533
+ screen.remove(contentBox);
534
+ onBack();
535
+ });
536
+
537
+ screen.key(['q', 'Q', 'C-c'], () => {
538
+ cleanup();
539
+ screen.destroy();
540
+ process.exit(0);
541
+ });
542
+
543
+ contentBox.setContent('{cyan-fg}\u23f3 Loading historical data...{/cyan-fg}');
544
+ screen.render();
545
+ await render();
546
+
547
+ refreshIntervalId = setInterval(render, REFRESH_INTERVAL);
548
+ }