pinokiod 7.3.3 → 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.
- package/kernel/git.js +29 -1
- package/kernel/resource_usage/gpu.js +149 -8
- package/package.json +1 -1
- package/test/git-defaults.test.js +53 -0
- package/test/resource-usage-gpu.test.js +126 -0
package/kernel/git.js
CHANGED
|
@@ -1169,6 +1169,32 @@ class Git {
|
|
|
1169
1169
|
}
|
|
1170
1170
|
}
|
|
1171
1171
|
}
|
|
1172
|
+
const clone = (value) => {
|
|
1173
|
+
if (typeof value !== "object" || value === null) return value
|
|
1174
|
+
return JSON.parse(JSON.stringify(value))
|
|
1175
|
+
}
|
|
1176
|
+
const sameConfigValue = (a, b) => JSON.stringify(a) === JSON.stringify(b)
|
|
1177
|
+
const isCredentialSection = (section) => section === "credential" || section.startsWith('credential "')
|
|
1178
|
+
const resetCredentialDefaults = () => {
|
|
1179
|
+
const templateCredentialSections = Object.fromEntries(
|
|
1180
|
+
Object.entries(templateConfig).filter(([section]) => isCredentialSection(section))
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
for (const section of Object.keys(config)) {
|
|
1184
|
+
if (!isCredentialSection(section)) continue
|
|
1185
|
+
if (!Object.prototype.hasOwnProperty.call(templateCredentialSections, section)) {
|
|
1186
|
+
delete config[section]
|
|
1187
|
+
dirty = true
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
for (const [section, tplSection] of Object.entries(templateCredentialSections)) {
|
|
1192
|
+
if (!sameConfigValue(config[section], tplSection)) {
|
|
1193
|
+
config[section] = clone(tplSection)
|
|
1194
|
+
dirty = true
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1172
1198
|
|
|
1173
1199
|
for (const [section, tplSection] of Object.entries(templateConfig)) {
|
|
1174
1200
|
if (typeof tplSection !== "object" || tplSection === null) continue
|
|
@@ -1196,6 +1222,8 @@ class Git {
|
|
|
1196
1222
|
dirty = true
|
|
1197
1223
|
}
|
|
1198
1224
|
|
|
1225
|
+
resetCredentialDefaults()
|
|
1226
|
+
|
|
1199
1227
|
const githubCredentialSection = config['credential "https://github'] && config['credential "https://github']['com"']
|
|
1200
1228
|
if (githubCredentialSection && typeof githubCredentialSection === "object") {
|
|
1201
1229
|
if (!githubCredentialSection.provider) {
|
|
@@ -1203,7 +1231,7 @@ class Git {
|
|
|
1203
1231
|
dirty = true
|
|
1204
1232
|
}
|
|
1205
1233
|
const helper = typeof githubCredentialSection.helper === "string" ? githubCredentialSection.helper : ""
|
|
1206
|
-
if (!helper
|
|
1234
|
+
if (!helper) {
|
|
1207
1235
|
githubCredentialSection.helper = "manager"
|
|
1208
1236
|
dirty = true
|
|
1209
1237
|
}
|
|
@@ -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 (
|
|
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 (
|
|
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
|
-
|
|
279
|
-
if (
|
|
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
|
-
|
|
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,11 +1,14 @@
|
|
|
1
1
|
const assert = require('node:assert/strict')
|
|
2
|
+
const { execFile } = require('node:child_process')
|
|
2
3
|
const fs = require('node:fs/promises')
|
|
3
4
|
const os = require('node:os')
|
|
4
5
|
const path = require('node:path')
|
|
5
6
|
const test = require('node:test')
|
|
7
|
+
const { promisify } = require('node:util')
|
|
6
8
|
const ini = require('ini')
|
|
7
9
|
|
|
8
10
|
const KernelGit = require('../kernel/git')
|
|
11
|
+
const execFileAsync = promisify(execFile)
|
|
9
12
|
|
|
10
13
|
async function withTempHome(fn) {
|
|
11
14
|
const homedir = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-git-defaults-'))
|
|
@@ -67,6 +70,56 @@ test('Git defaults migrate the legacy gh GitHub helper to Git Credential Manager
|
|
|
67
70
|
})
|
|
68
71
|
})
|
|
69
72
|
|
|
73
|
+
test('Git defaults reset credential sections to built-in defaults before Git reads config', async () => {
|
|
74
|
+
await withTempHome(async (homedir) => {
|
|
75
|
+
const staleHelpers = [
|
|
76
|
+
'!custom-helper auth git-credential',
|
|
77
|
+
'!gh auth git-credential',
|
|
78
|
+
'!gh.exe auth git-credential',
|
|
79
|
+
`!'${path.win32.join('Z:\\', 'Any Folder', 'bin', 'gh.exe')}' auth git-credential`,
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
for (const [index, helper] of staleHelpers.entries()) {
|
|
83
|
+
const caseHome = path.join(homedir, `case-${index}`)
|
|
84
|
+
await fs.mkdir(caseHome)
|
|
85
|
+
const gitconfigPath = path.join(caseHome, 'gitconfig')
|
|
86
|
+
await fs.writeFile(gitconfigPath, [
|
|
87
|
+
'[credential "https://github.com"]',
|
|
88
|
+
` helper = ${helper}`,
|
|
89
|
+
' provider = github',
|
|
90
|
+
'[credential "https://gist.github.com"]',
|
|
91
|
+
` helper = ${helper}`,
|
|
92
|
+
'[credential "https://enterprise.example"]',
|
|
93
|
+
` helper = ${helper}`,
|
|
94
|
+
'[credential]',
|
|
95
|
+
` helper = ${helper}`,
|
|
96
|
+
' gitHubAuthModes = device',
|
|
97
|
+
' namespace = stale',
|
|
98
|
+
'[user]',
|
|
99
|
+
' name = custom',
|
|
100
|
+
' email = custom@example.test',
|
|
101
|
+
'',
|
|
102
|
+
].join('\n'))
|
|
103
|
+
const git = new KernelGit(createKernel(caseHome))
|
|
104
|
+
|
|
105
|
+
await git.ensureDefaults()
|
|
106
|
+
|
|
107
|
+
const content = await fs.readFile(gitconfigPath, 'utf8')
|
|
108
|
+
assert.doesNotMatch(content, /git-credential/i)
|
|
109
|
+
const { stdout } = await execFileAsync('git', ['config', '--file', gitconfigPath, '--list'])
|
|
110
|
+
assert.match(stdout, /credential\.helper=manager/)
|
|
111
|
+
assert.match(stdout, /credential\.githubauthmodes=oauth/)
|
|
112
|
+
assert.match(stdout, /credential\.namespace=pinokio/)
|
|
113
|
+
assert.match(stdout, /credential\.https:\/\/github\.com\.helper=manager/)
|
|
114
|
+
assert.doesNotMatch(stdout, /credential\.https:\/\/gist\.github\.com/)
|
|
115
|
+
assert.doesNotMatch(stdout, /credential\.https:\/\/enterprise\.example/)
|
|
116
|
+
const config = ini.parse(content)
|
|
117
|
+
assert.equal(config.user.name, 'custom')
|
|
118
|
+
assert.equal(config.user.email, 'custom@example.test')
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
70
123
|
test('Git isomorphic auth callback returns GCM credentials for GitHub URLs', async () => {
|
|
71
124
|
const git = new KernelGit(createKernel('/tmp/pinokio'))
|
|
72
125
|
let requestedUrl = ''
|
|
@@ -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
|
+
})
|