@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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1559
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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);
|