@torrent-tv/proxy 2.39.0 → 2.39.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/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 2.39.1
2
+
3
+ - **Fix**: The block duplication of 2.39.0 is removed, because measured against what a viewer actually feels it never paid. 2.39.0 was shipped on a measurement of the median wait for a piece; the quantity that matters is the seconds the picture stands still, and measured on that (2026-08-19, a reader paced at the film's own byte rate with an eight-second lead, arms alternated per position) it is neutral at best and costly at worst. On a well-seeded film every arm read 67 MB in 59 s and stopped for at most 2.6 s — nothing to improve. With the download capped just above the film's rate, which is what a home line IS whenever the swarm can fill it, duplication made the picture stop for **28.8 s against 12.8 s** at one position and left the other unchanged: under one shared budget a duplicate spends the very bytes it is trying to go around. The regime where it could pay — several slow peers, each with capacity of its own — could not be reproduced: the thin-swarm candidate turned out to have no live swarm at all (4 peers, 179.5 s of a 209 s run spent stopped). A lever with no measured gain and a measured cost does not stay on by default. Narrowing the read window to the blocked piece was tried in the same experiment and is not shipped for the same reason: it was never better and reached **44.8 s against 12.8 s**. What stays is the measurement that decided it (`research/tail-duplication-2026-08-19.md`) and the `tail …` line from 2.38.1, which is what will say whether a real thin swarm ever behaves differently.
4
+
1
5
  ## 2.39.0
2
6
 
3
7
  - **New**: When a reader is blocked and nothing can be steered, the blocks it is still waiting on are asked of a second wire as well. WebTorrent reserves each block for exactly one wire, so once `Piece.reserve()` answers -1 the read ends when the holder of the last block delivers it, however fast the rest of the swarm is. The library's own `_hotswap` does exactly the right thing — `piece.cancel(block)` frees the reservation while the first request stays in flight — but only for a wire under 48 KB/s and twice as slow as the asker, and measured on a real swarm the tails a reader waits on sit at 109-886 KB/s. Peers the library rightly calls good, because for bulk downloading they are; the gate is about throughput across a torrent and knows nothing about a reader blocked on one piece now. Speed is not even what is wrong with them: two blocks — 32 KB — on a wire measured at 109 KB/s is 0.3 s of transfer, and that read waited 4.6 s, because the blocks are queued behind that wire's other work. **Measured against itself, same film, same positions, arms alternated, six pairs across two pacing rates: the median wait for a piece fell in all six — 39 %, 48 %, 68 %, 6 %, 50 % and 28 %.** In the one pair where the swarm had spare capacity throughout, the wait fell 7226→5174 ms and the lead the reader kept ended at +2.0 MB instead of +0.3 MB. Cost: one duplicate per candidate wire per attempt, about 3 % extra traffic, and only while a reader is blocked with every block already spoken for. The tail measurement added in 2.38.1 reports what it placed: `duplicated 14 blocks`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.39.0",
3
+ "version": "2.39.1",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -197,83 +197,3 @@ export function describePieceTail(torrent, pieceIndex) {
197
197
  outstanding.sort((left, right) => left.bytesPerSecond - right.bytesPerSecond);
198
198
  return { chunks, missing, outstanding };
199
199
  }
200
-
201
- /**
202
- * Ask a second wire for blocks the reader is still waiting on.
203
- *
204
- * WebTorrent reserves each block for exactly one wire, so once `Piece.reserve()`
205
- * answers -1 there is no request left to place and the read ends when the
206
- * holder of the last block delivers it — however fast the rest of the swarm is.
207
- * The library's own remedy is `_hotswap`, and it does precisely the right
208
- * thing: `piece.cancel(blockIndex)` frees the reservation while leaving the
209
- * first wire's request in flight, so a second wire can be asked for the same
210
- * block and whichever arrives first wins. What it will not do is apply that to
211
- * these tails. It is gated on speed — the held-up wire must be under 48 KB/s
212
- * (`3 * BLOCK_LENGTH`) and at least twice as slow as the asker — and measured
213
- * on a real swarm 2026-08-19, the tails a reader waits on sit at 109, 317 and
214
- * 870 KB/s. Peers the library rightly considers good, because for bulk
215
- * downloading they are; the gate is about throughput across a torrent and knows
216
- * nothing about a reader blocked on one piece now.
217
- *
218
- * Speed is not even what is wrong with them: two blocks — 32 KB — on a wire
219
- * measured at 109 KB/s is 0.3 s of transfer, and that read waited 4.6 s. The
220
- * blocks are not travelling slowly, they are queued behind that wire's other
221
- * work, which no average speed can show.
222
- *
223
- * The cost is bounded by the same measurement: the tails were 2 to 14 blocks of
224
- * 512, so 32 to 224 KB against the 8 MiB piece they hold up. At most one
225
- * duplicate is asked per candidate wire per call, and the caller repeats every
226
- * half second, so a longer tail is covered across attempts rather than in one
227
- * burst.
228
- *
229
- * Safe against the completion path: a block that arrives twice is dropped by
230
- * `Piece.set`, which writes only into an empty slot, and a piece already
231
- * flushed answers `false` from `Piece.init` so the second arrival returns
232
- * before `flush()` is reached.
233
- *
234
- * @param {object} torrent
235
- * @param {number} pieceIndex
236
- * @returns {{ duplicated: number, missing: number, wires: number }}
237
- * `duplicated` counts requests the library actually placed for a block that
238
- * was already outstanding elsewhere.
239
- */
240
- export function duplicateTailFor(torrent, pieceIndex) {
241
- const piece = torrent?.pieces?.[pieceIndex];
242
- const buffer = Array.isArray(piece?._buffer) ? piece._buffer : null;
243
- if (!piece || !buffer || typeof piece.cancel !== "function") {
244
- return { duplicated: 0, missing: 0, wires: 0 };
245
- }
246
- const chunks = Number.isFinite(piece._chunks) ? piece._chunks : buffer.length;
247
- const missing = [];
248
- for (let index = 0; index < chunks; index += 1) {
249
- if (!buffer[index]) {
250
- missing.push(index);
251
- }
252
- }
253
- const candidates = wiresForPiece(torrent, pieceIndex);
254
- if (missing.length === 0 || candidates.length === 0) {
255
- return { duplicated: 0, missing: missing.length, wires: candidates.length };
256
- }
257
-
258
- let duplicated = 0;
259
- // One block per wire: that is what the pipelines can usefully take at once,
260
- // and it needs no number of its own.
261
- for (let index = 0; index < Math.min(missing.length, candidates.length); index += 1) {
262
- // Freeing the reservation is what makes the block askable again; the
263
- // request already in flight is deliberately left alone, because the point
264
- // is to have two of them.
265
- piece.cancel(missing[index]);
266
- // `false` is hotswap: the library's own swap must not run on top of this,
267
- // or it would free a THIRD wire's block as well.
268
- if (torrent._request(candidates[index], pieceIndex, false) === true) {
269
- duplicated += 1;
270
- continue;
271
- }
272
- // The wire's pipeline is full. The block stays in the piece's cancellation
273
- // stack and will be handed to whoever asks next, which is harmless — it is
274
- // already in flight elsewhere — but there is no point asking the remaining
275
- // wires, whose pipelines are no emptier.
276
- break;
277
- }
278
- return { duplicated, missing: missing.length, wires: candidates.length };
279
- }
@@ -22,12 +22,7 @@
22
22
 
23
23
  import { findSharedStore } from "../piece-store/shared-piece-store.js";
24
24
  import { logger } from "../../utils/logger.js";
25
- import {
26
- askFastestWiresFor,
27
- canPlaceRequests,
28
- describePieceTail,
29
- duplicateTailFor
30
- } from "./fastest-wires.js";
25
+ import { askFastestWiresFor, canPlaceRequests, describePieceTail } from "./fastest-wires.js";
31
26
  import { minimumBufferFrom, requiredSpeedFrom } from "../supply-margin.js";
32
27
 
33
28
  /** Only waits at least this long are reported; sequential reading stays silent. */
@@ -577,20 +572,11 @@ export async function* readFragments({
577
572
  // again every half second and the piece changes under it; the last such
578
573
  // reading is kept, so the line describes the most recent failure.
579
574
  let tailWhenNothingPlaced = null;
580
- // What duplicating the tail placed, summed over the wait. Measured
581
- // 2026-08-19 on a real swarm: the blocks a reader waits on are 2-14 of
582
- // 512 and sit on wires the library considers fast, so its own hotswap
583
- // never fires for them — see `duplicateTailFor`.
584
- let duplicated = 0;
585
575
  const pushToFastest = () => {
586
576
  try {
587
577
  const result = askFastestWiresFor(torrent, pieceIndex);
588
578
  if (result.asked === 0) {
589
579
  tailWhenNothingPlaced = describePieceTail(torrent, pieceIndex);
590
- // Nothing could be placed the ordinary way, which means every block
591
- // is spoken for. That is exactly when a second copy of the last
592
- // blocks is worth asking for.
593
- duplicated += duplicateTailFor(torrent, pieceIndex).duplicated;
594
580
  }
595
581
  pushed = {
596
582
  asked: pushed.asked + result.asked,
@@ -696,10 +682,7 @@ export async function* readFragments({
696
682
  (wire.choking ? " (choking)" : ""))
697
683
  .join(" ")
698
684
  : "nobody")
699
- : "") +
700
- // What we did about the tail, so the next session says by number
701
- // whether a second copy of those blocks shortens the wait.
702
- (duplicated > 0 ? `; duplicated ${duplicated} blocks` : "")
685
+ : "")
703
686
  );
704
687
  }
705
688
 
@@ -1,123 +0,0 @@
1
- /**
2
- * @file Asking a second wire for the blocks a reader is still waiting on.
3
- *
4
- * Measured on a real swarm 2026-08-19: a blocked read's tail is 2-14 blocks of
5
- * 512, and it sits on wires at 109-886 KB/s — above the 48 KB/s gate that
6
- * WebTorrent's own `_hotswap` requires, so the library never duplicates them.
7
- * The mechanism is the library's: free the reservation with `Piece.cancel` and
8
- * leave the first request in flight, then ask a second wire through the
9
- * library's own request path.
10
- */
11
-
12
- import test from "node:test";
13
- import assert from "node:assert/strict";
14
- import { duplicateTailFor } from "../services/torrent-worker/fastest-wires.js";
15
-
16
- /** A piece with `chunks` blocks, of which `missing` at the end are absent. */
17
- function piece(chunks, missing) {
18
- const buffer = new Array(chunks);
19
- for (let index = 0; index < chunks - missing; index += 1) {
20
- buffer[index] = new Uint8Array(1);
21
- }
22
- const cancelled = [];
23
- return {
24
- _buffer: buffer,
25
- _chunks: chunks,
26
- cancel: (index) => cancelled.push(index),
27
- cancelled
28
- };
29
- }
30
-
31
- function wire({ speed = 500_000, has = true } = {}) {
32
- return {
33
- peerPieces: { get: () => has },
34
- downloadSpeed: () => speed,
35
- peerChoking: false,
36
- destroyed: false,
37
- requests: []
38
- };
39
- }
40
-
41
- test("the missing blocks are freed and asked of a second wire", () => {
42
- const target = piece(512, 3);
43
- const placed = [];
44
- const torrent = {
45
- pieces: [target],
46
- wires: [wire({ speed: 900_000 }), wire({ speed: 800_000 }), wire({ speed: 700_000 })],
47
- _request: (wireAsked, index) => {
48
- placed.push({ wireAsked, index });
49
- return true;
50
- }
51
- };
52
-
53
- const result = duplicateTailFor(torrent, 0);
54
-
55
- assert.equal(result.missing, 3, "the tail is what is still absent from the piece");
56
- assert.equal(result.duplicated, 3);
57
- assert.deepEqual(target.cancelled, [509, 510, 511], "each block's reservation is freed first");
58
- assert.equal(placed.length, 3, "and each is then asked of a wire through the library's own path");
59
- assert.equal(placed[0].wireAsked, torrent.wires[0], "fastest wire first");
60
- });
61
-
62
- test("at most one duplicate per wire, however long the tail", () => {
63
- const target = piece(512, 40);
64
- const torrent = {
65
- pieces: [target],
66
- wires: [wire(), wire()],
67
- _request: () => true
68
- };
69
-
70
- const result = duplicateTailFor(torrent, 0);
71
-
72
- assert.equal(result.duplicated, 2, "two wires, two duplicates — the rest waits for the next attempt");
73
- assert.equal(target.cancelled.length, 2, "and no reservation is freed that nobody was asked for");
74
- });
75
-
76
- test("a wire whose pipeline is full ends the attempt", () => {
77
- const target = piece(512, 5);
78
- const torrent = {
79
- pieces: [target],
80
- wires: [wire(), wire(), wire()],
81
- // The library refuses when the wire already has as many requests as its
82
- // measured speed justifies.
83
- _request: () => false
84
- };
85
-
86
- const result = duplicateTailFor(torrent, 0);
87
-
88
- assert.equal(result.duplicated, 0);
89
- assert.equal(
90
- target.cancelled.length,
91
- 1,
92
- "only the block that was actually offered is freed; the rest keep their reservations"
93
- );
94
- });
95
-
96
- test("a piece with nothing missing is left alone", () => {
97
- const target = piece(4, 0);
98
- const torrent = { pieces: [target], wires: [wire()], _request: () => true };
99
-
100
- const result = duplicateTailFor(torrent, 0);
101
-
102
- assert.equal(result.duplicated, 0);
103
- assert.equal(result.missing, 0);
104
- assert.equal(target.cancelled.length, 0);
105
- });
106
-
107
- test("with no wire holding the piece there is nothing to duplicate onto", () => {
108
- const target = piece(8, 2);
109
- const torrent = { pieces: [target], wires: [wire({ has: false })], _request: () => true };
110
-
111
- const result = duplicateTailFor(torrent, 0);
112
-
113
- assert.equal(result.wires, 0);
114
- assert.equal(result.duplicated, 0);
115
- assert.equal(target.cancelled.length, 0, "and no reservation is freed for nobody");
116
- });
117
-
118
- test("a completed piece is not touched", () => {
119
- assert.deepEqual(
120
- duplicateTailFor({ pieces: [null], wires: [wire()], _request: () => true }, 0),
121
- { duplicated: 0, missing: 0, wires: 0 }
122
- );
123
- });