@torrent-tv/proxy 2.102.4 → 2.104.0

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.102.4",
3
+ "version": "2.104.0",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -26,6 +26,8 @@ import { deriveSourceKey } from "../../utils/torrent-source-key.js";
26
26
  import { noteTorrentDestroyed } from "./destroyed-torrents.js";
27
27
  import { PieceTimings, describePieceTiming } from "./piece-timing.js";
28
28
  import { TailRepeat } from "./worker/tail-repeat.js";
29
+ import { tracePieceStorePut } from "./worker/trace-piece-store-put.js";
30
+ import { beginSwarmLifecycleTrace, noteSwarmDiscovery, noteSwarmMetadata, noteSwarmPeer } from "./worker/trace-swarm-lifecycle.js";
29
31
 
30
32
  /**
31
33
  * The second-peer requests for the last blocks of a piece somebody waits for,
@@ -966,7 +968,8 @@ export class TorrentPool {
966
968
  /**
967
969
  * When each torrent was added, and when its first peer connected.
968
970
  *
969
- * @type {WeakMap<import("webtorrent").Torrent, { addedAt: number, firstPeerAt: number | null }>}
971
+ * @type {WeakMap<import("webtorrent").Torrent, { addedAt: number, addedAtMonotonic: number, firstPeerAt: number | null,
972
+ * firstPeerAtMonotonic: number | null, metadataWaitStartedAt: number | null }>}
970
973
  */
971
974
  #swarmTimingByTorrent = new WeakMap();
972
975
  #waitTraceByTorrent = new WeakMap();
@@ -1508,10 +1511,16 @@ export class TorrentPool {
1508
1511
  if (waitTrace?.trace === true && typeof waitTrace.sourceKey === "string") {
1509
1512
  this.#waitTraceByTorrent.set(torrent, waitTrace);
1510
1513
  }
1514
+ const alreadyTracked = this.#swarmTimingByTorrent.has(torrent);
1515
+ if (!alreadyTracked) {
1516
+ this.#swarmTimingByTorrent.set(torrent, { addedAt: Date.now(), addedAtMonotonic: performance.now(),
1517
+ firstPeerAt: null, firstPeerAtMonotonic: null, peerWaitStartedAt: null, metadataWaitStartedAt: null });
1518
+ }
1511
1519
  const trace = (process, phase, fields = {}) => {
1512
1520
  const context = this.#waitTraceByTorrent.get(torrent);
1513
1521
  if (context?.trace === true) context.emitWaitTrace?.(context.sourceKey, process, phase, fields);
1514
1522
  };
