@torrent-tv/proxy 2.9.90 → 2.9.92
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 +10 -0
- package/package.json +1 -1
- package/services/hls-session-manager.js +159 -61
- package/services/torrent-worker/piece-reader.js +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## 2.9.92
|
|
2
|
+
|
|
3
|
+
- **Fix**: A seek acts on what the viewer asked for, instead of waiting out guards built for a signal that no longer exists. Three delays sat in front of every seek, all of them there because a far segment REQUEST used to steer the encoder and the player's playlist scan produced dozens of them. Requests stopped steering anything when the position became explicit, so what arrives now is only ever a position the viewer stated. The settle window drops from 1.2 s to 300 ms (the browser already collapses a drag into one report at 300 ms — this was a second debounce on an already-debounced signal, and it cost 1.2 s of every measured seek). The floor between restarts drops from 4 s to 500 ms, now a guard against a client spamming the endpoint rather than a policy about noise. And a run in progress is no longer protected for up to 30 s while it reaches its first segment: finishing a segment for a position the viewer has left is work nobody wants, and the hold could delay a genuine second seek by the whole grace. Measured cost of the old behaviour, 2026-08-04: two seeks 1.3 s apart produced two restarts 4.4 s apart, the first encoding 119.5 s of content before the second killed it.
|
|
4
|
+
- **Chore**: Removed `ENCODER_STALL_MS`, declared with a paragraph describing a watchdog that was never wired to anything.
|
|
5
|
+
|
|
6
|
+
## 2.9.91
|
|
7
|
+
|
|
8
|
+
- **Fix**: The encoder no longer runs away from the viewer. Nothing bounded how far ahead it produced: measured 2026-08-04, three minutes after a film was opened the encode had reached 00:39:24 of a 01:26:51 source at 12.8x while the viewer was still at the start, and the torrent had pulled 80% of 4.7 GB to feed it — the pool owner's bandwidth and disk spent on a viewer who may watch two minutes, the pieces being read evicted from memory by pieces forty minutes ahead, and the swarm busy with anything but the segment being waited for. An encoder more than two minutes of content ahead of the last segment its viewer asked for is now **suspended**, and released once the viewer is within a minute of it — or at once when a segment is requested. Suspended rather than killed on purpose: restarting costs about nine seconds on this hardware, so a viewer reaching the end of the produced range would stall every time, while suspending keeps the process, its input and its position. POSIX only; where `SIGSTOP` does not exist the attempt fails once, is logged, and that session keeps the old behaviour. Every path that terminates an encoder now releases it first — a suspended process does not act on `SIGTERM` until it is continued, which would have hung the wait a seek performs before starting its replacement.
|
|
9
|
+
- **New**: A reader reports what it waited for. When a read blocks a second or more on a piece, the log names the piece, its position in the read, and the offset the read started at. The first segment after a seek-restart costs 9.2-9.4 s and there was no way to tell whether that is the swarm, the piece picker or ffmpeg; now there is.
|
|
10
|
+
|
|
1
11
|
## 2.9.90
|
|
2
12
|
|
|
3
13
|
- **New**: The output container is chosen per session, by the viewer, instead of once per proxy. `POST /api/transcode-sessions` accepts `segmentFormat`; `--segment-format` remains the default for a client that expresses no preference, and an unrecognised value falls back to it rather than to the library default. The browser is the only party that knows what its media stack will accept for the tracks it asked to be copied: a copied MP3 track cannot be appended from fMP4 at all (`audio/mp4; codecs="mp4a.69"` is refused by MediaSource) but works from MPEG-TS, which hls.js demuxes itself and hands to a plain `audio/mpeg` buffer — the same file, the same browser, silent loop one way and normal playback the other. Sessions are keyed by container too, so two viewers wanting different ones do not share an encoder. Nothing branches on the format outside `services/segment-formats/`; the manager now reads it off the session.
|
package/package.json
CHANGED
|
@@ -43,26 +43,32 @@ const DEFAULT_SEGMENT_DURATION_SEC = 4;
|
|
|
43
43
|
// is allowed to be before we restart ffmpeg at that position (server-side seek).
|
|
44
44
|
// Requests within the window are served by waiting for the running encode.
|
|
45
45
|
const MAX_LOOKAHEAD_SEGMENTS = 8;
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
// run
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
46
|
+
// Floor between actual restarts. It used to be 4 s, from when a far segment
|
|
47
|
+
// REQUEST could steer the encoder and a playlist scan produced a burst of them.
|
|
48
|
+
// Requests no longer steer anything (see #ensureEncodingFor) — every restart
|
|
49
|
+
// now comes from a position the viewer stated — so this is no longer a policy
|
|
50
|
+
// about noise, only a guard against a client that spams the seek endpoint.
|
|
51
|
+
// Measured cost of the old value 2026-08-04: two seeks 1.3 s apart produced two
|
|
52
|
+
// restarts 4.4 s apart, the first encoding 119.5 s of content nobody wanted
|
|
53
|
+
// before the second killed it.
|
|
54
|
+
const RESTART_COOLDOWN_MS = 500;
|
|
55
|
+
// How far ahead of the viewer the encoder may run before it is stopped, and how
|
|
56
|
+
// far it must fall back to before it is let go again.
|
|
57
|
+
//
|
|
58
|
+
// Nothing used to bound this. Measured 2026-08-04 on the copy path: three
|
|
59
|
+
// minutes after a film was opened the encode had reached 00:39:24 of a 01:26:51
|
|
60
|
+
// source at 12.8x while the viewer was still at the start, and the torrent had
|
|
61
|
+
// pulled 80% of 4.7 GB to feed it. That costs the pool owner's bandwidth and
|
|
62
|
+
// disk for a viewer who may watch two minutes, evicts from memory the pieces
|
|
63
|
+
// the viewer is actually reading, and competes for the swarm with the segment
|
|
64
|
+
// being waited on.
|
|
65
|
+
//
|
|
66
|
+
// In seconds of content rather than segments, because a segment is 4 s of
|
|
67
|
+
// re-encoded video but a whole keyframe interval on the copy path. Generous
|
|
68
|
+
// enough that ordinary watching never touches it: the encoder fills two minutes
|
|
69
|
+
// ahead, stops, and is released as soon as the viewer has spent a minute of it.
|
|
70
|
+
const LOOKAHEAD_PAUSE_SECONDS = 120;
|
|
71
|
+
const LOOKAHEAD_RESUME_SECONDS = 60;
|
|
66
72
|
// Seek debounce. A far (out-of-window) segment request is a server-side seek.
|
|
67
73
|
// Rather than restart ffmpeg on the first one, wait a short quiet period:
|
|
68
74
|
// further far requests re-arm it and update the target to the latest index, so
|
|
@@ -84,10 +90,17 @@ const ENCODER_STALL_MS = 12_000;
|
|
|
84
90
|
// encoding 125 s of content before reaching the viewer's position. Field
|
|
85
91
|
// 2026-08-02: a seek took 56 s, of which ~50 s was this backoff.
|
|
86
92
|
const SEEK_BACKOFF_SEGMENTS = 1;
|
|
87
|
-
|
|
88
|
-
//
|
|
93
|
+
// How long to wait for a scrub to stop moving before acting on it. Small,
|
|
94
|
+
// because the browser already collapses a drag into ONE report
|
|
95
|
+
// (`SEEK_REPORT_DEBOUNCE_MS`, 300 ms) and only reports where it settled — this
|
|
96
|
+
// is a second debounce on an already-debounced signal, and every millisecond of
|
|
97
|
+
// it is dead time in front of the viewer. It was 1.2 s when the encoder was
|
|
98
|
+
// also steered by segment requests, which arrive in bursts of dozens; measured
|
|
99
|
+
// 2026-08-04, that cost 1.2 s of every seek.
|
|
100
|
+
const SEEK_SETTLE_MS = 300;
|
|
101
|
+
// Hard cap on the total settle wait, measured from the first request of a
|
|
89
102
|
// burst, so a still-moving scrubber cannot delay a genuine seek forever.
|
|
90
|
-
const SEEK_SETTLE_MAX_MS =
|
|
103
|
+
const SEEK_SETTLE_MAX_MS = 1_000;
|
|
91
104
|
// Grace period to wait for the PREVIOUS ffmpeg process to exit (per signal
|
|
92
105
|
// escalation step: SIGTERM, then SIGKILL) before spawning its replacement into
|
|
93
106
|
// the same session directory. See #startEncodeRun.
|
|
@@ -787,6 +800,7 @@ export class HlsSessionManager {
|
|
|
787
800
|
// benchmark (the only path that can pick/step resolution). Cheap no-op scan
|
|
788
801
|
// otherwise.
|
|
789
802
|
this.budgetTimer = setInterval(() => {
|
|
803
|
+
this.#enforceLookAhead();
|
|
790
804
|
void this.#enforceRealtimeBudget();
|
|
791
805
|
}, BUDGET_CHECK_INTERVAL_MS);
|
|
792
806
|
this.budgetTimer.unref();
|
|
@@ -1160,6 +1174,12 @@ export class HlsSessionManager {
|
|
|
1160
1174
|
// Bumped by every viewer seek; a held segment request that started under
|
|
1161
1175
|
// an older value gives up at once. See requestSeek.
|
|
1162
1176
|
waitEpoch: 0,
|
|
1177
|
+
// Highest segment the viewer has actually asked for, and whether the
|
|
1178
|
+
// encoder is currently suspended for running too far past it.
|
|
1179
|
+
// See #enforceLookAhead.
|
|
1180
|
+
lastRequestedSegment: null,
|
|
1181
|
+
encoderPaused: false,
|
|
1182
|
+
encoderPauseUnsupported: false,
|
|
1163
1183
|
seekFirstFarAt: 0,
|
|
1164
1184
|
// Circuit breaker: consecutive FAST failures (see SEEK_FAST_FAIL_MS) at
|
|
1165
1185
|
// seekFailureTarget. Reset whenever a run starts at a DIFFERENT target or
|
|
@@ -1549,6 +1569,94 @@ export class HlsSessionManager {
|
|
|
1549
1569
|
return true;
|
|
1550
1570
|
}
|
|
1551
1571
|
|
|
1572
|
+
/**
|
|
1573
|
+
* Stop encoders that have run too far ahead of their viewer, and release
|
|
1574
|
+
* those the viewer has caught up with.
|
|
1575
|
+
*
|
|
1576
|
+
* The encoder is SUSPENDED, not killed. Killing would be simpler, but
|
|
1577
|
+
* restarting it costs about nine seconds on this hardware — the torrent has
|
|
1578
|
+
* to serve a fresh position and ffmpeg has to reach its first keyframe — so a
|
|
1579
|
+
* viewer reaching the end of the produced range would stall every time.
|
|
1580
|
+
* Suspending keeps the process, its open input and its position, and costs
|
|
1581
|
+
* nothing to undo.
|
|
1582
|
+
*
|
|
1583
|
+
* POSIX only. `SIGSTOP` does not exist on Windows, where `process.kill`
|
|
1584
|
+
* throws; the attempt is made once per session and, if it fails, that session
|
|
1585
|
+
* simply keeps its old unbounded behaviour rather than breaking.
|
|
1586
|
+
*
|
|
1587
|
+
* @returns {void}
|
|
1588
|
+
*/
|
|
1589
|
+
#enforceLookAhead() {
|
|
1590
|
+
for (const session of this.sessionsById.values()) {
|
|
1591
|
+
if (!session || session.state === "disposed" || !session.ffmpeg) {
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
const encodedTo = Number(session.progress?.processedSeconds);
|
|
1595
|
+
if (!Number.isFinite(encodedTo)) {
|
|
1596
|
+
continue;
|
|
1597
|
+
}
|
|
1598
|
+
// Where the viewer is. Before the first segment request, the position the
|
|
1599
|
+
// run started at — so a session nobody has read from yet is bounded too.
|
|
1600
|
+
const viewerAt = Number.isInteger(session.lastRequestedSegment)
|
|
1601
|
+
? this.#segmentStartTime(session, session.lastRequestedSegment)
|
|
1602
|
+
: this.#segmentStartTime(session, session.encodeStartIndex ?? 0);
|
|
1603
|
+
const ahead = encodedTo - viewerAt;
|
|
1604
|
+
if (!session.encoderPaused && ahead > LOOKAHEAD_PAUSE_SECONDS) {
|
|
1605
|
+
this.#pauseEncoder(session, `${Math.round(ahead)}s ahead of the viewer`);
|
|
1606
|
+
} else if (session.encoderPaused && ahead <= LOOKAHEAD_RESUME_SECONDS) {
|
|
1607
|
+
this.#resumeEncoder(session, `${Math.round(ahead)}s ahead of the viewer`);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
/**
|
|
1613
|
+
* Suspend a session's encoder. No-op when already paused or unsupported here.
|
|
1614
|
+
*
|
|
1615
|
+
* @param {HlsSession} session
|
|
1616
|
+
* @param {string} reason
|
|
1617
|
+
* @returns {void}
|
|
1618
|
+
*/
|
|
1619
|
+
#pauseEncoder(session, reason) {
|
|
1620
|
+
if (session.encoderPaused || session.encoderPauseUnsupported || !session.ffmpeg?.pid) {
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
try {
|
|
1624
|
+
process.kill(session.ffmpeg.pid, "SIGSTOP");
|
|
1625
|
+
} catch (error) {
|
|
1626
|
+
session.encoderPauseUnsupported = true;
|
|
1627
|
+
logger.info(
|
|
1628
|
+
`transcode ${session.id} cannot suspend the encoder on this platform ` +
|
|
1629
|
+
`(${error instanceof Error ? error.message : String(error)}); look-ahead stays unbounded`
|
|
1630
|
+
);
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
session.encoderPaused = true;
|
|
1634
|
+
logger.info(
|
|
1635
|
+
`transcode ${session.id} encoder suspended — ${reason} ` +
|
|
1636
|
+
`"${session.fileName}"`
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* Let a suspended encoder run again.
|
|
1642
|
+
*
|
|
1643
|
+
* @param {HlsSession} session
|
|
1644
|
+
* @param {string} reason
|
|
1645
|
+
* @returns {void}
|
|
1646
|
+
*/
|
|
1647
|
+
#resumeEncoder(session, reason) {
|
|
1648
|
+
if (!session.encoderPaused || !session.ffmpeg?.pid) {
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
try {
|
|
1652
|
+
process.kill(session.ffmpeg.pid, "SIGCONT");
|
|
1653
|
+
} catch {
|
|
1654
|
+
// The process is gone; the exit handler will deal with it.
|
|
1655
|
+
}
|
|
1656
|
+
session.encoderPaused = false;
|
|
1657
|
+
logger.info(`transcode ${session.id} encoder resumed — ${reason} "${session.fileName}"`);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1552
1660
|
async #enforceRealtimeBudget() {
|
|
1553
1661
|
if (this.videoEncoder?.kind !== "software") {
|
|
1554
1662
|
return;
|
|
@@ -1728,6 +1836,9 @@ export class HlsSessionManager {
|
|
|
1728
1836
|
async #startEncodeRun(session, startIndex) {
|
|
1729
1837
|
const generation = ++session.encodeRunGeneration;
|
|
1730
1838
|
const previousFfmpeg = session.ffmpeg;
|
|
1839
|
+
// A suspended process does not act on SIGTERM until it is continued, so the
|
|
1840
|
+
// wait below would never end. Let it run before asking it to stop.
|
|
1841
|
+
this.#resumeEncoder(session, "terminating for a new run");
|
|
1731
1842
|
if (previousFfmpeg && !hasChildExited(previousFfmpeg)) {
|
|
1732
1843
|
try {
|
|
1733
1844
|
previousFfmpeg.kill("SIGTERM");
|
|
@@ -1760,6 +1871,7 @@ export class HlsSessionManager {
|
|
|
1760
1871
|
// Terminate any existing encode process before starting a new one. The
|
|
1761
1872
|
// old process's exit handler no-ops because session.ffmpeg is reassigned
|
|
1762
1873
|
// below (it checks identity).
|
|
1874
|
+
this.#resumeEncoder(session, "terminating");
|
|
1763
1875
|
if (session.ffmpeg && !session.ffmpeg.killed) {
|
|
1764
1876
|
try {
|
|
1765
1877
|
session.ffmpeg.kill("SIGTERM");
|
|
@@ -1933,6 +2045,7 @@ export class HlsSessionManager {
|
|
|
1933
2045
|
// judged finished — see getFileStream.
|
|
1934
2046
|
session.usesExplicitCuts = Boolean(cutTimes && cutTimes.length > 0);
|
|
1935
2047
|
session.encodeStartIndex = safeIndex;
|
|
2048
|
+
session.encoderPaused = false;
|
|
1936
2049
|
session.pendingRestartIndex = -1;
|
|
1937
2050
|
session.lastRestartAt = Date.now();
|
|
1938
2051
|
session.state = session.state === "disposed" ? "disposed" : "starting";
|
|
@@ -2347,48 +2460,21 @@ export class HlsSessionManager {
|
|
|
2347
2460
|
session.seekSettleTimer.unref?.();
|
|
2348
2461
|
return;
|
|
2349
2462
|
}
|
|
2350
|
-
//
|
|
2351
|
-
//
|
|
2352
|
-
//
|
|
2353
|
-
//
|
|
2354
|
-
//
|
|
2355
|
-
//
|
|
2356
|
-
//
|
|
2357
|
-
//
|
|
2358
|
-
//
|
|
2359
|
-
//
|
|
2360
|
-
// synthetic VOD playlist, so from its point of view they all exist), and
|
|
2361
|
-
// each far-enough probe looked like a fresh seek to us. Waiting for the
|
|
2362
|
-
// first segment makes the scan harmless — it can no longer steer the
|
|
2363
|
-
// encoder — and one genuine seek now reliably completes.
|
|
2364
|
-
//
|
|
2365
|
-
// Bounded by RUN_FIRST_SEGMENT_GRACE_MS so a wedged run cannot block seeks
|
|
2366
|
-
// forever; the encoder-stall watchdog and the exit handler cover a run that
|
|
2367
|
-
// dies outright.
|
|
2463
|
+
// A run in progress is NOT protected any more. It used to be: a restart was
|
|
2464
|
+
// held for up to 30 s while the current run reached its first segment,
|
|
2465
|
+
// because a far segment REQUEST could steer the encoder and the player's
|
|
2466
|
+
// playlist scan produced dozens of them — restarts at #617 → #717 → #732 →
|
|
2467
|
+
// #732 every 5-7 s, none producing anything (field 2026-08-02). Requests
|
|
2468
|
+
// stopped steering anything when the position became explicit, so the only
|
|
2469
|
+
// thing that can arrive here is a position the viewer has stated, and
|
|
2470
|
+
// finishing a segment for where they no longer are is work nobody wants.
|
|
2471
|
+
// Holding it was also expensive in the other direction: a genuine second
|
|
2472
|
+
// seek could be delayed by the whole grace.
|
|
2368
2473
|
const producedThisRun = this.#producedSecondsThisRun(session);
|
|
2369
2474
|
const runIsAlive = session.ffmpeg != null && !hasChildExited(session.ffmpeg);
|
|
2370
|
-
if (
|
|
2371
|
-
runIsAlive &&
|
|
2372
|
-
producedThisRun < this.segmentDurationSec &&
|
|
2373
|
-
sinceLastRestart < RUN_FIRST_SEGMENT_GRACE_MS
|
|
2374
|
-
) {
|
|
2375
|
-
logger.info(
|
|
2376
|
-
`transcode ${session.id} seek #${target} HELD — current run has produced ` +
|
|
2377
|
-
`${producedThisRun.toFixed(1)}s of the ${this.segmentDurationSec}s first segment ` +
|
|
2378
|
-
`(${(sinceLastRestart / 1000).toFixed(1)}s into a ${RUN_FIRST_SEGMENT_GRACE_MS / 1000}s grace)`
|
|
2379
|
-
);
|
|
2380
|
-
session.seekSettleTimer = setTimeout(() => this.#fireSettledSeek(session), SEEK_SETTLE_MS);
|
|
2381
|
-
session.seekSettleTimer.unref?.();
|
|
2382
|
-
return;
|
|
2383
|
-
}
|
|
2384
|
-
// Why the restart was allowed — the counterpart of the HELD line above.
|
|
2385
|
-
// Without it a restart is indistinguishable from the runaway ping-pong this
|
|
2386
|
-
// guard exists to stop, and diagnosing a field report becomes guesswork.
|
|
2387
2475
|
const allowedBecause = !runIsAlive
|
|
2388
2476
|
? "run is dead"
|
|
2389
|
-
:
|
|
2390
|
-
? `run produced ${producedThisRun.toFixed(1)}s (first segment done)`
|
|
2391
|
-
: `grace of ${RUN_FIRST_SEGMENT_GRACE_MS / 1000}s expired`;
|
|
2477
|
+
: `viewer moved; run had produced ${producedThisRun.toFixed(1)}s`;
|
|
2392
2478
|
// The start is exactly what requestSeek computed — one segment before the
|
|
2393
2479
|
// viewer's position — and nothing else may move it.
|
|
2394
2480
|
//
|
|
@@ -2585,6 +2671,17 @@ export class HlsSessionManager {
|
|
|
2585
2671
|
|
|
2586
2672
|
const filePath = path.join(session.dirPath, fileName);
|
|
2587
2673
|
const isPlaylist = fileName === PLAYLIST_FILE_NAME;
|
|
2674
|
+
if (!isPlaylist) {
|
|
2675
|
+
// Where the viewer actually is. Recorded for every segment request,
|
|
2676
|
+
// served or not, because it is what bounds how far ahead the encoder is
|
|
2677
|
+
// allowed to run — see #enforceLookAhead.
|
|
2678
|
+
const requested = session.segmentFormat.segmentIndexFromName(fileName);
|
|
2679
|
+
if (requested >= 0) {
|
|
2680
|
+
session.lastRequestedSegment = requested;
|
|
2681
|
+
// A viewer who has caught up must not wait out the monitor's interval.
|
|
2682
|
+
this.#resumeEncoder(session, "a segment was requested");
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2588
2685
|
try {
|
|
2589
2686
|
await access(filePath);
|
|
2590
2687
|
|
|
@@ -2793,6 +2890,7 @@ export class HlsSessionManager {
|
|
|
2793
2890
|
session.seekSettleTimer = null;
|
|
2794
2891
|
}
|
|
2795
2892
|
|
|
2893
|
+
this.#resumeEncoder(session, "session disposed");
|
|
2796
2894
|
if (session.ffmpeg && !session.ffmpeg.killed) {
|
|
2797
2895
|
session.ffmpeg.kill("SIGTERM");
|
|
2798
2896
|
await waitForChildExit(session.ffmpeg);
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { findSharedStore } from "../piece-store/shared-piece-store.js";
|
|
24
|
+
import { logger } from "../../utils/logger.js";
|
|
25
|
+
|
|
26
|
+
/** Only waits at least this long are reported; sequential reading stays silent. */
|
|
27
|
+
const PIECE_WAIT_LOG_MS = 1_000;
|
|
24
28
|
|
|
25
29
|
/**
|
|
26
30
|
* How far ahead of the read head pieces are asked for.
|
|
@@ -318,7 +322,21 @@ export async function* readFragments({
|
|
|
318
322
|
criticalMark = markCritical(torrent, pieceIndex, Math.min(lastPiece, pieceIndex + criticalRun), criticalMark);
|
|
319
323
|
}
|
|
320
324
|
|
|
325
|
+
const waitStartedAt = Date.now();
|
|
321
326
|
await whenPieceReady(torrent, pieceIndex, cancellation);
|
|
327
|
+
// What a reader spent waiting for data, attributed to the exact piece. A
|
|
328
|
+
// seek's cost is dominated by the first segment after the encoder
|
|
329
|
+
// restarts (measured 9.2-9.4 s), and without this there is no way to say
|
|
330
|
+
// whether that is the swarm, the picker, or ffmpeg. Logged only when the
|
|
331
|
+
// wait is long enough to matter, so ordinary sequential reading is silent.
|
|
332
|
+
const waitedMs = Date.now() - waitStartedAt;
|
|
333
|
+
if (waitedMs >= PIECE_WAIT_LOG_MS) {
|
|
334
|
+
logger.info(
|
|
335
|
+
`piece-reader: waited ${waitedMs}ms for piece ${pieceIndex} ` +
|
|
336
|
+
`(${pieceIndex - firstPiece + 1} of ${lastPiece - firstPiece + 1} in a read from ` +
|
|
337
|
+
`${(start / 1024 / 1024).toFixed(0)}MB of "${file.name}")`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
322
340
|
|
|
323
341
|
// Pinned BEFORE it is located, and before any await that could let an
|
|
324
342
|
// eviction run: the offset is only meaningful while the piece is held.
|