pinokiod 7.3.4 → 7.3.5

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.
@@ -7,6 +7,7 @@ const { execFileText, normalizePid } = require("./process_tree")
7
7
 
8
8
  const DEFAULT_GPU_TTL_MS = 10000
9
9
  const DEFAULT_GPU_TIMEOUT_MS = 2500
10
+ const DEFAULT_WINDOWS_GPU_COUNTER_TTL_MS = 30000
10
11
  const MIB = 1024 * 1024
11
12
 
12
13
  function unique(values) {
@@ -98,6 +99,19 @@ function addGpuProcess(processes, pid, bytes) {
98
99
  processes.set(normalizedPid, current)
99
100
  }
100
101
 
102
+ function mergeGpuProcess(processes, pid, bytes) {
103
+ const normalizedPid = normalizePid(pid)
104
+ if (!normalizedPid || !Number.isFinite(bytes) || bytes < 0) {
105
+ return
106
+ }
107
+ const current = processes.get(normalizedPid) || {
108
+ pid: normalizedPid,
109
+ usedGpuMemoryBytes: 0
110
+ }
111
+ current.usedGpuMemoryBytes = Math.max(current.usedGpuMemoryBytes || 0, bytes)
112
+ processes.set(normalizedPid, current)
113
+ }
114
+
101
115
  function parseNvidiaCsv(stdout) {
102
116
  const processes = new Map()
103
117
  for (const line of String(stdout || "").split(/\r?\n/)) {
@@ -111,6 +125,55 @@ function parseNvidiaCsv(stdout) {
111
125
  return processes
112
126
  }
113
127
 
128
+ function parseCsvLine(line) {
129
+ const values = []
130
+ let current = ""
131
+ let inQuotes = false
132
+ const text = String(line || "")
133
+ for (let i = 0; i < text.length; i += 1) {
134
+ const char = text[i]
135
+ if (char === "\"") {
136
+ if (inQuotes && text[i + 1] === "\"") {
137
+ current += "\""
138
+ i += 1
139
+ } else {
140
+ inQuotes = !inQuotes
141
+ }
142
+ } else if (char === "," && !inQuotes) {
143
+ values.push(current)
144
+ current = ""
145
+ } else {
146
+ current += char
147
+ }
148
+ }
149
+ values.push(current)
150
+ return values
151
+ }
152
+
153
+ function parseWindowsGpuProcessMemoryCsv(stdout) {
154
+ const rows = []
155
+ for (const line of String(stdout || "").split(/\r?\n/)) {
156
+ const trimmed = line.trim()
157
+ if (!trimmed || !trimmed.startsWith("\"")) continue
158
+ const row = parseCsvLine(trimmed)
159
+ if (row.length > 1) rows.push(row)
160
+ }
161
+ if (rows.length < 2) {
162
+ return new Map()
163
+ }
164
+ const headers = rows[0]
165
+ const values = rows[rows.length - 1]
166
+ const processes = new Map()
167
+ for (let i = 1; i < headers.length && i < values.length; i += 1) {
168
+ const instanceName = String(headers[i] || "")
169
+ const match = /pid[_\s-]*(\d+)/i.exec(instanceName)
170
+ const pid = normalizePid(match && match[1])
171
+ const bytes = parseMemoryToBytes(values[i])
172
+ addGpuProcess(processes, pid, bytes)
173
+ }
174
+ return processes
175
+ }
176
+
114
177
  function findObjectValue(object, predicate) {
115
178
  if (!object || typeof object !== "object" || Array.isArray(object)) {
116
179
  return null
@@ -160,26 +223,29 @@ function parseAmdJson(stdout) {
160
223
  class GpuSampler {
161
224
  constructor(options = {}) {
162
225
  this.kernel = options.kernel || null
226
+ this.platform = options.platform || (this.kernel && this.kernel.platform) || os.platform()
163
227
  this.ttlMs = options.ttlMs || DEFAULT_GPU_TTL_MS
164
228
  this.timeoutMs = options.timeoutMs || DEFAULT_GPU_TIMEOUT_MS
229
+ this.windowsCounterTtlMs = options.windowsCounterTtlMs || DEFAULT_WINDOWS_GPU_COUNTER_TTL_MS
165
230
  this.current = null
166
231
  this.inFlight = null
232
+ this.windowsCounterCurrent = null
233
+ this.windowsCounterInFlight = null
167
234
  this.providerBackoff = new Map()
168
235
  }
169
236
 
170
237
  nvidiaCandidates() {
171
- const platform = os.platform()
172
238
  const candidates = [
173
239
  process.env.NVIDIA_SMI,
174
240
  "nvidia-smi",
175
241
  ...getPinokioCondaCandidates(this.kernel, ["nvidia-smi"])
176
242
  ]
177
- if (platform === "win32") {
243
+ if (this.platform === "win32") {
178
244
  candidates.push(
179
245
  "C:\\Program Files\\NVIDIA Corporation\\NVSMI\\nvidia-smi.exe",
180
246
  "C:\\Windows\\System32\\nvidia-smi.exe"
181
247
  )
182
- } else if (platform === "linux") {
248
+ } else if (this.platform === "linux") {
183
249
  candidates.push(
184
250
  "/usr/bin/nvidia-smi",
185
251
  "/usr/local/bin/nvidia-smi",
@@ -190,13 +256,22 @@ class GpuSampler {
190
256
  return executableCandidates(candidates)
191
257
  }
192
258
 
259
+ windowsGpuCounterCandidates() {
260
+ return executableCandidates([
261
+ process.env.TYPEPERF,
262
+ "typeperf",
263
+ "C:\\Windows\\System32\\typeperf.exe",
264
+ "C:\\Windows\\Sysnative\\typeperf.exe"
265
+ ])
266
+ }
267
+
193
268
  amdCandidates() {
194
269
  const candidates = [
195
270
  process.env.AMD_SMI,
196
271
  "amd-smi",
197
272
  ...getPinokioCondaCandidates(this.kernel, ["amd-smi"])
198
273
  ]
199
- if (os.platform() === "linux") {
274
+ if (this.platform === "linux") {
200
275
  candidates.push("/opt/rocm/bin/amd-smi", "/usr/bin/amd-smi", "/usr/local/bin/amd-smi")
201
276
  }
202
277
  return executableCandidates(candidates)
@@ -211,6 +286,63 @@ class GpuSampler {
211
286
  this.providerBackoff.set(provider, Date.now() + ms)
212
287
  }
213
288
 
289
+ async collectWindowsGpuProcessMemoryOnce() {
290
+ if (this.platform !== "win32" || this.isBackedOff("windows-gpu-process-memory")) {
291
+ return null
292
+ }
293
+ let lastError = null
294
+ for (const command of this.windowsGpuCounterCandidates()) {
295
+ try {
296
+ const { stdout } = await execFileText(command, [
297
+ "\\GPU Process Memory(*)\\Dedicated Usage",
298
+ "-sc",
299
+ "1"
300
+ ], { timeoutMs: Math.max(this.timeoutMs, 3000) })
301
+ return {
302
+ provider: "windows-gpu-process-memory",
303
+ processes: parseWindowsGpuProcessMemoryCsv(stdout),
304
+ error: null,
305
+ collectedAt: Date.now()
306
+ }
307
+ } catch (error) {
308
+ lastError = error
309
+ if (error && error.code === "ENOENT") {
310
+ continue
311
+ }
312
+ break
313
+ }
314
+ }
315
+ this.backoff("windows-gpu-process-memory", 60000)
316
+ return {
317
+ provider: "windows-gpu-process-memory",
318
+ processes: new Map(),
319
+ error: lastError && lastError.message ? lastError.message : "Windows GPU process memory counters unavailable",
320
+ collectedAt: Date.now()
321
+ }
322
+ }
323
+
324
+ async collectWindowsGpuProcessMemory() {
325
+ if (this.platform !== "win32" || this.isBackedOff("windows-gpu-process-memory")) {
326
+ return null
327
+ }
328
+ const now = Date.now()
329
+ if (this.windowsCounterCurrent && now - this.windowsCounterCurrent.collectedAt < this.windowsCounterTtlMs) {
330
+ return this.windowsCounterCurrent
331
+ }
332
+ if (this.windowsCounterInFlight) {
333
+ return this.windowsCounterInFlight
334
+ }
335
+ this.windowsCounterInFlight = this.collectWindowsGpuProcessMemoryOnce().then((result) => {
336
+ if (result && !result.error) {
337
+ this.windowsCounterCurrent = result
338
+ }
339
+ return result
340
+ }).finally(() => {
341
+ this.windowsCounterInFlight = null
342
+ })
343
+ return this.windowsCounterInFlight
344
+ }
345
+
214
346
  async collectNvidia() {
215
347
  if (this.isBackedOff("nvidia")) {
216
348
  return null
@@ -245,7 +377,7 @@ class GpuSampler {
245
377
  }
246
378
 
247
379
  async collectAmd() {
248
- if (os.platform() !== "linux" || this.isBackedOff("amd")) {
380
+ if (this.platform !== "linux" || this.isBackedOff("amd")) {
249
381
  return null
250
382
  }
251
383
  let lastError = null
@@ -275,8 +407,15 @@ class GpuSampler {
275
407
 
276
408
  async collect() {
277
409
  const results = []
278
- const nvidia = await this.collectNvidia()
279
- if (nvidia) results.push(nvidia)
410
+
411
+ if (this.platform === "win32") {
412
+ const windowsGpuProcessMemory = await this.collectWindowsGpuProcessMemory()
413
+ if (windowsGpuProcessMemory) results.push(windowsGpuProcessMemory)
414
+ } else {
415
+ const nvidia = await this.collectNvidia()
416
+ if (nvidia) results.push(nvidia)
417
+ }
418
+
280
419
  const amd = await this.collectAmd()
281
420
  if (amd) results.push(amd)
282
421
 
@@ -288,7 +427,7 @@ class GpuSampler {
288
427
  if (result.provider) providers.push(result.provider)
289
428
  if (result.error) errors.push({ provider: result.provider, error: result.error })
290
429
  for (const entry of result.processes.values()) {
291
- addGpuProcess(processes, entry.pid, entry.usedGpuMemoryBytes)
430
+ mergeGpuProcess(processes, entry.pid, entry.usedGpuMemoryBytes)
292
431
  }
293
432
  }
294
433
  return {
@@ -344,6 +483,8 @@ function sumGpuMemory(snapshot, pids) {
344
483
 
345
484
  module.exports = {
346
485
  GpuSampler,
486
+ parseNvidiaCsv,
347
487
  parseMemoryToBytes,
488
+ parseWindowsGpuProcessMemoryCsv,
348
489
  sumGpuMemory
349
490
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "7.3.4",
3
+ "version": "7.3.5",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,126 @@
1
+ const assert = require("node:assert/strict")
2
+ const test = require("node:test")
3
+
4
+ const {
5
+ GpuSampler,
6
+ parseNvidiaCsv,
7
+ parseWindowsGpuProcessMemoryCsv
8
+ } = require("../kernel/resource_usage/gpu")
9
+
10
+ const MIB = 1024 * 1024
11
+
12
+ function gpuProcess(pid, bytes) {
13
+ return {
14
+ pid,
15
+ usedGpuMemoryBytes: bytes
16
+ }
17
+ }
18
+
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"
24
+ ].join("\n"))
25
+
26
+ assert.equal(processes.get(1234).usedGpuMemoryBytes, 384 * MIB)
27
+ assert.equal(processes.has(5678), false)
28
+ })
29
+
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"))
36
+
37
+ assert.equal(processes.get(1234).usedGpuMemoryBytes, 384 * MIB)
38
+ assert.equal(processes.has(999), false)
39
+ })
40
+
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([
48
+ [1234, gpuProcess(1234, 500 * MIB)]
49
+ ]),
50
+ error: null
51
+ }
52
+ }
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
+ }
62
+ }
63
+ sampler.collectAmd = async () => null
64
+
65
+ const snapshot = await sampler.collect()
66
+
67
+ assert.equal(nvidiaCalls, 0)
68
+ assert.deepEqual(snapshot.providers, ["windows-gpu-process-memory"])
69
+ assert.equal(snapshot.processes.get(1234).usedGpuMemoryBytes, 500 * MIB)
70
+ })
71
+
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"
79
+ })
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
88
+ }
89
+ }
90
+ sampler.collectAmd = async () => null
91
+
92
+ const snapshot = await sampler.collect()
93
+
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)
98
+ })
99
+
100
+ test("GpuSampler merges overlapping provider samples by PID without double-counting", async () => {
101
+ const sampler = new GpuSampler({ platform: "linux" })
102
+ sampler.collectNvidia = async () => ({
103
+ provider: "nvidia-smi",
104
+ processes: new Map([
105
+ [1234, gpuProcess(1234, 300 * MIB)],
106
+ [3333, gpuProcess(3333, 200 * MIB)]
107
+ ]),
108
+ error: null
109
+ })
110
+ sampler.collectAmd = async () => ({
111
+ provider: "amd-smi",
112
+ processes: new Map([
113
+ [1234, gpuProcess(1234, 500 * MIB)],
114
+ [2222, gpuProcess(2222, 100 * MIB)]
115
+ ]),
116
+ error: null
117
+ })
118
+
119
+ const snapshot = await sampler.collect()
120
+
121
+ assert.equal(snapshot.available, true)
122
+ assert.deepEqual(snapshot.providers, ["nvidia-smi", "amd-smi"])
123
+ assert.equal(snapshot.processes.get(1234).usedGpuMemoryBytes, 500 * MIB)
124
+ assert.equal(snapshot.processes.get(2222).usedGpuMemoryBytes, 100 * MIB)
125
+ assert.equal(snapshot.processes.get(3333).usedGpuMemoryBytes, 200 * MIB)
126
+ })