@torrent-tv/proxy 2.9.98 → 2.9.100
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 +11 -0
- package/package.json +1 -1
- package/routes/api/sources/warm/post.js +83 -0
- package/server.js +6 -1
- package/services/hls-session-manager.js +36 -3
- package/services/playback-planner.js +17 -1
- package/services/torrent-pool.js +39 -0
- package/services/torrent-worker/piece-reader.js +56 -2
- package/test/read-window.test.js +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
## 2.9.100
|
|
2
|
+
|
|
3
|
+
- **New**: A long wait for a piece now says who was working on it. The open question about a seek is that a single 8 MiB piece takes 3.0-4.6 s while the swarm as a whole moves 4-6 MB/s, so only about 2 MB/s reaches the piece being waited for — and whether that is because few peers hold it, few are being asked, or each is slow could not be told apart from outside. The line now carries the rate achieved on that piece and, sampled at its peak while waiting, how many connected peers had it, how many were asked, and how many blocks were in flight.
|
|
4
|
+
- **New**: The keyframe index is read alongside the codec probe instead of after it. Both wait for the same tail of the file; measured 2026-08-04, a probe of 722-1206 ms was followed by an index read of 311-430 ms, all of it before the first segment could be produced. Started together the second is free. Fire and forget, sharing the cache a session would fill itself.
|
|
5
|
+
- **Chore**: Every encoder run is numbered in the log. A burst of seeks starts several runs within a second and every line about them carries the session id, which is the same for all of them — so the command that failed could not be told from the ones that succeeded around it. That is the state the unexplained `Cannot write moov atom before AC3 packets` was found in.
|
|
6
|
+
|
|
7
|
+
## 2.9.99
|
|
8
|
+
|
|
9
|
+
- **New**: A source can be told to start before anyone asks to play it — `POST /api/sources/:sourceKey/warm`. Everything a cold torrent must do first takes seconds and none of it depends on which file is wanted: announce to the trackers, connect to peers, be unchoked by them. Given a file index it also fetches the two pieces at that file's edges, which is what the codec probe reads and what took **6.7 s of the 10.3 s** before playback in the session measured 2026-08-04. All of it used to begin only once a file had been chosen, because it was buried inside the playback plan. The route returns as soon as the work is under way and reports a refusal rather than an error — nothing is broken if a warm-up does not happen, since the ordinary path still does all of it.
|
|
10
|
+
- **Chore**: Two callers asking for the same file's edges at once now share one prefetch instead of opening a second pair of readers, each claiming a window and holding pieces. That happens by design on a single-video torrent, where the warm-up and the playback plan both want them.
|
|
11
|
+
|
|
1
12
|
## 2.9.98
|
|
2
13
|
|
|
3
14
|
- **Fix**: The upload is no longer raised at moments when nobody wants a byte. A torrent with no reader was counted as starving whenever its download read low — which it always does while the encoder is held back for running ahead of the viewer. Measured: four cycles of 512 KB/s and back in three minutes, each reported as `earn unchoke … down=0KB/s`. Starvation now requires somebody to be waiting.
|
package/package.json
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { logger } from "../../../../utils/logger.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Start fetching a source before anyone asks to play it.
|
|
5
|
+
*
|
|
6
|
+
* POST /api/sources/:sourceKey/warm { fileIndex?: number }
|
|
7
|
+
*
|
|
8
|
+
* Everything a torrent must do before the first byte of video can be served
|
|
9
|
+
* takes seconds and none of it depends on the viewer: announce to the
|
|
10
|
+
* trackers, connect to peers, be unchoked by them, and fetch the two pieces at
|
|
11
|
+
* the file's edges that the codec probe reads. Measured 2026-08-04 on a cold
|
|
12
|
+
* 7.4 GB torrent: 6.7 s of the 10.3 s before playback was those two pieces
|
|
13
|
+
* arriving, with the swarm ramping from nothing.
|
|
14
|
+
*
|
|
15
|
+
* That work used to begin only when a file had been chosen, because it was
|
|
16
|
+
* buried inside the playback plan. It can begin as soon as the viewer has
|
|
17
|
+
* picked a TORRENT — while they are still reading the list of episodes — and
|
|
18
|
+
* then most or all of it has happened by the time they choose.
|
|
19
|
+
*
|
|
20
|
+
* Returns as soon as the work is under way. The caller is not waiting for a
|
|
21
|
+
* result; it is only saying "you may start". Failures are logged and answered
|
|
22
|
+
* as `started: false` rather than as an error, because nothing is broken if a
|
|
23
|
+
* warm-up does not happen — the ordinary path still does all of it.
|
|
24
|
+
*
|
|
25
|
+
* @param {import("fastify").FastifyRequest} req
|
|
26
|
+
* @param {import("fastify").FastifyReply} reply
|
|
27
|
+
* @param {{
|
|
28
|
+
* sourceRegistry: ReturnType<import("../../../../store/source-registry.js").createSourceRegistry>,
|
|
29
|
+
* torrentPool: import("../../../../services/torrent-pool.js").TorrentPool
|
|
30
|
+
* }} deps
|
|
31
|
+
* @returns {Promise<void>}
|
|
32
|
+
*/
|
|
33
|
+
export async function handleApiSourceWarmPost(req, reply, { sourceRegistry, torrentPool }) {
|
|
34
|
+
const sourceKey = typeof req.params.sourceKey === "string" ? req.params.sourceKey.trim() : "";
|
|
35
|
+
if (!sourceKey) {
|
|
36
|
+
return reply.code(400).send({ error: "sourceKey is required." });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const sourceRecord = sourceRegistry.get(sourceKey);
|
|
40
|
+
if (!sourceRecord) {
|
|
41
|
+
return reply.code(404).send({ error: "Source key was not found." });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const body = req.body && typeof req.body === "object" && !Array.isArray(req.body) ? req.body : {};
|
|
45
|
+
const requestedIndex = Number(body.fileIndex);
|
|
46
|
+
const fileIndex = Number.isInteger(requestedIndex) && requestedIndex >= 0 ? requestedIndex : null;
|
|
47
|
+
|
|
48
|
+
// Adding the torrent is what announces to the trackers and starts connecting
|
|
49
|
+
// to peers, and it is also what a magnet needs in order to fetch its
|
|
50
|
+
// metadata. It is awaited because everything else needs the torrent object,
|
|
51
|
+
// and because until it resolves there is nothing to report.
|
|
52
|
+
let torrent;
|
|
53
|
+
try {
|
|
54
|
+
torrent = await torrentPool.getTorrent(sourceRecord.sourceType, sourceRecord.source);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
57
|
+
logger.warn(`warm ${sourceKey.slice(0, 8)}: could not add the torrent: ${message}`);
|
|
58
|
+
return reply.send({ started: false, swarm: false, edges: false });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The edges are only worth fetching once it is known WHICH file will be
|
|
62
|
+
// played: on a season pack, warming twenty episodes' worth would spend the
|
|
63
|
+
// pool owner's bandwidth on nineteen files nobody opened. The caller passes
|
|
64
|
+
// an index when the torrent holds a single video, and again later if it
|
|
65
|
+
// wants to.
|
|
66
|
+
let edges = false;
|
|
67
|
+
if (fileIndex !== null && torrent.files?.[fileIndex]) {
|
|
68
|
+
edges = true;
|
|
69
|
+
// Deliberately not awaited: this is the multi-second part, and the point of
|
|
70
|
+
// the whole route is that the viewer goes on choosing while it happens.
|
|
71
|
+
Promise.resolve(torrentPool.prefetchFileEdges(torrent, fileIndex)).catch((error) => {
|
|
72
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
73
|
+
logger.warn(`warm ${sourceKey.slice(0, 8)}: file edges failed: ${message}`);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
logger.info(
|
|
78
|
+
`warm ${sourceKey.slice(0, 8)}: swarm started for "${torrent.name}"` +
|
|
79
|
+
(edges ? `, fetching the edges of file ${fileIndex}` : ", file not chosen yet")
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return reply.send({ started: true, swarm: true, edges });
|
|
83
|
+
}
|
package/server.js
CHANGED
|
@@ -19,6 +19,7 @@ import { handleHealthzGet } from "./routes/healthz/get.js";
|
|
|
19
19
|
import { handleApiSourcesPost } from "./routes/api/sources/post.js";
|
|
20
20
|
import { handleApiSourceStatsGet } from "./routes/api/sources/stats/get.js";
|
|
21
21
|
import { handleApiSourceFilesGet } from "./routes/api/sources/files/get.js";
|
|
22
|
+
import { handleApiSourceWarmPost } from "./routes/api/sources/warm/post.js";
|
|
22
23
|
import { handleApiPlaybackPlanPost } from "./routes/api/playback-plan/post.js";
|
|
23
24
|
import { handleApiSubtitlesGet } from "./routes/api/subtitles/get.js";
|
|
24
25
|
import { handleApiTranscodeSessionsPost } from "./routes/api/transcode-sessions/post.js";
|
|
@@ -162,7 +163,8 @@ export async function startProxyServer({ host, port, transcodeAudio, ffmpegBin,
|
|
|
162
163
|
transcodeAudioEnabled: transcodeAudio,
|
|
163
164
|
localBaseUrl: hlsSessionManager.localBaseUrl,
|
|
164
165
|
sourceRegistry,
|
|
165
|
-
torrentPool
|
|
166
|
+
torrentPool,
|
|
167
|
+
warmKeyframeIndex: (params) => hlsSessionManager.warmKeyframeIndex(params)
|
|
166
168
|
});
|
|
167
169
|
|
|
168
170
|
app.get("/health", async (req, reply) => handleHealthGet(req, reply, { version }));
|
|
@@ -176,6 +178,9 @@ export async function startProxyServer({ host, port, transcodeAudio, ffmpegBin,
|
|
|
176
178
|
app.get("/api/sources/:sourceKey/files", async (req, reply) =>
|
|
177
179
|
handleApiSourceFilesGet(req, reply, { sourceRegistry, torrentPool })
|
|
178
180
|
);
|
|
181
|
+
app.post("/api/sources/:sourceKey/warm", async (req, reply) =>
|
|
182
|
+
handleApiSourceWarmPost(req, reply, { sourceRegistry, torrentPool })
|
|
183
|
+
);
|
|
179
184
|
app.post("/api/playback-plan", async (req, reply) =>
|
|
180
185
|
handleApiPlaybackPlanPost(req, reply, { playbackPlanner })
|
|
181
186
|
);
|
|
@@ -1379,6 +1379,29 @@ export class HlsSessionManager {
|
|
|
1379
1379
|
return null;
|
|
1380
1380
|
}
|
|
1381
1381
|
|
|
1382
|
+
/**
|
|
1383
|
+
* Read the file's keyframe index into the cache before a session needs it.
|
|
1384
|
+
*
|
|
1385
|
+
* The index lives at the END of a Matroska file, which is also where the
|
|
1386
|
+
* codec probe reads — both wait for the same piece to arrive, and they used
|
|
1387
|
+
* to do it one after the other: measured 2026-08-04, a probe of 722-1206 ms
|
|
1388
|
+
* followed by an index read of 311-430 ms, all of it before the first
|
|
1389
|
+
* segment. Started together, the second costs nothing.
|
|
1390
|
+
*
|
|
1391
|
+
* Never rejects and is never awaited by the caller: a session that finds
|
|
1392
|
+
* nothing cached simply reads it itself, as before.
|
|
1393
|
+
*
|
|
1394
|
+
* @param {{ sourceKey: string, fileIndex: number, inputUrl: URL, logName: string }} params
|
|
1395
|
+
* @returns {Promise<void>}
|
|
1396
|
+
*/
|
|
1397
|
+
async warmKeyframeIndex({ sourceKey, fileIndex, inputUrl, logName }) {
|
|
1398
|
+
try {
|
|
1399
|
+
await this.#readContainerKeyframes({ sourceKey, fileIndex, inputUrl, logName });
|
|
1400
|
+
} catch {
|
|
1401
|
+
// Best effort by construction.
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1382
1405
|
async #readContainerKeyframes({ sourceKey, fileIndex, inputUrl, logName }) {
|
|
1383
1406
|
const cacheKey = `${sourceKey}:${fileIndex}`;
|
|
1384
1407
|
if (this.keyframeIndexCache.has(cacheKey)) {
|
|
@@ -2165,7 +2188,15 @@ export class HlsSessionManager {
|
|
|
2165
2188
|
// were verified to handle a copied AC-3 track on this very host, so the
|
|
2166
2189
|
// arguments that run actually received are the missing evidence. One line
|
|
2167
2190
|
// per run, and a run happens at most every few seconds.
|
|
2168
|
-
|
|
2191
|
+
// Numbered, because a burst of seeks starts several runs in one second and
|
|
2192
|
+
// every line about them carries the SESSION id, which is the same for all.
|
|
2193
|
+
// Without a run number the command that failed cannot be told from the two
|
|
2194
|
+
// that succeeded around it — which is exactly the state the unexplained
|
|
2195
|
+
// `Cannot write moov atom before AC3 packets` was found in.
|
|
2196
|
+
session.runCounter = (session.runCounter ?? 0) + 1;
|
|
2197
|
+
const runLabel = `run#${session.runCounter}`;
|
|
2198
|
+
session.runLabel = runLabel;
|
|
2199
|
+
logger.info(`transcode ${session.id} ${runLabel} ffmpeg ${describeFfmpegArgs(args)}`);
|
|
2169
2200
|
|
|
2170
2201
|
const ffmpeg = spawn(this.ffmpegBin, args, {
|
|
2171
2202
|
cwd: session.dirPath,
|
|
@@ -2191,7 +2222,7 @@ export class HlsSessionManager {
|
|
|
2191
2222
|
session.budgetSlowSince = 0;
|
|
2192
2223
|
|
|
2193
2224
|
logger.info(
|
|
2194
|
-
`transcode ${session.id} encode-run from segment #${safeIndex} ` +
|
|
2225
|
+
`transcode ${session.id} ${session.runLabel} encode-run from segment #${safeIndex} ` +
|
|
2195
2226
|
`(${formatSeconds(startSeconds)}) "${session.fileName}"`
|
|
2196
2227
|
);
|
|
2197
2228
|
|
|
@@ -2379,7 +2410,9 @@ export class HlsSessionManager {
|
|
|
2379
2410
|
session.state = "failed";
|
|
2380
2411
|
session.progress.state = "failed";
|
|
2381
2412
|
session.progress.updatedAt = Date.now();
|
|
2382
|
-
logger.error(
|
|
2413
|
+
logger.error(
|
|
2414
|
+
`transcode ${session.id} ${session.runLabel ?? "run#?"} encode-run failed: ${session.lastError}`
|
|
2415
|
+
);
|
|
2383
2416
|
});
|
|
2384
2417
|
}
|
|
2385
2418
|
|
|
@@ -263,7 +263,12 @@ export function createPlaybackPlanner({
|
|
|
263
263
|
transcodeAudioEnabled,
|
|
264
264
|
localBaseUrl,
|
|
265
265
|
sourceRegistry,
|
|
266
|
-
torrentPool
|
|
266
|
+
torrentPool,
|
|
267
|
+
// Optional. Called once the file's edges are downloaded, so the keyframe
|
|
268
|
+
// index — which reads the same tail of the file — is fetched alongside the
|
|
269
|
+
// codec probe instead of after it. Late-bound to the HLS session manager,
|
|
270
|
+
// which owns the cache both of them share.
|
|
271
|
+
warmKeyframeIndex
|
|
267
272
|
}) {
|
|
268
273
|
/** @type {Map<string, PlaybackPlan>} */
|
|
269
274
|
const cache = new Map();
|
|
@@ -365,6 +370,17 @@ export function createPlaybackPlanner({
|
|
|
365
370
|
// file, and an unsupported codec like xvid gets copied → black video.
|
|
366
371
|
await torrentPool.prefetchFileEdges(torrent, fileIndex);
|
|
367
372
|
edgesReadyMs = Date.now() - planEntryMs;
|
|
373
|
+
// The keyframe index reads the tail of the file, which the probe has just
|
|
374
|
+
// waited for as well. Started here it overlaps the probe instead of
|
|
375
|
+
// following the whole plan — worth 311-430 ms of the time before the
|
|
376
|
+
// first segment. Fire and forget: the session reads it itself if this has
|
|
377
|
+
// not finished, and both share one cache entry.
|
|
378
|
+
warmKeyframeIndex?.({
|
|
379
|
+
sourceKey,
|
|
380
|
+
fileIndex,
|
|
381
|
+
inputUrl: new URL(directUrl),
|
|
382
|
+
logName: file.name
|
|
383
|
+
});
|
|
368
384
|
let probe = await probeStreamCodecs({ ffmpegBin, inputUrl: directUrl, userAgent });
|
|
369
385
|
const probeDeadline = Date.now() + Math.max(0, maxWaitMs);
|
|
370
386
|
let attempt = 0;
|
package/services/torrent-pool.js
CHANGED
|
@@ -383,6 +383,14 @@ export class TorrentPool {
|
|
|
383
383
|
*/
|
|
384
384
|
#readPositionByTorrent = new Map();
|
|
385
385
|
|
|
386
|
+
/**
|
|
387
|
+
* Edge prefetches currently running, keyed by infoHash and file index, so two
|
|
388
|
+
* callers asking at the same time share one.
|
|
389
|
+
*
|
|
390
|
+
* @type {Map<string, Promise<void>>}
|
|
391
|
+
*/
|
|
392
|
+
#edgePrefetches = new Map();
|
|
393
|
+
|
|
386
394
|
/** Global disk cap in bytes (0 = disabled). */
|
|
387
395
|
#maxDiskBytes = 0;
|
|
388
396
|
|
|
@@ -1034,6 +1042,37 @@ export class TorrentPool {
|
|
|
1034
1042
|
fileIndex,
|
|
1035
1043
|
{ headBytes = 256 * 1024, tailBytes = 2 * 1024 * 1024, timeoutMs = 300_000 } = {}
|
|
1036
1044
|
) {
|
|
1045
|
+
if (!torrent || !Array.isArray(torrent.files)) {
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
// Two callers can ask for the same edges at once: the warm-up that starts
|
|
1049
|
+
// when a torrent is picked, and the playback plan a moment later. Reading
|
|
1050
|
+
// the same two pieces twice costs nothing in bandwidth — the torrent
|
|
1051
|
+
// fetches each piece once — but it does open a second pair of readers, each
|
|
1052
|
+
// claiming a window and holding pieces. One is enough.
|
|
1053
|
+
const inFlightKey = `${torrent.infoHash}:${fileIndex}`;
|
|
1054
|
+
const running = this.#edgePrefetches.get(inFlightKey);
|
|
1055
|
+
if (running) {
|
|
1056
|
+
return running;
|
|
1057
|
+
}
|
|
1058
|
+
const prefetch = this.#prefetchFileEdgesOnce(torrent, fileIndex, { headBytes, tailBytes, timeoutMs });
|
|
1059
|
+
this.#edgePrefetches.set(inFlightKey, prefetch);
|
|
1060
|
+
try {
|
|
1061
|
+
return await prefetch;
|
|
1062
|
+
} finally {
|
|
1063
|
+
this.#edgePrefetches.delete(inFlightKey);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* The body of {@link prefetchFileEdges}, without the de-duplication.
|
|
1069
|
+
*
|
|
1070
|
+
* @param {import("webtorrent").Torrent} torrent
|
|
1071
|
+
* @param {number} fileIndex
|
|
1072
|
+
* @param {{ headBytes: number, tailBytes: number, timeoutMs: number }} options
|
|
1073
|
+
* @returns {Promise<void>}
|
|
1074
|
+
*/
|
|
1075
|
+
async #prefetchFileEdgesOnce(torrent, fileIndex, { headBytes, tailBytes, timeoutMs }) {
|
|
1037
1076
|
if (!torrent || !Array.isArray(torrent.files)) {
|
|
1038
1077
|
return;
|
|
1039
1078
|
}
|
|
@@ -154,6 +154,41 @@ function clearCritical(torrent, { from, to }) {
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Who is working on the piece a reader is blocked on, right now.
|
|
159
|
+
*
|
|
160
|
+
* The open question about a seek: a single 8 MiB piece takes 3.0-4.6 s to
|
|
161
|
+
* arrive while the swarm as a whole is moving 4-6 MB/s, so roughly 2 MB/s is
|
|
162
|
+
* reaching the piece that is actually being waited for. Whether that is because
|
|
163
|
+
* few peers hold it, few are being asked, or each is slow cannot be told apart
|
|
164
|
+
* from the outside — these three counts tell them apart.
|
|
165
|
+
*
|
|
166
|
+
* `wire.requests` is what has been asked of that peer and not yet answered; a
|
|
167
|
+
* block is 16 KB, so `blocks x 16 KB` is the work in flight on this piece.
|
|
168
|
+
*
|
|
169
|
+
* @param {import("webtorrent").Torrent} torrent
|
|
170
|
+
* @param {number} pieceIndex
|
|
171
|
+
* @returns {{ peers: number, holders: number, askedOf: number, blocks: number }}
|
|
172
|
+
*/
|
|
173
|
+
export function pieceSupply(torrent, pieceIndex) {
|
|
174
|
+
const wires = Array.isArray(torrent?.wires) ? torrent.wires : [];
|
|
175
|
+
let holders = 0;
|
|
176
|
+
let askedOf = 0;
|
|
177
|
+
let blocks = 0;
|
|
178
|
+
for (const wire of wires) {
|
|
179
|
+
if (wire?.peerPieces?.get?.(pieceIndex)) {
|
|
180
|
+
holders += 1;
|
|
181
|
+
}
|
|
182
|
+
const requests = Array.isArray(wire?.requests) ? wire.requests : [];
|
|
183
|
+
const forThisPiece = requests.filter((request) => request?.piece === pieceIndex).length;
|
|
184
|
+
if (forThisPiece > 0) {
|
|
185
|
+
askedOf += 1;
|
|
186
|
+
blocks += forThisPiece;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return { peers: wires.length, holders, askedOf, blocks };
|
|
190
|
+
}
|
|
191
|
+
|
|
157
192
|
/**
|
|
158
193
|
* Wait until a piece has been downloaded and verified.
|
|
159
194
|
*
|
|
@@ -421,7 +456,20 @@ export async function* readFragments({
|
|
|
421
456
|
}
|
|
422
457
|
|
|
423
458
|
const waitStartedAt = Date.now();
|
|
424
|
-
|
|
459
|
+
// Sampled while waiting rather than after: once the piece lands, nothing
|
|
460
|
+
// is outstanding on it any more and every count reads zero.
|
|
461
|
+
let supply = null;
|
|
462
|
+
const supplyProbe = setInterval(() => {
|
|
463
|
+
const sample = pieceSupply(torrent, pieceIndex);
|
|
464
|
+
if (!supply || sample.blocks > supply.blocks) {
|
|
465
|
+
supply = sample;
|
|
466
|
+
}
|
|
467
|
+
}, 500);
|
|
468
|
+
try {
|
|
469
|
+
await whenPieceReady(torrent, pieceIndex, cancellation);
|
|
470
|
+
} finally {
|
|
471
|
+
clearInterval(supplyProbe);
|
|
472
|
+
}
|
|
425
473
|
// What a reader spent waiting for data, attributed to the exact piece. A
|
|
426
474
|
// seek's cost is dominated by the first segment after the encoder
|
|
427
475
|
// restarts (measured 9.2-9.4 s), and without this there is no way to say
|
|
@@ -429,10 +477,16 @@ export async function* readFragments({
|
|
|
429
477
|
// wait is long enough to matter, so ordinary sequential reading is silent.
|
|
430
478
|
const waitedMs = Date.now() - waitStartedAt;
|
|
431
479
|
if (waitedMs >= PIECE_WAIT_LOG_MS) {
|
|
480
|
+
const rateKbps = Math.round(pieceLength / 1024 / (waitedMs / 1000));
|
|
432
481
|
logger.info(
|
|
433
482
|
`piece-reader: waited ${waitedMs}ms for piece ${pieceIndex} ` +
|
|
434
483
|
`(${pieceIndex - firstPiece + 1} of ${lastPiece - firstPiece + 1} in a read from ` +
|
|
435
|
-
`${(start / 1024 / 1024).toFixed(0)}MB of "${file.name}")`
|
|
484
|
+
`${(start / 1024 / 1024).toFixed(0)}MB of "${file.name}") ` +
|
|
485
|
+
`— ${rateKbps}KB/s on this piece; ` +
|
|
486
|
+
(supply
|
|
487
|
+
? `${supply.holders}/${supply.peers} peers had it, ${supply.askedOf} were asked, ` +
|
|
488
|
+
`${supply.blocks} blocks (${Math.round((supply.blocks * 16384) / 1024)}KB) in flight at peak`
|
|
489
|
+
: "no sample taken")
|
|
436
490
|
);
|
|
437
491
|
}
|
|
438
492
|
|
package/test/read-window.test.js
CHANGED
|
@@ -219,7 +219,8 @@ test("criticality marks the window being waited for, not the whole range", async
|
|
|
219
219
|
try {
|
|
220
220
|
const iterator = readFragments({
|
|
221
221
|
torrent, fileIndex: 0, start: 0, end: 8000 * PIECE - 1,
|
|
222
|
-
cancellation: { isCancelled: () => false }
|
|
222
|
+
cancellation: { isCancelled: () => false },
|
|
223
|
+
windowBytes: WINDOW_PIECES * PIECE
|
|
223
224
|
});
|
|
224
225
|
const pending = iterator.next();
|
|
225
226
|
await new Promise((resolve) => setImmediate(resolve));
|