@torrent-tv/proxy 2.97.12 → 2.97.13
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
package/server.js
CHANGED
|
@@ -716,10 +716,7 @@ export async function startProxyServer({
|
|
|
716
716
|
if (original) {
|
|
717
717
|
const ranges = await containerOrchestrator.inspect(params, "source-ranges");
|
|
718
718
|
if (ranges.kind !== "result") return ranges;
|
|
719
|
-
// The torrent's piece is the unit its stretch is copied in, so a copy
|
|
720
|
-
// nobody needs any more stops at the next piece (`OriginalInput.js`).
|
|
721
719
|
sources.push({ sourceKey: params.sourceKey, fileIndex, timeShiftSeconds,
|
|
722
|
-
pieceLength: torrentPool.knownTorrent(params.sourceKey)?.pieceLength ?? null,
|
|
723
720
|
input: { ...ranges.value, original: true, selections: wanted.map(track => ({ track,
|
|
724
721
|
index: track.declaredIndex ?? choices.find(choice => choice.type === track.type)?.index ?? 0 })) } });
|
|
725
722
|
continue;
|
|
@@ -208,7 +208,7 @@ export class EncodeInputs {
|
|
|
208
208
|
this.#requests.delete(key);
|
|
209
209
|
this.#wanted.delete(key);
|
|
210
210
|
// A copy already answered may still be copying its stretch; released,
|
|
211
|
-
//
|
|
211
|
+
// the stretch is dropped when its read returns.
|
|
212
212
|
request.result?.release?.();
|
|
213
213
|
}
|
|
214
214
|
this.#retryMemory();
|
|
@@ -61,29 +61,17 @@ export async function admitOriginalInput({ sources, reserve, readRanges, now = D
|
|
|
61
61
|
let released = false;
|
|
62
62
|
let failed = false;
|
|
63
63
|
retained = true;
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const target = Buffer.allocUnsafeSlow(last - first + 1);
|
|
72
|
-
try {
|
|
73
|
-
for (let at = first; at <= last; at += unit) {
|
|
74
|
-
if (released) return false;
|
|
75
|
-
const buffers = await read(source, [[at, Math.min(last, at + unit - 1)]]);
|
|
76
|
-
if (buffers === null) { failed = true; return false; }
|
|
77
|
-
buffers[0].copy(target, at - first);
|
|
78
|
-
}
|
|
79
|
-
} catch {
|
|
80
|
-
failed = true;
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
64
|
+
// ONE READ FOR THE STRETCH. Copied a torrent piece per read (2.97.12), a
|
|
65
|
+
// stretch took 33.1 s for 83.9 MB on the field's AVI against 126.5 MB in
|
|
66
|
+
// 16.3 s as one read beside another copy (torrent-tv/meta#178): each read
|
|
67
|
+
// waits its turn on two busy threads, and the stretch paid that once per
|
|
68
|
+
// piece. A release therefore discards the stretch when its read returns.
|
|
69
|
+
const copyLater = job => read(job.entry.source, [job.entry.source.input.ranges[job.position]]).then(buffers => {
|
|
70
|
+
if (buffers === null) { failed = true; return false; }
|
|
83
71
|
if (released) return false;
|
|
84
|
-
job.entry.buffers[job.position] =
|
|
72
|
+
job.entry.buffers[job.position] = buffers[0];
|
|
85
73
|
return true;
|
|
86
|
-
};
|
|
74
|
+
}, () => { failed = true; return false; });
|
|
87
75
|
// The stretches, one source after another: they share one reader, and the
|
|
88
76
|
// first source's stretch is the one FFmpeg reaches first.
|
|
89
77
|
const whole = (async () => {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import os from "node:os";
|
|
11
|
+
import { monitorEventLoopDelay } from "node:perf_hooks";
|
|
11
12
|
import { logger } from "../../../utils/logger.js";
|
|
12
13
|
import { availableShareFrom } from "../available-share.js";
|
|
13
14
|
import { baseDrawFrom, costPerMegabyteFrom } from "../torrent-cost.js";
|
|
@@ -15,6 +16,24 @@ import { medianOf, movedBeyondScatter, READINGS_KEPT, scatterOf } from "../learn
|
|
|
15
16
|
import { ENCODE_RUN_STATE, processCanBeSignalled } from "../encode-run-state.js";
|
|
16
17
|
import { SourceFiles } from "../../media/SourceFile.js";
|
|
17
18
|
|
|
19
|
+
/**
|
|
20
|
+
* What the torrent thread says of itself over one interval: how late its event
|
|
21
|
+
* loop ran, and how long reads for readers took inside it, from a command's
|
|
22
|
+
* arrival to its answer (torrent-tv/meta#178). Empty where it says nothing.
|
|
23
|
+
*
|
|
24
|
+
* @param {{ readerMs: number | null, readerReads: number | null, loopP99Ms: number | null, loopMaxMs: number | null }} now
|
|
25
|
+
* @param {{ readerMs: number | null, readerReads: number | null }} previous
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
function threadSays(now, previous) {
|
|
29
|
+
const loop = Number.isFinite(now.loopP99Ms) && Number.isFinite(now.loopMaxMs)
|
|
30
|
+
? `; its loop late p99=${Math.round(now.loopP99Ms)}ms max=${Math.round(now.loopMaxMs)}ms` : "";
|
|
31
|
+
const reads = Number.isFinite(now.readerReads) && Number.isFinite(previous.readerReads) && now.readerReads > previous.readerReads
|
|
32
|
+
&& Number.isFinite(now.readerMs) && Number.isFinite(previous.readerMs)
|
|
33
|
+
? `; ${now.readerReads - previous.readerReads} read(s) for readers took ${Math.round(now.readerMs - previous.readerMs)}ms in it` : "";
|
|
34
|
+
return loop + reads;
|
|
35
|
+
}
|
|
36
|
+
|
|
18
37
|
export class HostLoad {
|
|
19
38
|
/** What this reads and asks of the rest of the proxy, and nothing else. @type {object} */
|
|
20
39
|
#host;
|
|
@@ -35,6 +54,8 @@ export class HostLoad {
|
|
|
35
54
|
|
|
36
55
|
/** The previous reading of the torrent thread's processor and the bytes downloaded. */
|
|
37
56
|
#torrentSample = null;
|
|
57
|
+
/** This thread's event-loop lateness, begun at the first machine reading. */
|
|
58
|
+
#mainLoop = null;
|
|
38
59
|
|
|
39
60
|
/** A reading of the torrent thread is under way; it asks the worker, which may not answer. */
|
|
40
61
|
#readingTorrent = false;
|
|
@@ -186,6 +207,10 @@ export class HostLoad {
|
|
|
186
207
|
uploaded: totals?.uploaded ?? null,
|
|
187
208
|
served: totals?.served ?? null,
|
|
188
209
|
threadCpuSeconds: totals?.threadCpuSeconds ?? null,
|
|
210
|
+
readerMs: totals?.readerMs ?? null,
|
|
211
|
+
readerReads: totals?.readerReads ?? null,
|
|
212
|
+
loopP99Ms: totals?.loopP99Ms ?? null,
|
|
213
|
+
loopMaxMs: totals?.loopMaxMs ?? null,
|
|
189
214
|
metadata: this.#host.readMetadataActivity?.()
|
|
190
215
|
};
|
|
191
216
|
this.#learnThreadCost(now);
|
|
@@ -241,7 +266,7 @@ export class HostLoad {
|
|
|
241
266
|
`torrent: downloaded ${(megabytes / elapsedSec).toFixed(2)} MB/s, sent to peers ${rateOf("uploaded")}, ` +
|
|
242
267
|
`handed to readers ${rateOf("served")} over ${elapsedSec.toFixed(1)}s, ` +
|
|
243
268
|
`limit ${Number.isFinite(limit) ? `${(limit / 1e6).toFixed(2)} MB/s` : "none"}; ` +
|
|
244
|
-
`its thread drew ${Math.round(cpuSeconds / elapsedSec * 100)}% of the machine`
|
|
269
|
+
`its thread drew ${Math.round(cpuSeconds / elapsedSec * 100)}% of the machine` + threadSays(now, previous)
|
|
245
270
|
);
|
|
246
271
|
}
|
|
247
272
|
if (megabytes === 0) {
|
|
@@ -456,7 +481,8 @@ export class HostLoad {
|
|
|
456
481
|
try {
|
|
457
482
|
const totals = await this.#host.getTorrentTotals();
|
|
458
483
|
const read = (key) => { const value = Number(totals?.[key]); return Number.isFinite(value) ? value : null; };
|
|
459
|
-
return { downloaded: read("downloaded"), uploaded: read("uploaded"), served: read("served"), threadCpuSeconds: read("threadCpuSeconds")
|
|
484
|
+
return { downloaded: read("downloaded"), uploaded: read("uploaded"), served: read("served"), threadCpuSeconds: read("threadCpuSeconds"),
|
|
485
|
+
readerMs: read("readerMs"), readerReads: read("readerReads"), loopP99Ms: read("loopP99Ms"), loopMaxMs: read("loopMaxMs") };
|
|
460
486
|
} catch {
|
|
461
487
|
return null; // the pool is busy or gone; a reading missed is not a fault
|
|
462
488
|
}
|
|
@@ -525,6 +551,16 @@ export class HostLoad {
|
|
|
525
551
|
*/
|
|
526
552
|
async #sayMachine(share, proxyShare, running, suspended) {
|
|
527
553
|
const machine = await this.#host.readMachineState();
|
|
554
|
+
// HOW LATE THIS THREAD'S LOOP RAN over the interval: every answer to FFmpeg
|
|
555
|
+
// and every hand-over of a copy waits for it (torrent-tv/meta#178). Begun at
|
|
556
|
+
// the first reading, so a check that never reads the machine starts no timer.
|
|
557
|
+
if (!this.#mainLoop) {
|
|
558
|
+
this.#mainLoop = monitorEventLoopDelay({ resolution: 20 });
|
|
559
|
+
this.#mainLoop.enable();
|
|
560
|
+
}
|
|
561
|
+
const late = this.#mainLoop.count > 0
|
|
562
|
+
? ` loop p99=${Math.round(this.#mainLoop.percentile(99) / 1e6)}ms max=${Math.round(this.#mainLoop.max / 1e6)}ms` : "";
|
|
563
|
+
this.#mainLoop.reset();
|
|
528
564
|
const asPercent = (value) => (value === null || value === undefined ? "n/a" : `${Math.round(value * 100)}%`);
|
|
529
565
|
const free = Number.isFinite(share.systemShare) ? Math.max(0, 1 - share.systemShare) : null;
|
|
530
566
|
logger.info(
|
|
@@ -536,7 +572,7 @@ export class HostLoad {
|
|
|
536
572
|
` over=${share.elapsedSec.toFixed(1)}s` +
|
|
537
573
|
// What the offer will multiply a prediction by, in the same line as the
|
|
538
574
|
// readings it comes from.
|
|
539
|
-
` available=${asPercent(this.hostAvailability.share)} free=${asPercent(free)}`
|
|
575
|
+
` available=${asPercent(this.hostAvailability.share)} free=${asPercent(free)}` + late
|
|
540
576
|
);
|
|
541
577
|
}
|
|
542
578
|
|
|
@@ -167,7 +167,7 @@ export async function readHeldBytes(torrent, fileIndex, start, end, logger = nul
|
|
|
167
167
|
const missingSaid = new WeakMap();
|
|
168
168
|
|
|
169
169
|
/** Acquire every input piece before copying any of the segment's ranges. */
|
|
170
|
-
export async function readHeldRanges(torrent, fileIndex, ranges, maxBytes, logger = null) {
|
|
170
|
+
export async function readHeldRanges(torrent, fileIndex, ranges, maxBytes, logger = null, onRead = null) {
|
|
171
171
|
const file = torrent?.files?.[fileIndex];
|
|
172
172
|
const pieceLength = Number(torrent?.pieceLength);
|
|
173
173
|
const store = pieceStoreOf(torrent);
|
|
@@ -223,6 +223,7 @@ export async function readHeldRanges(torrent, fileIndex, ranges, maxBytes, logge
|
|
|
223
223
|
});
|
|
224
224
|
});
|
|
225
225
|
if (!buffer || buffer.length !== length) return null;
|
|
226
|
+
onRead?.(length);
|
|
226
227
|
for (const { rangeIndex, absoluteStart, absoluteEnd } of span.ranges) {
|
|
227
228
|
const from = Math.max(absoluteStart, pieceStart);
|
|
228
229
|
const to = Math.min(absoluteEnd, pieceStart + pieceLength - 1);
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import { isUsableTorrentHandle, requireCurrentTorrent } from "./handle-state.js";
|
|
24
24
|
import "./install-webrtc-shim.js";
|
|
25
25
|
import { parentPort, workerData } from "node:worker_threads";
|
|
26
|
+
import { monitorEventLoopDelay, performance } from "node:perf_hooks";
|
|
26
27
|
import { Readable } from "node:stream";
|
|
27
28
|
import { heldFileBytes } from "./held-file-bytes.js";
|
|
28
29
|
import { createSendStream } from "./channel.js";
|
|
@@ -108,6 +109,18 @@ const openReads = new Map();
|
|
|
108
109
|
*/
|
|
109
110
|
let servedBytes = 0;
|
|
110
111
|
|
|
112
|
+
/**
|
|
113
|
+
* How long reads for readers took inside this thread, from the command's
|
|
114
|
+
* arrival to its answer, and how many there were; and how late this thread's
|
|
115
|
+
* event loop ran. A copy for an encoder took 16.3 s for 126.5 MB as one read
|
|
116
|
+
* and 33.1 s for 83.9 MB as one read per piece on the field's AVI, and nothing
|
|
117
|
+
* said whether the time was spent here or on the way (torrent-tv/meta#178).
|
|
118
|
+
*/
|
|
119
|
+
let readerMs = 0;
|
|
120
|
+
let readerReads = 0;
|
|
121
|
+
const loopDelay = monitorEventLoopDelay({ resolution: 20 });
|
|
122
|
+
loopDelay.enable();
|
|
123
|
+
|
|
111
124
|
/**
|
|
112
125
|
* Shorthand for this file. The same path as `logger.info` anywhere else in the
|
|
113
126
|
* thread — kept only because it reads better at the hundred call sites here.
|
|
@@ -432,7 +445,12 @@ async function runCommand(command, params, id) {
|
|
|
432
445
|
// bytes, so the price of a downloaded byte is taken over one interval
|
|
433
446
|
// whether or not anything encodes (torrent-tv/meta#178).
|
|
434
447
|
const usage = process.threadCpuUsage();
|
|
435
|
-
|
|
448
|
+
// The loop's lateness over the interval since the last reading, in ms.
|
|
449
|
+
const loopP99Ms = loopDelay.count > 0 ? loopDelay.percentile(99) / 1e6 : null;
|
|
450
|
+
const loopMaxMs = loopDelay.count > 0 ? loopDelay.max / 1e6 : null;
|
|
451
|
+
loopDelay.reset();
|
|
452
|
+
return { downloaded, uploaded, served: servedBytes, threadCpuSeconds: (usage.user + usage.system) / 1e6,
|
|
453
|
+
readerMs, readerReads, loopP99Ms, loopMaxMs };
|
|
436
454
|
}
|
|
437
455
|
|
|
438
456
|
|
|
@@ -528,8 +546,11 @@ async function runCommand(command, params, id) {
|
|
|
528
546
|
case Command.READ_HELD: {
|
|
529
547
|
const known = resolvedTorrents.get(params.sourceKey);
|
|
530
548
|
const torrent = isUsableTorrentHandle(known) ? known : null;
|
|
549
|
+
const startedAt = performance.now();
|
|
531
550
|
const bytes = await readWhileInUse(openReads, id, torrent, params.fileIndex,
|
|
532
551
|
() => readHeldBytes(torrent, params.fileIndex, params.start, params.end, logger));
|
|
552
|
+
readerMs += performance.now() - startedAt;
|
|
553
|
+
readerReads += 1;
|
|
533
554
|
servedBytes += bytes?.length ?? 0;
|
|
534
555
|
return { bytes };
|
|
535
556
|
}
|
|
@@ -537,9 +558,13 @@ async function runCommand(command, params, id) {
|
|
|
537
558
|
case Command.READ_HELD_RANGES: {
|
|
538
559
|
const known = resolvedTorrents.get(params.sourceKey);
|
|
539
560
|
const torrent = isUsableTorrentHandle(known) ? known : null;
|
|
561
|
+
const startedAt = performance.now();
|
|
562
|
+
// Counted as each piece is read, so a long copy lands in the intervals
|
|
563
|
+
// it was made in rather than in the one it ended in.
|
|
540
564
|
const buffers = await readWhileInUse(openReads, id, torrent, params.fileIndex,
|
|
541
|
-
() => readHeldRanges(torrent, params.fileIndex, params.ranges, params.maxBytes, logger));
|
|
542
|
-
|
|
565
|
+
() => readHeldRanges(torrent, params.fileIndex, params.ranges, params.maxBytes, logger, (length) => { servedBytes += length; }));
|
|
566
|
+
readerMs += performance.now() - startedAt;
|
|
567
|
+
readerReads += 1;
|
|
543
568
|
return { buffers };
|
|
544
569
|
}
|
|
545
570
|
|