@appkit/llamacpp-cli 1.4.1 → 1.6.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 (91) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/MONITORING-ACCURACY-FIX.md +199 -0
  3. package/PER-PROCESS-METRICS.md +190 -0
  4. package/README.md +136 -1
  5. package/dist/cli.js +21 -4
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/create.d.ts.map +1 -1
  8. package/dist/commands/create.js +12 -3
  9. package/dist/commands/create.js.map +1 -1
  10. package/dist/commands/monitor.d.ts +2 -0
  11. package/dist/commands/monitor.d.ts.map +1 -0
  12. package/dist/commands/monitor.js +126 -0
  13. package/dist/commands/monitor.js.map +1 -0
  14. package/dist/commands/ps.d.ts +3 -1
  15. package/dist/commands/ps.d.ts.map +1 -1
  16. package/dist/commands/ps.js +75 -5
  17. package/dist/commands/ps.js.map +1 -1
  18. package/dist/commands/server-show.d.ts.map +1 -1
  19. package/dist/commands/server-show.js +10 -3
  20. package/dist/commands/server-show.js.map +1 -1
  21. package/dist/commands/start.d.ts.map +1 -1
  22. package/dist/commands/start.js +14 -2
  23. package/dist/commands/start.js.map +1 -1
  24. package/dist/lib/history-manager.d.ts +46 -0
  25. package/dist/lib/history-manager.d.ts.map +1 -0
  26. package/dist/lib/history-manager.js +157 -0
  27. package/dist/lib/history-manager.js.map +1 -0
  28. package/dist/lib/metrics-aggregator.d.ts +40 -0
  29. package/dist/lib/metrics-aggregator.d.ts.map +1 -0
  30. package/dist/lib/metrics-aggregator.js +211 -0
  31. package/dist/lib/metrics-aggregator.js.map +1 -0
  32. package/dist/lib/system-collector.d.ts +80 -0
  33. package/dist/lib/system-collector.d.ts.map +1 -0
  34. package/dist/lib/system-collector.js +311 -0
  35. package/dist/lib/system-collector.js.map +1 -0
  36. package/dist/tui/HistoricalMonitorApp.d.ts +5 -0
  37. package/dist/tui/HistoricalMonitorApp.d.ts.map +1 -0
  38. package/dist/tui/HistoricalMonitorApp.js +490 -0
  39. package/dist/tui/HistoricalMonitorApp.js.map +1 -0
  40. package/dist/tui/MonitorApp.d.ts +4 -0
  41. package/dist/tui/MonitorApp.d.ts.map +1 -0
  42. package/dist/tui/MonitorApp.js +315 -0
  43. package/dist/tui/MonitorApp.js.map +1 -0
  44. package/dist/tui/MultiServerMonitorApp.d.ts +4 -0
  45. package/dist/tui/MultiServerMonitorApp.d.ts.map +1 -0
  46. package/dist/tui/MultiServerMonitorApp.js +712 -0
  47. package/dist/tui/MultiServerMonitorApp.js.map +1 -0
  48. package/dist/types/history-types.d.ts +30 -0
  49. package/dist/types/history-types.d.ts.map +1 -0
  50. package/dist/types/history-types.js +11 -0
  51. package/dist/types/history-types.js.map +1 -0
  52. package/dist/types/monitor-types.d.ts +123 -0
  53. package/dist/types/monitor-types.d.ts.map +1 -0
  54. package/dist/types/monitor-types.js +3 -0
  55. package/dist/types/monitor-types.js.map +1 -0
  56. package/dist/types/server-config.d.ts +1 -0
  57. package/dist/types/server-config.d.ts.map +1 -1
  58. package/dist/types/server-config.js.map +1 -1
  59. package/dist/utils/downsample-utils.d.ts +35 -0
  60. package/dist/utils/downsample-utils.d.ts.map +1 -0
  61. package/dist/utils/downsample-utils.js +107 -0
  62. package/dist/utils/downsample-utils.js.map +1 -0
  63. package/dist/utils/file-utils.d.ts +6 -0
  64. package/dist/utils/file-utils.d.ts.map +1 -1
  65. package/dist/utils/file-utils.js +38 -0
  66. package/dist/utils/file-utils.js.map +1 -1
  67. package/dist/utils/process-utils.d.ts +35 -2
  68. package/dist/utils/process-utils.d.ts.map +1 -1
  69. package/dist/utils/process-utils.js +220 -25
  70. package/dist/utils/process-utils.js.map +1 -1
  71. package/docs/images/.gitkeep +1 -0
  72. package/package.json +5 -1
  73. package/src/cli.ts +21 -4
  74. package/src/commands/create.ts +14 -4
  75. package/src/commands/monitor.ts +110 -0
  76. package/src/commands/ps.ts +88 -5
  77. package/src/commands/server-show.ts +10 -3
  78. package/src/commands/start.ts +15 -2
  79. package/src/lib/history-manager.ts +172 -0
  80. package/src/lib/metrics-aggregator.ts +257 -0
  81. package/src/lib/system-collector.ts +315 -0
  82. package/src/tui/HistoricalMonitorApp.ts +548 -0
  83. package/src/tui/MonitorApp.ts +386 -0
  84. package/src/tui/MultiServerMonitorApp.ts +792 -0
  85. package/src/types/history-types.ts +39 -0
  86. package/src/types/monitor-types.ts +162 -0
  87. package/src/types/server-config.ts +1 -0
  88. package/src/utils/downsample-utils.ts +128 -0
  89. package/src/utils/file-utils.ts +40 -0
  90. package/src/utils/process-utils.ts +243 -25
  91. package/test-load.sh +100 -0
