@torrent-tv/proxy 2.107.49 → 2.107.50
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/{server/WaitLedger.js → encode/OutputWaitLedger.js} +37 -7
- package/services/encode/output/OutputCatalog.js +46 -2
- package/services/server/OutputLifecycle.js +2 -1
- package/services/server/SegmentServing.js +14 -0
- package/services/server/transcode-session-files.js +9 -4
- package/services/server/wire-outputs.js +11 -2
package/package.json
CHANGED
|
@@ -58,6 +58,23 @@ export class WaitLedger {
|
|
|
58
58
|
/** @type {Map<string, Map<string, number>>} */
|
|
59
59
|
#counts = new Map();
|
|
60
60
|
|
|
61
|
+
/** @type {Map<string, object>} */
|
|
62
|
+
#owners = new Map();
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Begin a live output-key lifetime. Reopening a key starts a clean history;
|
|
66
|
+
* the returned identity lets a held response prove that it still belongs to
|
|
67
|
+
* this lifetime when it eventually completes.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} key
|
|
70
|
+
* @param {object} identity
|
|
71
|
+
*/
|
|
72
|
+
open(key, identity) {
|
|
73
|
+
if (!key || !identity || this.#owners.get(key) === identity) return;
|
|
74
|
+
this.forget(key);
|
|
75
|
+
this.#owners.set(key, identity);
|
|
76
|
+
}
|
|
77
|
+
|
|
61
78
|
/**
|
|
62
79
|
* Somebody waited this long for something the map ranked this highly.
|
|
63
80
|
*
|
|
@@ -66,11 +83,13 @@ export class WaitLedger {
|
|
|
66
83
|
* @param {number} rank - The map's rank for the thing waited for.
|
|
67
84
|
* @param {number} topRank - The highest rank the map states, so the rank can
|
|
68
85
|
* be read as a position rather than as an absolute number.
|
|
86
|
+
* @param {object} [identity] - The output-key lifetime that started this wait.
|
|
69
87
|
*/
|
|
70
|
-
note(key, waitedMs, rank, topRank) {
|
|
88
|
+
note(key, waitedMs, rank, topRank, identity = undefined) {
|
|
71
89
|
if (!key || !Number.isFinite(waitedMs) || waitedMs < 0) {
|
|
72
90
|
return;
|
|
73
91
|
}
|
|
92
|
+
if (identity !== undefined && this.#owners.get(key) !== identity) return;
|
|
74
93
|
const band = bandOf(rank, topRank);
|
|
75
94
|
let byBand = this.#waits.get(key);
|
|
76
95
|
if (!byBand) {
|
|
@@ -128,15 +147,26 @@ export class WaitLedger {
|
|
|
128
147
|
|
|
129
148
|
/**
|
|
130
149
|
* @param {string} key
|
|
150
|
+
* @param {object} [identity] - Ignore release from an older output lifetime.
|
|
131
151
|
*/
|
|
132
|
-
forget(key) {
|
|
152
|
+
forget(key, identity = undefined) {
|
|
153
|
+
if (identity !== undefined && this.#owners.get(key) !== identity) return false;
|
|
133
154
|
this.#waits.delete(key);
|
|
134
155
|
this.#counts.delete(key);
|
|
156
|
+
if (identity === undefined || this.#owners.get(key) === identity) this.#owners.delete(key);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Release every record owned by this proxy before its shutdown completes. */
|
|
161
|
+
clear() {
|
|
162
|
+
this.#waits.clear();
|
|
163
|
+
this.#counts.clear();
|
|
164
|
+
this.#owners.clear();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Number of output keys with retained measurements. */
|
|
168
|
+
get size() {
|
|
169
|
+
return this.#waits.size;
|
|
135
170
|
}
|
|
136
171
|
}
|
|
137
172
|
|
|
138
|
-
/**
|
|
139
|
-
* One ledger for the process, because the question is asked in two layers and
|
|
140
|
-
* the answer is comparable only if the scale is the same.
|
|
141
|
-
*/
|
|
142
|
-
export const waits = new WaitLedger();
|
|
@@ -26,7 +26,11 @@ export class OutputCatalog {
|
|
|
26
26
|
|
|
27
27
|
#byId = new Map();
|
|
28
28
|
#lifetime = new Map();
|
|
29
|
+
/** @type {Map<string, object>} */
|
|
30
|
+
#waitIdentities = new Map();
|
|
29
31
|
#now;
|
|
32
|
+
#waitIdentityStarted;
|
|
33
|
+
#waitIdentityEnded;
|
|
30
34
|
|
|
31
35
|
/**
|
|
32
36
|
* @param {object} [params]
|
|
@@ -35,11 +39,15 @@ export class OutputCatalog {
|
|
|
35
39
|
* source file is, which the torrent reports.
|
|
36
40
|
* @param {(address: string) => { index: number, size: number }} [params.largestPieceOf]
|
|
37
41
|
* The biggest piece an output has made, which the disk knows.
|
|
42
|
+
* @param {(address: string, identity: object) => void} [params.waitIdentityStarted]
|
|
43
|
+
* @param {(address: string, identity: object) => void} [params.waitIdentityEnded]
|
|
38
44
|
*/
|
|
39
45
|
constructor({
|
|
40
46
|
now = Date.now,
|
|
41
47
|
fileLengthOf = () => 0,
|
|
42
|
-
largestPieceOf = () => ({ index: -1, size: 0 })
|
|
48
|
+
largestPieceOf = () => ({ index: -1, size: 0 }),
|
|
49
|
+
waitIdentityStarted = () => {},
|
|
50
|
+
waitIdentityEnded = () => {}
|
|
43
51
|
} = {}) {
|
|
44
52
|
this.#now = now;
|
|
45
53
|
// Two facts this catalog needs and does not own, taken as plain functions
|
|
@@ -48,15 +56,22 @@ export class OutputCatalog {
|
|
|
48
56
|
// it is part of the output itself (`OutputSpec`).
|
|
49
57
|
this.fileLengthOf = fileLengthOf;
|
|
50
58
|
this.largestPieceOf = largestPieceOf;
|
|
59
|
+
this.#waitIdentityStarted = waitIdentityStarted;
|
|
60
|
+
this.#waitIdentityEnded = waitIdentityEnded;
|
|
51
61
|
}
|
|
52
62
|
|
|
53
63
|
set(id, output) {
|
|
54
64
|
const known = this.#byId.get(id);
|
|
65
|
+
const oldAddress = known?.outputKey;
|
|
55
66
|
this.#byId.set(id, output);
|
|
56
67
|
if (known !== output) {
|
|
57
68
|
const at = this.#now();
|
|
58
69
|
this.#lifetime.set(id, { startedAt: at, lastAccessedAt: at });
|
|
59
70
|
}
|
|
71
|
+
if (known !== output && oldAddress !== output?.outputKey) {
|
|
72
|
+
this.#forgetWaitIdentityIfUnused(oldAddress);
|
|
73
|
+
}
|
|
74
|
+
this.#openWaitIdentity(output?.outputKey);
|
|
60
75
|
return this;
|
|
61
76
|
}
|
|
62
77
|
|
|
@@ -69,13 +84,42 @@ export class OutputCatalog {
|
|
|
69
84
|
}
|
|
70
85
|
|
|
71
86
|
delete(id) {
|
|
87
|
+
const output = this.#byId.get(id);
|
|
72
88
|
this.#lifetime.delete(id);
|
|
73
|
-
|
|
89
|
+
const deleted = this.#byId.delete(id);
|
|
90
|
+
if (deleted) this.#forgetWaitIdentityIfUnused(output?.outputKey);
|
|
91
|
+
return deleted;
|
|
74
92
|
}
|
|
75
93
|
|
|
76
94
|
clear() {
|
|
95
|
+
const addresses = new Set([...this.#byId.values()].map(output => output?.outputKey).filter(Boolean));
|
|
77
96
|
this.#lifetime.clear();
|
|
78
97
|
this.#byId.clear();
|
|
98
|
+
for (const address of addresses) this.#forgetWaitIdentityIfUnused(address);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The shared active lifetime for the output addressed by a live session. */
|
|
102
|
+
waitIdentity(id) {
|
|
103
|
+
const output = this.#byId.get(id);
|
|
104
|
+
return output ? this.#waitIdentities.get(output.outputKey) ?? null : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#openWaitIdentity(address) {
|
|
108
|
+
if (typeof address !== "string" || !address || this.#waitIdentities.has(address)) return;
|
|
109
|
+
const identity = {};
|
|
110
|
+
this.#waitIdentities.set(address, identity);
|
|
111
|
+
this.#waitIdentityStarted(address, identity);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#forgetWaitIdentityIfUnused(address) {
|
|
115
|
+
if (typeof address !== "string" || !address) return;
|
|
116
|
+
for (const output of this.#byId.values()) {
|
|
117
|
+
if (output?.outputKey === address) return;
|
|
118
|
+
}
|
|
119
|
+
const identity = this.#waitIdentities.get(address);
|
|
120
|
+
if (!identity) return;
|
|
121
|
+
this.#waitIdentities.delete(address);
|
|
122
|
+
this.#waitIdentityEnded(address, identity);
|
|
79
123
|
}
|
|
80
124
|
|
|
81
125
|
touch(outputOrId, at = this.#now()) {
|
|
@@ -55,7 +55,7 @@ export class OutputLifecycle {
|
|
|
55
55
|
#host;
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
-
* @param {object} host - `retention`, `outputNeeded`, `outputWriting`, `outputReading`, `planEncodersSoon`, `now`, `invalidateWaits`, `segmentFormatOfKey`, `viewerSegmentsOn`, `budgetTimer`, `cleanupTimer`, `encodeRuns`, `keyframeTables`, `machineBudget`, `outputTimes`, `outputs`, `returns`, `segmentStore`, `sessionTtlMs`, `sourceFiles`, `timelines`, `viewers`
|
|
58
|
+
* @param {object} host - `retention`, `outputNeeded`, `outputWriting`, `outputReading`, `planEncodersSoon`, `now`, `invalidateWaits`, `clearWaits`, `segmentFormatOfKey`, `viewerSegmentsOn`, `budgetTimer`, `cleanupTimer`, `encodeRuns`, `keyframeTables`, `machineBudget`, `outputTimes`, `outputs`, `returns`, `segmentStore`, `sessionTtlMs`, `sourceFiles`, `timelines`, `viewers`
|
|
59
59
|
*/
|
|
60
60
|
constructor(host) {
|
|
61
61
|
this.#host = host;
|
|
@@ -267,6 +267,7 @@ export class OutputLifecycle {
|
|
|
267
267
|
for (const sessionId of activeIds) {
|
|
268
268
|
await this.disposeSession(sessionId);
|
|
269
269
|
}
|
|
270
|
+
this.#host.clearWaits?.();
|
|
270
271
|
// Everything this process owns, root included. See SegmentStore.dropAll.
|
|
271
272
|
this.#host.segmentStore.dropAll("the proxy is shutting down");
|
|
272
273
|
}
|
|
@@ -78,6 +78,20 @@ export class SegmentServing {
|
|
|
78
78
|
state.waitListeners.clear();
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/** Identity of the live output-key lifetime named by this session. */
|
|
82
|
+
waitIdentity(sessionId) {
|
|
83
|
+
return this.#host.outputs.waitIdentity(sessionId);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Record a completed wait through the ledger operation wired by the server. */
|
|
87
|
+
noteWait(ranked, waitedMs, identity) {
|
|
88
|
+
this.#host.noteWait?.(ranked, waitedMs, identity);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
waitBandOf(rank, topRank) {
|
|
92
|
+
return this.#host.waitBandOf?.(rank, topRank) ?? "later";
|
|
93
|
+
}
|
|
94
|
+
|
|
81
95
|
/** Observe storage and lifecycle before a request checks file availability. */
|
|
82
96
|
subscribeFileChange(sessionId) {
|
|
83
97
|
const output = isOutputName(sessionId) ? this.#host.outputs.get(sessionId) : null;
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { logger } from "../../utils/logger.js";
|
|
2
|
-
import { bandOf, waits } from "./WaitLedger.js";
|
|
3
|
-
|
|
4
2
|
/**
|
|
5
3
|
* Which viewer is asking, or an empty string where the transport names nobody.
|
|
6
4
|
*
|
|
@@ -84,6 +82,12 @@ export async function serveSessionFile(req, reply, { serving, viewerRequests, se
|
|
|
84
82
|
// a plain HTTP transport, where no loader of ours builds the URL, and then
|
|
85
83
|
// the single shared position decides as it always did.
|
|
86
84
|
const consumerId = consumerOf(req);
|
|
85
|
+
// Hold the output-key lifetime that this request entered. Disposal followed
|
|
86
|
+
// by reopening the same key must not let its old completion write into the
|
|
87
|
+
// new output's measurements.
|
|
88
|
+
const waitIdentity = typeof serving.waitIdentity === "function"
|
|
89
|
+
? serving.waitIdentity(sessionId)
|
|
90
|
+
: undefined;
|
|
87
91
|
// Readiness or a terminal state ends the wait. A caller leaving cancels it;
|
|
88
92
|
// elapsed time is not evidence that media preparation failed.
|
|
89
93
|
const holdStartedAt = Date.now();
|
|
@@ -132,11 +136,12 @@ export async function serveSessionFile(req, reply, { serving, viewerRequests, se
|
|
|
132
136
|
// like, and without them the table would hold only the failures.
|
|
133
137
|
const ranked = result.ranked ?? null;
|
|
134
138
|
if (ranked) {
|
|
135
|
-
|
|
139
|
+
serving.noteWait?.(ranked, heldMs, waitIdentity);
|
|
136
140
|
}
|
|
137
141
|
logger.info(
|
|
138
142
|
`[hold] ${fileName} ${outcome} after ${heldMs}ms` +
|
|
139
|
-
(ranked ? ` (the map wants it ${
|
|
143
|
+
(ranked ? ` (the map wants it ${serving.waitBandOf?.(ranked.rank, ranked.topRank) ?? "later"}, ` +
|
|
144
|
+
`rank ${ranked.rank} of ${ranked.topRank})` : "")
|
|
140
145
|
);
|
|
141
146
|
}
|
|
142
147
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { logger } from "../../utils/logger.js";
|
|
12
12
|
import { KeyframeTables } from "../media/KeyframeTables.js";
|
|
13
|
-
import {
|
|
13
|
+
import { bandOf, WaitLedger } from "../encode/OutputWaitLedger.js";
|
|
14
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";
|
|
@@ -244,6 +244,9 @@ export function wireOutputs({
|
|
|
244
244
|
// Told when an encoder starts or ends: the room this host has for one more
|
|
245
245
|
// encode changes with it, and the server chooses proxies by that room.
|
|
246
246
|
onEncodersChanged = null}) {
|
|
247
|
+
// Long-lived wait diagnostics belong to this wired output graph: each live
|
|
248
|
+
// output key owns its record, and the graph releases it at the last session.
|
|
249
|
+
const waits = new WaitLedger();
|
|
247
250
|
const parts = {};
|
|
248
251
|
const audioDescriptionOf = (output) => (getCachedAudioTracks?.({
|
|
249
252
|
sourceKey: output.file.sourceKey, fileIndex: output.spec.grid?.fileIndex ?? output.file.fileIndex
|
|
@@ -552,6 +555,9 @@ export function wireOutputs({
|
|
|
552
555
|
get lookaheadSeconds() { return parts.lookaheadSeconds; },
|
|
553
556
|
get outputTimes() { return parts.outputTimes; },
|
|
554
557
|
get outputs() { return parts.outputs; },
|
|
558
|
+
noteWait: (ranked, waitedMs, identity) =>
|
|
559
|
+
waits.note(ranked.address, waitedMs, ranked.rank, ranked.topRank, identity),
|
|
560
|
+
waitBandOf: bandOf,
|
|
555
561
|
get segmentStore() { return parts.segmentStore; },
|
|
556
562
|
get startupWaitMs() { return parts.startupWaitMs; },
|
|
557
563
|
get viewers() { return parts.viewers; },
|
|
@@ -565,6 +571,7 @@ export function wireOutputs({
|
|
|
565
571
|
outputReading: key => parts.segmentStore.isReading(key) || parts.viewers.responsesHold(key),
|
|
566
572
|
planEncodersSoon: () => parts.encodeRuns.planEncodersSoon(),
|
|
567
573
|
invalidateWaits: (output) => parts.serving.invalidateWaits(output),
|
|
574
|
+
clearWaits: () => waits.clear(),
|
|
568
575
|
segmentFormatOfKey,
|
|
569
576
|
// Where the viewers of one output stand, in its segments: what the store
|
|
570
577
|
// the output retention policy reads to give disk back in the right order.
|
|
@@ -960,7 +967,9 @@ export function wireOutputs({
|
|
|
960
967
|
// belongs to, the steps, the soundtracks, the height a step is named by.
|
|
961
968
|
parts.outputs = new OutputCatalog({
|
|
962
969
|
fileLengthOf: (session) => parts.hostLoad.fileLengthByKey.get(session.file.key) ?? 0,
|
|
963
|
-
largestPieceOf: (address) => parts.segmentStore.largestPiece(address)
|
|
970
|
+
largestPieceOf: (address) => parts.segmentStore.largestPiece(address),
|
|
971
|
+
waitIdentityStarted: (address, identity) => waits.open(address, identity),
|
|
972
|
+
waitIdentityEnded: (address, identity) => waits.forget(address, identity)
|
|
964
973
|
});
|
|
965
974
|
// Where produced segments live, addressed by WHAT they are rather than by
|
|
966
975
|
// which session's encoder wrote them. Two sessions of one output — two
|