@torrent-tv/proxy 2.92.3 → 2.92.4
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/package.json
CHANGED
|
@@ -119,7 +119,8 @@ export async function readMachineState() {
|
|
|
119
119
|
* @param {{ takenAt: number, processCpuSeconds: number | null, system: CpuTotals | null }} before
|
|
120
120
|
* @param {{ takenAt: number, processCpuSeconds: number | null, system: CpuTotals | null }} after
|
|
121
121
|
* @param {number} [cores=os.cpus().length]
|
|
122
|
-
* @returns {{ elapsedSec: number, processShare: number | null, systemShare: number | null, iowaitShare: number | null } | null}
|
|
122
|
+
* @returns {{ elapsedSec: number, processShare: number | null, systemShare: number | null, iowaitShare: number | null, rejected: string[] } | null}
|
|
123
|
+
* `rejected` names the readings that were not a share of the machine at all.
|
|
123
124
|
*/
|
|
124
125
|
export function shareOfMachine(before, after, cores = os.cpus().length) {
|
|
125
126
|
const elapsedSec = (after.takenAt - before.takenAt) / 1000;
|
|
@@ -127,16 +128,33 @@ export function shareOfMachine(before, after, cores = os.cpus().length) {
|
|
|
127
128
|
return null;
|
|
128
129
|
}
|
|
129
130
|
const machineSeconds = elapsedSec * cores;
|
|
131
|
+
// A share of the machine lies between 0 and 1. The counters are whole clock
|
|
132
|
+
// ticks, so each reading can be off by one tick and the allowance is exactly
|
|
133
|
+
// that: derived from the tick and the interval, not chosen. A value beyond it
|
|
134
|
+
// is not a measurement of anything (field 2026-10-06: `system=2422%` over
|
|
135
|
+
// 1.7 s, then `1832%` over 0.1 s), and it is refused here instead of being
|
|
136
|
+
// stored as a fact the next decision rests on.
|
|
137
|
+
const allowance = 1 / (CLOCK_TICKS_PER_SECOND * elapsedSec);
|
|
138
|
+
/** @type {string[]} */
|
|
139
|
+
const rejected = [];
|
|
140
|
+
const shareOf = (name, seconds) => {
|
|
141
|
+
const share = seconds / machineSeconds;
|
|
142
|
+
if (!(share >= -allowance && share <= 1 + allowance)) {
|
|
143
|
+
rejected.push(`${name}=${Math.round(share * 100)}%`);
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
return share;
|
|
147
|
+
};
|
|
130
148
|
const processShare = before.processCpuSeconds !== null && after.processCpuSeconds !== null
|
|
131
|
-
? (after.processCpuSeconds - before.processCpuSeconds)
|
|
149
|
+
? shareOf("process", after.processCpuSeconds - before.processCpuSeconds)
|
|
132
150
|
: null;
|
|
133
151
|
const systemShare = before.system !== null && after.system !== null
|
|
134
|
-
? (after.system.busySeconds - before.system.busySeconds)
|
|
152
|
+
? shareOf("system", after.system.busySeconds - before.system.busySeconds)
|
|
135
153
|
: null;
|
|
136
154
|
const iowaitShare = before.system !== null && after.system !== null
|
|
137
|
-
? (after.system.iowaitSeconds - before.system.iowaitSeconds)
|
|
155
|
+
? shareOf("iowait", after.system.iowaitSeconds - before.system.iowaitSeconds)
|
|
138
156
|
: null;
|
|
139
|
-
return { elapsedSec, processShare, systemShare, iowaitShare };
|
|
157
|
+
return { elapsedSec, processShare, systemShare, iowaitShare, rejected };
|
|
140
158
|
}
|
|
141
159
|
|
|
142
160
|
/**
|
|
@@ -19,7 +19,12 @@ export class HostLoad {
|
|
|
19
19
|
/** What this reads and asks of the rest of the proxy, and nothing else. @type {object} */
|
|
20
20
|
#host;
|
|
21
21
|
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* How much of the machine a new encoder could have, from the LAST reading;
|
|
24
|
+
* undefined before one, and `known: false` whenever the last reading was
|
|
25
|
+
* missing or was not a share of the machine. It is never older than the
|
|
26
|
+
* reading that made it: every pass either replaces it or says unknown.
|
|
27
|
+
*/
|
|
23
28
|
hostAvailability;
|
|
24
29
|
|
|
25
30
|
/** The previous reading of the machine, to compare the next one against. */
|
|
@@ -28,6 +33,12 @@ export class HostLoad {
|
|
|
28
33
|
/** The previous reading taken while nothing was encoding, for the torrent's own cost. */
|
|
29
34
|
#idleLoadSample = null;
|
|
30
35
|
|
|
36
|
+
/** The previous reading of the whole machine taken while nothing was encoding. */
|
|
37
|
+
#idleAvailabilitySample = null;
|
|
38
|
+
|
|
39
|
+
/** A reading is under way; the next tick must not start another beside it. */
|
|
40
|
+
#reporting = false;
|
|
41
|
+
|
|
31
42
|
/**
|
|
32
43
|
* How long each file is in bytes, from its declared source statistics. The torrent moves the CONTAINER, so this — not the video stream's
|
|
33
44
|
* bitrate — is what its work should be priced against.
|
|
@@ -335,7 +346,88 @@ export class HostLoad {
|
|
|
335
346
|
}
|
|
336
347
|
}
|
|
337
348
|
|
|
349
|
+
/**
|
|
350
|
+
* One reading of the machine, never beside another. The tick fires without
|
|
351
|
+
* waiting for the previous one, and two readings in flight at once each stamp
|
|
352
|
+
* their own time after their own await and then replace one another as the
|
|
353
|
+
* "previous" sample, so a delta of counters is divided by an interval that is
|
|
354
|
+
* not its own (field 2026-10-06: three readings inside 0.1 s).
|
|
355
|
+
*
|
|
356
|
+
* Only the reading of the MACHINE is held to one at a time: its awaits are
|
|
357
|
+
* files of `/proc` and `/sys`, which answer. The torrent's cost asks the
|
|
358
|
+
* torrent worker, which may not answer, and a reading waiting on it would
|
|
359
|
+
* stop every later one — the machine's availability would then stay as it
|
|
360
|
+
* was for as long as the worker did.
|
|
361
|
+
*/
|
|
338
362
|
async reportHostLoad() {
|
|
363
|
+
if (this.#reporting) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
this.#reporting = true;
|
|
367
|
+
let nothingEncoding = false;
|
|
368
|
+
try {
|
|
369
|
+
nothingEncoding = await this.#reportMachineOnce();
|
|
370
|
+
} finally {
|
|
371
|
+
this.#reporting = false;
|
|
372
|
+
}
|
|
373
|
+
if (nothingEncoding) {
|
|
374
|
+
await this.#learnTorrentCost();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* The machine's availability while nothing is encoding, taken the same way as
|
|
380
|
+
* while something is. Without it the last figure from the last encoder stayed
|
|
381
|
+
* in force for ever: it refused every new encoder, and a refused encoder is
|
|
382
|
+
* the only thing that would have taken the next reading.
|
|
383
|
+
*/
|
|
384
|
+
async #learnIdleAvailability() {
|
|
385
|
+
const system = (await this.#host.readSystemCpu?.()) ?? null;
|
|
386
|
+
const sample = { takenAt: Date.now(), system, proxyCpuSeconds: this.#host.readProxyCpuSeconds() };
|
|
387
|
+
const previous = this.#idleAvailabilitySample;
|
|
388
|
+
this.#idleAvailabilitySample = sample;
|
|
389
|
+
if (previous === null) {
|
|
390
|
+
this.hostAvailability = availableShareFrom({});
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const share = this.#host.shareOfMachine(
|
|
394
|
+
{ takenAt: previous.takenAt, processCpuSeconds: 0, system: previous.system },
|
|
395
|
+
{ takenAt: sample.takenAt, processCpuSeconds: 0, system: sample.system }
|
|
396
|
+
);
|
|
397
|
+
this.#setAvailability(share, previous, sample);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* @param {{ elapsedSec: number, processShare: number | null, systemShare: number | null, rejected?: string[] } | null} share
|
|
402
|
+
* @param {{ proxyCpuSeconds: number | null }} previous
|
|
403
|
+
* @param {{ proxyCpuSeconds: number | null }} sample
|
|
404
|
+
* @returns {number | null} The proxy's own share of the machine, for the log.
|
|
405
|
+
*/
|
|
406
|
+
#setAvailability(share, previous, sample) {
|
|
407
|
+
if (share === null) {
|
|
408
|
+
this.hostAvailability = availableShareFrom({});
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
if (share.rejected?.length) {
|
|
412
|
+
logger.warn(
|
|
413
|
+
`host-load: ignored a reading over ${share.elapsedSec.toFixed(1)}s that is not a share of the machine ` +
|
|
414
|
+
`(${share.rejected.join(" ")}); availability is unknown until the next one`
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
const cores = Math.max(1, os.cpus().length);
|
|
418
|
+
const proxyShare = Number.isFinite(previous.proxyCpuSeconds)
|
|
419
|
+
? (sample.proxyCpuSeconds - previous.proxyCpuSeconds) / (share.elapsedSec * cores)
|
|
420
|
+
: null;
|
|
421
|
+
this.hostAvailability = availableShareFrom({
|
|
422
|
+
systemBusy: share.systemShare,
|
|
423
|
+
encoderShare: share.processShare,
|
|
424
|
+
proxyShare
|
|
425
|
+
});
|
|
426
|
+
return proxyShare;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** @returns {Promise<boolean>} True when no encoder is running, which is when the torrent's cost can be taken. */
|
|
430
|
+
async #reportMachineOnce() {
|
|
339
431
|
const encoding = [...this.#host.outputs.values()].filter(
|
|
340
432
|
(session) => processCanBeSignalled(this.#host.runStateOf(session))
|
|
341
433
|
);
|
|
@@ -346,11 +438,12 @@ export class HostLoad {
|
|
|
346
438
|
// in which every encoder was suspended, the torrent's price could have
|
|
347
439
|
// been taken, and none was. What this process spends now is the download,
|
|
348
440
|
// the verification, the piece store and the delivery. Item 7.
|
|
349
|
-
await this.#
|
|
441
|
+
await this.#learnIdleAvailability();
|
|
350
442
|
this.#hostLoadSample = null;
|
|
351
|
-
return;
|
|
443
|
+
return true;
|
|
352
444
|
}
|
|
353
445
|
this.#idleLoadSample = null;
|
|
446
|
+
this.#idleAvailabilitySample = null;
|
|
354
447
|
// EVERY encoder, added up. One of them is meaningless on a host that runs a
|
|
355
448
|
// picture and an audio track at once, and picking the first would have
|
|
356
449
|
// reported whichever the map happened to hold.
|
|
@@ -387,7 +480,7 @@ export class HostLoad {
|
|
|
387
480
|
const previous = this.#hostLoadSample;
|
|
388
481
|
this.#hostLoadSample = sample;
|
|
389
482
|
if (previous === null) {
|
|
390
|
-
return; // the first reading is only something to compare against
|
|
483
|
+
return false; // the first reading is only something to compare against
|
|
391
484
|
}
|
|
392
485
|
// Summed over the pids both readings hold, so nothing is measured against a
|
|
393
486
|
// process that was not there before. Unknown stays unknown: on a host with
|
|
@@ -405,7 +498,8 @@ export class HostLoad {
|
|
|
405
498
|
{ takenAt: sample.takenAt, processCpuSeconds: encoderDelta, system: sample.system }
|
|
406
499
|
);
|
|
407
500
|
if (share === null) {
|
|
408
|
-
|
|
501
|
+
this.hostAvailability = availableShareFrom({});
|
|
502
|
+
return false;
|
|
409
503
|
}
|
|
410
504
|
// How many of them are stopped by the look-ahead cap. Without this a zero
|
|
411
505
|
// share reads as an encoder being starved of the machine, when it is an
|
|
@@ -417,19 +511,11 @@ export class HostLoad {
|
|
|
417
511
|
const running = encoding.length - suspended;
|
|
418
512
|
const machine = await this.#host.readMachineState();
|
|
419
513
|
const asPercent = (value) => (value === null ? "n/a" : `${Math.round(value * 100)}%`);
|
|
420
|
-
const cores = Math.max(1, os.cpus().length);
|
|
421
|
-
const proxyShare = Number.isFinite(previous.proxyCpuSeconds)
|
|
422
|
-
? (sample.proxyCpuSeconds - previous.proxyCpuSeconds) / (share.elapsedSec * cores)
|
|
423
|
-
: null;
|
|
424
514
|
// Kept for the quality offer, which predicts from a benchmark taken on a
|
|
425
515
|
// QUIET host: the same reading that is printed here says how much of the
|
|
426
516
|
// machine a new encoder could actually have. Only what nobody has been
|
|
427
517
|
// charged for is subtracted — see `encode/available-share.js`.
|
|
428
|
-
|
|
429
|
-
systemBusy: share.systemShare,
|
|
430
|
-
encoderShare: share.processShare,
|
|
431
|
-
proxyShare
|
|
432
|
-
});
|
|
518
|
+
const proxyShare = this.#setAvailability(share, previous, sample);
|
|
433
519
|
logger.info(
|
|
434
520
|
`host-load: ffmpeg=${asPercent(share.processShare)} proxy=${asPercent(proxyShare)} ` +
|
|
435
521
|
`system=${asPercent(share.systemShare)} ` +
|
|
@@ -441,6 +527,7 @@ export class HostLoad {
|
|
|
441
527
|
// readings it comes from.
|
|
442
528
|
` available=${asPercent(this.hostAvailability.share)}`
|
|
443
529
|
);
|
|
530
|
+
return false;
|
|
444
531
|
}
|
|
445
532
|
|
|
446
533
|
/**
|