@torrent-tv/proxy 2.9.74 → 2.9.75
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 +374 -369
- package/bin/cli.js +8 -0
- package/package.json +1 -1
- package/server.js +229 -228
- package/services/piece-store/shared-piece-store.js +107 -6
- package/services/torrent-worker/client.js +264 -264
- package/services/torrent-worker/pool-adapter.js +179 -179
- package/services/torrent-worker/worker.js +37 -1
- package/test/shared-piece-store.test.js +76 -0
|
@@ -26,11 +26,56 @@
|
|
|
26
26
|
* and nothing here disagrees.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
+
import os from "node:os";
|
|
29
30
|
import { PieceLru } from "./piece-lru.js";
|
|
30
31
|
import { DiskTier } from "./disk-tier.js";
|
|
31
32
|
|
|
32
|
-
/**
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Live stores, so the worker can report on them.
|
|
35
|
+
*
|
|
36
|
+
* WebTorrent constructs the store itself, deep inside its own wrappers, so
|
|
37
|
+
* there is no handle to reach for from outside. Registering here is what makes
|
|
38
|
+
* the store's behaviour visible in the field at all — without it the first
|
|
39
|
+
* strange case has nothing to go on.
|
|
40
|
+
*
|
|
41
|
+
* @type {Set<SharedPieceStore>}
|
|
42
|
+
*/
|
|
43
|
+
const liveStores = new Set();
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A snapshot of every live store, for logging.
|
|
47
|
+
*
|
|
48
|
+
* @returns {{ name: string, resident: number, capacity: number, spilled: number, fromMemory: number, fromDisk: number, spills: number, revivals: number, blockedByPins: number }[]}
|
|
49
|
+
*/
|
|
50
|
+
export function collectStoreStats() {
|
|
51
|
+
return [...liveStores].map((store) => store.stats());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Ceiling for the automatic budget, and the share of free memory it will take.
|
|
56
|
+
*
|
|
57
|
+
* A flat default would be a guess dressed as a decision: the proxy runs on
|
|
58
|
+
* whatever the owner has, from a Pi to a rented box, and the budget is **per
|
|
59
|
+
* torrent** — several viewers mean several of these. Measured on the field host
|
|
60
|
+
* after one session: the proxy container sat at 796 MB with 4.1 GB free and 1.3
|
|
61
|
+
* GB already in swap, so a fixed half-gigabyte per torrent is not something to
|
|
62
|
+
* hand out blindly. Hence: a quarter of what is free, capped.
|
|
63
|
+
*/
|
|
64
|
+
const MEMORY_BUDGET_CEILING_BYTES = 512 * 1024 * 1024;
|
|
65
|
+
const FREE_MEMORY_SHARE = 0.25;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Budget for one torrent's resident pieces when the caller names none.
|
|
69
|
+
*
|
|
70
|
+
* @returns {number}
|
|
71
|
+
*/
|
|
72
|
+
function defaultMemoryBytes() {
|
|
73
|
+
const share = Math.floor(os.freemem() * FREE_MEMORY_SHARE);
|
|
74
|
+
return Math.max(MIN_BUDGET_BYTES, Math.min(MEMORY_BUDGET_CEILING_BYTES, share));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Floor for the automatic budget — below this the store thrashes to disk. */
|
|
78
|
+
const MIN_BUDGET_BYTES = 64 * 1024 * 1024;
|
|
34
79
|
/**
|
|
35
80
|
* Never keep fewer than this many pieces resident, whatever the budget says.
|
|
36
81
|
*
|
|
@@ -63,7 +108,22 @@ export class SharedPieceStore {
|
|
|
63
108
|
#freeSlots = [];
|
|
64
109
|
#lru;
|
|
65
110
|
#disk;
|
|
111
|
+
/** Slots backed by memory right now; grows towards {@link capacity}. */
|
|
112
|
+
#allocatedSlots = 0;
|
|
66
113
|
#closed = false;
|
|
114
|
+
#name;
|
|
115
|
+
/**
|
|
116
|
+
* What the store has actually been doing. Reported, not just kept: the
|
|
117
|
+
* balance between memory and disk reads is the number that says whether the
|
|
118
|
+
* budget is right, and it cannot be guessed from outside.
|
|
119
|
+
*/
|
|
120
|
+
#counters = {
|
|
121
|
+
fromMemory: 0,
|
|
122
|
+
fromDisk: 0,
|
|
123
|
+
spills: 0,
|
|
124
|
+
revivals: 0,
|
|
125
|
+
blockedByPins: 0
|
|
126
|
+
};
|
|
67
127
|
|
|
68
128
|
/**
|
|
69
129
|
* @param {number} chunkLength - Piece length, and therefore the slot size.
|
|
@@ -86,20 +146,44 @@ export class SharedPieceStore {
|
|
|
86
146
|
|
|
87
147
|
const memoryBytes = Number.isFinite(options.memoryBytes) && options.memoryBytes > 0
|
|
88
148
|
? options.memoryBytes
|
|
89
|
-
:
|
|
149
|
+
: defaultMemoryBytes();
|
|
90
150
|
this.#capacity = Math.max(MIN_RESIDENT_PIECES, Math.floor(memoryBytes / chunkLength));
|
|
91
151
|
|
|
92
|
-
|
|
152
|
+
// Grows into the budget instead of taking it up front. The budget is per
|
|
153
|
+
// torrent, so claiming all of it on `add` would charge a host for pieces
|
|
154
|
+
// nobody has asked for — and a torrent that is merely open, or one being
|
|
155
|
+
// probed for its codecs, needs a handful of slots, not the ceiling.
|
|
156
|
+
this.#shared = new SharedArrayBuffer(MIN_RESIDENT_PIECES * chunkLength, {
|
|
157
|
+
maxByteLength: this.#capacity * chunkLength
|
|
158
|
+
});
|
|
93
159
|
this.#pool = Buffer.from(this.#shared);
|
|
94
|
-
for (let slot = 0; slot <
|
|
160
|
+
for (let slot = 0; slot < MIN_RESIDENT_PIECES; slot += 1) {
|
|
95
161
|
this.#freeSlots.push(slot);
|
|
96
162
|
}
|
|
163
|
+
this.#allocatedSlots = MIN_RESIDENT_PIECES;
|
|
97
164
|
this.#lru = new PieceLru(this.#capacity);
|
|
165
|
+
this.#name = options.name ?? "pieces";
|
|
98
166
|
this.#disk = new DiskTier({
|
|
99
167
|
directory: options.path ?? ".",
|
|
100
|
-
name: `${
|
|
168
|
+
name: `${this.#name}.pieces`,
|
|
101
169
|
chunkLength
|
|
102
170
|
});
|
|
171
|
+
liveStores.add(this);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* What this store has been doing, for the periodic report.
|
|
176
|
+
*
|
|
177
|
+
* @returns {{ name: string, resident: number, capacity: number, spilled: number, fromMemory: number, fromDisk: number, spills: number, revivals: number, blockedByPins: number }}
|
|
178
|
+
*/
|
|
179
|
+
stats() {
|
|
180
|
+
return {
|
|
181
|
+
name: this.#name,
|
|
182
|
+
resident: this.#slotOf.size,
|
|
183
|
+
capacity: this.#capacity,
|
|
184
|
+
spilled: this.#disk.size,
|
|
185
|
+
...this.#counters
|
|
186
|
+
};
|
|
103
187
|
}
|
|
104
188
|
|
|
105
189
|
/** `abstract-chunk-store` exposes the piece size under this name. */
|
|
@@ -189,16 +273,28 @@ export class SharedPieceStore {
|
|
|
189
273
|
return free;
|
|
190
274
|
}
|
|
191
275
|
|
|
276
|
+
// Room left in the budget: take more memory rather than evicting. Growing
|
|
277
|
+
// replaces the view over the pool, so every slot offset stays valid — the
|
|
278
|
+
// bytes do not move.
|
|
279
|
+
if (this.#allocatedSlots < this.#capacity) {
|
|
280
|
+
this.#allocatedSlots += 1;
|
|
281
|
+
this.#shared.grow(this.#allocatedSlots * this.#chunkLength);
|
|
282
|
+
this.#pool = Buffer.from(this.#shared);
|
|
283
|
+
return this.#allocatedSlots - 1;
|
|
284
|
+
}
|
|
285
|
+
|
|
192
286
|
const victim = this.#lru.evictionCandidate();
|
|
193
287
|
if (victim === null) {
|
|
194
288
|
// Every resident piece is being read. Taking one anyway is precisely the
|
|
195
289
|
// failure this store exists to make impossible.
|
|
290
|
+
this.#counters.blockedByPins += 1;
|
|
196
291
|
throw new Error("Every resident piece is pinned; no slot can be freed.");
|
|
197
292
|
}
|
|
198
293
|
|
|
199
294
|
const slot = this.#slotOf.get(victim);
|
|
200
295
|
const bytes = this.#pool.subarray(slot * this.#chunkLength, slot * this.#chunkLength + this.#lengthOf(victim));
|
|
201
296
|
await this.#disk.write(victim, bytes);
|
|
297
|
+
this.#counters.spills += 1;
|
|
202
298
|
|
|
203
299
|
this.#slotOf.delete(victim);
|
|
204
300
|
this.#lru.remove(victim);
|
|
@@ -265,6 +361,7 @@ export class SharedPieceStore {
|
|
|
265
361
|
const slot = this.#slotOf.get(index);
|
|
266
362
|
if (slot !== undefined) {
|
|
267
363
|
this.#lru.touch(index);
|
|
364
|
+
this.#counters.fromMemory += 1;
|
|
268
365
|
const start = slot * this.#chunkLength + offset;
|
|
269
366
|
return Buffer.from(this.#pool.subarray(start, start + length));
|
|
270
367
|
}
|
|
@@ -283,6 +380,8 @@ export class SharedPieceStore {
|
|
|
283
380
|
await this.#disk.read(index, target);
|
|
284
381
|
this.#slotOf.set(index, revived);
|
|
285
382
|
this.#lru.touch(index);
|
|
383
|
+
this.#counters.fromDisk += 1;
|
|
384
|
+
this.#counters.revivals += 1;
|
|
286
385
|
const start = revived * this.#chunkLength + offset;
|
|
287
386
|
return Buffer.from(this.#pool.subarray(start, start + length));
|
|
288
387
|
};
|
|
@@ -298,6 +397,7 @@ export class SharedPieceStore {
|
|
|
298
397
|
*/
|
|
299
398
|
close(callback = () => undefined) {
|
|
300
399
|
this.#closed = true;
|
|
400
|
+
liveStores.delete(this);
|
|
301
401
|
this.#disk.close().then(() => callback(null), (error) => callback(error));
|
|
302
402
|
}
|
|
303
403
|
|
|
@@ -309,6 +409,7 @@ export class SharedPieceStore {
|
|
|
309
409
|
*/
|
|
310
410
|
destroy(callback = () => undefined) {
|
|
311
411
|
this.#closed = true;
|
|
412
|
+
liveStores.delete(this);
|
|
312
413
|
this.#slotOf.clear();
|
|
313
414
|
this.#disk.destroy().then(() => callback(null), (error) => callback(error));
|
|
314
415
|
}
|