@torrent-tv/proxy 2.101.0 → 2.101.2
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/server.js +7 -4
- package/services/encode/ProcessorBudget.js +104 -0
- package/services/encode/quality/HostLoad.js +101 -9
- package/services/server/ViewerRequests.js +24 -6
- package/services/server/wire-outputs.js +25 -8
- package/services/storage/machine-memory.js +8 -7
- package/services/storage/machine-processor.js +395 -0
- package/services/storage/processor-report.js +82 -3
- package/services/viewer/playback-readiness.js +229 -17
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -1151,10 +1151,13 @@ export async function startProxyServer({
|
|
|
1151
1151
|
});
|
|
1152
1152
|
startProcessorReport({
|
|
1153
1153
|
log: (line) => logger.info(line),
|
|
1154
|
-
|
|
1155
|
-
.flatMap((output) => outputParts.encodeRuns.liveRunsOf(output))
|
|
1156
|
-
|
|
1157
|
-
|
|
1154
|
+
ffmpegRuns: () => [...outputParts.outputs.values()]
|
|
1155
|
+
.flatMap((output) => outputParts.encodeRuns.liveRunsOf(output).map((run) => ({
|
|
1156
|
+
pid: run.process?.pid,
|
|
1157
|
+
label: `${output.outputKey} #${run.from}..#${run.to}`,
|
|
1158
|
+
film: Number(run.progress?.processedSeconds) - Number(run.progress?.startPositionSeconds ?? 0)
|
|
1159
|
+
})))
|
|
1160
|
+
.filter((run) => Number.isInteger(run.pid)),
|
|
1158
1161
|
detailed: () => startupProfile.active()
|
|
1159
1162
|
});
|
|
1160
1163
|
// What the last life of this process left on the disk. The kernel kills this
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The one owner of what this machine's processor gives the encoders
|
|
3
|
+
* (torrent-tv/meta#180, phase 2: one owner, results unchanged).
|
|
4
|
+
*
|
|
5
|
+
* Before it, the same two readings reached the processor's decisions by six
|
|
6
|
+
* routes: the share of the machine other work leaves (`HostLoad`'s latest
|
|
7
|
+
* reading) was read separately by the admission, the quality offer, the cost
|
|
8
|
+
* model and the download share, and the contention penalties measured at
|
|
9
|
+
* startup were handed separately to the cost model, the runs and the plan. A
|
|
10
|
+
* reading that changes, or a new one (the processor's bound and measured free
|
|
11
|
+
* amount, `storage/machine-processor.js`), would have had to be threaded to
|
|
12
|
+
* each of them again. Now each asks this object, and the arithmetic stays in
|
|
13
|
+
* the two files that define it (`available-share.js`, `contention.js`).
|
|
14
|
+
*
|
|
15
|
+
* It lives in the encoding component because every question it answers is the
|
|
16
|
+
* encoders', and that component may import nothing outside itself; it is
|
|
17
|
+
* assembled beside the machine budget in `server/wire-outputs.js`. Its unit is
|
|
18
|
+
* today's: shares of the whole machine and seconds of the whole machine per
|
|
19
|
+
* second of film. Nothing here changes an answer.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { correctForAvailability, leftForEncoders } from "./available-share.js";
|
|
23
|
+
import { contentionPenalty } from "./contention.js";
|
|
24
|
+
|
|
25
|
+
export class ProcessorBudget {
|
|
26
|
+
/** @type {() => ({ share: number, forEncoders?: number, known: boolean } | null | undefined)} */
|
|
27
|
+
#reading;
|
|
28
|
+
|
|
29
|
+
/** @type {import("./contention.js").ContentionPenalties | null} */
|
|
30
|
+
#penalties;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {object} params
|
|
34
|
+
* @param {() => ({ share: number, forEncoders?: number, known: boolean } | null | undefined)} params.reading -
|
|
35
|
+
* The latest share of the machine other work leaves, as `HostLoad` keeps it.
|
|
36
|
+
* @param {import("./contention.js").ContentionPenalties | null} [params.penalties] -
|
|
37
|
+
* How much slower one job runs beside others, measured at startup.
|
|
38
|
+
*/
|
|
39
|
+
constructor({ reading, penalties = null }) {
|
|
40
|
+
if (typeof reading !== "function") {
|
|
41
|
+
throw new TypeError("ProcessorBudget requires reading");
|
|
42
|
+
}
|
|
43
|
+
this.#reading = reading;
|
|
44
|
+
this.#penalties = penalties instanceof Map ? penalties : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The latest share of the machine other work leaves, or null before the
|
|
49
|
+
* first reading.
|
|
50
|
+
*
|
|
51
|
+
* @returns {{ share: number, forEncoders?: number, known: boolean } | null}
|
|
52
|
+
*/
|
|
53
|
+
availability() {
|
|
54
|
+
return this.#reading() ?? null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The share of the machine an encoder gets now; 1 where the host does not
|
|
59
|
+
* report its load.
|
|
60
|
+
*
|
|
61
|
+
* @returns {number}
|
|
62
|
+
*/
|
|
63
|
+
left() {
|
|
64
|
+
return leftForEncoders(this.availability());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A speed measured alone on a quiet machine, as it would run now.
|
|
69
|
+
*
|
|
70
|
+
* @param {number} aloneSpeed
|
|
71
|
+
* @returns {number}
|
|
72
|
+
*/
|
|
73
|
+
speedOf(aloneSpeed) {
|
|
74
|
+
return correctForAvailability(aloneSpeed, this.availability());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The measured penalties, as the plan and the runs read them.
|
|
79
|
+
*
|
|
80
|
+
* @returns {import("./contention.js").ContentionPenalties | null}
|
|
81
|
+
*/
|
|
82
|
+
get penalties() {
|
|
83
|
+
return this.#penalties;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Replace the penalties, as a new measurement of them does.
|
|
88
|
+
*
|
|
89
|
+
* @param {import("./contention.js").ContentionPenalties | null} penalties
|
|
90
|
+
*/
|
|
91
|
+
set penalties(penalties) {
|
|
92
|
+
this.#penalties = penalties instanceof Map ? penalties : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* How much slower one job runs with `others` beside it.
|
|
97
|
+
*
|
|
98
|
+
* @param {number} others
|
|
99
|
+
* @returns {{ penalty: number, measured: boolean, from: number }}
|
|
100
|
+
*/
|
|
101
|
+
penaltyFor(others) {
|
|
102
|
+
return contentionPenalty(others, this.#penalties);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -16,6 +16,7 @@ import { baseDrawFrom, costPerMegabyteFrom } from "../torrent-cost.js";
|
|
|
16
16
|
import { medianOf, movedBeyondScatter, READINGS_KEPT, scatterOf } from "../learned-median.js";
|
|
17
17
|
import { ENCODE_RUN_STATE, processCanBeSignalled } from "../encode-run-state.js";
|
|
18
18
|
import { SourceFiles } from "../../media/SourceFile.js";
|
|
19
|
+
import { costKindForSession } from "./EncodeCost.js";
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* What the torrent thread says of itself over one interval: how late its event
|
|
@@ -537,7 +538,8 @@ export class HostLoad {
|
|
|
537
538
|
this.#host.readSystemCpu?.() ?? null,
|
|
538
539
|
this.#host.readEndedChildrenCpuSeconds?.() ?? null
|
|
539
540
|
]);
|
|
540
|
-
const sample = { takenAt: Date.now(), system, endedChildren, byPid: new Map(), proxyCpuSeconds: this.#host.readProxyCpuSeconds()
|
|
541
|
+
const sample = { takenAt: Date.now(), system, endedChildren, byPid: new Map(), proxyCpuSeconds: this.#host.readProxyCpuSeconds(),
|
|
542
|
+
processor: this.#host.sampleProcessor?.() ?? null };
|
|
541
543
|
const previous = this.#idleAvailabilitySample;
|
|
542
544
|
this.#idleAvailabilitySample = sample;
|
|
543
545
|
if (previous === null) {
|
|
@@ -556,7 +558,7 @@ export class HostLoad {
|
|
|
556
558
|
// stands is seen beside what it is free for (torrent-tv/meta#178); a proxy
|
|
557
559
|
// nobody watches stays quiet.
|
|
558
560
|
if (share !== null && watched) {
|
|
559
|
-
await this.#sayMachine(share, proxyShare, 0, suspended);
|
|
561
|
+
await this.#sayMachine(share, proxyShare, 0, suspended, false, this.#processorLine(previous, sample, share, proxyShare));
|
|
560
562
|
}
|
|
561
563
|
}
|
|
562
564
|
|
|
@@ -566,7 +568,7 @@ export class HostLoad {
|
|
|
566
568
|
* and the processor share divide; `free` is what nothing used over the
|
|
567
569
|
* interval. Unknown stays `n/a`, never zero.
|
|
568
570
|
*/
|
|
569
|
-
async #sayMachine(share, proxyShare, running, suspended, incomplete = false) {
|
|
571
|
+
async #sayMachine(share, proxyShare, running, suspended, incomplete = false, processor = "") {
|
|
570
572
|
const machine = await this.#host.readMachineState();
|
|
571
573
|
// HOW LATE THIS THREAD'S LOOP RAN over the interval: every answer to FFmpeg
|
|
572
574
|
// and every hand-over of a copy waits for it (torrent-tv/meta#178). Begun at
|
|
@@ -591,10 +593,74 @@ export class HostLoad {
|
|
|
591
593
|
// What the offer will multiply a prediction by, in the same line as the
|
|
592
594
|
// readings it comes from.
|
|
593
595
|
` available=${asPercent(this.hostAvailability.share)} for-encoders=${asPercent(this.hostAvailability.forEncoders)}` +
|
|
594
|
-
` free=${asPercent(free)}` + late
|
|
596
|
+
` free=${asPercent(free)}` + processor + late
|
|
595
597
|
);
|
|
596
598
|
}
|
|
597
599
|
|
|
600
|
+
/**
|
|
601
|
+
* The processor's upper bound and its measured free amount over the same
|
|
602
|
+
* interval, for the machine's line (torrent-tv/meta#180, phase 1). Read
|
|
603
|
+
* beside today's figures and used by no decision; empty where the host does
|
|
604
|
+
* not take these readings.
|
|
605
|
+
*
|
|
606
|
+
* @param {{ processor?: object | null }} previous
|
|
607
|
+
* @param {{ processor?: object | null }} sample
|
|
608
|
+
* @param {{ processShare: number | null, systemShare: number | null }} share
|
|
609
|
+
* @param {number | null} proxyShare
|
|
610
|
+
* @returns {string}
|
|
611
|
+
*/
|
|
612
|
+
#processorLine(previous, sample, share, proxyShare) {
|
|
613
|
+
if (typeof this.#host.processorBetween !== "function" || typeof this.#host.describeProcessor !== "function") {
|
|
614
|
+
return "";
|
|
615
|
+
}
|
|
616
|
+
const ourShare = Number.isFinite(proxyShare) && Number.isFinite(share.processShare) ? proxyShare + share.processShare : null;
|
|
617
|
+
return this.#host.describeProcessor(this.#host.processorBetween(previous.processor, sample.processor, {
|
|
618
|
+
systemShare: share.systemShare,
|
|
619
|
+
ourShare,
|
|
620
|
+
hostCores: Math.max(1, os.cpus().length)
|
|
621
|
+
}));
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* What each run's FFmpeg spent per second of film over the interval between
|
|
626
|
+
* two readings, with the runs beside it (torrent-tv/meta#180, phase 1).
|
|
627
|
+
*
|
|
628
|
+
* In CPU seconds per second of film (`k`), which is not the unit of
|
|
629
|
+
* `EncodeCost` (seconds of the whole machine per second of film, from
|
|
630
|
+
* speed): a run that uses all it is given has `cost = k / cores`. A `k` read
|
|
631
|
+
* beside other runs already carries their contention, so it is said with
|
|
632
|
+
* them; a run is alone only when no other run was alive at either reading.
|
|
633
|
+
* Only runs read at both ends that made film in between are said. For the
|
|
634
|
+
* log only.
|
|
635
|
+
*
|
|
636
|
+
* @param {{ takenAt: number, byPid: Map<number, number>, runs?: Map<number, { film: number | null, label: string, kind: string }> }} previous
|
|
637
|
+
* @param {{ takenAt: number, byPid: Map<number, number>, runs?: Map<number, { film: number | null, label: string, kind: string }> }} sample
|
|
638
|
+
*/
|
|
639
|
+
#sayRunCosts(previous, sample) {
|
|
640
|
+
if (!previous.runs || !sample.runs) return;
|
|
641
|
+
const elapsedSec = (sample.takenAt - previous.takenAt) / 1000;
|
|
642
|
+
if (!(elapsedSec > 0)) return;
|
|
643
|
+
const alive = new Map([...previous.runs, ...sample.runs]);
|
|
644
|
+
const said = [];
|
|
645
|
+
for (const [pid, run] of sample.runs) {
|
|
646
|
+
const before = previous.runs.get(pid);
|
|
647
|
+
const cpuNow = sample.byPid.get(pid);
|
|
648
|
+
const cpuBefore = previous.byPid.get(pid);
|
|
649
|
+
if (!before || !Number.isFinite(cpuNow) || !Number.isFinite(cpuBefore) || cpuNow < cpuBefore) continue;
|
|
650
|
+
if (!Number.isFinite(run.film) || !Number.isFinite(before.film)) continue;
|
|
651
|
+
const film = run.film - before.film;
|
|
652
|
+
if (!(film > 0)) continue;
|
|
653
|
+
const cpu = cpuNow - cpuBefore;
|
|
654
|
+
const beside = [...alive].filter(([other]) => other !== pid).map(([, other]) => other.kind);
|
|
655
|
+
said.push(`${run.label} ${run.kind} k=${(cpu / film).toFixed(3)} CPU-s per film-s ` +
|
|
656
|
+
`(${cpu.toFixed(2)} CPU-s, ${film.toFixed(2)} film-s, speed ${(film / elapsedSec).toFixed(2)}x, ` +
|
|
657
|
+
`${(cpu / elapsedSec).toFixed(2)} cores), beside ${beside.length}` + (beside.length > 0 ? ` [${beside.join(", ")}]` : ""));
|
|
658
|
+
}
|
|
659
|
+
if (said.length > 0) {
|
|
660
|
+
logger.info(`processor: run cost over ${elapsedSec.toFixed(1)}s: ${said.join("; ")}`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
598
664
|
/**
|
|
599
665
|
* @param {{ elapsedSec: number, processShare: number | null, systemShare: number | null, rejected?: string[] } | null} share
|
|
600
666
|
* @param {{ proxyCpuSeconds: number | null }} previous
|
|
@@ -658,9 +724,17 @@ export class HostLoad {
|
|
|
658
724
|
// that ends meanwhile drops its process, and read after the await it threw
|
|
659
725
|
// `Cannot read properties of null (reading 'pid')`, seven times in one
|
|
660
726
|
// field session, each losing that reading (r64, torrent-tv/meta#178).
|
|
661
|
-
|
|
727
|
+
// With the film each has made and what it is, for its cost over the
|
|
728
|
+
// interval (torrent-tv/meta#180).
|
|
729
|
+
const runs = encoding.flatMap((session) => this.#host.liveRunsOf(session)
|
|
662
730
|
.filter((run) => Number.isInteger(run.process?.pid))
|
|
663
|
-
.map((run) => ({
|
|
731
|
+
.map((run) => ({
|
|
732
|
+
pid: run.process.pid,
|
|
733
|
+
startedAt: run.startedAt,
|
|
734
|
+
film: filmMadeBy(run),
|
|
735
|
+
label: `${session.outputKey ?? session.id} #${run.from}..#${run.to}`,
|
|
736
|
+
kind: costKindForSession(session)
|
|
737
|
+
})));
|
|
664
738
|
const [system, endedChildren, ...cpuReadings] = await Promise.all([
|
|
665
739
|
this.#host.readSystemCpu(),
|
|
666
740
|
this.#host.readEndedChildrenCpuSeconds?.() ?? null,
|
|
@@ -670,12 +744,15 @@ export class HostLoad {
|
|
|
670
744
|
const byPid = new Map();
|
|
671
745
|
/** @type {Map<number, number>} */
|
|
672
746
|
const startedAt = new Map();
|
|
747
|
+
/** @type {Map<number, { film: number | null, label: string, kind: string }>} */
|
|
748
|
+
const runsByPid = new Map();
|
|
673
749
|
runs.forEach((run, index) => {
|
|
674
750
|
const seconds = cpuReadings[index];
|
|
675
751
|
if (seconds !== null) {
|
|
676
752
|
byPid.set(run.pid, seconds);
|
|
677
753
|
startedAt.set(run.pid, run.startedAt);
|
|
678
754
|
}
|
|
755
|
+
runsByPid.set(run.pid, { film: run.film, label: run.label, kind: run.kind });
|
|
679
756
|
});
|
|
680
757
|
const sample = {
|
|
681
758
|
takenAt: Date.now(),
|
|
@@ -687,7 +764,9 @@ export class HostLoad {
|
|
|
687
764
|
// piece store and the delivery. None of it is in the encode budget, and
|
|
688
765
|
// on the addon host it is most of what the machine does while encoders
|
|
689
766
|
// are suspended.
|
|
690
|
-
proxyCpuSeconds: this.#host.readProxyCpuSeconds()
|
|
767
|
+
proxyCpuSeconds: this.#host.readProxyCpuSeconds(),
|
|
768
|
+
runs: runsByPid,
|
|
769
|
+
processor: this.#host.sampleProcessor?.() ?? null
|
|
691
770
|
};
|
|
692
771
|
const previous = this.#hostLoadSample;
|
|
693
772
|
this.#hostLoadSample = sample;
|
|
@@ -699,6 +778,7 @@ export class HostLoad {
|
|
|
699
778
|
// `/proc` there are no readings at all, and the share is null rather than a
|
|
700
779
|
// confident zero.
|
|
701
780
|
const encoders = encoderCpuBetween(previous, sample);
|
|
781
|
+
this.#sayRunCosts(previous, sample);
|
|
702
782
|
const share = this.#host.shareOfMachine(
|
|
703
783
|
{ takenAt: previous.takenAt, processCpuSeconds: encoders === null ? null : 0, system: previous.system },
|
|
704
784
|
{ takenAt: sample.takenAt, processCpuSeconds: encoders?.seconds ?? null, system: sample.system }
|
|
@@ -718,7 +798,7 @@ export class HostLoad {
|
|
|
718
798
|
if (encoders && !encoders.complete) {
|
|
719
799
|
// A lower bound of our own load is not a reading of what other work left
|
|
720
800
|
// us: the last availability stands, and the line says so.
|
|
721
|
-
await this.#sayMachine(share, null, running, suspended, true);
|
|
801
|
+
await this.#sayMachine(share, null, running, suspended, true, this.#processorLine(previous, sample, share, null));
|
|
722
802
|
return false;
|
|
723
803
|
}
|
|
724
804
|
// Kept for the quality offer, which predicts from a benchmark taken on a
|
|
@@ -726,7 +806,7 @@ export class HostLoad {
|
|
|
726
806
|
// machine a new encoder could actually have. Only what nobody has been
|
|
727
807
|
// charged for is subtracted — see `encode/available-share.js`.
|
|
728
808
|
const proxyShare = this.#setAvailability(share, previous, sample);
|
|
729
|
-
await this.#sayMachine(share, proxyShare, running, suspended);
|
|
809
|
+
await this.#sayMachine(share, proxyShare, running, suspended, false, this.#processorLine(previous, sample, share, proxyShare));
|
|
730
810
|
return false;
|
|
731
811
|
}
|
|
732
812
|
|
|
@@ -756,3 +836,15 @@ export class HostLoad {
|
|
|
756
836
|
: 0;
|
|
757
837
|
}
|
|
758
838
|
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* The film a run has made since it started, from its progress, or null before
|
|
842
|
+
* it has said any.
|
|
843
|
+
*
|
|
844
|
+
* @param {{ progress?: { processedSeconds?: number, startPositionSeconds?: number } }} run
|
|
845
|
+
* @returns {number | null}
|
|
846
|
+
*/
|
|
847
|
+
function filmMadeBy(run) {
|
|
848
|
+
const made = Number(run.progress?.processedSeconds) - Number(run.progress?.startPositionSeconds ?? 0);
|
|
849
|
+
return Number.isFinite(made) && made >= 0 ? made : null;
|
|
850
|
+
}
|
|
@@ -658,6 +658,7 @@ export class ViewerRequests {
|
|
|
658
658
|
fileLength: stats?.fileLength,
|
|
659
659
|
pieceLength: stats?.pieceLength,
|
|
660
660
|
downloadRateReadings: this.#sourceRateReadings(measurement, output.file.sourceKey, stats, now),
|
|
661
|
+
downloadRateSummaries: this.#sourceRateSummaries(measurement, output.file.sourceKey),
|
|
661
662
|
downloadForecast,
|
|
662
663
|
});
|
|
663
664
|
}
|
|
@@ -774,11 +775,14 @@ export class ViewerRequests {
|
|
|
774
775
|
bufferLimitSeconds: input.bufferLimitSeconds,
|
|
775
776
|
lookaheadSeconds: input.lookaheadSeconds,
|
|
776
777
|
link: input.linkReadings,
|
|
777
|
-
sources: input.sources.map(({ id, complete, residence, downloadForecast, downloadRateReadings
|
|
778
|
+
sources: input.sources.map(({ id, complete, residence, downloadForecast, downloadRateReadings,
|
|
779
|
+
downloadRateSummaries }) => ({
|
|
778
780
|
id, complete, residenceRanges: residence?.length ?? 0,
|
|
779
781
|
forecastAt: downloadForecast?.measuredAt ?? null, plannedPieces: downloadForecast?.ranges.length ?? 0,
|
|
780
782
|
unknownPieces: downloadForecast?.ranges.filter(range => range.availableAt === null).length ?? 0,
|
|
781
|
-
|
|
783
|
+
// The rate the forecast reads, and both summaries it was chosen from.
|
|
784
|
+
downloadRateBytesPerSecond: this.#host.playbackReadiness.forecastRate(downloadRateReadings, now, 0, "time"),
|
|
785
|
+
downloadRateSummaries
|
|
782
786
|
})),
|
|
783
787
|
tracks: tracks.map(({ id, processedSeconds, readings, segments, readySegmentIndices }) => ({
|
|
784
788
|
id, processedSeconds, readings, segments: segments.length, prepared: readySegmentIndices.length
|
|
@@ -818,7 +822,7 @@ export class ViewerRequests {
|
|
|
818
822
|
const rates = (forecast.operations ?? []).map(operation =>
|
|
819
823
|
`${operation.track} ${operation.operation} ${Number.isFinite(operation.speed) ? operation.speed.toFixed(2) : "?"}x`);
|
|
820
824
|
const downloads = (input.sources ?? []).map(source => {
|
|
821
|
-
const rate = source.downloadRateReadings
|
|
825
|
+
const rate = this.#host.playbackReadiness.forecastRate(source.downloadRateReadings, now, 0, "time");
|
|
822
826
|
return `${Number.isFinite(rate) ? (rate / 1_048_576).toFixed(2) : "?"} MB/s`;
|
|
823
827
|
});
|
|
824
828
|
logger.info(`playback held ${session.id} consumer=${consumerId}: delay ${seconds(delay)}s by ` +
|
|
@@ -949,7 +953,14 @@ export class ViewerRequests {
|
|
|
949
953
|
}
|
|
950
954
|
// Totals that went back belong to a torrent added again: read on from them.
|
|
951
955
|
if (limit) source.limit = limit;
|
|
952
|
-
return sourceService(source.free.snapshot(), source.delivered.snapshot()
|
|
956
|
+
return sourceService(source.free.snapshot(), source.delivered.snapshot(),
|
|
957
|
+
(summary) => this.#host.playbackReadiness.forecastRate(summary, now, 0, "time"));
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/** Both summaries a source's service was chosen from, for the forecast's log line. */
|
|
961
|
+
#sourceRateSummaries(state, id) {
|
|
962
|
+
const source = state.sources.get(id);
|
|
963
|
+
return source ? { free: source.free.snapshot(), delivered: source.delivered.snapshot() } : null;
|
|
953
964
|
}
|
|
954
965
|
}
|
|
955
966
|
|
|
@@ -962,12 +973,19 @@ export class ViewerRequests {
|
|
|
962
973
|
* downloaded 5-18 MB/s, answered "service not advancing", and held the viewer
|
|
963
974
|
* (r54, torrent-tv/meta#178). What the swarm did carry, it can carry.
|
|
964
975
|
*
|
|
976
|
+
* The two are compared by the rate the forecast reads from them, bytes over
|
|
977
|
+
* the span measured. Compared by their last readings, a summary whose last
|
|
978
|
+
* reading was 10.7 MB/s and whose span read 0 B/s won over 3.4-16.7 MB/s
|
|
979
|
+
* carried, and the forecast held the viewer with no delay to state (r74,
|
|
980
|
+
* torrent-tv/meta#185).
|
|
981
|
+
*
|
|
965
982
|
* @param {object | null} free - A rate summary (`RateTrend.snapshot`).
|
|
966
983
|
* @param {object | null} delivered
|
|
984
|
+
* @param {(summary: object) => number | null} rateOf - The rate the forecast reads from a summary.
|
|
967
985
|
* @returns {object | null}
|
|
968
986
|
*/
|
|
969
|
-
export function sourceService(free, delivered) {
|
|
987
|
+
export function sourceService(free, delivered, rateOf) {
|
|
970
988
|
if (!free) return delivered;
|
|
971
989
|
if (!delivered) return free;
|
|
972
|
-
return delivered
|
|
990
|
+
return (rateOf(delivered) ?? 0) > (rateOf(free) ?? 0) ? delivered : free;
|
|
973
991
|
}
|
|
@@ -101,6 +101,8 @@ import { contentOf, LocalObservations, PROXY_ROOT } from "../encode/LocalObserva
|
|
|
101
101
|
import { configurationKeyOf } from "../encode/fingerprint.js";
|
|
102
102
|
import { qualityStateOf } from "../encode/quality/OutputQualityState.js";
|
|
103
103
|
import { downloadShare } from "../storage/processor-share.js";
|
|
104
|
+
import { describeProcessor, processorBetween, sampleProcessor } from "../storage/machine-processor.js";
|
|
105
|
+
import { ProcessorBudget } from "../encode/ProcessorBudget.js";
|
|
104
106
|
import { ENCODE_RUN_STATE, processCanBeSignalled } from "../encode/encode-run-state.js";
|
|
105
107
|
import { secondsUntilClosed } from "../encode/piece-close-time.js";
|
|
106
108
|
|
|
@@ -303,6 +305,11 @@ export function wireOutputs({
|
|
|
303
305
|
readProxyCpuSeconds,
|
|
304
306
|
readSystemCpu,
|
|
305
307
|
shareOfMachine,
|
|
308
|
+
// The processor's upper bound and measured free amount, said beside the
|
|
309
|
+
// machine's line and used by no decision yet (torrent-tv/meta#180).
|
|
310
|
+
sampleProcessor: () => sampleProcessor(),
|
|
311
|
+
processorBetween,
|
|
312
|
+
describeProcessor,
|
|
306
313
|
liveRunsOf: (...args) => parts.encodeRuns.liveRunsOf(...args),
|
|
307
314
|
runStateOf: (...args) => parts.encodeRuns.runStateOf(...args),
|
|
308
315
|
get getSourceStats() { return parts.getSourceStats; },
|
|
@@ -344,7 +351,7 @@ export function wireOutputs({
|
|
|
344
351
|
producedNumbers: (...args) => parts.serving.producedNumbers(...args),
|
|
345
352
|
servesAudioSeparately: (...args) => parts.renditions.servesAudioSeparately(...args),
|
|
346
353
|
disposeSession: (...args) => parts.lifecycle.disposeSession(...args),
|
|
347
|
-
get contentionPenalties() { return parts.
|
|
354
|
+
get contentionPenalties() { return parts.processor?.penalties ?? null; },
|
|
348
355
|
get encodeCost() { return parts.encodeCost; },
|
|
349
356
|
get encodeOrchestrator() { return parts.encodeOrchestrator; },
|
|
350
357
|
get ffmpegBin() { return parts.ffmpegBin; },
|
|
@@ -730,7 +737,7 @@ export function wireOutputs({
|
|
|
730
737
|
files.set(key, file);
|
|
731
738
|
}
|
|
732
739
|
const watched = [...files.values()].filter((file) => Number.isFinite(file.bytesPerFilmSecond) && file.bytesPerFilmSecond > 0);
|
|
733
|
-
const share = downloadShare({ availableShare: parts.
|
|
740
|
+
const share = downloadShare({ availableShare: parts.processor?.availability()?.share ?? 1,
|
|
734
741
|
baseDraw: parts.hostLoad.baseDraw, costPerMegabyte: parts.hostLoad.observedTorrentCostPerMegabyte, files: watched });
|
|
735
742
|
// Kept beside the readings it is judged against: what the torrents moved
|
|
736
743
|
// over each interval is said next to the limit in force (meta#178).
|
|
@@ -738,7 +745,17 @@ export function wireOutputs({
|
|
|
738
745
|
void Promise.resolve(parts.setDownloadRate(share.bytesPerSecond, `film speed ${share.filmSpeed.toFixed(2)}x ` +
|
|
739
746
|
`over ${watched.length} file(s), ${Math.round(share.capacity * 100)}% of the machine for the proxy`)).catch(() => {});
|
|
740
747
|
};
|
|
741
|
-
|
|
748
|
+
// The one owner of what the processor gives the encoders: the share of the
|
|
749
|
+
// machine other work leaves and the measured contention penalties, read by
|
|
750
|
+
// every place that decides on the processor (torrent-tv/meta#180).
|
|
751
|
+
parts.processor = new ProcessorBudget({ reading: () => parts.hostLoad.hostAvailability, penalties: contentionPenalties });
|
|
752
|
+
// Kept as the name the penalties were always set and read by, now the budget's.
|
|
753
|
+
Object.defineProperty(parts, "contentionPenalties", {
|
|
754
|
+
get: () => parts.processor.penalties,
|
|
755
|
+
set: (penalties) => { parts.processor.penalties = penalties; },
|
|
756
|
+
enumerable: true,
|
|
757
|
+
configurable: true
|
|
758
|
+
});
|
|
742
759
|
// Seconds of film per second when the picture is COPIED, measured at
|
|
743
760
|
// startup. Nothing else prices that branch: the other benchmarks measure
|
|
744
761
|
// encoding and decoding, and a copy does neither.
|
|
@@ -907,11 +924,11 @@ export function wireOutputs({
|
|
|
907
924
|
host: () => ({
|
|
908
925
|
benchmark: parts.softwarePresetBenchmark,
|
|
909
926
|
decodeModel: parts.decodeCostModel,
|
|
910
|
-
contentionPenalties: parts.
|
|
927
|
+
contentionPenalties: parts.processor.penalties,
|
|
911
928
|
copySpeedX: parts.copySpeedX,
|
|
912
929
|
audioCalibration,
|
|
913
930
|
audioDescriptionOf,
|
|
914
|
-
availability: parts.
|
|
931
|
+
availability: parts.processor.availability() ?? undefined,
|
|
915
932
|
// WHICH encoder this host settled on. Only the software ladder is
|
|
916
933
|
// benchmarked, so a reading taken off a hardware encoder cannot be
|
|
917
934
|
// split into its decode and encode halves and is not filed as one.
|
|
@@ -951,7 +968,7 @@ export function wireOutputs({
|
|
|
951
968
|
encodeCost: parts.encodeCost,
|
|
952
969
|
occupiedCostSec: (fileKey) => parts.admission?.occupiedCost({ exceptFileKey: fileKey }).costSec ?? 0,
|
|
953
970
|
occupancyKnownFor: (fileKey) => (parts.admission?.occupiedCost({ exceptFileKey: fileKey }).unpriced ?? 0) === 0,
|
|
954
|
-
availability: () => parts.
|
|
971
|
+
availability: () => parts.processor.availability(),
|
|
955
972
|
outputs: parts.outputs,
|
|
956
973
|
stateFor: (session) => parts.encodeRuns.runStateOf(session),
|
|
957
974
|
// WHICH HEIGHTS A LIVE VIEWER HAS ON SCREEN, as numbers. Who is watching
|
|
@@ -1045,7 +1062,7 @@ export function wireOutputs({
|
|
|
1045
1062
|
finished: (address) => parts.outputs.outputsOn(address).some((session) => finishedOutput(session)),
|
|
1046
1063
|
loadOf: (address) => parts.encodeCost.loadOfOutput(address),
|
|
1047
1064
|
loadForCandidate: (spec, file) => parts.encodeCost.loadForCandidate(spec, file),
|
|
1048
|
-
availability: () => parts.
|
|
1065
|
+
availability: () => parts.processor.availability()
|
|
1049
1066
|
});
|
|
1050
1067
|
parts.encodeOrchestrator = new EncodeOrchestrator({
|
|
1051
1068
|
inputDemandChanged: (address, windows) => {
|
|
@@ -1061,7 +1078,7 @@ export function wireOutputs({
|
|
|
1061
1078
|
maxRunsFor: (address) => parts.encodeRuns.maxRunsForOutput(address),
|
|
1062
1079
|
makeRun: ({ address, from, to, because }) => parts.encodeRuns.makeRunAt(address, from, to, because),
|
|
1063
1080
|
segmentSeconds: parts.segmentDurationSec,
|
|
1064
|
-
contentionPenalties: parts.
|
|
1081
|
+
contentionPenalties: parts.processor.penalties,
|
|
1065
1082
|
startingSpeedFor: (address) => parts.encodeCost.speedForOutput(address),
|
|
1066
1083
|
segmentCoverage,
|
|
1067
1084
|
// HOW IT ASKS TO DECIDE AGAIN. A plan that refuses to place anything
|
|
@@ -163,7 +163,7 @@ function hostAvailable(readFile) {
|
|
|
163
163
|
function containerRoom(readFile, hostTotal) {
|
|
164
164
|
let membership;
|
|
165
165
|
try {
|
|
166
|
-
membership =
|
|
166
|
+
membership = cgroupOf(readFile("/proc/self/cgroup"), "memory");
|
|
167
167
|
} catch {
|
|
168
168
|
return null; // silent-ok: not Linux, or /proc is not readable.
|
|
169
169
|
}
|
|
@@ -187,18 +187,19 @@ function containerRoom(readFile, hostTotal) {
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
/**
|
|
190
|
-
* Which cgroup this process is in, for
|
|
190
|
+
* Which cgroup this process is in, for one controller (`memory`, `cpu`).
|
|
191
191
|
*
|
|
192
|
-
* A host in hybrid mode lists both versions;
|
|
193
|
-
*
|
|
192
|
+
* A host in hybrid mode lists both versions; a controller mounted on version 1
|
|
193
|
+
* is named in the line that lists it, and only then is version 2 the answer.
|
|
194
194
|
*
|
|
195
195
|
* @param {string} text - `/proc/self/cgroup`.
|
|
196
|
+
* @param {string} controller
|
|
196
197
|
* @returns {{ version: 1 | 2, path: string } | null}
|
|
197
198
|
*/
|
|
198
|
-
function
|
|
199
|
+
export function cgroupOf(text, controller) {
|
|
199
200
|
for (const line of text.split("\n")) {
|
|
200
201
|
const match = /^\d+:([^:]*):(.*)$/.exec(line);
|
|
201
|
-
if (match && match[1].split(",").includes(
|
|
202
|
+
if (match && match[1].split(",").includes(controller)) {
|
|
202
203
|
return { version: 1, path: match[2] };
|
|
203
204
|
}
|
|
204
205
|
}
|
|
@@ -219,7 +220,7 @@ function ownCgroup(text) {
|
|
|
219
220
|
* @param {string} path
|
|
220
221
|
* @returns {string[]}
|
|
221
222
|
*/
|
|
222
|
-
function levelsOf(mount, path) {
|
|
223
|
+
export function levelsOf(mount, path) {
|
|
223
224
|
const parts = path.split("/").filter(Boolean);
|
|
224
225
|
if (parts.includes("..")) {
|
|
225
226
|
return [];
|