@torrent-tv/proxy 2.96.4 → 2.96.5
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
package/server.js
CHANGED
|
@@ -630,7 +630,9 @@ export async function startProxyServer({
|
|
|
630
630
|
error.canRetry = true;
|
|
631
631
|
throw error;
|
|
632
632
|
},
|
|
633
|
-
|
|
633
|
+
// `timing`, when given, is told how long each read of a file waited behind
|
|
634
|
+
// the reads asked before it and how long it took (torrent-tv/meta#166).
|
|
635
|
+
resolveEncodeInput: async (output, fromIndex, toIndex, timing = null) => {
|
|
634
636
|
const grid = output.timeline?.published ?? output.timeline?.boundaries;
|
|
635
637
|
const from = grid?.[fromIndex], to = grid?.[toIndex + 1];
|
|
636
638
|
if (!Number.isFinite(from) || !(to > from)) return { kind: "terminal", reason: "output-interval-not-declared" };
|
|
@@ -650,6 +652,13 @@ export async function startProxyServer({
|
|
|
650
652
|
for (const [fileIndex, choices] of selected) {
|
|
651
653
|
const params = await containerOver({ sourceKey: output.file.sourceKey, fileIndex });
|
|
652
654
|
if (!params) return { kind: "needs-source" };
|
|
655
|
+
if (timing) {
|
|
656
|
+
params.onTimed = (_statement, waitedMs, readMs) => {
|
|
657
|
+
timing.reads += 1;
|
|
658
|
+
timing.queuedMs += waitedMs;
|
|
659
|
+
timing.readMs += readMs;
|
|
660
|
+
};
|
|
661
|
+
}
|
|
653
662
|
const tracks = await containerOrchestrator.inspect(params, "tracks");
|
|
654
663
|
if (tracks.kind !== "result") return tracks;
|
|
655
664
|
const container = containerOrchestrator.known(params.sourceKey, fileIndex);
|
|
@@ -59,12 +59,36 @@ export class EncodeInputs {
|
|
|
59
59
|
this.#now = now;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
/**
|
|
63
|
-
|
|
62
|
+
/**
|
|
63
|
+
* One line when a request's state changes, so a wait that never ends names
|
|
64
|
+
* what it waits for. `detail` is said with it and does not count as a change.
|
|
65
|
+
*/
|
|
66
|
+
#note(request, state, detail = "") {
|
|
64
67
|
if (request.noted === state) return;
|
|
65
68
|
request.noted = state;
|
|
66
69
|
this.#log(`encode input #${request.from}..#${request.to} of ${request.output.outputKey ?? request.output.id}: ${state}` +
|
|
67
|
-
` (held ${this.#held} of ${this.#allowed} allowed)`);
|
|
70
|
+
` (held ${this.#held} of ${this.#allowed} allowed)${detail}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The container's statement of an interval, its time added to the request's account. */
|
|
74
|
+
async #resolveTimed(timing, output, from, to) {
|
|
75
|
+
const startedAt = this.#now();
|
|
76
|
+
try {
|
|
77
|
+
return await this.#resolve(output, from, to, timing);
|
|
78
|
+
} finally {
|
|
79
|
+
timing.resolutions += 1;
|
|
80
|
+
timing.resolveMs += this.#now() - startedAt;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** `work`'s time added to one field of the request's account. */
|
|
85
|
+
async #spent(timing, field, work) {
|
|
86
|
+
const startedAt = this.#now();
|
|
87
|
+
try {
|
|
88
|
+
return await work();
|
|
89
|
+
} finally {
|
|
90
|
+
timing[field] += this.#now() - startedAt;
|
|
91
|
+
}
|
|
68
92
|
}
|
|
69
93
|
|
|
70
94
|
held() { return this.#held; }
|
|
@@ -216,8 +240,14 @@ export class EncodeInputs {
|
|
|
216
240
|
request.stale = false;
|
|
217
241
|
request.revision = this.#revision;
|
|
218
242
|
request.memoryRevision = this.#memoryRevision;
|
|
243
|
+
// Where the time from the request to its input goes, across every attempt:
|
|
244
|
+
// field 2026-10-10, a picture's request took 38 s before its copy began and
|
|
245
|
+
// nothing said on what (torrent-tv/meta#166).
|
|
246
|
+
request.timing ??= { askedAt: this.#now(), attempts: 0, resolutions: 0, resolveMs: 0,
|
|
247
|
+
reads: 0, queuedMs: 0, readMs: 0, heldMs: 0, budgetMs: 0 };
|
|
248
|
+
request.timing.attempts += 1;
|
|
219
249
|
request.promise = (async () => {
|
|
220
|
-
const resolved = await this.#
|
|
250
|
+
const resolved = await this.#resolveTimed(request.timing, request.output, request.from, request.from);
|
|
221
251
|
if (this.#requests.get(request.key) !== request) return;
|
|
222
252
|
let result;
|
|
223
253
|
if (resolved.kind !== "result") {
|
|
@@ -240,7 +270,8 @@ export class EncodeInputs {
|
|
|
240
270
|
request.result = result;
|
|
241
271
|
this.#note(request, result.kind === "result" ? `ready #${result.from}..#${result.to}, ${result.bytes ?? "?"} bytes` +
|
|
242
272
|
(Number.isFinite(result.copyMs) ? `, copied in ${result.copyMs}ms` : "")
|
|
243
|
-
: `${result.kind}${result.reason ? ` ${result.reason}` : ""}${Number.isFinite(result.bytes) ? ` ${result.bytes} bytes` : ""}
|
|
273
|
+
: `${result.kind}${result.reason ? ` ${result.reason}` : ""}${Number.isFinite(result.bytes) ? ` ${result.bytes} bytes` : ""}`,
|
|
274
|
+
describeTiming(request.timing, this.#now()));
|
|
244
275
|
if (result.kind === "result" || result.kind === "terminal") this.#changed(request.output, result);
|
|
245
276
|
})().catch(error => {
|
|
246
277
|
if (this.#requests.get(request.key) === request) {
|
|
@@ -261,7 +292,7 @@ export class EncodeInputs {
|
|
|
261
292
|
#reserveFor(request) {
|
|
262
293
|
return async bytes => {
|
|
263
294
|
this.#wanted.set(request.key, { wanted: bytes, required: bytes });
|
|
264
|
-
await this.#revise();
|
|
295
|
+
await this.#spent(request.timing, "budgetMs", () => this.#revise());
|
|
265
296
|
const capacity = this.#capacity();
|
|
266
297
|
if (capacity !== null && bytes > capacity && this.#urgent(request.output, request.from)) return {
|
|
267
298
|
kind: "terminal", reason: "source-input-exceeds-memory-capacity", bytes, capacity
|
|
@@ -303,7 +334,7 @@ export class EncodeInputs {
|
|
|
303
334
|
if (!stretches.has(end)) {
|
|
304
335
|
// A longer stretch that cannot be stated is not one to copy; the
|
|
305
336
|
// shorter one already stated still runs.
|
|
306
|
-
const resolved = await this.#
|
|
337
|
+
const resolved = await this.#resolveTimed(request.timing, output, from, end).catch(() => null);
|
|
307
338
|
stretches.set(end, resolved?.kind === "result" && resolved.sources?.every(source => source.input.original === true)
|
|
308
339
|
? { sources: resolved.sources, bytes: originalInputBytes(resolved.sources) } : null);
|
|
309
340
|
}
|
|
@@ -311,7 +342,7 @@ export class EncodeInputs {
|
|
|
311
342
|
};
|
|
312
343
|
const fits = async (end, maxBytes) => {
|
|
313
344
|
const stretch = await at(end);
|
|
314
|
-
return Boolean(stretch) && stretch.bytes <= maxBytes && await this.#present(stretch.sources, held);
|
|
345
|
+
return Boolean(stretch) && stretch.bytes <= maxBytes && await this.#present(stretch.sources, held, request.timing);
|
|
315
346
|
};
|
|
316
347
|
// Grows by doubling and then halves back: a handful of resolutions find
|
|
317
348
|
// the end whether it is a piece away or the rest of the film.
|
|
@@ -333,7 +364,7 @@ export class EncodeInputs {
|
|
|
333
364
|
let end = target === null || this.#heldRanges === null ? from : await longest(target);
|
|
334
365
|
if (this.#requests.get(request.key) !== request) return undefined;
|
|
335
366
|
this.#wanted.set(request.key, { wanted: stretches.get(end).bytes, required: minimum });
|
|
336
|
-
await this.#revise();
|
|
367
|
+
await this.#spent(request.timing, "budgetMs", () => this.#revise());
|
|
337
368
|
if (this.#requests.get(request.key) !== request) return undefined;
|
|
338
369
|
const capacity = this.#capacity();
|
|
339
370
|
if (capacity !== null && minimum > capacity && this.#urgent(output, from)) {
|
|
@@ -371,10 +402,10 @@ export class EncodeInputs {
|
|
|
371
402
|
}
|
|
372
403
|
|
|
373
404
|
/** Whether every range of these sources is downloaded whole now. */
|
|
374
|
-
async #present(sources, held) {
|
|
405
|
+
async #present(sources, held, timing) {
|
|
375
406
|
for (const source of sources) {
|
|
376
407
|
const name = `${source.sourceKey}:${source.fileIndex}`;
|
|
377
|
-
if (!held.has(name)) held.set(name, await Promise.resolve().then(() => this.#heldRanges(source)).catch(() => null) ?? []);
|
|
408
|
+
if (!held.has(name)) held.set(name, await this.#spent(timing, "heldMs", () => Promise.resolve().then(() => this.#heldRanges(source)).catch(() => null)) ?? []);
|
|
378
409
|
const ranges = held.get(name);
|
|
379
410
|
if (!source.input.ranges.every(([start, end]) => ranges.some(([first, last]) => first <= start && last >= end))) return false;
|
|
380
411
|
}
|
|
@@ -382,5 +413,24 @@ export class EncodeInputs {
|
|
|
382
413
|
}
|
|
383
414
|
}
|
|
384
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Where a request has spent its time since it was asked, as one clause of its
|
|
418
|
+
* log line. The resolutions include the reads of the file they made, and those
|
|
419
|
+
* reads are split into the time spent behind earlier reads of the same file and
|
|
420
|
+
* the time of the read itself; what is left of the total is the copy and the
|
|
421
|
+
* waits between attempts for bytes or memory.
|
|
422
|
+
*
|
|
423
|
+
* @param {object | undefined} timing
|
|
424
|
+
* @param {number} now
|
|
425
|
+
* @returns {string}
|
|
426
|
+
*/
|
|
427
|
+
function describeTiming(timing, now) {
|
|
428
|
+
if (!timing) return "";
|
|
429
|
+
return `; asked ${Math.max(0, now - timing.askedAt)}ms ago over ${timing.attempts} attempt(s): ` +
|
|
430
|
+
`${timing.resolutions} resolution(s) ${timing.resolveMs}ms ` +
|
|
431
|
+
`(${timing.reads} file read(s): queued ${timing.queuedMs}ms, reading ${timing.readMs}ms), ` +
|
|
432
|
+
`held ranges ${timing.heldMs}ms, budget ${timing.budgetMs}ms`;
|
|
433
|
+
}
|
|
434
|
+
|
|
385
435
|
function addressOf(output) { return output.outputKey ?? output.id; }
|
|
386
436
|
function keyOf(output, from) { return `${addressOf(output)}:${from}`; }
|
|
@@ -137,10 +137,16 @@ export class ContainerOrchestrator {
|
|
|
137
137
|
if (!lifetime) this.#lifetimes.set(key, lifetime = {});
|
|
138
138
|
const isCurrent = () => this.#lifetimes.get(key) === lifetime && params.isCurrent?.() !== false;
|
|
139
139
|
const previous = this.#reads.get(key) ?? Promise.resolve();
|
|
140
|
+
// Reads of one file run one after another, so an answer can wait for every
|
|
141
|
+
// read asked before it. Told to whoever asked, apart from the read itself,
|
|
142
|
+
// because the two call for different remedies (torrent-tv/meta#166).
|
|
143
|
+
const askedAt = Date.now();
|
|
140
144
|
const reading = previous.catch(() => undefined).then(async () => {
|
|
141
145
|
if (!isCurrent()) return { result: { kind: "terminal", reason: "request-obsolete", requestId: params.requestId } };
|
|
146
|
+
const startedAt = Date.now();
|
|
142
147
|
const revision = params.onReadStart?.(statement);
|
|
143
148
|
const result = await this.#inspectRead({ ...params, isCurrent }, statement);
|
|
149
|
+
params.onTimed?.(statement, startedAt - askedAt, Date.now() - startedAt);
|
|
144
150
|
return { result, revision };
|
|
145
151
|
});
|
|
146
152
|
this.#reads.set(key, reading);
|