@torrent-tv/proxy 2.103.0 → 2.104.1
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
|
@@ -27,6 +27,7 @@ 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
29
|
import { tracePieceStorePut } from "./worker/trace-piece-store-put.js";
|
|
30
|
+
import { beginSwarmLifecycleTrace, noteSwarmDiscovery, noteSwarmMetadata, noteSwarmPeer } from "./worker/trace-swarm-lifecycle.js";
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* The second-peer requests for the last blocks of a piece somebody waits for,
|
|
@@ -967,7 +968,8 @@ export class TorrentPool {
|
|
|
967
968
|
/**
|
|
968
969
|
* When each torrent was added, and when its first peer connected.
|
|
969
970
|
*
|
|
970
|
-
* @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 }>}
|
|
971
973
|
*/
|
|
972
974
|
#swarmTimingByTorrent = new WeakMap();
|
|
973
975
|
#waitTraceByTorrent = new WeakMap();
|
|
@@ -1509,10 +1511,16 @@ export class TorrentPool {
|
|
|
1509
1511
|
if (waitTrace?.trace === true && typeof waitTrace.sourceKey === "string") {
|
|
1510
1512
|
this.#waitTraceByTorrent.set(torrent, waitTrace);
|
|
1511
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
|
+
}
|
|
1512
1519
|
const trace = (process, phase, fields = {}) => {
|
|
1513
1520
|
const context = this.#waitTraceByTorrent.get(torrent);
|
|
1514
1521
|
if (context?.trace === true) context.emitWaitTrace?.(context.sourceKey, process, phase, fields);
|
|
1515
1522
|
};
|
|
1523
|
+
const pool = this;
|
|
1516
1524
|
const wrap = (method, process) => {
|
|
1517
1525
|
const original = torrent[method];
|
|
1518
1526
|
if (typeof original !== "function") return;
|
|
@@ -1521,20 +1529,32 @@ export class TorrentPool {
|
|
|
1521
1529
|
trace(process, "begin", { startedAt });
|
|
1522
1530
|
try {
|
|
1523
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
|
+
};
|
|
1524
1538
|
if (result && typeof result.then === "function") {
|
|
1525
1539
|
return result.then(value => {
|
|
1526
|
-
|
|
1540
|
+
noteDiscoveryEnd();
|
|
1541
|
+
const endedAt = performance.now();
|
|
1542
|
+
trace(process, "end", { startedAt, endedAt, durationMs: endedAt - startedAt, outcome: "resolved" });
|
|
1527
1543
|
return value;
|
|
1528
1544
|
}, error => {
|
|
1529
|
-
|
|
1545
|
+
const endedAt = performance.now();
|
|
1546
|
+
trace(process, "end", { startedAt, endedAt, durationMs: endedAt - startedAt, outcome: "error",
|
|
1530
1547
|
error: String(error?.message ?? error) });
|
|
1531
1548
|
throw error;
|
|
1532
1549
|
});
|
|
1533
1550
|
}
|
|
1534
|
-
|
|
1551
|
+
noteDiscoveryEnd();
|
|
1552
|
+
const endedAt = performance.now();
|
|
1553
|
+
trace(process, "end", { startedAt, endedAt, durationMs: endedAt - startedAt, outcome: "returned" });
|
|
1535
1554
|
return result;
|
|
1536
1555
|
} catch (error) {
|
|
1537
|
-
|
|
1556
|
+
const endedAt = performance.now();
|
|
1557
|
+
trace(process, "end", { startedAt, endedAt, durationMs: endedAt - startedAt, outcome: "error",
|
|
1538
1558
|
error: String(error?.message ?? error) });
|
|
1539
1559
|
throw error;
|
|
1540
1560
|
}
|
|
@@ -1542,7 +1562,6 @@ export class TorrentPool {
|
|
|
1542
1562
|
};
|
|
1543
1563
|
if (waitTrace?.trace === true && !this.#waitTraceHooks.has(torrent)) {
|
|
1544
1564
|
this.#waitTraceHooks.add(torrent);
|
|
1545
|
-
wrap("_onListening", "W1");
|
|
1546
1565
|
wrap("_startDiscovery", "W2");
|
|
1547
1566
|
wrap("_onStore", "W6");
|
|
1548
1567
|
tracePieceStorePut(pieceStoreOf(torrent), trace);
|
|
@@ -1557,8 +1576,17 @@ export class TorrentPool {
|
|
|
1557
1576
|
}, ...args);
|
|
1558
1577
|
};
|
|
1559
1578
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
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
|
+
});
|
|
1562
1590
|
}
|
|
1563
1591
|
// Attached ONCE per torrent. WebTorrent answers a duplicate add by handing
|
|
1564
1592
|
// back the torrent it already has, and this used to run again on it: a
|
|
@@ -1569,7 +1597,7 @@ export class TorrentPool {
|
|
|
1569
1597
|
// once as a .torrent and once as a magnet is exactly that case, and the
|
|
1570
1598
|
// roadmap already records it happening. Trace hooks above are installed
|
|
1571
1599
|
// separately and only for an explicitly traced source.
|
|
1572
|
-
if (
|
|
1600
|
+
if (alreadyTracked) {
|
|
1573
1601
|
return;
|
|
1574
1602
|
}
|
|
1575
1603
|
// A torrent nobody has asked for yet does not exist: this is called the
|
|
@@ -1590,9 +1618,6 @@ export class TorrentPool {
|
|
|
1590
1618
|
// for the whole life of the torrent, which is the same fault as three
|
|
1591
1619
|
// different hashes with the hash removed.
|
|
1592
1620
|
const label = () => String(torrent.infoHash ?? "?").slice(0, 8);
|
|
1593
|
-
const addedAt = Date.now();
|
|
1594
|
-
this.#swarmTimingByTorrent.set(torrent, { addedAt, firstPeerAt: null });
|
|
1595
|
-
|
|
1596
1621
|
torrent.on("download-request-cancelled", ({ piece, offset, length, reason, requestId }) => {
|
|
1597
1622
|
logger.info(`download request=${requestId ?? `${torrent.infoHash ?? "unknown"}:${piece}:${offset}`} cancel bytes=${length} reason=${reason}`);
|
|
1598
1623
|
});
|
|
@@ -1657,6 +1682,7 @@ export class TorrentPool {
|
|
|
1657
1682
|
return;
|
|
1658
1683
|
}
|
|
1659
1684
|
timing.firstPeerAt = Date.now();
|
|
1685
|
+
timing.firstPeerAtMonotonic ??= performance.now();
|
|
1660
1686
|
const waited = secondsToFirstPeer(timing.addedAt, timing.firstPeerAt);
|
|
1661
1687
|
const byTracker = this.#lastAnnounceByTorrent.get(torrent);
|
|
1662
1688
|
const offered = bestAnnounce(byTracker ? byTracker.values() : []);
|
|
@@ -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);
|
|
@@ -477,43 +494,50 @@ async function runCommand(command, params, id) {
|
|
|
477
494
|
const statsStartedAt = performance.now();
|
|
478
495
|
emitWaitTrace(params.sourceKey, "W10", "begin", { startedAt: statsStartedAt,
|
|
479
496
|
fileIndex: params.fileIndex, forecast: params.forecast === true });
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
endedAt: performance.now(), outcome: whole ? "whole-file" : "pending", fileIndex: params.fileIndex });
|
|
485
|
-
return whole ? wholeFileStats(whole.length) : { pending: true };
|
|
486
|
-
}
|
|
487
|
-
// What this file's own interruptions demand, measured by the reader in
|
|
488
|
-
// this thread. It travels with the stats because the caller asking for
|
|
489
|
-
// them is the one that has to decide with them — the browser's smallest
|
|
490
|
-
// safe buffer, and the speed a quality step must sustain. Null until a
|
|
491
|
-
// second interruption has been seen: one wait shows no interval, and an
|
|
492
|
-
// interval invented from one point is exactly what this work removes.
|
|
493
|
-
const file = Array.isArray(torrent?.files) ? torrent.files[params.fileIndex] : null;
|
|
494
|
-
const forecastAt = performance.now();
|
|
495
|
-
const forecast = file && params.forecast === true ? downloadForecastOf(torrent) : null;
|
|
496
|
-
const forecastMs = params.forecast === true ? performance.now() - forecastAt : 0;
|
|
497
|
-
const stats = pool.getFileStats(torrent, params.fileIndex);
|
|
498
|
-
const ranges = [];
|
|
499
|
-
if (forecast) for (const [piece, availableAt] of forecast.arrivals) {
|
|
500
|
-
const start = piece * torrent.pieceLength;
|
|
501
|
-
const end = Math.min(torrent.length, (piece + 1) * torrent.pieceLength);
|
|
502
|
-
if (end > start) ranges.push({ start, end, availableAt });
|
|
503
|
-
}
|
|
504
|
-
emitWaitTrace(params.sourceKey, "W10", "end", { startedAt: statsStartedAt,
|
|
505
|
-
endedAt: performance.now(), outcome: "ok", fileIndex: params.fileIndex,
|
|
506
|
-
forecast: params.forecast === true });
|
|
507
|
-
return {
|
|
508
|
-
...stats,
|
|
509
|
-
...(params.forecast === true ? { downloadForecast: {
|
|
510
|
-
measuredAt: forecast?.measuredAt ?? Date.now(), global: true, ranges
|
|
511
|
-
} } : {}),
|
|
512
|
-
supply: supplyFiguresFor(torrent?.infoHash, file?.name, params.segmentSeconds ?? 4),
|
|
513
|
-
// How long this answer waited for the arrival forecast, which the
|
|
514
|
-
// caller says beside its own time (ViewerRequests, torrent-tv/meta#178).
|
|
515
|
-
forecastMs
|
|
497
|
+
const finishStatsTrace = (outcome, fields = {}) => {
|
|
498
|
+
const endedAt = performance.now();
|
|
499
|
+
emitWaitTrace(params.sourceKey, "W10", "end", { startedAt: statsStartedAt, endedAt,
|
|
500
|
+
durationMs: endedAt - statsStartedAt, outcome, fileIndex: params.fileIndex, ...fields });
|
|
516
501
|
};
|
|
502
|
+
try {
|
|
503
|
+
const torrent = await knownTorrent(params.sourceKey);
|
|
504
|
+
if (!torrent) {
|
|
505
|
+
const whole = wholeSources.fileOf(params.sourceKey, params.fileIndex);
|
|
506
|
+
finishStatsTrace(whole ? "whole-file" : "pending");
|
|
507
|
+
return whole ? wholeFileStats(whole.length) : { pending: true };
|
|
508
|
+
}
|
|
509
|
+
// What this file's own interruptions demand, measured by the reader in
|
|
510
|
+
// this thread. It travels with the stats because the caller asking for
|
|
511
|
+
// them is the one that has to decide with them — the browser's smallest
|
|
512
|
+
// safe buffer, and the speed a quality step must sustain. Null until a
|
|
513
|
+
// second interruption has been seen: one wait shows no interval, and an
|
|
514
|
+
// interval invented from one point is exactly what this work removes.
|
|
515
|
+
const file = Array.isArray(torrent?.files) ? torrent.files[params.fileIndex] : null;
|
|
516
|
+
const forecastAt = performance.now();
|
|
517
|
+
const forecast = file && params.forecast === true ? downloadForecastOf(torrent) : null;
|
|
518
|
+
const forecastMs = params.forecast === true ? performance.now() - forecastAt : 0;
|
|
519
|
+
const stats = pool.getFileStats(torrent, params.fileIndex);
|
|
520
|
+
const ranges = [];
|
|
521
|
+
if (forecast) for (const [piece, availableAt] of forecast.arrivals) {
|
|
522
|
+
const start = piece * torrent.pieceLength;
|
|
523
|
+
const end = Math.min(torrent.length, (piece + 1) * torrent.pieceLength);
|
|
524
|
+
if (end > start) ranges.push({ start, end, availableAt });
|
|
525
|
+
}
|
|
526
|
+
finishStatsTrace("ok", { forecast: params.forecast === true });
|
|
527
|
+
return {
|
|
528
|
+
...stats,
|
|
529
|
+
...(params.forecast === true ? { downloadForecast: {
|
|
530
|
+
measuredAt: forecast?.measuredAt ?? Date.now(), global: true, ranges
|
|
531
|
+
} } : {}),
|
|
532
|
+
supply: supplyFiguresFor(torrent?.infoHash, file?.name, params.segmentSeconds ?? 4),
|
|
533
|
+
// How long this answer waited for the arrival forecast, which the
|
|
534
|
+
// caller says beside its own time (ViewerRequests, torrent-tv/meta#178).
|
|
535
|
+
forecastMs
|
|
536
|
+
};
|
|
537
|
+
} catch (error) {
|
|
538
|
+
finishStatsTrace("error", { error: String(error?.message ?? error) });
|
|
539
|
+
throw error;
|
|
540
|
+
}
|
|
517
541
|
}
|
|
518
542
|
|
|
519
543
|
case Command.PRIORITY_MAP: {
|
|
@@ -527,18 +551,27 @@ async function runCommand(command, params, id) {
|
|
|
527
551
|
// Nor may one for a file held whole, which has nothing left to fetch.
|
|
528
552
|
const torrent = await knownTorrent(params.sourceKey);
|
|
529
553
|
if (!torrent) {
|
|
554
|
+
emitWaitTrace(params.sourceKey, "W7", "event", { outcome: "source-not-ready", fileIndex: params.fileIndex });
|
|
530
555
|
return { appliedMs: null, receivedAt, repliedAt: performance.timeOrigin + performance.now() };
|
|
531
556
|
}
|
|
532
557
|
// What applying a map costs this thread, told to whoever sent it: every
|
|
533
558
|
// zone becomes a window of the register and the swarm is told again
|
|
534
559
|
// (torrent-tv/meta#166).
|
|
535
560
|
const startedAt = performance.now();
|
|
536
|
-
emitWaitTrace(params.sourceKey, "W7", "begin", { fileIndex: params.fileIndex });
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
561
|
+
emitWaitTrace(params.sourceKey, "W7", "begin", { startedAt, fileIndex: params.fileIndex });
|
|
562
|
+
try {
|
|
563
|
+
ensureArrivalsWired(params.sourceKey, torrent);
|
|
564
|
+
pool.applyPriorityMap(torrent, params.fileIndex, zones, params.durationSeconds);
|
|
565
|
+
torrent.emit("priority-map-changed", params.fileIndex);
|
|
566
|
+
const endedAt = performance.now();
|
|
567
|
+
emitWaitTrace(params.sourceKey, "W7", "end", { startedAt, endedAt, durationMs: endedAt - startedAt,
|
|
568
|
+
outcome: "applied", fileIndex: params.fileIndex });
|
|
569
|
+
} catch (error) {
|
|
570
|
+
const endedAt = performance.now();
|
|
571
|
+
emitWaitTrace(params.sourceKey, "W7", "end", { startedAt, endedAt, durationMs: endedAt - startedAt,
|
|
572
|
+
outcome: "error", error: String(error?.message ?? error), fileIndex: params.fileIndex });
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
542
575
|
return { appliedMs: performance.now() - startedAt, receivedAt, repliedAt: performance.timeOrigin + performance.now() };
|
|
543
576
|
}
|
|
544
577
|
|