pinokiod 7.3.5 → 7.3.6

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.
@@ -55,8 +55,7 @@ class ResourceUsageService {
55
55
  })
56
56
  this.gpuSampler = new GpuSampler({
57
57
  kernel: this.kernel,
58
- ttlMs: 10000,
59
- timeoutMs: 2500
58
+ ttlMs: 5000
60
59
  })
61
60
  this.cpuAverages = new Map()
62
61
  this.workspaceCache = new Map()
@@ -76,6 +75,12 @@ class ResourceUsageService {
76
75
  return preferences
77
76
  }
78
77
 
78
+ stop() {
79
+ if (this.gpuSampler && typeof this.gpuSampler.stop === "function") {
80
+ this.gpuSampler.stop()
81
+ }
82
+ }
83
+
79
84
  getShellRootGroups() {
80
85
  if (!this.kernel || !this.kernel.shell || typeof this.kernel.path !== "function") {
81
86
  return new Map()
@@ -243,8 +248,8 @@ class ResourceUsageService {
243
248
  ? await this.macFootprintSampler.getFootprintByPid(this.selectFootprintPids(allPids))
244
249
  : null
245
250
 
246
- const gpuSnapshot = preferences.show_vram && allPids.size > 0
247
- ? await this.gpuSampler.getSnapshot()
251
+ const gpuSnapshot = preferences.show_vram && this.platform !== "darwin" && allPids.size > 0
252
+ ? await this.gpuSampler.getSnapshot(allPids)
248
253
  : null
249
254
 
250
255
  const nextCache = new Map()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "7.3.5",
3
+ "version": "7.3.6",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -56,6 +56,7 @@
56
56
  "key-store": "^1.2.0",
57
57
  "kill-sync": "^1.0.3",
58
58
  "kleur": "^4.1.5",
59
+ "koffi": "^3.0.2",
59
60
  "lodash": "^4.17.21",
60
61
  "marked": "^5.0.1",
61
62
  "mime-types": "^2.1.35",
package/server/index.js CHANGED
@@ -396,6 +396,9 @@ class Server {
396
396
  }
397
397
  }
398
398
  stop() {
399
+ if (this.resourceUsage && typeof this.resourceUsage.stop === 'function') {
400
+ this.resourceUsage.stop()
401
+ }
399
402
  this.server.close()
400
403
  }
401
404
  killProcessTree(pid, label) {
@@ -19,10 +19,11 @@ document.documentElement.dataset.installSpinnerVariant = "grid-shift"
19
19
  <style>
20
20
  body {
21
21
  --install-status-bg: var(--pinokio-chrome-accent-bg-light);
22
+ --install-spinner-fg: rgba(0,0,0,0.8);
22
23
  --install-status-fg: var(--pinokio-chrome-accent-fg-light);
23
24
  --install-status-fg-soft: rgba(225, 178, 97, 0.34);
24
25
  --install-status-border: rgba(15, 23, 42, 0.12);
25
- --install-status-detail: rgba(235, 222, 197, 0.82);
26
+ --install-status-detail: rgba(0, 0, 0, 0.6);
26
27
  height: 100%;
27
28
  overflow: hidden;
28
29
  /*
@@ -33,6 +34,7 @@ body {
33
34
  }
34
35
  body.dark {
35
36
  --install-status-bg: var(--pinokio-chrome-accent-bg-dark);
37
+ --install-spinner-fg: white;
36
38
  --install-status-fg: var(--pinokio-chrome-accent-fg-dark);
37
39
  --install-status-fg-soft: rgba(194, 178, 138, 0.28);
38
40
  --install-status-border: rgba(255, 255, 255, 0.08);
@@ -114,9 +116,6 @@ body.dark {
114
116
  width: 100%;
115
117
  margin: 0;
116
118
  overflow: hidden;
117
- border-top: 1px solid var(--install-status-border);
118
- background: var(--install-status-bg);
119
- color: var(--install-status-fg);
120
119
  }
121
120
  #status-screen code {
122
121
  font-family: "RobotoMono", monospace;
@@ -132,7 +131,6 @@ body.dark {
132
131
  flex: 1;
133
132
  }
134
133
  .install-status-title {
135
- color: var(--install-status-fg);
136
134
  font-size: 15px;
137
135
  line-height: 1.2;
138
136
  font-weight: 700;
@@ -189,13 +187,12 @@ body.dark {
189
187
  border-radius: 2px;
190
188
  }
191
189
  .install-status-grid-anchor {
192
- background: var(--install-status-fg);
193
190
  opacity: 0.16;
194
191
  }
195
192
  .install-status-grid-tile,
196
193
  .install-status-grid-chaser,
197
194
  .install-status-grid-pixel {
198
- background: var(--install-status-fg);
195
+ background: var(--install-spinner-fg);
199
196
  }
200
197
  .install-status-grid-pixel {
201
198
  width: 4px;
@@ -1,10 +1,17 @@
1
1
  const assert = require("node:assert/strict")
2
+ const fs = require("node:fs")
3
+ const os = require("node:os")
4
+ const path = require("node:path")
2
5
  const test = require("node:test")
3
6
 
4
7
  const {
5
8
  GpuSampler,
6
- parseNvidiaCsv,
7
- parseWindowsGpuProcessMemoryCsv
9
+ NvmlGpuMemoryClient,
10
+ collectLinuxDrmFdinfoProcesses,
11
+ decodeWindowsMultiSz,
12
+ extractPidFromWindowsGpuInstance,
13
+ isDedicatedDrmMemoryRegion,
14
+ parseLinuxDrmFdinfo
8
15
  } = require("../kernel/resource_usage/gpu")
9
16
 
10
17
  const MIB = 1024 * 1024
@@ -16,111 +23,331 @@ function gpuProcess(pid, bytes) {
16
23
  }
17
24
  }
18
25
 
19
- test("parseNvidiaCsv treats nounits memory as MiB and sums duplicate PIDs", () => {
20
- const processes = parseNvidiaCsv([
21
- "1234, 256",
22
- "1234, 128",
23
- "5678, N/A"
26
+ test("extractPidFromWindowsGpuInstance handles full PDH counter paths", () => {
27
+ assert.equal(extractPidFromWindowsGpuInstance("app_pid_3456_phys_0"), 3456)
28
+ assert.equal(extractPidFromWindowsGpuInstance("\\\\HOST\\GPU Process Memory(pid_1234_luid_0x00000000_phys_0)\\Dedicated Usage"), 1234)
29
+ assert.equal(extractPidFromWindowsGpuInstance("\\\\HOST\\GPU Process Memory(_total)\\Dedicated Usage"), null)
30
+ })
31
+
32
+ test("decodeWindowsMultiSz decodes double-null UTF-16 string lists", () => {
33
+ const text = "one\u0000two\u0000\u0000"
34
+ const buffer = Buffer.from(text, "utf16le")
35
+
36
+ assert.deepEqual(decodeWindowsMultiSz(buffer, text.length), ["one", "two"])
37
+ })
38
+
39
+ test("parseLinuxDrmFdinfo counts dedicated DRM memory regions only", () => {
40
+ const amdgpu = parseLinuxDrmFdinfo([
41
+ "drm-driver:\tamdgpu",
42
+ "drm-pdev:\t0000:03:00.0",
43
+ "drm-client-id:\t17",
44
+ "drm-memory-vram:\t4 MiB",
45
+ "drm-memory-gtt:\t128 MiB"
24
46
  ].join("\n"))
25
47
 
26
- assert.equal(processes.get(1234).usedGpuMemoryBytes, 384 * MIB)
27
- assert.equal(processes.has(5678), false)
48
+ const intel = parseLinuxDrmFdinfo([
49
+ "drm-driver:\ti915",
50
+ "drm-pdev:\t0000:00:02.0",
51
+ "drm-client-id:\t9",
52
+ "drm-resident-local0:\t8 MiB",
53
+ "drm-resident-system:\t64 MiB"
54
+ ].join("\n"))
55
+
56
+ assert.equal(amdgpu.dedicatedBytes, 4 * MIB)
57
+ assert.equal(intel.dedicatedBytes, 8 * MIB)
58
+ assert.equal(isDedicatedDrmMemoryRegion("vram"), true)
59
+ assert.equal(isDedicatedDrmMemoryRegion("local0"), true)
60
+ assert.equal(isDedicatedDrmMemoryRegion("gtt"), false)
61
+ assert.equal(isDedicatedDrmMemoryRegion("system"), false)
28
62
  })
29
63
 
30
- test("parseWindowsGpuProcessMemoryCsv extracts dedicated GPU bytes from process counter instances", () => {
31
- const processes = parseWindowsGpuProcessMemoryCsv([
32
- "\"(PDH-CSV 4.0)\",\"\\\\HOST\\GPU Process Memory(pid_1234_luid_0x00000000_0x00011111_phys_0)\\Dedicated Usage\",\"\\\\HOST\\GPU Process Memory(pid_1234_luid_0x00000000_0x00011111_phys_1)\\Dedicated Usage\",\"\\\\HOST\\GPU Process Memory(_total)\\Dedicated Usage\"",
33
- "\"06/18/2026 12:00:00.000\",\"268435456.000000\",\"134217728.000000\",\"1047527424.000000\"",
34
- "The command completed successfully."
35
- ].join("\r\n"))
64
+ test("collectLinuxDrmFdinfoProcesses deduplicates DRM client fds", async () => {
65
+ const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pinokio-drm-fdinfo-"))
66
+ try {
67
+ await fs.promises.mkdir(path.join(root, "1234", "fdinfo"), { recursive: true })
68
+ await fs.promises.mkdir(path.join(root, "5678", "fdinfo"), { recursive: true })
69
+ await fs.promises.writeFile(path.join(root, "1234", "fdinfo", "3"), [
70
+ "drm-driver:\tamdgpu",
71
+ "drm-pdev:\t0000:03:00.0",
72
+ "drm-client-id:\t17",
73
+ "drm-resident-vram:\t12 MiB"
74
+ ].join("\n"))
75
+ await fs.promises.writeFile(path.join(root, "1234", "fdinfo", "4"), [
76
+ "drm-driver:\tamdgpu",
77
+ "drm-pdev:\t0000:03:00.0",
78
+ "drm-client-id:\t17",
79
+ "drm-resident-vram:\t11 MiB"
80
+ ].join("\n"))
81
+ await fs.promises.writeFile(path.join(root, "1234", "fdinfo", "5"), [
82
+ "drm-driver:\tamdgpu",
83
+ "drm-pdev:\t0000:03:00.0",
84
+ "drm-client-id:\t18",
85
+ "drm-resident-vram:\t5 MiB"
86
+ ].join("\n"))
87
+ await fs.promises.writeFile(path.join(root, "5678", "fdinfo", "9"), [
88
+ "drm-driver:\tamdgpu",
89
+ "drm-pdev:\t0000:03:00.0",
90
+ "drm-client-id:\t22",
91
+ "drm-resident-gtt:\t64 MiB"
92
+ ].join("\n"))
36
93
 
37
- assert.equal(processes.get(1234).usedGpuMemoryBytes, 384 * MIB)
38
- assert.equal(processes.has(999), false)
94
+ const processes = await collectLinuxDrmFdinfoProcesses([1234, 5678], { procRoot: root })
95
+
96
+ assert.equal(processes.get(1234).usedGpuMemoryBytes, 17 * MIB)
97
+ assert.equal(processes.has(5678), false)
98
+ } finally {
99
+ await fs.promises.rm(root, { recursive: true, force: true })
100
+ }
39
101
  })
40
102
 
41
- test("GpuSampler uses Windows GPU counters before nvidia-smi on Windows", async () => {
42
- const sampler = new GpuSampler({ platform: "win32" })
43
- let nvidiaCalls = 0
44
- sampler.collectWindowsGpuProcessMemory = async () => {
45
- return {
46
- provider: "windows-gpu-process-memory",
47
- processes: new Map([
103
+ test("GpuSampler uses Windows PDH only on Windows", async () => {
104
+ const sampler = new GpuSampler({
105
+ platform: "win32",
106
+ windowsPdhClient: {
107
+ collect: () => new Map([
48
108
  [1234, gpuProcess(1234, 500 * MIB)]
49
- ]),
50
- error: null
109
+ ])
51
110
  }
111
+ })
112
+
113
+ const snapshot = await sampler.collect(new Set([1234]))
114
+
115
+ assert.equal(snapshot.available, true)
116
+ assert.deepEqual(snapshot.providers, ["windows-pdh"])
117
+ assert.equal(snapshot.processes.get(1234).usedGpuMemoryBytes, 500 * MIB)
118
+ })
119
+
120
+ test("GpuSampler reports Windows VRAM unavailable when PDH fails", async () => {
121
+ const originalWarn = console.warn
122
+ console.warn = () => {}
123
+
124
+ try {
125
+ const sampler = new GpuSampler({
126
+ platform: "win32",
127
+ windowsPdhClient: {
128
+ collect: () => {
129
+ throw new Error("pdh unavailable")
130
+ }
131
+ }
132
+ })
133
+
134
+ const snapshot = await sampler.collect(new Set([1234]))
135
+
136
+ assert.equal(snapshot.available, false)
137
+ assert.deepEqual(snapshot.providers, ["windows-pdh"])
138
+ assert.equal(snapshot.processes.size, 0)
139
+ assert.equal(snapshot.errors[0].provider, "windows-pdh")
140
+ } finally {
141
+ console.warn = originalWarn
52
142
  }
53
- sampler.collectNvidia = async () => {
54
- nvidiaCalls += 1
55
- return {
56
- provider: "nvidia-smi",
57
- processes: new Map([
58
- [1234, gpuProcess(1234, 500 * MIB)]
59
- ]),
60
- error: null
61
- }
143
+ })
144
+
145
+ test("GpuSampler logs provider failures once per backoff window", async () => {
146
+ const originalWarn = console.warn
147
+ const warnings = []
148
+ console.warn = (...args) => {
149
+ warnings.push(args)
62
150
  }
63
- sampler.collectAmd = async () => null
64
151
 
65
- const snapshot = await sampler.collect()
152
+ try {
153
+ const sampler = new GpuSampler({
154
+ platform: "win32",
155
+ windowsPdhClient: {
156
+ collect: () => {
157
+ const error = new Error("pdh unavailable")
158
+ error.code = "PDH_TEST"
159
+ throw error
160
+ }
161
+ }
162
+ })
66
163
 
67
- assert.equal(nvidiaCalls, 0)
68
- assert.deepEqual(snapshot.providers, ["windows-gpu-process-memory"])
69
- assert.equal(snapshot.processes.get(1234).usedGpuMemoryBytes, 500 * MIB)
164
+ await sampler.collect(new Set([1234]))
165
+ await sampler.collect(new Set([1234]))
166
+
167
+ assert.equal(warnings.length, 1)
168
+ assert.equal(warnings[0][0], "[resource-usage:gpu] provider failed")
169
+ assert.deepEqual(warnings[0][1], {
170
+ provider: "windows-pdh",
171
+ platform: "win32",
172
+ pid_count: 1,
173
+ error: "pdh unavailable",
174
+ code: "PDH_TEST"
175
+ })
176
+ } finally {
177
+ console.warn = originalWarn
178
+ }
70
179
  })
71
180
 
72
- test("GpuSampler does not query nvidia-smi on Windows when OS counters fail", async () => {
73
- const sampler = new GpuSampler({ platform: "win32" })
74
- let nvidiaCalls = 0
75
- sampler.collectWindowsGpuProcessMemory = async () => ({
76
- provider: "windows-gpu-process-memory",
77
- processes: new Map(),
78
- error: "counter unavailable"
181
+ test("GpuSampler uses Linux DRM fdinfo before native library providers", async () => {
182
+ const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pinokio-drm-sampler-"))
183
+ try {
184
+ await fs.promises.mkdir(path.join(root, "1234", "fdinfo"), { recursive: true })
185
+ await fs.promises.writeFile(path.join(root, "1234", "fdinfo", "3"), [
186
+ "drm-driver:\ti915",
187
+ "drm-pdev:\t0000:00:02.0",
188
+ "drm-client-id:\t4",
189
+ "drm-resident-local0:\t32 MiB"
190
+ ].join("\n"))
191
+
192
+ const sampler = new GpuSampler({ platform: "linux", procRoot: root })
193
+ let nvmlCalls = 0
194
+ let amdCalls = 0
195
+ let rocmCalls = 0
196
+ sampler.collectNvml = async () => {
197
+ nvmlCalls += 1
198
+ return null
199
+ }
200
+ sampler.collectAmdSmi = async () => {
201
+ amdCalls += 1
202
+ return null
203
+ }
204
+ sampler.collectRocmSmi = async () => {
205
+ rocmCalls += 1
206
+ return null
207
+ }
208
+
209
+ const snapshot = await sampler.collect(new Set([1234]))
210
+
211
+ assert.equal(nvmlCalls, 0)
212
+ assert.equal(amdCalls, 0)
213
+ assert.equal(rocmCalls, 0)
214
+ assert.deepEqual(snapshot.providers, ["linux-drm-fdinfo"])
215
+ assert.equal(snapshot.available, true)
216
+ assert.equal(snapshot.processes.get(1234).usedGpuMemoryBytes, 32 * MIB)
217
+ } finally {
218
+ await fs.promises.rm(root, { recursive: true, force: true })
219
+ }
220
+ })
221
+
222
+ test("GpuSampler uses NVML when fdinfo does not cover target PID", async () => {
223
+ const sampler = new GpuSampler({
224
+ platform: "linux",
225
+ nvmlClient: {
226
+ collect: () => new Map([
227
+ [1234, gpuProcess(1234, 700 * MIB)]
228
+ ])
229
+ }
79
230
  })
80
- sampler.collectNvidia = async () => {
81
- nvidiaCalls += 1
82
- return {
83
- provider: "nvidia-smi",
84
- processes: new Map([
85
- [1234, gpuProcess(1234, 300 * MIB)]
86
- ]),
87
- error: null
231
+ sampler.collectLinuxDrmFdinfo = async () => null
232
+ let amdCalls = 0
233
+ sampler.collectAmdSmi = async () => {
234
+ amdCalls += 1
235
+ return null
236
+ }
237
+
238
+ const snapshot = await sampler.collect(new Set([1234]))
239
+
240
+ assert.equal(amdCalls, 0)
241
+ assert.deepEqual(snapshot.providers, ["linux-nvml"])
242
+ assert.equal(snapshot.processes.get(1234).usedGpuMemoryBytes, 700 * MIB)
243
+ })
244
+
245
+ test("NvmlGpuMemoryClient sums the same PID across devices", () => {
246
+ const client = new NvmlGpuMemoryClient({ koffi: { sizeof: () => 1 } })
247
+ const compute = { name: "compute" }
248
+ const graphics = { name: "graphics" }
249
+ client.init = () => {}
250
+ client.getDeviceHandles = () => ["gpu0", "gpu1"]
251
+ client.functions = { compute, graphics, mps: null }
252
+ client.collectProcessList = (device, entry) => {
253
+ const samples = {
254
+ "gpu0:compute": [{ pid: 1234, usedGpuMemory: 300 * MIB }],
255
+ "gpu0:graphics": [{ pid: 1234, usedGpuMemory: 250 * MIB }],
256
+ "gpu1:compute": [{ pid: 1234, usedGpuMemory: 400 * MIB }],
257
+ "gpu1:graphics": [{ pid: 1234, usedGpuMemory: 100 * MIB }]
88
258
  }
259
+ return samples[`${device}:${entry && entry.name}`] || []
89
260
  }
90
- sampler.collectAmd = async () => null
91
261
 
92
- const snapshot = await sampler.collect()
262
+ const processes = client.collect([1234])
93
263
 
94
- assert.equal(nvidiaCalls, 0)
95
- assert.equal(snapshot.available, false)
96
- assert.deepEqual(snapshot.providers, ["windows-gpu-process-memory"])
97
- assert.equal(snapshot.processes.has(1234), false)
264
+ assert.equal(processes.get(1234).usedGpuMemoryBytes, 700 * MIB)
265
+ })
266
+
267
+
268
+ test("GpuSampler uses AMD SMI after fdinfo and NVML miss", async () => {
269
+ const sampler = new GpuSampler({
270
+ platform: "linux",
271
+ amdSmiClient: {
272
+ collect: () => new Map([
273
+ [2222, gpuProcess(2222, 300 * MIB)]
274
+ ])
275
+ }
276
+ })
277
+ sampler.collectLinuxDrmFdinfo = async () => null
278
+ sampler.collectNvml = async () => null
279
+ let rocmCalls = 0
280
+ sampler.collectRocmSmi = async () => {
281
+ rocmCalls += 1
282
+ return null
283
+ }
284
+
285
+ const snapshot = await sampler.collect(new Set([2222]))
286
+
287
+ assert.equal(rocmCalls, 0)
288
+ assert.deepEqual(snapshot.providers, ["linux-amdsmi"])
289
+ assert.equal(snapshot.processes.get(2222).usedGpuMemoryBytes, 300 * MIB)
290
+ })
291
+
292
+ test("GpuSampler uses ROCm SMI after AMD SMI misses", async () => {
293
+ const sampler = new GpuSampler({
294
+ platform: "linux",
295
+ rocmSmiClient: {
296
+ collect: () => new Map([
297
+ [3333, gpuProcess(3333, 200 * MIB)]
298
+ ])
299
+ }
300
+ })
301
+ sampler.collectLinuxDrmFdinfo = async () => null
302
+ sampler.collectNvml = async () => null
303
+ sampler.collectAmdSmi = async () => null
304
+
305
+ const snapshot = await sampler.collect(new Set([3333]))
306
+
307
+ assert.deepEqual(snapshot.providers, ["linux-rocm-smi"])
308
+ assert.equal(snapshot.processes.get(3333).usedGpuMemoryBytes, 200 * MIB)
98
309
  })
99
310
 
100
311
  test("GpuSampler merges overlapping provider samples by PID without double-counting", async () => {
101
312
  const sampler = new GpuSampler({ platform: "linux" })
102
- sampler.collectNvidia = async () => ({
103
- provider: "nvidia-smi",
313
+ sampler.collectLinuxDrmFdinfo = async () => ({
314
+ provider: "linux-drm-fdinfo",
104
315
  processes: new Map([
105
- [1234, gpuProcess(1234, 300 * MIB)],
106
- [3333, gpuProcess(3333, 200 * MIB)]
316
+ [1234, gpuProcess(1234, 300 * MIB)]
107
317
  ]),
108
318
  error: null
109
319
  })
110
- sampler.collectAmd = async () => ({
111
- provider: "amd-smi",
320
+ sampler.collectNvml = async () => ({
321
+ provider: "linux-nvml",
112
322
  processes: new Map([
113
323
  [1234, gpuProcess(1234, 500 * MIB)],
114
- [2222, gpuProcess(2222, 100 * MIB)]
324
+ [3333, gpuProcess(3333, 200 * MIB)]
115
325
  ]),
116
326
  error: null
117
327
  })
328
+ sampler.collectAmdSmi = async () => null
329
+ sampler.collectRocmSmi = async () => null
118
330
 
119
331
  const snapshot = await sampler.collect()
120
332
 
121
333
  assert.equal(snapshot.available, true)
122
- assert.deepEqual(snapshot.providers, ["nvidia-smi", "amd-smi"])
334
+ assert.deepEqual(snapshot.providers, ["linux-drm-fdinfo", "linux-nvml"])
123
335
  assert.equal(snapshot.processes.get(1234).usedGpuMemoryBytes, 500 * MIB)
124
- assert.equal(snapshot.processes.get(2222).usedGpuMemoryBytes, 100 * MIB)
125
336
  assert.equal(snapshot.processes.get(3333).usedGpuMemoryBytes, 200 * MIB)
126
337
  })
338
+
339
+ test("GpuSampler does not collect VRAM providers on macOS", async () => {
340
+ const sampler = new GpuSampler({ platform: "darwin" })
341
+ sampler.collectNvml = async () => {
342
+ throw new Error("NVML should not be queried on macOS")
343
+ }
344
+ sampler.collectAmdSmi = async () => {
345
+ throw new Error("AMD SMI should not be queried on macOS")
346
+ }
347
+
348
+ const snapshot = await sampler.collect()
349
+
350
+ assert.equal(snapshot.available, false)
351
+ assert.deepEqual(snapshot.providers, [])
352
+ assert.equal(snapshot.processes.size, 0)
353
+ })