@torrent-tv/proxy 2.98.15 → 2.98.17
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 +1 -1
- package/services/encode/EncodeRuns.js +16 -4
- package/services/encode/host-load.js +28 -0
- package/services/encode/quality/HostLoad.js +37 -23
- package/services/encode/quality/encoder-cpu.js +66 -0
- package/services/encode/run-open.js +12 -2
- package/services/server/wire-outputs.js +2 -1
package/package.json
CHANGED
|
@@ -1319,8 +1319,13 @@ export class EncodeRuns {
|
|
|
1319
1319
|
/**
|
|
1320
1320
|
* How long this run took to open its original input, for sizing the next
|
|
1321
1321
|
* stretch: from its spawn to the name of its first closed piece, less what
|
|
1322
|
-
* encoding that piece costs at
|
|
1323
|
-
*
|
|
1322
|
+
* encoding that piece costs at a measured speed — the run's own where it
|
|
1323
|
+
* closed two pieces, else the output's newest reading, which is taken
|
|
1324
|
+
* between two closed pieces of a run too. Never a predicted speed: the
|
|
1325
|
+
* output's starting figure, scaled by a machine share that missed short
|
|
1326
|
+
* runs, read 0.08x on Home Assistant and recorded opens of 0.00 s (r41,
|
|
1327
|
+
* torrent-tv/meta#178). Nothing is noted where no speed was measured or the
|
|
1328
|
+
* rest is negative, and the open estimated before stands.
|
|
1324
1329
|
*
|
|
1325
1330
|
* @param {object} session
|
|
1326
1331
|
* @param {import("./EncodeRun.js").EncodeRun} run
|
|
@@ -1330,14 +1335,21 @@ export class EncodeRuns {
|
|
|
1330
1335
|
#noteOpen(session, run, ended) {
|
|
1331
1336
|
const grid = session.timeline?.published ?? session.timeline?.boundaries;
|
|
1332
1337
|
const pieceSeconds = Number(grid?.[run.from + 1]) - Number(grid?.[run.from]);
|
|
1333
|
-
const speed = run.speedX > 0 ? run.speedX : this.#
|
|
1338
|
+
const speed = run.speedX > 0 ? run.speedX : this.#speedReadings.windowOf(session).at(-1)?.speed ?? 0;
|
|
1334
1339
|
// FFmpeg's reads may have waited for the stretch being copied; that wait
|
|
1335
1340
|
// is the copy's, and the stretch is sized from the open (`run-open.js`).
|
|
1336
1341
|
const open = runOpenOf({
|
|
1337
1342
|
firstNamedMs: ended.firstNamedMs, firstNamedWallMs: ended.firstNamedWallMs, startedAt: run.startedAt,
|
|
1338
1343
|
waitedBefore: run.originalInput?.waitedBefore, pieceSeconds, speed
|
|
1339
1344
|
});
|
|
1340
|
-
if (!open)
|
|
1345
|
+
if (!open) {
|
|
1346
|
+
if (Number.isFinite(ended.firstNamedMs) && pieceSeconds > 0) {
|
|
1347
|
+
this.#host.logger.info(`encode input of run #${run.from}..#${run.to} on ${session.outputKey}: open not measured ` +
|
|
1348
|
+
`(first piece named at ${ended.firstNamedMs}ms, ${pieceSeconds.toFixed(3)}s of film, ` +
|
|
1349
|
+
`${speed > 0 ? `at a measured ${speed.toFixed(2)}x the piece takes longer than that` : "no speed measured between two closed pieces"})`);
|
|
1350
|
+
}
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1341
1353
|
const { openSeconds, waitedMs } = open;
|
|
1342
1354
|
this.#host.encodeOrchestrator.noteOpen?.(session.outputKey ?? "", openSeconds);
|
|
1343
1355
|
this.#host.logger.info(`encode input of run #${run.from}..#${run.to} on ${session.outputKey}: opened in ` +
|
|
@@ -92,6 +92,34 @@ export async function readProcessCpuSeconds(pid) {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* The CPU time every child of this process that has ended has used, from the
|
|
97
|
+
* `cutime` and `cstime` of `/proc/self/stat`: the kernel adds a child's time
|
|
98
|
+
* there when the child is waited for, which Node does as soon as it exits.
|
|
99
|
+
*
|
|
100
|
+
* What a process that started and ended between two readings of its own pid
|
|
101
|
+
* cost, which no reading of that pid can say: on Home Assistant a run of one
|
|
102
|
+
* piece started and ended between two readings 5 s apart, used 6.65 s of CPU,
|
|
103
|
+
* and both readings said `ffmpeg=0%` (r41, torrent-tv/meta#178).
|
|
104
|
+
*
|
|
105
|
+
* @returns {Promise<number | null>} Seconds of CPU, or null where there is no `/proc`.
|
|
106
|
+
*/
|
|
107
|
+
export async function readEndedChildrenCpuSeconds() {
|
|
108
|
+
try {
|
|
109
|
+
const text = await readFile("/proc/self/stat", "utf8");
|
|
110
|
+
const afterComm = text.slice(text.lastIndexOf(")") + 2).trim().split(/\s+/);
|
|
111
|
+
// After the comm and state fields, cutime is index 13 and cstime index 14.
|
|
112
|
+
const cutime = Number(afterComm[13]);
|
|
113
|
+
const cstime = Number(afterComm[14]);
|
|
114
|
+
if (!Number.isFinite(cutime) || !Number.isFinite(cstime)) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
return (cutime + cstime) / CLOCK_TICKS_PER_SECOND;
|
|
118
|
+
} catch {
|
|
119
|
+
return null; // not Linux, or /proc is not mounted
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
95
123
|
/**
|
|
96
124
|
* Is this still the machine the benchmarks were taken on: its clock and its
|
|
97
125
|
* temperature.
|
|
@@ -11,6 +11,7 @@ import os from "node:os";
|
|
|
11
11
|
import { monitorEventLoopDelay } from "node:perf_hooks";
|
|
12
12
|
import { logger } from "../../../utils/logger.js";
|
|
13
13
|
import { availableShareFrom } from "../available-share.js";
|
|
14
|
+
import { encoderCpuBetween } from "./encoder-cpu.js";
|
|
14
15
|
import { baseDrawFrom, costPerMegabyteFrom } from "../torrent-cost.js";
|
|
15
16
|
import { medianOf, movedBeyondScatter, READINGS_KEPT, scatterOf } from "../learned-median.js";
|
|
16
17
|
import { ENCODE_RUN_STATE, processCanBeSignalled } from "../encode-run-state.js";
|
|
@@ -532,17 +533,23 @@ export class HostLoad {
|
|
|
532
533
|
* the only thing that would have taken the next reading.
|
|
533
534
|
*/
|
|
534
535
|
async #learnIdleAvailability(watched, suspended) {
|
|
535
|
-
const system =
|
|
536
|
-
|
|
536
|
+
const [system, endedChildren] = await Promise.all([
|
|
537
|
+
this.#host.readSystemCpu?.() ?? null,
|
|
538
|
+
this.#host.readEndedChildrenCpuSeconds?.() ?? null
|
|
539
|
+
]);
|
|
540
|
+
const sample = { takenAt: Date.now(), system, endedChildren, byPid: new Map(), proxyCpuSeconds: this.#host.readProxyCpuSeconds() };
|
|
537
541
|
const previous = this.#idleAvailabilitySample;
|
|
538
542
|
this.#idleAvailabilitySample = sample;
|
|
539
543
|
if (previous === null) {
|
|
540
544
|
this.hostAvailability = availableShareFrom({});
|
|
541
545
|
return;
|
|
542
546
|
}
|
|
547
|
+
// Nothing runs at either reading, and a run may still have run between
|
|
548
|
+
// them: what it used is in the ended-children time (`encoder-cpu.js`).
|
|
549
|
+
const encoders = encoderCpuBetween(previous, sample);
|
|
543
550
|
const share = this.#host.shareOfMachine(
|
|
544
551
|
{ takenAt: previous.takenAt, processCpuSeconds: 0, system: previous.system },
|
|
545
|
-
{ takenAt: sample.takenAt, processCpuSeconds: 0, system: sample.system }
|
|
552
|
+
{ takenAt: sample.takenAt, processCpuSeconds: encoders?.seconds ?? 0, system: sample.system }
|
|
546
553
|
);
|
|
547
554
|
const proxyShare = this.#setAvailability(share, previous, sample);
|
|
548
555
|
// Said while something is watched, so what is free while every encoder
|
|
@@ -559,7 +566,7 @@ export class HostLoad {
|
|
|
559
566
|
* and the processor share divide; `free` is what nothing used over the
|
|
560
567
|
* interval. Unknown stays `n/a`, never zero.
|
|
561
568
|
*/
|
|
562
|
-
async #sayMachine(share, proxyShare, running, suspended) {
|
|
569
|
+
async #sayMachine(share, proxyShare, running, suspended, incomplete = false) {
|
|
563
570
|
const machine = await this.#host.readMachineState();
|
|
564
571
|
// HOW LATE THIS THREAD'S LOOP RAN over the interval: every answer to FFmpeg
|
|
565
572
|
// and every hand-over of a copy waits for it (torrent-tv/meta#178). Begun at
|
|
@@ -574,7 +581,8 @@ export class HostLoad {
|
|
|
574
581
|
const asPercent = (value) => (value === null || value === undefined ? "n/a" : `${Math.round(value * 100)}%`);
|
|
575
582
|
const free = Number.isFinite(share.systemShare) ? Math.max(0, 1 - share.systemShare) : null;
|
|
576
583
|
logger.info(
|
|
577
|
-
`host-load: ffmpeg=${asPercent(share.processShare)}
|
|
584
|
+
`host-load: ffmpeg=${incomplete ? `at least ${asPercent(share.processShare)} (a run was not read; availability kept)` : asPercent(share.processShare)} ` +
|
|
585
|
+
`proxy=${asPercent(proxyShare)} ` +
|
|
578
586
|
`system=${asPercent(share.systemShare)} ` +
|
|
579
587
|
`iowait=${asPercent(share.iowaitShare)} cpu=${machine.megahertz === null ? "n/a" : `${machine.megahertz}MHz`} ` +
|
|
580
588
|
`temp=${machine.celsius === null ? "n/a" : `${machine.celsius}C`} ` +
|
|
@@ -646,22 +654,28 @@ export class HostLoad {
|
|
|
646
654
|
// alone would print something like `ffmpeg=-598%`. Only pids present in
|
|
647
655
|
// BOTH readings are counted, so a process that came or went contributes
|
|
648
656
|
// nothing rather than a lie.
|
|
649
|
-
const
|
|
650
|
-
const [system, ...cpuReadings] = await Promise.all([
|
|
657
|
+
const runs = encoding.flatMap((session) => this.#host.liveRunsOf(session)).filter((run) => Number.isInteger(run.process?.pid));
|
|
658
|
+
const [system, endedChildren, ...cpuReadings] = await Promise.all([
|
|
651
659
|
this.#host.readSystemCpu(),
|
|
652
|
-
|
|
660
|
+
this.#host.readEndedChildrenCpuSeconds?.() ?? null,
|
|
661
|
+
...runs.map((run) => this.#host.readProcessCpuSeconds(run.process.pid))
|
|
653
662
|
]);
|
|
654
663
|
/** @type {Map<number, number>} */
|
|
655
664
|
const byPid = new Map();
|
|
656
|
-
|
|
665
|
+
/** @type {Map<number, number>} */
|
|
666
|
+
const startedAt = new Map();
|
|
667
|
+
runs.forEach((run, index) => {
|
|
657
668
|
const seconds = cpuReadings[index];
|
|
658
669
|
if (seconds !== null) {
|
|
659
|
-
byPid.set(pid, seconds);
|
|
670
|
+
byPid.set(run.process.pid, seconds);
|
|
671
|
+
startedAt.set(run.process.pid, run.startedAt);
|
|
660
672
|
}
|
|
661
673
|
});
|
|
662
674
|
const sample = {
|
|
663
675
|
takenAt: Date.now(),
|
|
664
676
|
byPid,
|
|
677
|
+
startedAt,
|
|
678
|
+
endedChildren,
|
|
665
679
|
system,
|
|
666
680
|
// The proxy's own CPU, across every thread: the torrent, the hashing, the
|
|
667
681
|
// piece store and the delivery. None of it is in the encode budget, and
|
|
@@ -674,20 +688,14 @@ export class HostLoad {
|
|
|
674
688
|
if (previous === null) {
|
|
675
689
|
return false; // the first reading is only something to compare against
|
|
676
690
|
}
|
|
677
|
-
//
|
|
678
|
-
//
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
|
|
682
|
-
for (const [pid, seconds] of sample.byPid) {
|
|
683
|
-
const before = previous.byPid?.get(pid);
|
|
684
|
-
if (before !== undefined && seconds >= before) {
|
|
685
|
-
encoderDelta = (encoderDelta ?? 0) + (seconds - before);
|
|
686
|
-
}
|
|
687
|
-
}
|
|
691
|
+
// Every process counted once, including runs that started or ended between
|
|
692
|
+
// the readings (`encoder-cpu.js`). Unknown stays unknown: on a host with no
|
|
693
|
+
// `/proc` there are no readings at all, and the share is null rather than a
|
|
694
|
+
// confident zero.
|
|
695
|
+
const encoders = encoderCpuBetween(previous, sample);
|
|
688
696
|
const share = this.#host.shareOfMachine(
|
|
689
|
-
{ takenAt: previous.takenAt, processCpuSeconds:
|
|
690
|
-
{ takenAt: sample.takenAt, processCpuSeconds:
|
|
697
|
+
{ takenAt: previous.takenAt, processCpuSeconds: encoders === null ? null : 0, system: previous.system },
|
|
698
|
+
{ takenAt: sample.takenAt, processCpuSeconds: encoders?.seconds ?? null, system: sample.system }
|
|
691
699
|
);
|
|
692
700
|
if (share === null) {
|
|
693
701
|
this.hostAvailability = availableShareFrom({});
|
|
@@ -701,6 +709,12 @@ export class HostLoad {
|
|
|
701
709
|
// they stopped).
|
|
702
710
|
const suspended = encoding.filter((session) => this.#host.runStateOf(session) === ENCODE_RUN_STATE.SUSPENDED).length;
|
|
703
711
|
const running = encoding.length - suspended;
|
|
712
|
+
if (encoders && !encoders.complete) {
|
|
713
|
+
// A lower bound of our own load is not a reading of what other work left
|
|
714
|
+
// us: the last availability stands, and the line says so.
|
|
715
|
+
await this.#sayMachine(share, null, running, suspended, true);
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
704
718
|
// Kept for the quality offer, which predicts from a benchmark taken on a
|
|
705
719
|
// QUIET host: the same reading that is printed here says how much of the
|
|
706
720
|
// machine a new encoder could actually have. Only what nobody has been
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The CPU this proxy's FFmpeg processes used between two readings of the
|
|
3
|
+
* machine (torrent-tv/meta#178).
|
|
4
|
+
*
|
|
5
|
+
* A process's own counter says what it used since it started, and is gone once
|
|
6
|
+
* it has ended. Counting only processes present in both readings left out every
|
|
7
|
+
* run that started or ended between them: on Home Assistant a run of one piece
|
|
8
|
+
* started and ended between two readings 5 s apart, used 6.65 s of CPU, and
|
|
9
|
+
* both readings said `ffmpeg=0%` (r41). The availability left to encoders is
|
|
10
|
+
* the machine's load less theirs, so an encoder left out made the machine look
|
|
11
|
+
* busier with other work than it was.
|
|
12
|
+
*
|
|
13
|
+
* So three parts, each counted once:
|
|
14
|
+
* 1. a process in both readings: what its counter moved;
|
|
15
|
+
* 2. a process started after the first reading: its whole counter;
|
|
16
|
+
* 3. a process that ended between them: what the kernel added to this
|
|
17
|
+
* process's ended-children time (`cutime + cstime`), less what the first
|
|
18
|
+
* reading already counted of it.
|
|
19
|
+
* A part that cannot be told — a process that was there but unread, an
|
|
20
|
+
* ended-children time that went back, no such time at all while a process
|
|
21
|
+
* ended — makes the answer incomplete: a lower bound, never a measured zero.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {object} EncoderReading
|
|
26
|
+
* @property {number} takenAt - Wall clock, ms.
|
|
27
|
+
* @property {Map<number, number>} byPid - CPU seconds of each live FFmpeg process read.
|
|
28
|
+
* @property {Map<number, number>} [startedAt] - When each of them was spawned, wall clock ms.
|
|
29
|
+
* @property {number | null} endedChildren - This process's ended-children CPU seconds.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {EncoderReading} previous
|
|
34
|
+
* @param {EncoderReading} sample
|
|
35
|
+
* @returns {{ seconds: number, complete: boolean } | null} Null when nothing at all was read.
|
|
36
|
+
*/
|
|
37
|
+
export function encoderCpuBetween(previous, sample) {
|
|
38
|
+
const endedKnown = Number.isFinite(previous.endedChildren) && Number.isFinite(sample.endedChildren);
|
|
39
|
+
if (sample.byPid.size === 0 && previous.byPid.size === 0 && !endedKnown) return null;
|
|
40
|
+
let seconds = 0, complete = true;
|
|
41
|
+
for (const [pid, now] of sample.byPid) {
|
|
42
|
+
const before = previous.byPid.get(pid);
|
|
43
|
+
if (before !== undefined) {
|
|
44
|
+
if (now >= before) seconds += now - before;
|
|
45
|
+
else complete = false;
|
|
46
|
+
} else if (sample.startedAt?.get(pid) > previous.takenAt) {
|
|
47
|
+
seconds += now;
|
|
48
|
+
} else {
|
|
49
|
+
complete = false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
let countedOfEnded = 0, ended = 0;
|
|
53
|
+
for (const [pid, before] of previous.byPid) {
|
|
54
|
+
if (sample.byPid.has(pid)) continue;
|
|
55
|
+
countedOfEnded += before;
|
|
56
|
+
ended += 1;
|
|
57
|
+
}
|
|
58
|
+
if (endedKnown) {
|
|
59
|
+
const rest = sample.endedChildren - previous.endedChildren - countedOfEnded;
|
|
60
|
+
if (rest >= 0) seconds += rest;
|
|
61
|
+
else complete = false;
|
|
62
|
+
} else if (ended > 0) {
|
|
63
|
+
complete = false;
|
|
64
|
+
}
|
|
65
|
+
return { seconds, complete };
|
|
66
|
+
}
|
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
* wall clock, so they are taken up to the wall-clock moment the first piece
|
|
9
9
|
* was named — the run's start plus its time to that name, not that time alone,
|
|
10
10
|
* which as a moment lay before every wait and counted none of them.
|
|
11
|
+
*
|
|
12
|
+
* THE SPEED IS A MEASURED ONE, between two closed pieces, and a negative rest
|
|
13
|
+
* is no open at all: on Home Assistant runs of one piece took 6.0-7.5 s to it
|
|
14
|
+
* for 4 s of film, were priced at a speed of 0.08x that no run had measured,
|
|
15
|
+
* and recorded opens of 0.00 s; the next inputs were sized from them to one
|
|
16
|
+
* piece, and the runs stayed one piece each (r41, torrent-tv/meta#178). A rest
|
|
17
|
+
* below zero says the speed does not describe that piece, and nothing is
|
|
18
|
+
* recorded: the open estimated before stands.
|
|
11
19
|
*/
|
|
12
20
|
|
|
13
21
|
/**
|
|
@@ -18,12 +26,14 @@
|
|
|
18
26
|
* @param {((until: number) => number) | null | undefined} params.waitedBefore - The input's wall-clock waits for its copy before `until`, ms.
|
|
19
27
|
* @param {number} params.pieceSeconds - The first piece's film.
|
|
20
28
|
* @param {number} params.speed - Film seconds per second; 0 when not known.
|
|
21
|
-
* @returns {{ openSeconds: number, waitedMs: number } | null} Null where any term is
|
|
29
|
+
* @returns {{ openSeconds: number, waitedMs: number } | null} Null where any term is
|
|
30
|
+
* unknown, or where the piece's encoding at that speed exceeds the time to it.
|
|
22
31
|
*/
|
|
23
32
|
export function runOpenOf({ firstNamedMs, firstNamedWallMs, startedAt, waitedBefore, pieceSeconds, speed }) {
|
|
24
33
|
if (!Number.isFinite(firstNamedMs) || !(pieceSeconds > 0) || !(speed > 0)) return null;
|
|
25
34
|
const waitedMs = Number.isFinite(firstNamedWallMs) && startedAt > 0 && typeof waitedBefore === "function"
|
|
26
35
|
? waitedBefore(startedAt + firstNamedWallMs)
|
|
27
36
|
: 0;
|
|
28
|
-
|
|
37
|
+
const openSeconds = (firstNamedMs - waitedMs) / 1000 - pieceSeconds / speed;
|
|
38
|
+
return openSeconds >= 0 ? { waitedMs, openSeconds } : null;
|
|
29
39
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { logger } from "../../utils/logger.js";
|
|
12
12
|
import { KeyframeTables } from "../media/KeyframeTables.js";
|
|
13
13
|
import { waits } from "./WaitLedger.js";
|
|
14
|
-
import { readMachineState, readProcessCpuSeconds, readProxyCpuSeconds, readSystemCpu, shareOfMachine } from "../encode/host-load.js";
|
|
14
|
+
import { readEndedChildrenCpuSeconds, readMachineState, readProcessCpuSeconds, readProxyCpuSeconds, readSystemCpu, shareOfMachine } from "../encode/host-load.js";
|
|
15
15
|
import { minimumBufferFrom } from "../torrent/supply-margin.js";
|
|
16
16
|
import { PriorityOrchestrator } from "../viewer/PriorityOrchestrator.js";
|
|
17
17
|
import { urgentOutputsReady } from "../viewer/urgent-output-ready.js";
|
|
@@ -290,6 +290,7 @@ export function wireOutputs({
|
|
|
290
290
|
// What the torrent and the proxy itself spend on this host, and how fast each watched torrent moves.
|
|
291
291
|
parts.hostLoad = new HostLoad({
|
|
292
292
|
readMetadataActivity,
|
|
293
|
+
readEndedChildrenCpuSeconds,
|
|
293
294
|
readMachineState,
|
|
294
295
|
readProcessCpuSeconds,
|
|
295
296
|
readProxyCpuSeconds,
|