1523
+ const pool = this;
1515
1524
  const wrap = (method, process) => {
1516
1525
  const original = torrent[method];
1517
1526
  if (typeof original !== "function") return;
@@ -1520,20 +1529,32 @@ export class TorrentPool {
1520
1529
  trace(process, "begin", { startedAt });
1521
1530
  try {
1522
1531
  const result = original.apply(this, args);
1532
+ const noteDiscoveryEnd = () => {
1533
+ if (process === "W2" && result !== false) {
1534
+ const timing = pool.#swarmTimingByTorrent.get(torrent);
1535
+ if (timing) noteSwarmDiscovery(timing, trace, performance.now());
1536
+ }
1537
+ };
1523
1538
  if (result && typeof result.then === "function") {
1524
1539
  return result.then(value => {
1525
- trace(process, "end", { startedAt, endedAt: performance.now(), outcome: "resolved" });
1540
+ noteDiscoveryEnd();
1541
+ const endedAt = performance.now();
1542
+ trace(process, "end", { startedAt, endedAt, durationMs: endedAt - startedAt, outcome: "resolved" });
1526
1543
  return value;
1527
1544
  }, error => {
1528
- trace(process, "end", { startedAt, endedAt: performance.now(), outcome: "error",
1545
+ const endedAt = performance.now();
1546
+ trace(process, "end", { startedAt, endedAt, durationMs: endedAt - startedAt, outcome: "error",
1529
1547
  error: String(error?.message ?? error) });
1530
1548
  throw error;
1531
1549
  });
1532
1550
  }
1533
- trace(process, "end", { startedAt, endedAt: performance.now(), outcome: "returned" });
1551
+ noteDiscoveryEnd();
1552
+ const endedAt = performance.now();
1553
+ trace(process, "end", { startedAt, endedAt, durationMs: endedAt - startedAt, outcome: "returned" });
1534
1554
  return result;
1535
1555
  } catch (error) {
1536
- trace(process, "end", { startedAt, endedAt: performance.now(), outcome: "error",
1556
+ const endedAt = performance.now();
1557
+ trace(process, "end", { startedAt, endedAt, durationMs: endedAt - startedAt, outcome: "error",
1537
1558
  error: String(error?.message ?? error) });
1538
1559
  throw error;
1539
1560
  }
@@ -1541,9 +1562,9 @@ export class TorrentPool {
1541
1562
  };
1542
1563
  if (waitTrace?.trace === true && !this.#waitTraceHooks.has(torrent)) {
1543
1564
  this.#waitTraceHooks.add(torrent);
1544
- wrap("_onListening", "W1");
1545
1565
  wrap("_startDiscovery", "W2");
1546
1566
  wrap("_onStore", "W6");
1567
+ tracePieceStorePut(pieceStoreOf(torrent), trace);
1547
1568
  const verify = torrent._verifyPieces;
1548
1569
  if (typeof verify === "function") {
1549
1570
  torrent._verifyPieces = function (callback, ...args) {
@@ -1555,8 +1576,17 @@ export class TorrentPool {
1555
1576
  }, ...args);
1556
1577
  };
1557
1578
  }
1558
- torrent.on("metadata", metadata => trace("W4", "event", { files: metadata?.files?.length ?? null }));
1559
- torrent.on("wire", wire => trace("W3", "event", { peer: String(wire?.remoteAddress ?? "?") }));
1579
+ const timing = this.#swarmTimingByTorrent.get(torrent);
1580
+ if (timing) beginSwarmLifecycleTrace(timing, trace, Boolean(torrent.metadata));
1581
+ torrent.on("metadata", metadata => {
1582
+ const current = this.#swarmTimingByTorrent.get(torrent);
1583
+ if (current) noteSwarmMetadata(current, trace, metadata?.files?.length ?? null);
1584
+ });
1585
+ torrent.on("wire", wire => {
1586
+ trace("W3", "event", { peer: String(wire?.remoteAddress ?? "?") });
1587
+ const current = this.#swarmTimingByTorrent.get(torrent);
1588
+ if (current) noteSwarmPeer(current, trace, Boolean(torrent.metadata));
1589
+ });
1560
1590
  }
1561
1591
  // Attached ONCE per torrent. WebTorrent answers a duplicate add by handing
1562
1592
  // back the torrent it already has, and this used to run again on it: a
@@ -1567,7 +1597,7 @@ export class TorrentPool {
1567
1597
  // once as a .torrent and once as a magnet is exactly that case, and the
1568
1598
  // roadmap already records it happening. Trace hooks above are installed
1569
1599
  // separately and only for an explicitly traced source.
1570
- if (this.#swarmTimingByTorrent.has(torrent)) {
1600
+ if (alreadyTracked) {
1571
1601
  return;
1572
1602
  }
1573
1603
  // A torrent nobody has asked for yet does not exist: this is called the
@@ -1588,9 +1618,6 @@ export class TorrentPool {
1588
1618
  // for the whole life of the torrent, which is the same fault as three
1589
1619
  // different hashes with the hash removed.
1590
1620
  const label = () => String(torrent.infoHash ?? "?").slice(0, 8);
1591
- const addedAt = Date.now();
1592
- this.#swarmTimingByTorrent.set(torrent, { addedAt, firstPeerAt: null });
1593
-
1594
1621
  torrent.on("download-request-cancelled", ({ piece, offset, length, reason, requestId }) => {
1595
1622
  logger.info(`download request=${requestId ?? `${torrent.infoHash ?? "unknown"}:${piece}:${offset}`} cancel bytes=${length} reason=${reason}`);
1596
1623
  });
@@ -1655,6 +1682,7 @@ export class TorrentPool {
1655
1682
  return;
1656
1683
  }
1657
1684
  timing.firstPeerAt = Date.now();
1685
+ timing.firstPeerAtMonotonic ??= performance.now();
1658
1686
  const waited = secondsToFirstPeer(timing.addedAt, timing.firstPeerAt);
1659
1687
  const byTracker = this.#lastAnnounceByTorrent.get(torrent);
1660
1688
  const offered = bestAnnounce(byTracker ? byTracker.values() : []);
@@ -0,0 +1,28 @@
1
+ /** Record the torrent worker's verified-piece storage write without changing its callback API. */
2
+ export function tracePieceStorePut(store, trace, now = () => performance.now()) {
3
+ const put = store?.put;
4
+ if (typeof put !== "function") return false;
5
+
6
+ store.put = function (index, bytes, callback) {
7
+ const startedAt = now();
8
+ const fields = { piece: index, bytes: bytes?.byteLength ?? bytes?.length ?? null, startedAt };
9
+ trace("W9", "begin", fields);
10
+ let finished = false;
11
+ const finish = error => {
12
+ if (finished) return;
13
+ finished = true;
14
+ const endedAt = now();
15
+ trace("W9", "end", { ...fields, endedAt, durationMs: endedAt - startedAt,
16
+ outcome: error ? "error" : "stored", ...(error ? { error: String(error?.message ?? error) } : {}) });
17
+ callback?.(error);
18
+ };
19
+
20
+ try {
21
+ return put.call(this, index, bytes, finish);
22
+ } catch (error) {
23
+ finish(error);
24
+ throw error;
25
+ }
26
+ };
27
+ return true;
28
+ }
@@ -0,0 +1,49 @@
1
+ /** Record first-peer and metadata waits on the torrent worker's monotonic clock. */
2
+ export function beginSwarmLifecycleTrace(timing, trace, metadataAvailable, now = () => performance.now()) {
3
+ if (timing.firstPeerAtMonotonic !== null) {
4
+ trace("W3", "event", { outcome: "peer-already-connected" });
5
+ } else if (timing.peerWaitStartedAt !== null) {
6
+ trace("W3", "begin", { startedAt: timing.peerWaitStartedAt });
7
+ }
8
+
9
+ if (metadataAvailable) {
10
+ trace("W4", "event", { outcome: "already-available" });
11
+ } else if (timing.firstPeerAtMonotonic !== null) {
12
+ timing.metadataWaitStartedAt = now();
13
+ trace("W4", "begin", { startedAt: timing.metadataWaitStartedAt });
14
+ }
15
+ }
16
+
17
+ export function noteSwarmDiscovery(timing, trace, endedAt) {
18
+ if (timing.firstPeerAtMonotonic !== null || timing.peerWaitStartedAt !== null) return;
19
+ timing.peerWaitStartedAt = endedAt;
20
+ trace("W3", "begin", { startedAt: endedAt });
21
+ }
22
+
23
+ export function noteSwarmPeer(timing, trace, metadataAvailable, now = () => performance.now()) {
24
+ if (timing.firstPeerAtMonotonic !== null) return;
25
+ const endedAt = now();
26
+ timing.firstPeerAtMonotonic = endedAt;
27
+ if (timing.peerWaitStartedAt !== null) {
28
+ trace("W3", "end", { startedAt: timing.peerWaitStartedAt, endedAt,
29
+ durationMs: endedAt - timing.peerWaitStartedAt, outcome: "peer-connected" });
30
+ } else {
31
+ trace("W3", "event", { outcome: "peer-connected-before-trace" });
32
+ }
33
+ if (!metadataAvailable) {
34
+ timing.metadataWaitStartedAt = endedAt;
35
+ trace("W4", "begin", { startedAt: endedAt });
36
+ }
37
+ }
38
+
39
+ export function noteSwarmMetadata(timing, trace, files, now = () => performance.now()) {
40
+ const endedAt = now();
41
+ if (timing.metadataWaitStartedAt !== null) {
42
+ const startedAt = timing.metadataWaitStartedAt;
43
+ timing.metadataWaitStartedAt = null;
44
+ trace("W4", "end", { startedAt, endedAt, durationMs: endedAt - startedAt,
45
+ outcome: "available", files });
46
+ } else {
47
+ trace("W4", "event", { outcome: "available", files });
48
+ }
49
+ }
@@ -345,8 +345,10 @@ async function runCommand(command, params, id) {
345
345
  // Reusing the same promise for a repeated add also collapses two callers
346
346
  // racing to open the same torrent into one.
347
347
  let pending = torrentsByKey.get(params.sourceKey);
348
+ let addStartedAt = null;
348
349
  if (!pending) {
349
- emitWaitTrace(params.sourceKey, "W1", "event", { moment: "add-request" });
350
+ addStartedAt = performance.now();
351
+ emitWaitTrace(params.sourceKey, "W1", "begin", { startedAt: addStartedAt, moment: "add-request" });
350
352
  pending = pool.getTorrent(params.sourceType, params.source, {
351
353
  sourceKey: params.sourceKey, trace: params.trace === true, emitWaitTrace
352
354
  });
@@ -366,7 +368,22 @@ async function runCommand(command, params, id) {
366
368
  // empty list, and rebuilding it here would bring it back on every stream
367
369
  // and stats request, which all come through this command. Any other
368
370
  // destroyed torrent is rebuilt, because its files still need it.
369
- const added = await pending;
371
+ let added;
372
+ try {
373
+ added = await pending;
374
+ if (params.trace === true && addStartedAt !== null) {
375
+ const endedAt = performance.now();
376
+ emitWaitTrace(params.sourceKey, "W1", "end", { endedAt, durationMs: endedAt - addStartedAt,
377
+ outcome: "ready" });
378
+ }
379
+ } catch (error) {
380
+ if (params.trace === true && addStartedAt !== null) {
381
+ const endedAt = performance.now();
382
+ emitWaitTrace(params.sourceKey, "W1", "end", { endedAt, durationMs: endedAt - addStartedAt,
383
+ outcome: "error", error: String(error?.message ?? error) });
384
+ }
385
+ throw error;
386
+ }
370
387
  const described = isUsableTorrentHandle(added) ? null : wholeSources.describe(params.sourceKey);
371
388
  if (described) {
372
389
  announceWholeFiles(params.sourceKey, described);