@torrent-tv/proxy 2.79.0 → 2.80.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 +1580 -1558
- package/bin/cli.js +20 -1
- package/package.json +1 -1
- package/server.js +16 -0
- package/services/demand/DemandRegister.js +20 -0
- package/services/download/SwarmSelection.js +4 -1
- package/services/encode/EncodePlan.js +159 -31
- package/services/encode/SegmentStore.js +29 -0
- package/services/encode/run-budget.js +8 -0
- package/services/hls-session-manager.js +25 -8
- package/services/orchestrators/EncodeOrchestrator.js +27 -5
- package/services/piece-store/piece-lru.js +91 -31
- package/services/piece-store/shared-piece-store.js +1531 -1512
- package/services/priority/PriorityMap.js +57 -101
- package/services/priority/PriorityOrchestrator.js +149 -0
- package/services/torrent-pool.js +104 -0
- package/services/torrent-worker/client.js +20 -0
- package/services/torrent-worker/piece-reader.js +57 -217
- package/services/torrent-worker/pool-adapter.js +17 -0
- package/services/torrent-worker/protocol.js +10 -0
- package/services/torrent-worker/worker.js +11 -0
- package/services/usrsctp-state.js +35 -6
- package/test/demand-register.test.js +11 -0
- package/test/encode-plan.test.js +65 -0
- package/test/piece-lru.test.js +61 -0
- package/test/piece-store-eviction.test.js +30 -0
- package/test/priority-map-download.test.js +156 -0
- package/test/priority-map.test.js +50 -77
- package/test/read-window.test.js +46 -29
- package/test/usrsctp-state.test.js +18 -2
- package/test/read-bands.test.js +0 -140
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Two responsibilities, deliberately kept apart from any storage:
|
|
5
5
|
*
|
|
6
|
-
* - **
|
|
6
|
+
* - **how much a piece is wanted**, so the piece evicted is the one wanted
|
|
7
|
+
* least. That number comes from the priority map, which is the one place
|
|
8
|
+
* that knows where the viewers are; recency only separates pieces the map
|
|
9
|
+
* wants equally. Recency alone cannot answer it: a reader walking a film
|
|
10
|
+
* touches each piece once, so the piece the decoder will want in two seconds
|
|
11
|
+
* looks exactly as stale as one fetched forty minutes ago and never read
|
|
12
|
+
* again — and with the encoder running ahead of the viewer, the second kind
|
|
13
|
+
* is what fills the store;
|
|
7
14
|
* - **pinning**, so a piece being read cannot be evicted at all.
|
|
8
15
|
*
|
|
9
16
|
* The second is not a refinement of the first. webtor's seeder relies on recency
|
|
@@ -28,18 +35,16 @@ export class PieceLru {
|
|
|
28
35
|
/** Piece index → number of readers currently holding it. */
|
|
29
36
|
#pins = new Map();
|
|
30
37
|
/**
|
|
31
|
-
*
|
|
38
|
+
* Claimant → the piece range it states, and how much it wants it.
|
|
32
39
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* and with the encoder running ahead of the viewer, the second kind is what
|
|
37
|
-
* fills the store. Measured 2026-08-04: the hit rate fell from 100% to 45.7%
|
|
38
|
-
* with 221 pieces read back from disk in one session.
|
|
40
|
+
* The ranges are the priority map's zones, stated by whoever holds the map,
|
|
41
|
+
* plus the piece a read is stopped on. The number is the map's own: lower is
|
|
42
|
+
* more urgent, and a piece no range covers is wanted by nobody at all.
|
|
39
43
|
*
|
|
40
44
|
* A preference, not a pin. At the smallest budget the store guarantees only
|
|
41
|
-
* two resident pieces, so a hard hold on a
|
|
42
|
-
*
|
|
45
|
+
* two resident pieces, so a hard hold on a zone would deadlock it; when
|
|
46
|
+
* everything resident is wanted, the least wanted of them goes rather than
|
|
47
|
+
* nothing going at all.
|
|
43
48
|
*/
|
|
44
49
|
#protected = new Map();
|
|
45
50
|
#capacity;
|
|
@@ -164,8 +169,8 @@ export class PieceLru {
|
|
|
164
169
|
}
|
|
165
170
|
|
|
166
171
|
/**
|
|
167
|
-
* The least
|
|
168
|
-
*
|
|
172
|
+
* The least wanted piece that is free to go, or `null` when every resident
|
|
173
|
+
* piece is pinned.
|
|
169
174
|
*
|
|
170
175
|
* Returning `null` rather than evicting a pinned piece is the whole point:
|
|
171
176
|
* the caller must then wait or fail, never take memory out from under a
|
|
@@ -194,22 +199,63 @@ export class PieceLru {
|
|
|
194
199
|
* victim is inside one, and -1 when no reader has declared anything.
|
|
195
200
|
*/
|
|
196
201
|
evictionChoice() {
|
|
197
|
-
|
|
198
|
-
|
|
202
|
+
/** @type {{ index: number, want: number } | null} */
|
|
203
|
+
let victim = null;
|
|
204
|
+
// `#order` runs least-recently-used first, so among pieces the map wants
|
|
205
|
+
// equally the first one seen is the stalest — recency decides the tie and
|
|
206
|
+
// nothing else. A piece no zone covers is wanted by nobody, which is the
|
|
207
|
+
// most anything can be un-wanted, so the walk stops at the first of those:
|
|
208
|
+
// that is the ordinary case and it costs one step.
|
|
199
209
|
for (const index of this.#order) {
|
|
200
|
-
if (
|
|
201
|
-
|
|
210
|
+
if (this.#pins.has(index)) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const want = this.wantAt(index);
|
|
214
|
+
if (victim === null || want > victim.want) {
|
|
215
|
+
victim = { index, want };
|
|
216
|
+
if (want === Number.POSITIVE_INFINITY) {
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
202
219
|
}
|
|
203
220
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
221
|
+
if (victim === null) {
|
|
222
|
+
// Every resident piece is being read. The caller must wait or fail; it
|
|
223
|
+
// may never take memory out from under a reader.
|
|
224
|
+
return { index: null, protectionYielded: false, distance: -1 };
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
index: victim.index,
|
|
228
|
+
// The map wanted this piece and it is going anyway — the store is being
|
|
229
|
+
// asked to hold more than it has room for, and this piece comes back
|
|
230
|
+
// from disk. Reported so that thrashing is visible as thrashing rather
|
|
231
|
+
// than as ordinary work: 6565 spills and 7575 revivals in 44 minutes on
|
|
232
|
+
// 2026-09-02, with only 53.6 % of reads served from memory.
|
|
233
|
+
protectionYielded: victim.want !== Number.POSITIVE_INFINITY,
|
|
234
|
+
distance: this.#distanceToWindow(victim.index)
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* How much the priority map wants this piece, by the most urgent zone that
|
|
240
|
+
* covers it.
|
|
241
|
+
*
|
|
242
|
+
* Public because admission asks it too: whether an arriving piece displaces
|
|
243
|
+
* a resident one is the same comparison as which resident one goes, and
|
|
244
|
+
* answering them from two different quantities is how a store evicts what it
|
|
245
|
+
* has just decided to keep.
|
|
246
|
+
*
|
|
247
|
+
* @param {number} index
|
|
248
|
+
* @returns {number} Lower is more urgent. Infinity when no zone covers it,
|
|
249
|
+
* so a piece nobody asked for compares as less wanted than any zone.
|
|
250
|
+
*/
|
|
251
|
+
wantAt(index) {
|
|
252
|
+
let want = Number.POSITIVE_INFINITY;
|
|
253
|
+
for (const range of this.#protected.values()) {
|
|
254
|
+
if (index >= range.from && index <= range.to && range.urgency < want) {
|
|
255
|
+
want = range.urgency;
|
|
210
256
|
}
|
|
211
257
|
}
|
|
212
|
-
return
|
|
258
|
+
return want;
|
|
213
259
|
}
|
|
214
260
|
|
|
215
261
|
/**
|
|
@@ -279,14 +325,20 @@ export class PieceLru {
|
|
|
279
325
|
}
|
|
280
326
|
|
|
281
327
|
/**
|
|
282
|
-
* The piece that would be evicted next,
|
|
328
|
+
* The piece that would be evicted next, how much the map wants it, and how
|
|
329
|
+
* long it will be waited for.
|
|
330
|
+
*
|
|
331
|
+
* Both numbers, because that is the order admission compares them in: the
|
|
332
|
+
* map's level first, and the distance only between pieces the map wants
|
|
333
|
+
* equally.
|
|
283
334
|
*
|
|
284
|
-
* @returns {{ index: number | null, wait: number }}
|
|
335
|
+
* @returns {{ index: number | null, want: number, wait: number }}
|
|
285
336
|
*/
|
|
286
337
|
nextVictim() {
|
|
287
338
|
const choice = this.evictionChoice();
|
|
288
339
|
return {
|
|
289
340
|
index: choice.index,
|
|
341
|
+
want: choice.index === null ? Number.POSITIVE_INFINITY : this.wantAt(choice.index),
|
|
290
342
|
wait: choice.index === null ? -1 : this.waitFor(choice.index)
|
|
291
343
|
};
|
|
292
344
|
}
|
|
@@ -342,20 +394,28 @@ export class PieceLru {
|
|
|
342
394
|
}
|
|
343
395
|
|
|
344
396
|
/**
|
|
345
|
-
*
|
|
346
|
-
*
|
|
397
|
+
* State a range of pieces and how much they are wanted, replacing whatever
|
|
398
|
+
* that claimant stated before. Ranges from different claimants add up.
|
|
347
399
|
*
|
|
348
|
-
* @param {string|number} readerId - Identity of the
|
|
349
|
-
* is replaced rather than accumulated.
|
|
400
|
+
* @param {string|number} readerId - Identity of the claimant, so its own
|
|
401
|
+
* range is replaced rather than accumulated.
|
|
350
402
|
* @param {number} from - First piece, inclusive.
|
|
351
403
|
* @param {number} to - Last piece, inclusive.
|
|
404
|
+
* @param {number} [urgency] - The priority map's own number, lower being more
|
|
405
|
+
* urgent. A caller that states none is treated as wanting these pieces
|
|
406
|
+
* least of everyone who did state one, so an unstated range can never
|
|
407
|
+
* displace a stated one.
|
|
352
408
|
* @returns {void}
|
|
353
409
|
*/
|
|
354
|
-
protect(readerId, from, to) {
|
|
410
|
+
protect(readerId, from, to, urgency) {
|
|
355
411
|
if (!Number.isInteger(from) || !Number.isInteger(to) || to < from) {
|
|
356
412
|
return;
|
|
357
413
|
}
|
|
358
|
-
this.#protected.set(readerId, {
|
|
414
|
+
this.#protected.set(readerId, {
|
|
415
|
+
from,
|
|
416
|
+
to,
|
|
417
|
+
urgency: Number.isFinite(urgency) ? Number(urgency) : Number.MAX_SAFE_INTEGER
|
|
418
|
+
});
|
|
359
419
|
}
|
|
360
420
|
|
|
361
421
|
/**
|