@torrent-tv/proxy 2.98.12 → 2.98.14

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.98.12",
3
+ "version": "2.98.14",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -13,6 +13,7 @@ import { contentionPenalty } from "./contention.js";
13
13
  import { LOOKAHEAD_PAUSE_SECONDS } from "./CushionReport.js";
14
14
  import { SpeedReadings } from "./SpeedReadings.js";
15
15
  import { nextInputAtFirstPiece } from "./next-input.js";
16
+ import { runOpenOf } from "./run-open.js";
16
17
  import { ENCODE_RUN_STATE, liveRunsOf, runStateOf, wireState } from "./encode-run-state.js";
17
18
  import { ENCODE_EXIT } from "./encode-exit.js";
18
19
  import { EncodeRun, endOfRun } from "./EncodeRun.js";
@@ -1330,11 +1331,14 @@ export class EncodeRuns {
1330
1331
  const grid = session.timeline?.published ?? session.timeline?.boundaries;
1331
1332
  const pieceSeconds = Number(grid?.[run.from + 1]) - Number(grid?.[run.from]);
1332
1333
  const speed = run.speedX > 0 ? run.speedX : this.#host.encodeOrchestrator.speedOn?.(session.outputKey) ?? 0;
1333
- if (!Number.isFinite(ended.firstNamedMs) || !(pieceSeconds > 0) || !(speed > 0)) return;
1334
1334
  // FFmpeg's reads may have waited for the stretch being copied; that wait
1335
- // is the copy's, and the stretch is sized from the open (torrent-tv/meta#178).
1336
- const waitedMs = Number.isFinite(ended.firstNamedWallMs) ? run.originalInput?.waitedBefore?.(ended.firstNamedWallMs) ?? 0 : 0;
1337
- const openSeconds = Math.max(0, (ended.firstNamedMs - waitedMs) / 1000 - pieceSeconds / speed);
1335
+ // is the copy's, and the stretch is sized from the open (`run-open.js`).
1336
+ const open = runOpenOf({
1337
+ firstNamedMs: ended.firstNamedMs, firstNamedWallMs: ended.firstNamedWallMs, startedAt: run.startedAt,
1338
+ waitedBefore: run.originalInput?.waitedBefore, pieceSeconds, speed
1339
+ });
1340
+ if (!open) return;
1341
+ const { openSeconds, waitedMs } = open;
1338
1342
  this.#host.encodeOrchestrator.noteOpen?.(session.outputKey ?? "", openSeconds);
1339
1343
  this.#host.logger.info(`encode input of run #${run.from}..#${run.to} on ${session.outputKey}: opened in ` +
1340
1344
  `${openSeconds.toFixed(2)}s (first piece named at ${ended.firstNamedMs}ms, ${pieceSeconds.toFixed(3)}s of film at ${speed.toFixed(2)}x` +
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @file How long a run took to open its original input (torrent-tv/meta#178).
3
+ *
4
+ * Measured from the run's spawn to the name of its first closed piece, less
5
+ * what encoding that piece costs at the run's speed, and less the time
6
+ * FFmpeg's reads waited for the stretch being copied: that wait is the copy's,
7
+ * and the stretch is sized from the open. The input states those waits on the
8
+ * wall clock, so they are taken up to the wall-clock moment the first piece
9
+ * was named — the run's start plus its time to that name, not that time alone,
10
+ * which as a moment lay before every wait and counted none of them.
11
+ */
12
+
13
+ /**
14
+ * @param {object} params
15
+ * @param {number | null} params.firstNamedMs - Spawn to the first piece's name, pauses left out.
16
+ * @param {number | null} params.firstNamedWallMs - The same on the wall clock, pauses included.
17
+ * @param {number} params.startedAt - The run's spawn on the wall clock, ms.
18
+ * @param {((until: number) => number) | null | undefined} params.waitedBefore - The input's wall-clock waits for its copy before `until`, ms.
19
+ * @param {number} params.pieceSeconds - The first piece's film.
20
+ * @param {number} params.speed - Film seconds per second; 0 when not known.
21
+ * @returns {{ openSeconds: number, waitedMs: number } | null} Null where any term is unknown.
22
+ */
23
+ export function runOpenOf({ firstNamedMs, firstNamedWallMs, startedAt, waitedBefore, pieceSeconds, speed }) {
24
+ if (!Number.isFinite(firstNamedMs) || !(pieceSeconds > 0) || !(speed > 0)) return null;
25
+ const waitedMs = Number.isFinite(firstNamedWallMs) && startedAt > 0 && typeof waitedBefore === "function"
26
+ ? waitedBefore(startedAt + firstNamedWallMs)
27
+ : 0;
28
+ return { waitedMs, openSeconds: Math.max(0, (firstNamedMs - waitedMs) / 1000 - pieceSeconds / speed) };
29
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * @file When each piece was asked of the swarm, and how long its blocks were
3
+ * in flight (torrent-tv/meta#178).
4
+ *
5
+ * The order in which pieces were verified cannot say why one came last: it
6
+ * may have been asked for late, or asked for early with its last blocks held
7
+ * by slow peers, and the two need different fixes. Field 2026-10-14: the
8
+ * first input of a 1080p MKV waited 18.7 s for pieces 1 and 2, verified after
9
+ * pieces 3-33. WebTorrent states no outgoing request, so the torrent pool
10
+ * tells this record each block it asks and each block that arrives; the
11
+ * record answers once, when the piece is verified, and then forgets it.
12
+ */
13
+
14
+ /**
15
+ * @typedef {object} PieceTiming
16
+ * @property {number} askedAgoMs - From the first block asked to the verification.
17
+ * @property {number} asks - Blocks asked, duplicates included.
18
+ * @property {number} peers - Distinct peers asked.
19
+ * @property {number} slowestMs - The longest a block was in flight, from its first ask to its arrival.
20
+ * @property {string | null} slowestPeer - Who sent that block.
21
+ * @property {number} lastInFlightMs - How long the block that completed the piece was in flight.
22
+ * @property {string | null} lastPeer - Who sent it.
23
+ */
24
+
25
+ export class PieceTimings {
26
+ /** @type {Map<number, { firstAskAt: number, asks: number, peers: Set<string>, blocks: Map<number, number>, slowestMs: number, slowestPeer: string | null, lastInFlightMs: number, lastPeer: string | null }>} */
27
+ #pieces = new Map();
28
+
29
+ /**
30
+ * A block of `piece` at `offset` was asked of `peer`.
31
+ *
32
+ * @param {number} piece
33
+ * @param {number} offset
34
+ * @param {string} peer
35
+ * @param {number} now
36
+ */
37
+ asked(piece, offset, peer, now) {
38
+ let record = this.#pieces.get(piece);
39
+ if (!record) {
40
+ record = { firstAskAt: now, asks: 0, peers: new Set(), blocks: new Map(), slowestMs: 0, slowestPeer: null, lastInFlightMs: 0, lastPeer: null };
41
+ this.#pieces.set(piece, record);
42
+ }
43
+ record.asks += 1;
44
+ record.peers.add(peer);
45
+ if (!record.blocks.has(offset)) record.blocks.set(offset, now);
46
+ }
47
+
48
+ /**
49
+ * A block of `piece` at `offset` arrived from `peer`. A block never asked
50
+ * through this record — it is not one of ours — says nothing.
51
+ *
52
+ * @param {number} piece
53
+ * @param {number} offset
54
+ * @param {string} peer
55
+ * @param {number} now
56
+ */
57
+ arrived(piece, offset, peer, now) {
58
+ const record = this.#pieces.get(piece);
59
+ const askedAt = record?.blocks.get(offset);
60
+ if (askedAt === undefined) return;
61
+ const inFlightMs = now - askedAt;
62
+ if (inFlightMs >= record.slowestMs) {
63
+ record.slowestMs = inFlightMs;
64
+ record.slowestPeer = peer;
65
+ }
66
+ record.lastInFlightMs = inFlightMs;
67
+ record.lastPeer = peer;
68
+ }
69
+
70
+ /**
71
+ * The timing of `piece`, verified at `now`, or null when no block of it was
72
+ * asked through this record; forgotten either way.
73
+ *
74
+ * @param {number} piece
75
+ * @param {number} now
76
+ * @returns {PieceTiming | null}
77
+ */
78
+ verified(piece, now) {
79
+ const record = this.#pieces.get(piece);
80
+ this.#pieces.delete(piece);
81
+ if (!record) return null;
82
+ return {
83
+ askedAgoMs: now - record.firstAskAt,
84
+ asks: record.asks,
85
+ peers: record.peers.size,
86
+ slowestMs: record.slowestMs,
87
+ slowestPeer: record.slowestPeer,
88
+ lastInFlightMs: record.lastInFlightMs,
89
+ lastPeer: record.lastPeer
90
+ };
91
+ }
92
+ }
93
+
94
+ /**
95
+ * The timing said after a verified piece.
96
+ *
97
+ * @param {PieceTiming | null} timing
98
+ * @returns {string}
99
+ */
100
+ export function describePieceTiming(timing) {
101
+ if (!timing) return "";
102
+ return `; first block asked ${(timing.askedAgoMs / 1000).toFixed(1)}s before, ${timing.asks} block ask(s) to ${timing.peers} peer(s), ` +
103
+ `slowest block ${(timing.slowestMs / 1000).toFixed(1)}s in flight from ${timing.slowestPeer ?? "?"}, ` +
104
+ `the last one ${(timing.lastInFlightMs / 1000).toFixed(1)}s from ${timing.lastPeer ?? "?"}`;
105
+ }
@@ -24,6 +24,7 @@ import { withdrawClaim } from "./download/withdraw-claim.js";
24
24
  import { deadlineOfMapZone, levelOfMapZone, priorityOfMapZone } from "./map-zones.js";
25
25
  import { deriveSourceKey } from "../../utils/torrent-source-key.js";
26
26
  import { noteTorrentDestroyed } from "./destroyed-torrents.js";
27
+ import { PieceTimings, describePieceTiming } from "./piece-timing.js";
27
28
 
28
29
  /** How a window stated from the priority map names itself. */
29
30
  const MAP_CLAIMANT = "priority-map";
@@ -1502,21 +1503,35 @@ export class TorrentPool {
1502
1503
  const addedAt = Date.now();
1503
1504
  this.#swarmTimingByTorrent.set(torrent, { addedAt, firstPeerAt: null });
1504
1505
 
1505
- torrent.on("download-requested", ({ piece, deadlineAt, requestId, peer, queued }) => {
1506
- logger.info(`download request=${requestId} piece=${piece} deadline=${deadlineAt} peer=${peer} outstanding=${queued}`);
1507
- });
1508
1506
  torrent.on("download-request-cancelled", ({ piece, offset, length, reason, requestId }) => {
1509
1507
  logger.info(`download request=${requestId ?? `${torrent.infoHash ?? "unknown"}:${piece}:${offset}`} cancel bytes=${length} reason=${reason}`);
1510
1508
  });
1511
1509
  torrent.on("download-request-cancel-failed", ({ piece, offset, length, error }) => {
1512
1510
  logger.warn(`download request=${torrent.infoHash ?? "unknown"}:${piece}:${offset} cancel bytes=${length} failed: ${error}`);
1513
1511
  });
1514
- // WHEN each piece arrived. The requests above say what was asked for; this
1515
- // says what came and in which order, which is what decides how far an
1516
- // encoder reading the file can go before it meets a piece still missing
1517
- // (torrent-tv/meta#151). One short line per piece: a 2.3 GB film is 563.
1512
+ // WHEN each piece arrived, which decides how far an encoder reading the
1513
+ // file can go before it meets a piece still missing (torrent-tv/meta#151),
1514
+ // and WHY it came when it did: when its first block was asked and how long
1515
+ // its blocks were in flight, and from whom (`piece-timing.js`). One line
1516
+ // per piece: a 2.3 GB film is 563.
1517
+ const timings = new PieceTimings();
1518
+ torrent.on("wire", (wire) => {
1519
+ // WebTorrent asks a wire for a block by calling `request` and states
1520
+ // nothing; a request the wire refuses (choked, closed) is not queued and
1521
+ // not counted.
1522
+ if (typeof wire?.request !== "function") return;
1523
+ const peer = String(wire.remoteAddress ?? "?");
1524
+ const request = wire.request;
1525
+ wire.request = function (index, offset, length, callback) {
1526
+ const queued = this.requests.length;
1527
+ const answer = request.call(this, index, offset, length, callback);
1528
+ if (this.requests.length > queued) timings.asked(index, offset, peer, Date.now());
1529
+ return answer;
1530
+ };
1531
+ wire.on("piece", (index, offset) => timings.arrived(index, offset, peer, Date.now()));
1532
+ });
1518
1533
  torrent.on("verified", (piece) => {
1519
- logger.info(`download ${label()} verified piece=${piece}`);
1534
+ logger.info(`download ${label()} verified piece=${piece}${describePieceTiming(timings.verified(piece, Date.now()))}`);
1520
1535
  });
1521
1536
 
1522
1537
  // The first connected peer, said once, because the wait for it can BE the