@torrent-tv/proxy 2.81.2 → 2.83.0
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 +17 -0
- package/CLAUDE.md +11 -0
- package/docs/disk-architecture.md +161 -0
- package/docs/encode-architecture.md +36 -7
- package/package.json +1 -1
- package/server.js +38 -19
- package/services/disk/keep.js +48 -0
- package/services/disk/returns.js +103 -0
- package/services/encode/Encoder.js +42 -0
- package/services/encode/QsvEncoder.js +11 -0
- package/services/encode/SegmentStore.js +14 -0
- package/services/encode/SoftwareEncoder.js +5 -0
- package/services/encode/VaapiEncoder.js +13 -0
- package/services/encode/run-costs.js +37 -2
- package/services/encode/start-stop-cost.js +178 -0
- package/services/hls-session-manager.js +23 -1
- package/services/hwaccel.js +1854 -1843
- package/services/orchestrators/EncodeOrchestrator.js +11 -0
- package/services/piece-store/piece-disk-store.js +71 -6
- package/services/piece-store/piece-lru.js +17 -0
- package/services/piece-store/shared-piece-store.js +1556 -1549
- package/services/torrent-pool.js +8 -7
- package/test/keeping-period.test.js +83 -0
- package/test/piece-disk-store.test.js +88 -0
- package/test/startup-readings.test.js +119 -0
|
@@ -53,6 +53,17 @@ export class EncodeOrchestrator {
|
|
|
53
53
|
/** What a stop and a start have cost on this host. */
|
|
54
54
|
#costs = new RunCosts();
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* What starting and stopping an encoder was measured to cost on this host,
|
|
58
|
+
* before any viewer existed. Told once; real runs replace it as they end.
|
|
59
|
+
*
|
|
60
|
+
* @param {{ firstByteWaitSec: number, killCostSec: number } | null} measured
|
|
61
|
+
* @returns {void}
|
|
62
|
+
*/
|
|
63
|
+
noteStartupCosts(measured) {
|
|
64
|
+
this.#costs.noteStartup(measured);
|
|
65
|
+
}
|
|
66
|
+
|
|
56
67
|
/** The last reason a budget was cut, so the same one is not said twice. */
|
|
57
68
|
#lastBudgetReason = new Map();
|
|
58
69
|
|
|
@@ -72,10 +72,16 @@ export class PieceDiskStore {
|
|
|
72
72
|
|
|
73
73
|
#evictions = 0;
|
|
74
74
|
|
|
75
|
+
/** How many were thrown away for being behind every reader. */
|
|
76
|
+
#behind = 0;
|
|
77
|
+
|
|
75
78
|
#bytes = 0;
|
|
76
79
|
|
|
77
80
|
#now;
|
|
78
81
|
|
|
82
|
+
/** Where the live readers stand, from whoever holds that fact. @type {() => number[]} */
|
|
83
|
+
#readHeads;
|
|
84
|
+
|
|
79
85
|
/**
|
|
80
86
|
* @param {object} params
|
|
81
87
|
* @param {string} params.directory - Where this torrent's pieces live.
|
|
@@ -88,11 +94,12 @@ export class PieceDiskStore {
|
|
|
88
94
|
* means nobody has said yet, and nothing is evicted until somebody does.
|
|
89
95
|
* @param {() => number} [params.now]
|
|
90
96
|
*/
|
|
91
|
-
constructor({ directory, name, chunkLength, allowanceBytes = null, now = Date.now }) {
|
|
97
|
+
constructor({ directory, name, chunkLength, allowanceBytes = null, now = Date.now, readHeads = () => [] }) {
|
|
92
98
|
this.#directory = path.join(directory, name);
|
|
93
99
|
this.#chunkLength = chunkLength;
|
|
94
100
|
this.#allowanceBytes = Number.isFinite(allowanceBytes) && allowanceBytes >= 0 ? allowanceBytes : null;
|
|
95
101
|
this.#now = now;
|
|
102
|
+
this.#readHeads = typeof readHeads === "function" ? readHeads : () => [];
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
/** Where this store's pieces live, for logging and cleanup. */
|
|
@@ -139,6 +146,45 @@ export class PieceDiskStore {
|
|
|
139
146
|
return this.#stored.has(index);
|
|
140
147
|
}
|
|
141
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Throw away what no reader will ask for again, without waiting for the disk
|
|
151
|
+
* to be short.
|
|
152
|
+
*
|
|
153
|
+
* THE SECOND RULE, and it answers a different question from the ceiling.
|
|
154
|
+
* Material nobody needs should not sit on somebody's disk merely because
|
|
155
|
+
* there is room for it — the ceiling here is a share of what is free, and on
|
|
156
|
+
* a roomy host that is tens of gigabytes against a measured growth of 14 400
|
|
157
|
+
* MB in one viewing, so the ceiling alone never binds and nothing is ever
|
|
158
|
+
* removed until the torrent itself goes.
|
|
159
|
+
*
|
|
160
|
+
* A piece BEHIND every read head has been read and will not be read again
|
|
161
|
+
* unless somebody seeks back — and a seek back re-downloads it, which is the
|
|
162
|
+
* bargain this tier already makes when it drops a piece for room. Nothing is
|
|
163
|
+
* thrown away while any reader might still reach it.
|
|
164
|
+
*
|
|
165
|
+
* With no reader at all nothing is removed: a store between reads is not a
|
|
166
|
+
* store nobody wants, and the torrent going idle is what empties it whole.
|
|
167
|
+
*
|
|
168
|
+
* @param {number[]} readHeads - The first piece each live reader still wants.
|
|
169
|
+
* @returns {number} How many pieces were thrown away.
|
|
170
|
+
*/
|
|
171
|
+
forgetBehind(readHeads) {
|
|
172
|
+
const heads = (readHeads ?? []).filter((at) => Number.isInteger(at));
|
|
173
|
+
if (heads.length === 0) {
|
|
174
|
+
return 0;
|
|
175
|
+
}
|
|
176
|
+
const earliest = Math.min(...heads);
|
|
177
|
+
let removed = 0;
|
|
178
|
+
for (const index of [...this.#stored.keys()]) {
|
|
179
|
+
if (index < earliest && !this.#reading.has(index)) {
|
|
180
|
+
this.forget(index);
|
|
181
|
+
this.#behind += 1;
|
|
182
|
+
removed += 1;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return removed;
|
|
186
|
+
}
|
|
187
|
+
|
|
142
188
|
/**
|
|
143
189
|
* Write a piece out, making room for it first.
|
|
144
190
|
*
|
|
@@ -234,14 +280,15 @@ export class PieceDiskStore {
|
|
|
234
280
|
/**
|
|
235
281
|
* What it holds, what it may hold, and what it has had to throw away.
|
|
236
282
|
*
|
|
237
|
-
* @returns {{ pieces: number, bytes: number, allowanceBytes: number | null, evictions: number }}
|
|
283
|
+
* @returns {{ pieces: number, bytes: number, allowanceBytes: number | null, evictions: number, behind: number }}
|
|
238
284
|
*/
|
|
239
285
|
stats() {
|
|
240
286
|
return {
|
|
241
287
|
pieces: this.#stored.size,
|
|
242
288
|
bytes: this.#bytes,
|
|
243
289
|
allowanceBytes: this.#allowanceBytes,
|
|
244
|
-
evictions: this.#evictions
|
|
290
|
+
evictions: this.#evictions,
|
|
291
|
+
behind: this.#behind
|
|
245
292
|
};
|
|
246
293
|
}
|
|
247
294
|
|
|
@@ -349,14 +396,32 @@ export class PieceDiskStore {
|
|
|
349
396
|
* @returns {number | null}
|
|
350
397
|
*/
|
|
351
398
|
#leastRecentlyUsed(except) {
|
|
399
|
+
// WHERE THE READERS STAND DECIDES, and last use only settles ties.
|
|
400
|
+
//
|
|
401
|
+
// What lies behind every read head has been read and will not be read again
|
|
402
|
+
// unless somebody seeks back, so it goes before anything ahead of them,
|
|
403
|
+
// furthest behind first. It is the order the segments are given one layer
|
|
404
|
+
// up, and the order the priority map states, read from the other end.
|
|
405
|
+
// Without the heads there is nothing to order by and last use is all that
|
|
406
|
+
// is left — which is what this was, and what said nothing about what
|
|
407
|
+
// anybody is about to read.
|
|
408
|
+
const heads = this.#readHeads().filter((at) => Number.isInteger(at));
|
|
409
|
+
const earliest = heads.length > 0 ? Math.min(...heads) : null;
|
|
352
410
|
let victim = null;
|
|
353
|
-
let
|
|
411
|
+
let worst = null;
|
|
354
412
|
for (const [index, at] of this.#touched) {
|
|
355
413
|
if (index === except || this.#reading.has(index)) {
|
|
356
414
|
continue;
|
|
357
415
|
}
|
|
358
|
-
|
|
359
|
-
|
|
416
|
+
const behind = earliest !== null && index < earliest;
|
|
417
|
+
const score = { behind, distance: behind ? earliest - index : 0, at };
|
|
418
|
+
if (
|
|
419
|
+
worst === null
|
|
420
|
+
|| (score.behind && !worst.behind)
|
|
421
|
+
|| (score.behind === worst.behind && score.distance > worst.distance)
|
|
422
|
+
|| (score.behind === worst.behind && score.distance === worst.distance && score.at < worst.at)
|
|
423
|
+
) {
|
|
424
|
+
worst = score;
|
|
360
425
|
victim = index;
|
|
361
426
|
}
|
|
362
427
|
}
|
|
@@ -258,6 +258,23 @@ export class PieceLru {
|
|
|
258
258
|
return want;
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Where the live readers stand, as the first piece each of them still wants.
|
|
263
|
+
*
|
|
264
|
+
* A declared range begins at the piece its reader is about to read, so the
|
|
265
|
+
* earliest of those beginnings is the point behind which nothing will be
|
|
266
|
+
* asked for again unless somebody seeks back. That is what makes a spilled
|
|
267
|
+
* piece disposable without waiting for the disk to be short: a piece behind
|
|
268
|
+
* every reader has been read and will not be read again.
|
|
269
|
+
*
|
|
270
|
+
* @returns {number[]} One number per reader, unsorted. Empty when nobody has
|
|
271
|
+
* declared anything, which is a stronger statement than any position — no
|
|
272
|
+
* piece of this file is spoken for at all.
|
|
273
|
+
*/
|
|
274
|
+
readHeads() {
|
|
275
|
+
return [...this.#protected.values()].map((range) => range.from);
|
|
276
|
+
}
|
|
277
|
+
|
|
261
278
|
/**
|
|
262
279
|
* How many pieces the live readers between them are asking to keep, against
|
|
263
280
|
* how many this store may hold.
|