@torrent-tv/proxy 2.70.0 → 2.71.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 +20 -0
- package/CLAUDE.md +12 -0
- package/docs/download-architecture.md +175 -0
- package/docs/logs.md +8 -0
- package/package.json +1 -1
- package/services/demand/DemandRegister.js +182 -0
- package/services/demand/Urgency.js +137 -0
- package/services/demand/Window.js +118 -0
- package/services/demand/index.js +11 -0
- package/services/demand/pieces.js +140 -0
- package/services/download/SwarmSelection.js +367 -0
- package/services/download/index.js +8 -0
- package/services/download/registry.js +96 -0
- package/services/piece-store/shared-piece-store.js +16 -0
- package/services/playback-planner.js +15 -1
- package/services/torrent-pool.js +64 -197
- package/services/torrent-worker/fastest-wires.js +319 -279
- package/services/torrent-worker/piece-reader.js +149 -183
- package/services/torrent-worker/worker.js +23 -3
- package/test/demand-register.test.js +195 -0
- package/test/fastest-wires.test.js +23 -1
- package/test/read-bands.test.js +17 -10
- package/test/read-window.test.js +20 -8
- package/test/swarm-selection.test.js +220 -0
- package/utils/logger.js +62 -20
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { findSharedStore } from "../piece-store/shared-piece-store.js";
|
|
24
|
+
import { bytesOf, Urgency, urgencyName } from "../demand/index.js";
|
|
25
|
+
import { demandFor } from "../download/registry.js";
|
|
24
26
|
import { logger } from "../../utils/logger.js";
|
|
25
27
|
import {
|
|
26
28
|
askFastestWiresFor,
|
|
@@ -75,9 +77,17 @@ const READ_WINDOW_BYTES = 32 * 1024 * 1024;
|
|
|
75
77
|
* described above. Anything else — the default — alternates per read, so the two
|
|
76
78
|
* accumulate side by side from real viewing and the log can compare them.
|
|
77
79
|
*/
|
|
78
|
-
const READ_MODE_SETTING = process.env.TORRENT_TV_READ_MODE ?? "";
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Which level of urgency each band states.
|
|
83
|
+
*
|
|
84
|
+
* Not numbers handed to WebTorrent: measured against the vendored 2.8.5, the
|
|
85
|
+
* library keeps distinct non-zero priorities only until the first wire is
|
|
86
|
+
* served and round-robins them afterwards. The ordering is kept by WHAT is
|
|
87
|
+
* stated, in `services/demand/`, and these say which level each band belongs
|
|
88
|
+
* to (roadmap item 5).
|
|
89
|
+
*/
|
|
90
|
+
const BAND_URGENCY = [Urgency.NEAR, Urgency.AHEAD, Urgency.AHEAD, Urgency.BEHIND];
|
|
81
91
|
|
|
82
92
|
/**
|
|
83
93
|
* Where each band sits, given the urgent one and how far the trailing bands have
|
|
@@ -91,10 +101,10 @@ const BAND_PRIORITIES = [4, 3, 2, 1];
|
|
|
91
101
|
* picture they are watching.
|
|
92
102
|
*
|
|
93
103
|
* @param {{ urgent: { from: number, to: number }, pieceIndex: number, firstPiece: number, lastPiece: number, widths: { near: number, far: number } }} params
|
|
94
|
-
* @returns {Array<{ from: number, to: number,
|
|
104
|
+
* @returns {Array<{ from: number, to: number, urgency: number }>}
|
|
95
105
|
*/
|
|
96
106
|
export function bandsFrom({ urgent, pieceIndex, firstPiece, lastPiece, widths }) {
|
|
97
|
-
const bands = [{ from: urgent.from, to: urgent.to,
|
|
107
|
+
const bands = [{ from: urgent.from, to: urgent.to, urgency: BAND_URGENCY[0] }];
|
|
98
108
|
let edge = urgent.to;
|
|
99
109
|
for (let index = 0; index < 2; index += 1) {
|
|
100
110
|
if (edge >= lastPiece) {
|
|
@@ -106,13 +116,13 @@ export function bandsFrom({ urgent, pieceIndex, firstPiece, lastPiece, widths })
|
|
|
106
116
|
}
|
|
107
117
|
const from = edge + 1;
|
|
108
118
|
const to = Math.min(lastPiece, edge + width);
|
|
109
|
-
bands.push({ from, to,
|
|
119
|
+
bands.push({ from, to, urgency: BAND_URGENCY[index + 1] });
|
|
110
120
|
edge = to;
|
|
111
121
|
}
|
|
112
122
|
// Only once the lead has nothing left to cover: asking for the past while the
|
|
113
123
|
// future is still missing would take capacity from the picture being watched.
|
|
114
124
|
if (edge >= lastPiece && pieceIndex > firstPiece) {
|
|
115
|
-
bands.push({ from: firstPiece, to: pieceIndex - 1,
|
|
125
|
+
bands.push({ from: firstPiece, to: pieceIndex - 1, urgency: BAND_URGENCY[3] });
|
|
116
126
|
}
|
|
117
127
|
return bands;
|
|
118
128
|
}
|
|
@@ -172,7 +182,7 @@ export function sameBands(left, right) {
|
|
|
172
182
|
return left.every((band, index) =>
|
|
173
183
|
band.from === right[index].from &&
|
|
174
184
|
band.to === right[index].to &&
|
|
175
|
-
band.
|
|
185
|
+
band.urgency === right[index].urgency);
|
|
176
186
|
}
|
|
177
187
|
|
|
178
188
|
/**
|
|
@@ -217,100 +227,6 @@ export function nextWindowPieces({ current, base, ceiling, waitedMs, waitThresho
|
|
|
217
227
|
return Math.max(floor, now - 1);
|
|
218
228
|
}
|
|
219
229
|
|
|
220
|
-
/**
|
|
221
|
-
* Add this reader's window to the download set as a stream selection.
|
|
222
|
-
*
|
|
223
|
-
* `_select`/`_deselect` with the stream flag are what WebTorrent's own
|
|
224
|
-
* `FileIterator` uses; there is no public call for it, because the public
|
|
225
|
-
* `select` produces the merging, interval-subtracted kind whose bookkeeping
|
|
226
|
-
* cannot express "one of several readers wants this". Falls back to the public
|
|
227
|
-
* call if a future version drops the private one.
|
|
228
|
-
*
|
|
229
|
-
* @param {import("webtorrent").Torrent} torrent
|
|
230
|
-
* @param {{ from: number, to: number }} window
|
|
231
|
-
* @param {number} [priority] - 1 for what a reader needs next, 0 for the
|
|
232
|
-
* background fill of the rest of the file.
|
|
233
|
-
* @returns {void}
|
|
234
|
-
*/
|
|
235
|
-
function claimWindow(torrent, { from, to }, priority = 1) {
|
|
236
|
-
try {
|
|
237
|
-
if (typeof torrent._select === "function") {
|
|
238
|
-
torrent._select(from, to, priority, null, true);
|
|
239
|
-
} else if (typeof torrent.select === "function") {
|
|
240
|
-
torrent.select(from, to, priority);
|
|
241
|
-
}
|
|
242
|
-
} catch {
|
|
243
|
-
// Best effort — never fail a read because selection bookkeeping refused.
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Take this reader's window back out of the download set.
|
|
249
|
-
*
|
|
250
|
-
* The bounds must match the ones given to {@link claimWindow} exactly: a stream
|
|
251
|
-
* selection is removed by equality, not by overlap.
|
|
252
|
-
*
|
|
253
|
-
* @param {import("webtorrent").Torrent} torrent
|
|
254
|
-
* @param {{ from: number, to: number }} window
|
|
255
|
-
* @returns {void}
|
|
256
|
-
*/
|
|
257
|
-
function releaseWindow(torrent, { from, to }) {
|
|
258
|
-
try {
|
|
259
|
-
if (typeof torrent._deselect === "function") {
|
|
260
|
-
torrent._deselect(from, to, true);
|
|
261
|
-
} else if (typeof torrent.deselect === "function") {
|
|
262
|
-
torrent.deselect(from, to);
|
|
263
|
-
}
|
|
264
|
-
} catch {
|
|
265
|
-
// Best effort.
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Mark the piece a reader is blocked on, clearing the mark it set before.
|
|
271
|
-
*
|
|
272
|
-
* Criticality is never cleared by WebTorrent itself, so a reader that walked a
|
|
273
|
-
* film would leave every piece of it marked. Only the indices this reader set
|
|
274
|
-
* are cleared, so a second reader's mark on the same piece is not stolen — and
|
|
275
|
-
* the flag is advisory anyway.
|
|
276
|
-
*
|
|
277
|
-
* @param {import("webtorrent").Torrent} torrent
|
|
278
|
-
* @param {number} from
|
|
279
|
-
* @param {number} to
|
|
280
|
-
* @param {{ from: number, to: number } | null} previous
|
|
281
|
-
* @returns {{ from: number, to: number } | null}
|
|
282
|
-
*/
|
|
283
|
-
function markCritical(torrent, from, to, previous) {
|
|
284
|
-
if (previous && previous.from === from && previous.to === to) {
|
|
285
|
-
return previous;
|
|
286
|
-
}
|
|
287
|
-
if (previous) {
|
|
288
|
-
clearCritical(torrent, previous);
|
|
289
|
-
}
|
|
290
|
-
try {
|
|
291
|
-
torrent.critical?.(from, to);
|
|
292
|
-
} catch {
|
|
293
|
-
return null;
|
|
294
|
-
}
|
|
295
|
-
return { from, to };
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Drop critical marks this reader set.
|
|
300
|
-
*
|
|
301
|
-
* @param {import("webtorrent").Torrent} torrent
|
|
302
|
-
* @param {{ from: number, to: number }} mark
|
|
303
|
-
* @returns {void}
|
|
304
|
-
*/
|
|
305
|
-
function clearCritical(torrent, { from, to }) {
|
|
306
|
-
if (!Array.isArray(torrent._critical)) {
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
for (let index = from; index <= to; index += 1) {
|
|
310
|
-
torrent._critical[index] = false;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
230
|
/**
|
|
315
231
|
* Who is working on the piece a reader is blocked on, right now.
|
|
316
232
|
*
|
|
@@ -512,58 +428,69 @@ export function supplyFiguresFor(infoHash, fileName, segmentSeconds) {
|
|
|
512
428
|
const waitsBySteering = new Map();
|
|
513
429
|
|
|
514
430
|
/**
|
|
515
|
-
* Record one wait against
|
|
431
|
+
* Record one wait against the band the reader was stopped in.
|
|
432
|
+
*
|
|
433
|
+
* What this replaces: until 2026-09-02 each read was assigned at random to one
|
|
434
|
+
* of two ways of claiming — one band or four — and the waits were sorted by
|
|
435
|
+
* which. The comparison never decided anything, and could not: the split halved
|
|
436
|
+
* the sample for each arm, so on 2026-08-28 there were nine reads in one arm and
|
|
437
|
+
* three in the other and the ten-wait threshold was never reached in either; on
|
|
438
|
+
* 2026-08-29 the two arms printed together for the first and only time, as forty
|
|
439
|
+
* waits against one.
|
|
440
|
+
*
|
|
441
|
+
* This is the more useful question anyway. A wait belongs to a LEVEL, and the
|
|
442
|
+
* level says whether a width is wrong rather than whether the whole scheme is:
|
|
443
|
+
* long waits in the band being watched mean the urgent window is too narrow,
|
|
444
|
+
* long waits further out mean the lead is.
|
|
516
445
|
*
|
|
517
446
|
* @param {string} key
|
|
518
447
|
* @param {number} waitedMs
|
|
519
|
-
* @param {
|
|
448
|
+
* @param {number} urgency
|
|
520
449
|
* @returns {void}
|
|
521
450
|
*/
|
|
522
|
-
function
|
|
523
|
-
let
|
|
524
|
-
if (!
|
|
525
|
-
|
|
526
|
-
|
|
451
|
+
function noteWaitLevel(key, waitedMs, urgency) {
|
|
452
|
+
let byLevel = waitsByLevel.get(key);
|
|
453
|
+
if (!byLevel) {
|
|
454
|
+
byLevel = new Map();
|
|
455
|
+
waitsByLevel.set(key, byLevel);
|
|
527
456
|
}
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
while (
|
|
531
|
-
|
|
457
|
+
const waits = byLevel.get(urgency) ?? [];
|
|
458
|
+
waits.push(waitedMs);
|
|
459
|
+
while (waits.length > SUPPLY_WAIT_HISTORY) {
|
|
460
|
+
waits.shift();
|
|
532
461
|
}
|
|
462
|
+
byLevel.set(urgency, waits);
|
|
533
463
|
}
|
|
534
464
|
|
|
535
465
|
/**
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
* This is the whole of the comparison: the same file, the same swarm, the same
|
|
539
|
-
* viewer, waits sorted by which arm was in force. It appears in the periodic
|
|
540
|
-
* summary so a session can be read without collecting lines by hand.
|
|
466
|
+
* Where the waits fell, by level, or null while nothing has waited.
|
|
541
467
|
*
|
|
542
468
|
* @param {string} key
|
|
543
469
|
* @returns {string | null}
|
|
544
470
|
*/
|
|
545
|
-
function
|
|
546
|
-
const
|
|
547
|
-
if (!
|
|
471
|
+
function describeWaitLevels(key) {
|
|
472
|
+
const byLevel = waitsByLevel.get(key);
|
|
473
|
+
if (!byLevel || byLevel.size === 0) {
|
|
548
474
|
return null;
|
|
549
475
|
}
|
|
550
476
|
const middle = (values) => {
|
|
551
477
|
const sorted = [...values].sort((left, right) => left - right);
|
|
552
478
|
return sorted[Math.floor(sorted.length / 2)];
|
|
553
479
|
};
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
480
|
+
return [...byLevel.entries()]
|
|
481
|
+
.sort(([left], [right]) => left - right)
|
|
482
|
+
.map(([urgency, waits]) =>
|
|
483
|
+
`${urgencyName(urgency)} ${waits.length} waits median ${middle(waits)}ms ` +
|
|
484
|
+
`worst ${Math.max(...waits)}ms`)
|
|
485
|
+
.join(", ");
|
|
559
486
|
}
|
|
560
487
|
|
|
561
488
|
/**
|
|
562
|
-
* Waits split by
|
|
489
|
+
* Waits split by the level the reader was stopped in.
|
|
563
490
|
*
|
|
564
|
-
* @type {Map<string,
|
|
491
|
+
* @type {Map<string, Map<number, number[]>>}
|
|
565
492
|
*/
|
|
566
|
-
const
|
|
493
|
+
const waitsByLevel = new Map();
|
|
567
494
|
|
|
568
495
|
function noteSteeringOutcome(key, waitedMs, steered) {
|
|
569
496
|
let split = waitsBySteering.get(key);
|
|
@@ -712,7 +639,7 @@ function noteSupplyWait(key, label, waitedMs) {
|
|
|
712
639
|
// comparison with one side empty is not a comparison.
|
|
713
640
|
(describeSteering(key) ? ` — ${describeSteering(key)}` : "") +
|
|
714
641
|
// The comparison this release exists to make. Read it first.
|
|
715
|
-
(
|
|
642
|
+
(describeWaitLevels(key) ? ` — ${describeWaitLevels(key)}` : "")
|
|
716
643
|
);
|
|
717
644
|
}
|
|
718
645
|
|
|
@@ -735,6 +662,10 @@ export async function* readFragments({
|
|
|
735
662
|
if (!store) {
|
|
736
663
|
throw new Error("This torrent is not backed by a shared piece store.");
|
|
737
664
|
}
|
|
665
|
+
// What this reader wants goes here and nowhere else. `SwarmSelection` is the
|
|
666
|
+
// only thing that turns any of it into a request to the swarm, so a reader
|
|
667
|
+
// can no longer contradict the background fill or the pool.
|
|
668
|
+
const { register, selection } = demandFor(torrent);
|
|
738
669
|
|
|
739
670
|
const file = torrent.files?.[fileIndex];
|
|
740
671
|
if (!file) {
|
|
@@ -762,22 +693,6 @@ export async function* readFragments({
|
|
|
762
693
|
// `bytes 0-<EOF>` left a permanent selection over the entire file, and no
|
|
763
694
|
// later prioritisation could outrank it.
|
|
764
695
|
const basePieces = Math.max(1, Math.ceil(Math.max(1, windowBytes) / pieceLength));
|
|
765
|
-
/**
|
|
766
|
-
* How this reader claims what it wants: `flat` is one band of equal urgency,
|
|
767
|
-
* which is what every release before this did; `bands` puts the pieces the
|
|
768
|
-
* viewer is about to reach above the fill behind them, anchored at the first
|
|
769
|
-
* piece not already held.
|
|
770
|
-
*
|
|
771
|
-
* Alternated per read unless the deployment names one, so the comparison
|
|
772
|
-
* accumulates from real viewing instead of from a synthetic swarm — three
|
|
773
|
-
* such experiments in one day measured regimes the levers were not for and
|
|
774
|
-
* cost more than they settled.
|
|
775
|
-
*
|
|
776
|
-
* @type {"flat" | "bands"}
|
|
777
|
-
*/
|
|
778
|
-
const readMode = READ_MODE_SETTING === "flat" || READ_MODE_SETTING === "bands"
|
|
779
|
-
? READ_MODE_SETTING
|
|
780
|
-
: (Math.random() < 0.5 ? "flat" : "bands");
|
|
781
696
|
// What the window is RIGHT NOW. It starts at what the caller sized in seconds
|
|
782
697
|
// of playback and grows while the reader keeps being made to wait — see
|
|
783
698
|
// `nextWindowPieces`.
|
|
@@ -800,7 +715,7 @@ export async function* readFragments({
|
|
|
800
715
|
/** @type {{ from: number, to: number } | null} */
|
|
801
716
|
let window = null;
|
|
802
717
|
/** @type {{ from: number, to: number } | null} */
|
|
803
|
-
let
|
|
718
|
+
let blockedStated = false;
|
|
804
719
|
/**
|
|
805
720
|
* Drops the pin of the fragment currently in the consumer's hands, if it
|
|
806
721
|
* still holds one. See where it is assigned.
|
|
@@ -889,42 +804,70 @@ export async function* readFragments({
|
|
|
889
804
|
return lastWidths;
|
|
890
805
|
};
|
|
891
806
|
|
|
807
|
+
/**
|
|
808
|
+
* State one band as a need, in bytes.
|
|
809
|
+
*
|
|
810
|
+
* The claimant carries the level, so the four bands of one reader are four
|
|
811
|
+
* claimants and each is replaced on its own — restating the near band does
|
|
812
|
+
* not disturb what was said about the tail.
|
|
813
|
+
*
|
|
814
|
+
* @param {{ from: number, to: number, urgency: number }} band
|
|
815
|
+
* @returns {void}
|
|
816
|
+
*/
|
|
817
|
+
const stateBand = (band) => {
|
|
818
|
+
const range = bytesOf({
|
|
819
|
+
fileOffset: Number(file.offset),
|
|
820
|
+
fileLength: Number(file.length),
|
|
821
|
+
from: band.from,
|
|
822
|
+
to: band.to,
|
|
823
|
+
pieceLength
|
|
824
|
+
});
|
|
825
|
+
if (!range) {
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
register.state({
|
|
829
|
+
claimant: `${readerId}:${urgencyName(band.urgency)}`,
|
|
830
|
+
fileIndex,
|
|
831
|
+
byteStart: range.byteStart,
|
|
832
|
+
byteEnd: range.byteEnd,
|
|
833
|
+
urgency: band.urgency
|
|
834
|
+
});
|
|
835
|
+
};
|
|
836
|
+
|
|
892
837
|
const moveWindowTo = (pieceIndex) => {
|
|
893
|
-
const anchor =
|
|
838
|
+
const anchor = firstMissingFrom(pieceIndex);
|
|
894
839
|
const next = readWindowFor({ pieceIndex: anchor, lastPiece, windowPieces });
|
|
895
840
|
const sameUrgent = window && window.from === next.from && window.to === next.to;
|
|
896
|
-
if (sameUrgent && readMode === "flat") {
|
|
897
|
-
return;
|
|
898
|
-
}
|
|
899
841
|
const isJump = !window || next.from > window.to || next.from < window.from;
|
|
900
842
|
// A seek makes every width behind the urgent band meaningless: they were
|
|
901
843
|
// grown against a position the viewer has left, and what lies beyond the
|
|
902
844
|
// new one has to be earned from a standing start.
|
|
903
|
-
const wanted =
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
})
|
|
911
|
-
: [{ ...next, priority: BAND_PRIORITIES[0] }];
|
|
845
|
+
const wanted = bandsFrom({
|
|
846
|
+
urgent: next,
|
|
847
|
+
pieceIndex,
|
|
848
|
+
firstPiece,
|
|
849
|
+
lastPiece,
|
|
850
|
+
widths: bandWidths()
|
|
851
|
+
});
|
|
912
852
|
if (sameUrgent && sameBands(claimed, wanted)) {
|
|
913
853
|
return;
|
|
914
854
|
}
|
|
855
|
+
// Withdrawn by name, so a band that is no longer wanted goes and the rest
|
|
856
|
+
// stay. The swarm is told nothing here — `reconcile` is the only thing that
|
|
857
|
+
// speaks to the library, and it reads what is stated.
|
|
915
858
|
for (const band of claimed) {
|
|
916
|
-
|
|
859
|
+
register.withdraw(`${readerId}:${urgencyName(band.urgency)}`);
|
|
917
860
|
}
|
|
918
861
|
for (const band of wanted) {
|
|
919
|
-
|
|
862
|
+
stateBand(band);
|
|
920
863
|
}
|
|
864
|
+
// One call, and it does both: the swarm is told what to fetch and the store
|
|
865
|
+
// is told what will be read soon, from the same stated needs. The store used
|
|
866
|
+
// to be told separately here, which made the same intent two lists that
|
|
867
|
+
// could drift.
|
|
868
|
+
selection.reconcile();
|
|
921
869
|
claimed = wanted;
|
|
922
870
|
window = next;
|
|
923
|
-
// Tell the store these pieces are wanted, so it evicts something else.
|
|
924
|
-
// Without it the piece the decoder reads next looks exactly as stale as one
|
|
925
|
-
// the encoder fetched forty minutes ahead, and the second kind is what
|
|
926
|
-
// fills the store while the encoder runs ahead of the viewer.
|
|
927
|
-
store.protectRange?.(readerId, next.from, next.to);
|
|
928
871
|
if (isJump) {
|
|
929
872
|
waitBelongsToJump = true;
|
|
930
873
|
// A jump — a seek, not the window sliding along — can land on pieces that
|
|
@@ -966,7 +909,12 @@ export async function* readFragments({
|
|
|
966
909
|
// REQUESTED RANGE, which for ffmpeg's input means every piece to the
|
|
967
910
|
// end of the file — hundreds of them, at which point the flag says
|
|
968
911
|
// nothing. A window is what a reader genuinely needs next.
|
|
969
|
-
|
|
912
|
+
// Stated as its own need at the level that is being waited on, which
|
|
913
|
+
// is what carries the permission to take a block from a slow peer.
|
|
914
|
+
// Nothing here calls the library: `reconcile` reads what is stated.
|
|
915
|
+
stateBand({ from: pieceIndex, to: window.to, urgency: Urgency.BLOCKED });
|
|
916
|
+
blockedStated = true;
|
|
917
|
+
selection.reconcile();
|
|
970
918
|
}
|
|
971
919
|
|
|
972
920
|
const waitStartedAt = Date.now();
|
|
@@ -1009,6 +957,8 @@ export async function* readFragments({
|
|
|
1009
957
|
}
|
|
1010
958
|
pushed = {
|
|
1011
959
|
asked: pushed.asked + result.asked,
|
|
960
|
+
refusedWhileReserved:
|
|
961
|
+
(pushed.refusedWhileReserved ?? 0) + (result.refusedWhileReserved ?? 0),
|
|
1012
962
|
// Summed like the successes, so the line compares two totals over
|
|
1013
963
|
// the same attempts instead of a total against a snapshot.
|
|
1014
964
|
attempted: (pushed.attempted ?? 0) + result.attempted,
|
|
@@ -1078,7 +1028,15 @@ export async function* readFragments({
|
|
|
1078
1028
|
} else {
|
|
1079
1029
|
const supplyKey = `${torrent?.infoHash ?? "?"}/${file?.name ?? "?"}`;
|
|
1080
1030
|
noteSteeringOutcome(supplyKey, waitedMs, pushed.asked > 0 || duplicated > 0);
|
|
1081
|
-
|
|
1031
|
+
// Which band the reader was stopped in. A wait belongs to a level, and
|
|
1032
|
+
// the level says whether that band is too narrow rather than whether
|
|
1033
|
+
// banding is the wrong idea.
|
|
1034
|
+
noteWaitLevel(
|
|
1035
|
+
supplyKey,
|
|
1036
|
+
waitedMs,
|
|
1037
|
+
claimed.find((band) => pieceIndex >= band.from && pieceIndex <= band.to)?.urgency
|
|
1038
|
+
?? Urgency.BLOCKED
|
|
1039
|
+
);
|
|
1082
1040
|
waitCount += 1;
|
|
1083
1041
|
waitedTotalMs += waitedMs;
|
|
1084
1042
|
noteSupplyWait(supplyKey, file?.name ?? "", waitedMs);
|
|
@@ -1118,6 +1076,15 @@ export async function* readFragments({
|
|
|
1118
1076
|
// the piece onto faster holders shortens the tail — by number
|
|
1119
1077
|
// rather than by impression.
|
|
1120
1078
|
`; steered onto ${pushed.asked} of ${pushed.attempted} asks (${pushed.considered} peers held it)` +
|
|
1079
|
+
// How often a fast peer we picked was refused because every block
|
|
1080
|
+
// was already spoken for and the library declined to take one from
|
|
1081
|
+
// a slow holder. Its thresholds are constants, not settings: the
|
|
1082
|
+
// asker must be above 16 KB/s, the holder below 48 KB/s and twice
|
|
1083
|
+
// as slow. A number here is what would justify replacing that rule;
|
|
1084
|
+
// a zero says the thresholds are not what we are short of.
|
|
1085
|
+
((pushed.refusedWhileReserved ?? 0) > 0
|
|
1086
|
+
? `; ${pushed.refusedWhileReserved} refused with every block reserved`
|
|
1087
|
+
: "") +
|
|
1121
1088
|
(pushed.fastestBytesPerSecond > 0
|
|
1122
1089
|
? `, fastest ${Math.round(pushed.fastestBytesPerSecond / 1024)}KB/s`
|
|
1123
1090
|
: "") +
|
|
@@ -1137,16 +1104,13 @@ export async function* readFragments({
|
|
|
1137
1104
|
// What we did about the tail, so the next session says by number
|
|
1138
1105
|
// whether a second copy of those blocks shortens the wait.
|
|
1139
1106
|
(duplicated > 0 ? `; duplicated ${duplicated} blocks` : "") +
|
|
1140
|
-
//
|
|
1141
|
-
//
|
|
1142
|
-
//
|
|
1143
|
-
|
|
1144
|
-
(
|
|
1145
|
-
? ` ${
|
|
1146
|
-
|
|
1147
|
-
? ` (near ${lastWidths.near} far ${lastWidths.far} pieces, from the measured wait and surplus)`
|
|
1148
|
-
: " (widths not measured yet)")
|
|
1149
|
-
: "")
|
|
1107
|
+
// The bands this reader was claiming while the wait happened, by
|
|
1108
|
+
// level. A wait belongs to a level, and that is what says whether a
|
|
1109
|
+
// width is wrong rather than whether the whole scheme is.
|
|
1110
|
+
` ${claimed.map((band) => `${urgencyName(band.urgency)}:${band.from}-${band.to}`).join(" ")}` +
|
|
1111
|
+
(lastWidths.measured
|
|
1112
|
+
? ` (near ${lastWidths.near} far ${lastWidths.far} pieces, from the measured wait and surplus)`
|
|
1113
|
+
: " (widths not measured yet)")
|
|
1150
1114
|
);
|
|
1151
1115
|
}
|
|
1152
1116
|
|
|
@@ -1221,7 +1185,7 @@ export async function* readFragments({
|
|
|
1221
1185
|
const readSeconds = (Date.now() - readStartedAt) / 1000;
|
|
1222
1186
|
if (deliveredBytes > 0) {
|
|
1223
1187
|
logger.info(
|
|
1224
|
-
`read "${String(file?.name ?? "?").slice(0, 40)}"
|
|
1188
|
+
`read "${String(file?.name ?? "?").slice(0, 40)}" ` +
|
|
1225
1189
|
`delivered=${(deliveredBytes / 1e6).toFixed(1)}MB in ${readSeconds.toFixed(1)}s ` +
|
|
1226
1190
|
`waits=${waitCount} waited=${(waitedTotalMs / 1000).toFixed(1)}s`
|
|
1227
1191
|
);
|
|
@@ -1230,11 +1194,13 @@ export async function* readFragments({
|
|
|
1230
1194
|
// stops iterating — a band left behind would keep the swarm fetching for a
|
|
1231
1195
|
// reader that no longer exists.
|
|
1232
1196
|
for (const band of claimed) {
|
|
1233
|
-
|
|
1197
|
+
register.withdraw(`${readerId}:${urgencyName(band.urgency)}`);
|
|
1234
1198
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
clearCritical(torrent, criticalMark);
|
|
1199
|
+
if (blockedStated) {
|
|
1200
|
+
register.withdraw(`${readerId}:${urgencyName(Urgency.BLOCKED)}`);
|
|
1238
1201
|
}
|
|
1202
|
+
// Once, after everything has been withdrawn — and it releases this reader's
|
|
1203
|
+
// hold on memory as well, because both views come from the same statement.
|
|
1204
|
+
selection.reconcile();
|
|
1239
1205
|
}
|
|
1240
1206
|
}
|
|
@@ -32,6 +32,26 @@ import { CONTAINER_HEAD_BYTES, containerTracksOf } from "./container-tracks.js";
|
|
|
32
32
|
import { fillFileInBackground } from "./background-fill.js";
|
|
33
33
|
import { Command, Event } from "./protocol.js";
|
|
34
34
|
import { startMemoryReport, WORKER_MEMORY_SAMPLE_MS } from "../memory-report.js";
|
|
35
|
+
import { forwardLogsTo, logger } from "../../utils/logger.js";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Everything this thread logs goes to the main thread, which owns the file.
|
|
39
|
+
*
|
|
40
|
+
* Two threads cannot both write it — they would race on the rotation and could
|
|
41
|
+
* interleave mid-line — so there is one writer and this is how everyone else
|
|
42
|
+
* reaches it. Set before anything else runs, because a line written before this
|
|
43
|
+
* point reaches the console only, and the console is destroyed by every
|
|
44
|
+
* release.
|
|
45
|
+
*
|
|
46
|
+
* Until 2026-09-02 only the `log` function below took this route. Modules that
|
|
47
|
+
* called `logger.*` directly — the piece reader, the torrent pool, the
|
|
48
|
+
* background fill, the container track reader, the subtitle walk — wrote into a
|
|
49
|
+
* copy of the logger that had no file, and every one of their lines was lost:
|
|
50
|
+
* measured over a whole 49 938-line file, not one of them was in it.
|
|
51
|
+
*/
|
|
52
|
+
forwardLogsTo((_level, message) => {
|
|
53
|
+
parentPort.postMessage({ type: Event.LOG, message });
|
|
54
|
+
});
|
|
35
55
|
|
|
36
56
|
// Imported dynamically, and that is load-bearing: static imports are RESOLVED
|
|
37
57
|
// during linking, before any module body runs, so a statically imported pool
|
|
@@ -71,14 +91,14 @@ const fileClaims = createFileClaims();
|
|
|
71
91
|
const readsById = new Map();
|
|
72
92
|
|
|
73
93
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
94
|
+
* Shorthand for this file. The same path as `logger.info` anywhere else in the
|
|
95
|
+
* thread — kept only because it reads better at the hundred call sites here.
|
|
76
96
|
*
|
|
77
97
|
* @param {string} message
|
|
78
98
|
* @returns {void}
|
|
79
99
|
*/
|
|
80
100
|
function log(message) {
|
|
81
|
-
|
|
101
|
+
logger.info(message);
|
|
82
102
|
}
|
|
83
103
|
|
|
84
104
|
/**
|