@@ -0,0 +1,315 @@
1
+ import { execCommand, spawnAndReadOneLine } from '../utils/process-utils.js';
2
+ import { SystemMetrics } from '../types/monitor-types.js';
3
+
4
+ /**
5
+ * System metrics collector using macmon (optional) and vm_stat (fallback)
6
+ * Provides GPU, CPU, ANE, and memory metrics on macOS
7
+ */
8
+ export class SystemCollector {
9
+ private macmonPath: string;
10
+ private macmonAvailable: boolean | null = null;
11
+ private lastSystemMetrics: SystemMetrics | null = null;
12
+ private lastCollectionTime: number = 0;
13
+ private readonly CACHE_TTL_MS = 4000; // Cache for 4 seconds (longer than macmon spawn time)
14
+ private collectingLock: Promise<SystemMetrics> | null = null;
15
+ private pCoreCount: number = 0;
16
+ private eCoreCount: number = 0;
17
+ private totalCores: number = 0;
18
+
19
+ constructor(macmonPath: string = '/opt/homebrew/bin/macmon') {
20
+ this.macmonPath = macmonPath;
21
+ this.initializeCoreCount();
22
+ }
23
+
24
+ /**
25
+ * Get CPU core counts for weighted average calculation
26
+ */
27
+ private async initializeCoreCount(): Promise<void> {
28
+ try {
29
+ const { execCommand } = await import('../utils/process-utils.js');
30
+
31
+ // Try to get P-core and E-core counts separately (Apple Silicon)
32
+ try {
33
+ const pCores = await execCommand('sysctl -n hw.perflevel0.physicalcpu 2>/dev/null');
34
+ const eCores = await execCommand('sysctl -n hw.perflevel1.physicalcpu 2>/dev/null');
35
+ this.pCoreCount = parseInt(pCores, 10) || 0;
36
+ this.eCoreCount = parseInt(eCores, 10) || 0;
37
+ } catch {
38
+ // Fall back to total core count if perflevel not available
39
+ const total = await execCommand('sysctl -n hw.ncpu 2>/dev/null');
40
+ this.totalCores = parseInt(total, 10) || 0;
41
+ // Assume equal split if we can't get individual counts
42
+ this.pCoreCount = Math.floor(this.totalCores / 2);
43
+ this.eCoreCount = this.totalCores - this.pCoreCount;
44
+ }
45
+
46
+ this.totalCores = this.pCoreCount + this.eCoreCount;
47
+ } catch {
48
+ // Default to 8 cores if we can't detect
49
+ this.pCoreCount = 4;
50
+ this.eCoreCount = 4;
51
+ this.totalCores = 8;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Check if macmon is available
57
+ */
58
+ private async checkMacmonAvailability(): Promise<boolean> {
59
+ if (this.macmonAvailable !== null) {
60
+ return this.macmonAvailable;
61
+ }
62
+
63
+ try {
64
+ const result = await execCommand(`which ${this.macmonPath} 2>/dev/null`);
65
+ this.macmonAvailable = result.length > 0;
66
+ } catch {
67
+ this.macmonAvailable = false;
68
+ }
69
+
70
+ return this.macmonAvailable;
71
+ }
72
+
73
+ /**
74
+ * Parse macmon JSON output
75
+ * Expected format from 'macmon pipe':
76
+ * {
77
+ * "gpu_usage": [count, percentage],
78
+ * "pcpu_usage": [count, percentage],
79
+ * "ecpu_usage": [count, percentage],
80
+ * "ane_power": number,
81
+ * "temp": {"cpu_temp_avg": number, "gpu_temp_avg": number}
82
+ * }
83
+ */
84
+ private parseMacmonJson(jsonLine: string): {
85
+ gpuUsage?: number;
86
+ cpuUsage?: number;
87
+ aneUsage?: number;
88
+ temperature?: number;
89
+ } {
90
+ try {
91
+ const data = JSON.parse(jsonLine);
92
+
93
+ // GPU usage (second element of array, convert decimal to percentage)
94
+ const gpuUsage = data.gpu_usage?.[1] !== undefined
95
+ ? data.gpu_usage[1] * 100
96
+ : undefined;
97
+
98
+ // CPU usage (weighted average of P-cores and E-cores)
99
+ // Each core type reports 0.0-1.0 utilization
100
+ // Calculate weighted average: (P% * Pcount + E% * Ecount) / totalCores
101
+ const pcpuUsage = data.pcpu_usage?.[1] || 0; // 0.0-1.0
102
+ const ecpuUsage = data.ecpu_usage?.[1] || 0; // 0.0-1.0
103
+
104
+ let cpuUsage: number | undefined;
105
+ if (this.totalCores > 0) {
106
+ // Weighted average normalized to 0-100%
107
+ cpuUsage = ((pcpuUsage * this.pCoreCount) + (ecpuUsage * this.eCoreCount)) / this.totalCores * 100;
108
+ } else {
109
+ // Fallback: simple average if core counts not available
110
+ cpuUsage = ((pcpuUsage + ecpuUsage) / 2) * 100;
111
+ }
112
+
113
+ // ANE usage (estimate from power draw - macmon doesn't provide usage %)
114
+ // If ANE power > 0.1W, consider it active (rough estimate)
115
+ const aneUsage = data.ane_power > 0.1
116
+ ? Math.min((data.ane_power / 8.0) * 100, 100) // Assume ~8W max for ANE
117
+ : 0;
118
+
119
+ // Temperature (use GPU temp if available, otherwise CPU)
120
+ const temperature = data.temp?.gpu_temp_avg || data.temp?.cpu_temp_avg;
121
+
122
+ return {
123
+ gpuUsage,
124
+ cpuUsage: cpuUsage > 0 ? cpuUsage : undefined,
125
+ aneUsage: aneUsage > 1 ? aneUsage : undefined,
126
+ temperature,
127
+ };
128
+ } catch {
129
+ return {};
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Collect macmon metrics (GPU, CPU, ANE)
135
+ * Uses 'macmon pipe' which outputs one JSON line per update
136
+ * Spawns macmon, reads one line, and kills it to prevent process leaks
137
+ */
138
+ private async getMacmonMetrics(): Promise<{
139
+ gpuUsage?: number;
140
+ cpuUsage?: number;
141
+ aneUsage?: number;
142
+ temperature?: number;
143
+ } | null> {
144
+ const available = await this.checkMacmonAvailability();
145
+ if (!available) {
146
+ return null;
147
+ }
148
+
149
+ try {
150
+ // Spawn macmon pipe, read one line, and kill it
151
+ // This prevents orphaned macmon processes
152
+ // Timeout set to 5s because macmon can take 3-4s to produce first line
153
+ const output = await spawnAndReadOneLine(this.macmonPath, ['pipe'], 5000);
154
+
155
+ if (!output) {
156
+ return null;
157
+ }
158
+
159
+ return this.parseMacmonJson(output);
160
+ } catch {
161
+ return null;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Parse vm_stat output for memory metrics
167
+ * Expected format:
168
+ * Pages free: 123456.
169
+ * Pages active: 234567.
170
+ * Pages inactive: 345678.
171
+ * Pages speculative: 45678.
172
+ * Pages throttled: 0.
173
+ * Pages wired down: 123456.
174
+ * Pages purgeable count: 0.
175
+ * "Translation faults": 12345678.
176
+ * Pages copy-on-write: 123456.
177
+ * ...
178
+ */
179
+ private parseVmStatOutput(output: string): {
180
+ memoryUsed: number;
181
+ } {
182
+ const lines = output.split('\n');
183
+ const pageSize = 16384; // 16KB on Apple Silicon
184
+ let pagesActive = 0;
185
+ let pagesWired = 0;
186
+ let pagesCompressed = 0;
187
+
188
+ for (const line of lines) {
189
+ const match = line.match(/Pages (.*?):\s+(\d+)\./);
190
+ if (match) {
191
+ const name = match[1].toLowerCase();
192
+ const value = parseInt(match[2], 10);
193
+
194
+ if (name === 'active') pagesActive = value;
195
+ else if (name === 'wired down') pagesWired = value;
196
+ else if (name === 'compressed') pagesCompressed = value;
197
+ }
198
+ }
199
+
200
+ // Calculate used memory (active + wired + compressed)
201
+ // This matches what Activity Monitor and macmon report as "used"
202
+ const usedPages = pagesActive + pagesWired + pagesCompressed;
203
+ const memoryUsed = usedPages * pageSize;
204
+
205
+ return { memoryUsed };
206
+ }
207
+
208
+ /**
209
+ * Get total system memory from sysctl
210
+ * Returns installed RAM size in bytes
211
+ */
212
+ private async getTotalMemory(): Promise<number> {
213
+ try {
214
+ const output = await execCommand('sysctl -n hw.memsize 2>/dev/null');
215
+ return parseInt(output.trim(), 10) || 0;
216
+ } catch {
217
+ return 0;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Collect vm_stat memory metrics + total system memory from sysctl
223
+ */
224
+ private async getMemoryMetrics(): Promise<{
225
+ memoryUsed: number;
226
+ memoryTotal: number;
227
+ }> {
228
+ try {
229
+ // Get used memory from vm_stat
230
+ const vmStatOutput = await execCommand('vm_stat 2>/dev/null');
231
+ const { memoryUsed } = this.parseVmStatOutput(vmStatOutput);
232
+
233
+ // Get total installed RAM from sysctl (this is accurate)
234
+ const memoryTotal = await this.getTotalMemory();
235
+
236
+ return { memoryUsed, memoryTotal };
237
+ } catch {
238
+ // Fallback to zeros if commands fail
239
+ return { memoryUsed: 0, memoryTotal: 0 };
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Collect all system metrics
245
+ * Attempts macmon first (GPU/CPU/ANE), always gets memory from vm_stat + sysctl
246
+ * Caches results for 4s to prevent spawning multiple macmon processes
247
+ */
248
+ async collectSystemMetrics(): Promise<SystemMetrics> {
249
+ const now = Date.now();
250
+
251
+ // Return cached data if still fresh
252
+ if (this.lastSystemMetrics && (now - this.lastCollectionTime) < this.CACHE_TTL_MS) {
253
+ return this.lastSystemMetrics;
254
+ }
255
+
256
+ // If already collecting, wait for that to finish
257
+ if (this.collectingLock) {
258
+ return this.collectingLock;
259
+ }
260
+
261
+ // Start fresh collection
262
+ this.collectingLock = this.doCollectSystemMetrics();
263
+
264
+ try {
265
+ const metrics = await this.collectingLock;
266
+ this.lastSystemMetrics = metrics;
267
+ this.lastCollectionTime = now;
268
+ return metrics;
269
+ } finally {
270
+ this.collectingLock = null;
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Internal method to actually collect system metrics
276
+ * Called by collectSystemMetrics with caching/locking
277
+ */
278
+ private async doCollectSystemMetrics(): Promise<SystemMetrics> {
279
+ const warnings: string[] = [];
280
+ const now = Date.now();
281
+
282
+ // Try macmon first for GPU/CPU/ANE
283
+ const macmonMetrics = await this.getMacmonMetrics();
284
+
285
+ // Always get memory from vm_stat + sysctl (accurate total from sysctl)
286
+ const memoryMetrics = await this.getMemoryMetrics();
287
+
288
+ // Determine source and add warnings
289
+ let source: 'macmon' | 'vm_stat' | 'none';
290
+ if (macmonMetrics) {
291
+ source = 'macmon';
292
+ } else if (memoryMetrics.memoryTotal > 0) {
293
+ source = 'vm_stat';
294
+ warnings.push('macmon not available - showing memory metrics only');
295
+ } else {
296
+ source = 'none';
297
+ warnings.push('Unable to collect system metrics');
298
+ }
299
+
300
+ return {
301
+ gpuUsage: macmonMetrics?.gpuUsage,
302
+ cpuUsage: macmonMetrics?.cpuUsage,
303
+ aneUsage: macmonMetrics?.aneUsage,
304
+ temperature: macmonMetrics?.temperature,
305
+ memoryUsed: memoryMetrics.memoryUsed,
306
+ memoryTotal: memoryMetrics.memoryTotal,
307
+ timestamp: now,
308
+ source,
309
+ warnings: warnings.length > 0 ? warnings : undefined,
310
+ };
311
+ }
312
+ }
313
+
314
+ // Export singleton instance
315
+ export const systemCollector = new SystemCollector();