@torrent-tv/proxy 2.98.13 → 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.13",
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": {
@@ -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