@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
|
@@ -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 (
|
|
1336
|
-
const
|
|
1337
|
-
|
|
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
|
|
1515
|
-
//
|
|
1516
|
-
//
|
|
1517
|
-
//
|
|
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
|