@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.96.4",
3
+ "version": "2.96.5",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
package/server.js CHANGED
@@ -630,7 +630,9 @@ export async function startProxyServer({
630
630
  error.canRetry = true;
631
631
  throw error;
632
632
  },
633
- resolveEncodeInput: async (output, fromIndex, toIndex) => {
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
- /** One line when a request's state changes, so a wait that never ends names what it waits for. */
63
- #note(request, state) {
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.#resolve(request.output, request.from, request.from);
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.#resolve(output, from, end).catch(() => null);
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);