tassello 0.7.0 → 0.9.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.
@@ -0,0 +1,606 @@
1
+ // The robot's computer, as top, free, sensors, iotop and a Jetson's jtop see
2
+ // it: read of /proc and /sys every second while a page watches it, and kept
3
+ // for the last minutes, the charts' history (see src/shared/system.ts).
4
+ //
5
+ // The sensors are those the kernel has: hwmon chips and thermal zones
6
+ // (temperatures, fans, power rails: a Jetson's INA3221), Intel's RAPL when
7
+ // readable, devfreq's GPUs (a Jetson's), amdgpu's; and those of tools when
8
+ // there: nvidia-smi, a Raspberry Pi's vcgencmd (if its firmware's
9
+ // get_throttled is not in /sys), a Jetson's nvpmodel.
10
+ import { execFile } from 'node:child_process';
11
+ import { promises as fs } from 'node:fs';
12
+ import os from 'node:os';
13
+ import path from 'node:path';
14
+ import { trendPoint } from '../shared/system.js';
15
+ import { listDisks } from './disks.js';
16
+ import { busyShare, celsius, cpuCount, cpuinfoMhz, cpuModel, energyWatts, isTemperatureZone, mergeTemperatures, modelName, NVIDIA_QUERY, parseCpuTimes, parseDiskstats, parseLoadavg, parseMeminfo, parseNetDev, parseNvidiaSmi, parseNvpmodel, parseOsRelease, parseTegraRelease, parseThrottled, raplName, rate, wholeDisk, zoneLimits, } from './system_format.js';
17
+ import { errorMessage } from './util.js';
18
+ const PERIOD_MS = 1000;
19
+ const HISTORY_MS = 5 * 60_000;
20
+ // The sensors are looked for again this often: USB ones come and go
21
+ const DISCOVER_MS = 30_000;
22
+ // After the last page left, sampling goes on this long, for one reloading
23
+ const LINGER_MS = 30_000;
24
+ const SPACE_MS = 10_000;
25
+ // nvidia-smi takes tens of ms: run this often
26
+ const NVIDIA_MS = 2000;
27
+ const VCGENCMD_MS = 5000;
28
+ // A Jetson's power mode changes by hand, rarely
29
+ const INFO_MS = 60_000;
30
+ const TOOL_TIMEOUT_MS = 5000;
31
+ // The names devfreq's integrated GPUs have in a Jetson's device tree
32
+ const TEGRA_GPUS = new Set(['gk20a', 'gm20b', 'gp10b', 'gv11b', 'ga10b', 'gb10b', 'gpu']);
33
+ async function text(file) {
34
+ try {
35
+ return await fs.readFile(file, 'utf8');
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ async function number(file) {
42
+ const read = await text(file);
43
+ const value = read === null ? NaN : Number(read.trim());
44
+ return Number.isFinite(value) ? value : null;
45
+ }
46
+ async function list(folder) {
47
+ try {
48
+ return await fs.readdir(folder);
49
+ }
50
+ catch {
51
+ return [];
52
+ }
53
+ }
54
+ async function exists(file) {
55
+ try {
56
+ await fs.access(file);
57
+ return true;
58
+ }
59
+ catch {
60
+ return false;
61
+ }
62
+ }
63
+ function run(command, args) {
64
+ return new Promise((resolve, reject) => {
65
+ execFile(command, args, { timeout: TOOL_TIMEOUT_MS, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => error ? reject(new Error(stderr.trim() || error.message)) : resolve(stdout));
66
+ });
67
+ }
68
+ function label(read) {
69
+ return read?.trim() || null;
70
+ }
71
+ async function discoverChip(folder) {
72
+ const name = label(await text(path.join(folder, 'name')));
73
+ if (!name) {
74
+ return null;
75
+ }
76
+ const files = await list(folder);
77
+ const file = (name) => (files.includes(name) ? path.join(folder, name) : null);
78
+ const chip = { name, temperatures: [], fans: [], power: [], rails: [], underVoltage: file('in0_lcrit_alarm') };
79
+ for (const entry of files.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))) {
80
+ const [, kind, index] = /^(temp|fan|power|curr)(\d+)_(?:input|average)$/.exec(entry) ?? [];
81
+ const of = (suffix) => file(`${kind}${index}_${suffix}`);
82
+ const labelOf = async (prefix = kind) => label(await text(path.join(folder, `${prefix}${index}_label`)));
83
+ if (kind === 'temp') {
84
+ const [high, critical] = await Promise.all([of('max'), of('crit')].map((limit) => (limit ? number(limit) : null)));
85
+ chip.temperatures.push({ input: path.join(folder, entry), label: await labelOf(), high: celsius(high), critical: celsius(critical) });
86
+ }
87
+ else if (kind === 'fan') {
88
+ chip.fans.push({ input: path.join(folder, entry), label: await labelOf() });
89
+ }
90
+ else if (kind === 'power' && (entry.endsWith('_input') || !file(`power${index}_input`))) {
91
+ chip.power.push({ input: path.join(folder, entry), label: await labelOf() });
92
+ }
93
+ else if (kind === 'curr' && file(`in${index}_input`)) {
94
+ chip.rails.push({
95
+ label: (await labelOf('in')) ?? `${name} ${index}`,
96
+ volts: path.join(folder, `in${index}_input`),
97
+ amps: path.join(folder, entry),
98
+ });
99
+ }
100
+ }
101
+ return chip;
102
+ }
103
+ async function discoverZones() {
104
+ const zones = [];
105
+ for (const entry of await list('/sys/class/thermal')) {
106
+ if (!entry.startsWith('thermal_zone')) {
107
+ continue;
108
+ }
109
+ const folder = path.join('/sys/class/thermal', entry);
110
+ const type = label(await text(path.join(folder, 'type')));
111
+ if (!type || !isTemperatureZone(type)) {
112
+ continue;
113
+ }
114
+ const trips = [];
115
+ for (const file of await list(folder)) {
116
+ const trip = /^trip_point_(\d+)_type$/.exec(file);
117
+ if (trip) {
118
+ const [kind, value] = await Promise.all([text(path.join(folder, file)), number(path.join(folder, `trip_point_${trip[1]}_temp`))]);
119
+ const limit = celsius(value);
120
+ if (kind && limit !== null) {
121
+ trips.push({ type: kind.trim(), celsius: limit });
122
+ }
123
+ }
124
+ }
125
+ zones.push({ type, input: path.join(folder, 'temp'), ...zoneLimits(trips) });
126
+ }
127
+ return zones.sort((a, b) => a.input.localeCompare(b.input, undefined, { numeric: true }));
128
+ }
129
+ async function discoverTegraGpus() {
130
+ const gpus = [];
131
+ for (const entry of await list('/sys/class/devfreq')) {
132
+ const folder = path.join('/sys/class/devfreq', entry);
133
+ const node = label((await text(path.join(folder, 'device/of_node/name')))?.replace(/\0/g, '') ?? null);
134
+ if (node && TEGRA_GPUS.has(node) && (await exists(path.join(folder, 'device/load')))) {
135
+ gpus.push({ name: `Integrated GPU (${node})`, load: path.join(folder, 'device/load'), frequency: path.join(folder, 'cur_freq') });
136
+ }
137
+ }
138
+ return gpus;
139
+ }
140
+ async function discoverAmdGpus() {
141
+ const gpus = [];
142
+ for (const entry of await list('/sys/class/drm')) {
143
+ const device = path.join('/sys/class/drm', entry, 'device');
144
+ if (/^card\d+$/.test(entry) && (await exists(path.join(device, 'gpu_busy_percent')))) {
145
+ gpus.push({
146
+ busy: path.join(device, 'gpu_busy_percent'),
147
+ used: path.join(device, 'mem_info_vram_used'),
148
+ total: path.join(device, 'mem_info_vram_total'),
149
+ });
150
+ }
151
+ }
152
+ return gpus;
153
+ }
154
+ // Intel's energy counters: root's only, since Linux 5.10, unless allowed
155
+ async function discoverRapl() {
156
+ const domains = [];
157
+ for (const entry of (await list('/sys/class/powercap')).sort()) {
158
+ const folder = path.join('/sys/class/powercap', entry);
159
+ if (!/^intel-rapl:[\d:]+$/.test(entry) || (await number(path.join(folder, 'energy_uj'))) === null) {
160
+ continue;
161
+ }
162
+ const [name, range] = await Promise.all([text(path.join(folder, 'name')), number(path.join(folder, 'max_energy_range_uj'))]);
163
+ domains.push({ name: raplName(name?.trim() ?? entry), energy: path.join(folder, 'energy_uj'), range: range ?? 2 ** 32 });
164
+ }
165
+ return domains;
166
+ }
167
+ async function discoverIioRails() {
168
+ const rails = [];
169
+ const driver = '/sys/bus/i2c/drivers/ina3221x';
170
+ for (const device of await list(driver)) {
171
+ for (const iio of (await list(path.join(driver, device))).filter((entry) => entry.startsWith('iio:device'))) {
172
+ const folder = path.join(driver, device, iio);
173
+ for (const file of await list(folder)) {
174
+ const channel = /^rail_name_(\d+)$/.exec(file)?.[1];
175
+ const name = channel === undefined ? null : label(await text(path.join(folder, file)));
176
+ if (name) {
177
+ rails.push({
178
+ name,
179
+ power: path.join(folder, `in_power${channel}_input`),
180
+ volts: path.join(folder, `in_voltage${channel}_input`),
181
+ amps: path.join(folder, `in_current${channel}_input`),
182
+ });
183
+ }
184
+ }
185
+ }
186
+ }
187
+ return rails;
188
+ }
189
+ // A Raspberry Pi's firmware's: /sys/devices/platform/soc/soc:firmware, or
190
+ // where a Pi 5's device tree puts it
191
+ async function discoverThrottled() {
192
+ for (const bus of (await list('/sys/devices/platform')).filter((entry) => /^(soc|axi)/.test(entry))) {
193
+ for (const entry of (await list(path.join('/sys/devices/platform', bus))).filter((name) => name.includes('firmware'))) {
194
+ const file = path.join('/sys/devices/platform', bus, entry, 'get_throttled');
195
+ if (await exists(file)) {
196
+ return file;
197
+ }
198
+ }
199
+ }
200
+ return null;
201
+ }
202
+ async function discoverCpufreq(cores) {
203
+ const files = new Map();
204
+ await Promise.all(Array.from({ length: cores }, async (_, core) => {
205
+ const file = `/sys/devices/system/cpu/cpu${core}/cpufreq/scaling_cur_freq`;
206
+ if (await exists(file)) {
207
+ files.set(core, file);
208
+ }
209
+ }));
210
+ return files;
211
+ }
212
+ async function discover(cores, raspberryPi) {
213
+ const chipFolders = (await list('/sys/class/hwmon')).map((entry) => path.join('/sys/class/hwmon', entry));
214
+ const [chips, zones, tegraGpus, amdGpus, rapl, iioRails, throttled, blocks, cpufreq, nvidia] = await Promise.all([
215
+ Promise.all(chipFolders.map(discoverChip)),
216
+ discoverZones(),
217
+ discoverTegraGpus(),
218
+ discoverAmdGpus(),
219
+ discoverRapl(),
220
+ discoverIioRails(),
221
+ discoverThrottled(),
222
+ list('/sys/block'),
223
+ discoverCpufreq(cores),
224
+ run('nvidia-smi', ['-L']).then(() => true, () => false),
225
+ ]);
226
+ const vcgencmd = raspberryPi && !throttled
227
+ ? await run('vcgencmd', ['version']).then(() => true, () => false)
228
+ : false;
229
+ return {
230
+ chips: chips.filter((chip) => chip !== null),
231
+ zones,
232
+ tegraGpus,
233
+ amdGpus,
234
+ rapl,
235
+ iioRails,
236
+ throttled,
237
+ disks: blocks.filter(wholeDisk).sort(),
238
+ cpufreq,
239
+ nvidia,
240
+ vcgencmd,
241
+ };
242
+ }
243
+ async function readInfo() {
244
+ const [deviceTree, vendor, product, cpuinfo, present, osRelease, tegra] = await Promise.all([
245
+ text('/proc/device-tree/model'),
246
+ text('/sys/class/dmi/id/sys_vendor'),
247
+ text('/sys/class/dmi/id/product_name'),
248
+ text('/proc/cpuinfo'),
249
+ text('/sys/devices/system/cpu/present'),
250
+ text('/etc/os-release'),
251
+ text('/etc/nv_tegra_release'),
252
+ ]);
253
+ const powerMode = tegra === null ? null : await run('nvpmodel', ['-q']).then(parseNvpmodel, () => null);
254
+ return {
255
+ hostname: os.hostname(),
256
+ model: modelName(deviceTree, vendor, product),
257
+ cpu: cpuinfo ? cpuModel(cpuinfo) : null,
258
+ cores: (present && cpuCount(present)) || os.cpus().length,
259
+ memory: os.totalmem(),
260
+ os: osRelease ? parseOsRelease(osRelease) : null,
261
+ kernel: os.release(),
262
+ boot: Math.round((Date.now() - os.uptime() * 1000) / 1000) * 1000,
263
+ jetson: tegra === null ? null : { release: parseTegraRelease(tegra), powerMode },
264
+ };
265
+ }
266
+ export class SystemMonitor {
267
+ logger;
268
+ listeners = new Set();
269
+ history = [];
270
+ info = null;
271
+ infoJson = '';
272
+ infoAt = 0;
273
+ space = null;
274
+ spaceJson = '';
275
+ spaceAt = 0;
276
+ sensors = null;
277
+ sensorsAt = 0;
278
+ previous = null;
279
+ latest = null;
280
+ nvidia = { at: 0, gpus: [], running: false };
281
+ vcgencmd = { at: 0, bits: null, running: false };
282
+ // Under-voltage seen by rpi_volt, without the firmware's bits
283
+ underVoltageSeen = false;
284
+ timer = null;
285
+ linger = null;
286
+ running = false;
287
+ // A sample being read: one at a time, though stopped and started again
288
+ ticking = false;
289
+ constructor(logger) {
290
+ this.logger = logger;
291
+ }
292
+ // Returns the unwatch function
293
+ watch(listener) {
294
+ if (process.platform !== 'linux') {
295
+ listener({ error: 'The system monitor reads Linux’s /proc and /sys: the server runs on another system' });
296
+ return () => { };
297
+ }
298
+ this.listeners.add(listener);
299
+ if (this.linger) {
300
+ clearTimeout(this.linger);
301
+ this.linger = null;
302
+ }
303
+ const since = Date.now() - HISTORY_MS;
304
+ listener({
305
+ ...(this.info && { info: this.info }),
306
+ ...(this.space && { space: this.space }),
307
+ history: this.history.filter((point) => point.at >= since),
308
+ ...(this.latest && this.latest.at >= since && { sample: this.latest }),
309
+ });
310
+ if (!this.running) {
311
+ this.running = true;
312
+ if (!this.ticking) {
313
+ void this.tick();
314
+ }
315
+ }
316
+ return () => {
317
+ this.listeners.delete(listener);
318
+ if (this.listeners.size === 0 && !this.linger) {
319
+ this.linger = setTimeout(() => {
320
+ this.linger = null;
321
+ if (this.listeners.size === 0) {
322
+ this.stop();
323
+ }
324
+ }, LINGER_MS);
325
+ }
326
+ };
327
+ }
328
+ stop() {
329
+ this.running = false;
330
+ this.previous = null;
331
+ if (this.timer) {
332
+ clearTimeout(this.timer);
333
+ this.timer = null;
334
+ }
335
+ if (this.linger) {
336
+ clearTimeout(this.linger);
337
+ this.linger = null;
338
+ }
339
+ }
340
+ send(update) {
341
+ for (const listener of this.listeners) {
342
+ listener(update);
343
+ }
344
+ }
345
+ async tick() {
346
+ const started = Date.now();
347
+ this.ticking = true;
348
+ try {
349
+ await this.refresh(started);
350
+ const sample = await this.sample(started);
351
+ if (sample && this.running) {
352
+ this.latest = sample;
353
+ this.history.push(trendPoint(sample));
354
+ const since = started - HISTORY_MS;
355
+ while (this.history.length > 0 && this.history[0].at < since) {
356
+ this.history.shift();
357
+ }
358
+ this.send({ sample });
359
+ }
360
+ }
361
+ catch (e) {
362
+ this.logger.warn(`System monitor: ${errorMessage(e)}`);
363
+ }
364
+ this.ticking = false;
365
+ if (this.running) {
366
+ this.timer = setTimeout(() => void this.tick(), Math.max(100, PERIOD_MS - (Date.now() - started)));
367
+ }
368
+ }
369
+ // What changes rarely: the system's description, the sensors, the disks'
370
+ // space
371
+ async refresh(now) {
372
+ if (!this.info || now - this.infoAt >= INFO_MS) {
373
+ this.infoAt = now;
374
+ const info = await readInfo();
375
+ const json = JSON.stringify({ ...info, boot: 0 });
376
+ // Its boot time, of the uptime, moves by a second now and then
377
+ if (json !== this.infoJson) {
378
+ this.infoJson = json;
379
+ this.info = info;
380
+ this.send({ info });
381
+ }
382
+ }
383
+ if (!this.sensors || now - this.sensorsAt >= DISCOVER_MS) {
384
+ this.sensorsAt = now;
385
+ this.sensors = await discover(this.info?.cores ?? os.cpus().length, /^Raspberry Pi/.test(this.info?.model ?? ''));
386
+ }
387
+ if (!this.space || now - this.spaceAt >= SPACE_MS) {
388
+ this.spaceAt = now;
389
+ const space = await listDisks().catch(() => []);
390
+ const json = JSON.stringify(space);
391
+ if (json !== this.spaceJson) {
392
+ this.spaceJson = json;
393
+ this.space = space;
394
+ this.send({ space });
395
+ }
396
+ }
397
+ }
398
+ // null the first time: rates need a previous reading
399
+ async sample(at) {
400
+ const sensors = this.sensors;
401
+ const [stat, meminfo, loadavg, netDev, diskstats, temperatures, fans, mhz, power, gpus, throttled] = await Promise.all([
402
+ text('/proc/stat'),
403
+ text('/proc/meminfo'),
404
+ text('/proc/loadavg'),
405
+ text('/proc/net/dev'),
406
+ text('/proc/diskstats'),
407
+ this.temperatures(sensors),
408
+ this.fans(sensors),
409
+ this.frequencies(sensors),
410
+ this.power(sensors),
411
+ this.gpus(sensors, at),
412
+ this.throttled(sensors, at),
413
+ ]);
414
+ const times = parseCpuTimes(stat ?? '');
415
+ const networks = parseNetDev(netDev ?? '');
416
+ const disks = parseDiskstats(diskstats ?? '');
417
+ const previous = this.previous;
418
+ const seconds = previous ? (at - previous.at) / 1000 : 0;
419
+ this.previous = { at, cpu: times.all, cores: times.cores, networks, disks, energy: power.energy };
420
+ // Rates over a pause in sampling would be its averages
421
+ if (!previous || seconds <= 0 || seconds > (3 * PERIOD_MS) / 1000) {
422
+ return null;
423
+ }
424
+ const cores = Array.from({ length: this.info?.cores ?? 0 }, (_, core) => times.cores.has(core) ? (busyShare(previous.cores.get(core), times.cores.get(core)) ?? 0) : null);
425
+ const rails = [...power.rails];
426
+ for (const domain of sensors.rapl) {
427
+ const watts = energyWatts(previous.energy.get(domain.energy), power.energy.get(domain.energy) ?? NaN, domain.range, seconds);
428
+ if (watts !== null && Number.isFinite(watts)) {
429
+ rails.push({ name: domain.name, watts, volts: null, amps: null });
430
+ }
431
+ }
432
+ // A Jetson's GPU: its thermal zone and rail
433
+ for (const gpu of gpus) {
434
+ if (gpu.name.startsWith('Integrated GPU')) {
435
+ gpu.celsius ??= temperatures.find((temperature) => /^gpu[-_]therm/i.test(temperature.sensor))?.celsius ?? null;
436
+ gpu.watts ??= rails.find((rail) => /^(VDD|POM_5V)_GPU$/i.test(rail.name))?.watts ?? null;
437
+ }
438
+ }
439
+ const operstates = await Promise.all([...networks.keys()].map(async (name) => {
440
+ const [state, flags, device] = await Promise.all([
441
+ text(`/sys/class/net/${name}/operstate`),
442
+ text(`/sys/class/net/${name}/flags`),
443
+ exists(`/sys/class/net/${name}/device`),
444
+ ]);
445
+ // IFF_UP, and a carrier if the driver says
446
+ const up = (parseInt(flags ?? '0', 16) & 1) !== 0 && ['up', 'unknown'].includes(state?.trim() ?? '');
447
+ return { name, up, physical: device };
448
+ }));
449
+ return {
450
+ at,
451
+ cpu: busyShare(previous.cpu, times.all) ?? 0,
452
+ cores,
453
+ mhz: cores.map((share, core) => (share === null ? null : (mhz[core] ?? null))),
454
+ load: parseLoadavg(loadavg ?? ''),
455
+ memory: parseMeminfo(meminfo ?? ''),
456
+ temperatures: [
457
+ ...temperatures,
458
+ ...gpus.flatMap((gpu) => (gpu.celsius !== null && !gpu.name.startsWith('Integrated GPU') ? [{ sensor: 'GPU', label: gpu.name, celsius: gpu.celsius, high: null, critical: null }] : [])),
459
+ ],
460
+ fans,
461
+ throttled,
462
+ gpus,
463
+ power: rails,
464
+ disks: sensors.disks.flatMap((name) => {
465
+ const [before, after] = [previous.disks.get(name), disks.get(name)];
466
+ return after
467
+ ? [
468
+ {
469
+ name,
470
+ read: rate(before?.read, after.read, seconds),
471
+ written: rate(before?.written, after.written, seconds),
472
+ busy: Math.min(1, rate(before?.busyMs, after.busyMs, seconds) / 1000),
473
+ },
474
+ ]
475
+ : [];
476
+ }),
477
+ networks: operstates
478
+ .filter(({ name }) => name !== 'lo')
479
+ .map(({ name, up, physical }) => {
480
+ const [before, after] = [previous.networks.get(name), networks.get(name)];
481
+ return {
482
+ name,
483
+ received: rate(before?.received, after.received, seconds),
484
+ sent: rate(before?.sent, after.sent, seconds),
485
+ totalReceived: after.received,
486
+ totalSent: after.sent,
487
+ up,
488
+ physical,
489
+ };
490
+ }),
491
+ };
492
+ }
493
+ async temperatures(sensors) {
494
+ const [zones, chips] = await Promise.all([
495
+ Promise.all(sensors.zones.map(async (zone) => {
496
+ const value = celsius(await number(zone.input));
497
+ return value === null ? [] : [{ sensor: zone.type, label: null, celsius: value, high: zone.high, critical: zone.critical }];
498
+ })),
499
+ Promise.all(sensors.chips.flatMap((chip) => chip.temperatures.map(async (sensor) => {
500
+ const value = celsius(await number(sensor.input));
501
+ return value === null ? [] : [{ sensor: chip.name, label: sensor.label, celsius: value, high: sensor.high, critical: sensor.critical }];
502
+ }))),
503
+ ]);
504
+ return mergeTemperatures(zones.flat(), chips.flat());
505
+ }
506
+ async fans(sensors) {
507
+ const fans = await Promise.all(sensors.chips.flatMap((chip) => chip.fans.map(async (fan) => {
508
+ const rpm = await number(fan.input);
509
+ return rpm === null ? [] : [{ sensor: chip.name, label: fan.label, rpm }];
510
+ })));
511
+ return fans.flat();
512
+ }
513
+ // MHz of each core: cpufreq's, else /proc/cpuinfo's (VMs)
514
+ async frequencies(sensors) {
515
+ if (sensors.cpufreq.size === 0) {
516
+ const cpuinfo = await text('/proc/cpuinfo');
517
+ return cpuinfo ? cpuinfoMhz(cpuinfo).map(Math.round) : [];
518
+ }
519
+ const mhz = [];
520
+ await Promise.all([...sensors.cpufreq].map(async ([core, file]) => {
521
+ const khz = await number(file);
522
+ mhz[core] = khz === null ? null : Math.round(khz / 1000);
523
+ }));
524
+ return mhz;
525
+ }
526
+ // The rails read now; RAPL's energy counters, made watts by the sample
527
+ async power(sensors) {
528
+ const [chips, iio, energy] = await Promise.all([
529
+ Promise.all(sensors.chips.flatMap((chip) => [
530
+ ...chip.rails.map(async (rail) => {
531
+ const [mv, ma] = await Promise.all([number(rail.volts), number(rail.amps)]);
532
+ return mv === null || ma === null ? [] : [{ name: rail.label, watts: (mv * ma) / 1e6, volts: mv / 1000, amps: ma / 1000 }];
533
+ }),
534
+ ...chip.power.map(async (sensor) => {
535
+ const uw = await number(sensor.input);
536
+ return uw === null ? [] : [{ name: sensor.label ? `${chip.name} ${sensor.label}` : chip.name, watts: uw / 1e6, volts: null, amps: null }];
537
+ }),
538
+ ])),
539
+ Promise.all(sensors.iioRails.map(async (rail) => {
540
+ const [mw, mv, ma] = await Promise.all([number(rail.power), number(rail.volts), number(rail.amps)]);
541
+ return mw === null ? [] : [{ name: rail.name, watts: mw / 1000, volts: mv === null ? null : mv / 1000, amps: ma === null ? null : ma / 1000 }];
542
+ })),
543
+ Promise.all(sensors.rapl.map(async (domain) => [domain.energy, await number(domain.energy)])),
544
+ ]);
545
+ return {
546
+ rails: [...chips.flat(), ...iio.flat()],
547
+ energy: new Map(energy.filter((entry) => entry[1] !== null)),
548
+ };
549
+ }
550
+ async gpus(sensors, at) {
551
+ const [tegra, amd] = await Promise.all([
552
+ Promise.all(sensors.tegraGpus.map(async (gpu) => {
553
+ const [load, hz] = await Promise.all([number(gpu.load), number(gpu.frequency)]);
554
+ return {
555
+ name: gpu.name,
556
+ // Per mille
557
+ load: load === null ? null : Math.min(1, load / 1000),
558
+ mhz: hz === null ? null : Math.round(hz / 1e6),
559
+ memoryUsed: null,
560
+ memoryTotal: null,
561
+ celsius: null,
562
+ watts: null,
563
+ };
564
+ })),
565
+ Promise.all(sensors.amdGpus.map(async (gpu) => {
566
+ const [busy, used, total] = await Promise.all([number(gpu.busy), number(gpu.used), number(gpu.total)]);
567
+ return { name: 'AMD GPU', load: busy === null ? null : busy / 100, mhz: null, memoryUsed: used, memoryTotal: total, celsius: null, watts: null };
568
+ })),
569
+ ]);
570
+ if (sensors.nvidia && !this.nvidia.running && at - this.nvidia.at >= NVIDIA_MS) {
571
+ this.nvidia.running = true;
572
+ run('nvidia-smi', [`--query-gpu=${NVIDIA_QUERY}`, '--format=csv,noheader,nounits'])
573
+ .then((out) => (this.nvidia.gpus = parseNvidiaSmi(out)), () => (this.nvidia.gpus = []))
574
+ .finally(() => {
575
+ this.nvidia.at = Date.now();
576
+ this.nvidia.running = false;
577
+ });
578
+ }
579
+ return [...tegra, ...amd, ...(sensors.nvidia ? this.nvidia.gpus.map((gpu) => ({ ...gpu })) : [])];
580
+ }
581
+ async throttled(sensors, at) {
582
+ if (sensors.throttled) {
583
+ const read = await text(sensors.throttled);
584
+ return read === null ? null : parseThrottled(read);
585
+ }
586
+ if (sensors.vcgencmd) {
587
+ if (!this.vcgencmd.running && at - this.vcgencmd.at >= VCGENCMD_MS) {
588
+ this.vcgencmd.running = true;
589
+ run('vcgencmd', ['get_throttled'])
590
+ .then((out) => (this.vcgencmd.bits = parseThrottled(out)), () => (this.vcgencmd.bits = null))
591
+ .finally(() => {
592
+ this.vcgencmd.at = Date.now();
593
+ this.vcgencmd.running = false;
594
+ });
595
+ }
596
+ return this.vcgencmd.bits;
597
+ }
598
+ const volts = sensors.chips.find((chip) => chip.underVoltage);
599
+ if (volts?.underVoltage) {
600
+ const now = (await number(volts.underVoltage)) === 1;
601
+ this.underVoltageSeen ||= now;
602
+ return (now ? 1 : 0) | (this.underVoltageSeen ? 1 << 16 : 0);
603
+ }
604
+ return null;
605
+ }
606
+ }
@@ -0,0 +1,51 @@
1
+ import type { Gpu, Temperature } from '../shared/system.ts';
2
+ export interface CpuTimes {
3
+ busy: number;
4
+ total: number;
5
+ }
6
+ export declare function parseCpuTimes(text: string): {
7
+ all: CpuTimes | null;
8
+ cores: Map<number, CpuTimes>;
9
+ };
10
+ export declare function busyShare(before: CpuTimes | null | undefined, after: CpuTimes | null | undefined): number | null;
11
+ export declare function cpuCount(text: string): number;
12
+ export declare function parseMeminfo(text: string): {
13
+ total: number;
14
+ used: number;
15
+ cached: number;
16
+ swapTotal: number;
17
+ swapUsed: number;
18
+ };
19
+ export declare function parseLoadavg(text: string): [number, number, number];
20
+ export declare function parseNetDev(text: string): Map<string, {
21
+ received: number;
22
+ sent: number;
23
+ }>;
24
+ export declare function parseDiskstats(text: string): Map<string, {
25
+ read: number;
26
+ written: number;
27
+ busyMs: number;
28
+ }>;
29
+ export declare function wholeDisk(name: string): boolean;
30
+ export declare function rate(before: number | undefined, after: number, seconds: number): number;
31
+ export declare function energyWatts(before: number | undefined, after: number, range: number, seconds: number): number | null;
32
+ export declare function parseThrottled(text: string): number | null;
33
+ export declare const NVIDIA_QUERY = "utilization.gpu,memory.used,memory.total,temperature.gpu,power.draw,clocks.gr,name";
34
+ export declare function parseNvidiaSmi(text: string): Gpu[];
35
+ export declare function parseOsRelease(text: string): string | null;
36
+ export declare function parseTegraRelease(text: string): string | null;
37
+ export declare function parseNvpmodel(text: string): string | null;
38
+ export declare function cpuModel(cpuinfo: string): string | null;
39
+ export declare function cpuinfoMhz(cpuinfo: string): number[];
40
+ export declare function modelName(deviceTree: string | null, vendor: string | null, product: string | null): string | null;
41
+ export declare function isTemperatureZone(type: string): boolean;
42
+ export declare function zoneLimits(trips: {
43
+ type: string;
44
+ celsius: number;
45
+ }[]): {
46
+ high: number | null;
47
+ critical: number | null;
48
+ };
49
+ export declare function celsius(milli: number | null): number | null;
50
+ export declare function mergeTemperatures(zones: Temperature[], chips: Temperature[]): Temperature[];
51
+ export declare function raplName(name: string): string;