@torrent-tv/proxy 2.97.7 → 2.97.8
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 +1 -1
- package/services/encode/EncodeInputs.js +18 -5
- package/services/encode/EncodeRuns.js +1 -1
- package/services/encode/quality/HostLoad.js +194 -84
- package/services/server/wire-outputs.js +7 -3
- package/services/storage/processor-share.js +20 -15
- package/services/torrent/worker/worker.js +5 -1
package/package.json
CHANGED
|
@@ -50,11 +50,12 @@ export class EncodeInputs {
|
|
|
50
50
|
#failed;
|
|
51
51
|
#capacity;
|
|
52
52
|
#urgent;
|
|
53
|
+
#rankAt;
|
|
53
54
|
#log;
|
|
54
55
|
#now;
|
|
55
56
|
|
|
56
57
|
constructor({ resolve, readRanges, heldRanges = null, reviseBudget, changed, failed, capacity = () => null, urgent = () => true,
|
|
57
|
-
openOf = () => null, log = () => {}, now = Date.now }) {
|
|
58
|
+
rankAt = () => null, openOf = () => null, log = () => {}, now = Date.now }) {
|
|
58
59
|
this.#openOf = openOf;
|
|
59
60
|
this.#resolve = resolve;
|
|
60
61
|
this.#read = readRanges;
|
|
@@ -64,6 +65,7 @@ export class EncodeInputs {
|
|
|
64
65
|
this.#failed = failed;
|
|
65
66
|
this.#capacity = capacity;
|
|
66
67
|
this.#urgent = urgent;
|
|
68
|
+
this.#rankAt = rankAt;
|
|
67
69
|
this.#log = log;
|
|
68
70
|
this.#now = now;
|
|
69
71
|
}
|
|
@@ -71,12 +73,20 @@ export class EncodeInputs {
|
|
|
71
73
|
/**
|
|
72
74
|
* One line when a request's state changes, so a wait that never ends names
|
|
73
75
|
* what it waits for. `detail` is said with it and does not count as a change.
|
|
76
|
+
*
|
|
77
|
+
* It names the piece the input is prepared FOR and that piece's rank in the
|
|
78
|
+
* output's map: an input read from the keyframe before starts one piece
|
|
79
|
+
* early, and its own first piece would understate how urgently it is wanted
|
|
80
|
+
* (torrent-tv/meta#178).
|
|
74
81
|
*/
|
|
75
82
|
#note(request, state, detail = "") {
|
|
76
83
|
if (request.noted === state) return;
|
|
77
84
|
request.noted = state;
|
|
85
|
+
const serves = request.serves ?? request.from;
|
|
86
|
+
const rank = this.#rankAt(request.output, serves);
|
|
78
87
|
this.#log(`encode input #${request.from}..#${request.to} of ${request.output.outputKey ?? request.output.id}: ${state}` +
|
|
79
|
-
` (held ${this.#held} of ${this.#allowed} allowed
|
|
88
|
+
` (held ${this.#held} of ${this.#allowed} allowed; for #${serves}` +
|
|
89
|
+
`${rank ? `, rank ${rank.rank} of ${rank.topRank}` : ""})${detail}`);
|
|
80
90
|
}
|
|
81
91
|
|
|
82
92
|
/** The container's statement of an interval, its time added to the request's account. */
|
|
@@ -183,11 +193,14 @@ export class EncodeInputs {
|
|
|
183
193
|
* `to` is the last number the run may reach. A packet input is prepared for
|
|
184
194
|
* `from` alone; an original input for the longest stretch from `from` that
|
|
185
195
|
* is present, fits memory and copies no longer than an open of that input.
|
|
186
|
-
* The answer carries the stretch it holds as `from` and `to`.
|
|
196
|
+
* The answer carries the stretch it holds as `from` and `to`. `serves` is
|
|
197
|
+
* the piece the run is started for, which is `from` unless the input is read
|
|
198
|
+
* from the keyframe before.
|
|
187
199
|
*/
|
|
188
|
-
take(output, from, to) {
|
|
200
|
+
take(output, from, to, serves = from) {
|
|
189
201
|
const key = keyOf(output, from);
|
|
190
202
|
let existing = this.#requests.get(key);
|
|
203
|
+
if (existing) existing.serves = serves;
|
|
191
204
|
if (existing && existing.to !== to) {
|
|
192
205
|
// The plan's bound for this start moved. A copy already made beyond it
|
|
193
206
|
// holds the shorter stretch whole, so the run is commanded over its
|
|
@@ -203,7 +216,7 @@ export class EncodeInputs {
|
|
|
203
216
|
return existing.result;
|
|
204
217
|
}
|
|
205
218
|
if (existing?.pending || existing?.revision === this.#revision || existing?.result?.kind === "terminal") return null;
|
|
206
|
-
const request = existing ?? { output, from, to, key };
|
|
219
|
+
const request = existing ?? { output, from, to, key, serves };
|
|
207
220
|
this.#requests.set(key, request);
|
|
208
221
|
this.#prepare(request);
|
|
209
222
|
return null;
|
|
@@ -899,7 +899,7 @@ export class EncodeRuns {
|
|
|
899
899
|
const mayReach = this.#inputFailures.boundFor(session.outputKey, startIndex,
|
|
900
900
|
Number.isInteger(ordered?.to) && ordered.to >= startIndex ? ordered.to
|
|
901
901
|
: segmentCount > startIndex ? segmentCount - 1 : startIndex);
|
|
902
|
-
const admittedInput = this.#host.encodeInputs?.take(session, inputStart, mayReach) ?? null;
|
|
902
|
+
const admittedInput = this.#host.encodeInputs?.take(session, inputStart, mayReach, startIndex) ?? null;
|
|
903
903
|
if (!admittedInput) return null;
|
|
904
904
|
const inputEnd = admittedInput.original ? admittedInput.to : startIndex;
|
|
905
905
|
// The stretch is part of an original input's identity: two stretches from
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What the proxy's own work costs this host, read while it runs.
|
|
3
3
|
*
|
|
4
|
-
* The torrent's
|
|
5
|
-
* draws doing nothing priced, how fast each watched torrent is moving, the speed
|
|
4
|
+
* The torrent thread's processor per megabyte it downloads, the share of the
|
|
5
|
+
* machine this process draws doing nothing priced, how fast each watched torrent is moving, the speed
|
|
6
6
|
* each file's supply demands, how long each file is, and how much of the machine
|
|
7
7
|
* a new encoder could have. The encoders' own cost is `EncodeCost`'s.
|
|
8
8
|
*/
|
|
@@ -30,9 +30,32 @@ export class HostLoad {
|
|
|
30
30
|
/** The previous reading of the machine, to compare the next one against. */
|
|
31
31
|
#hostLoadSample = null;
|
|
32
32
|
|
|
33
|
-
/** The previous reading taken while nothing was encoding, for
|
|
33
|
+
/** The previous reading taken while nothing was encoding, for this process's base draw. */
|
|
34
34
|
#idleLoadSample = null;
|
|
35
35
|
|
|
36
|
+
/** The previous reading of the torrent thread's processor and the bytes downloaded. */
|
|
37
|
+
#torrentSample = null;
|
|
38
|
+
|
|
39
|
+
/** A reading of the torrent thread is under way; it asks the worker, which may not answer. */
|
|
40
|
+
#readingTorrent = false;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The share of the machine the torrent thread draws in intervals that
|
|
44
|
+
* downloaded nothing: what comes off its spending before the rest is priced
|
|
45
|
+
* per downloaded byte.
|
|
46
|
+
*/
|
|
47
|
+
#observedThreadBaseDraw = null;
|
|
48
|
+
|
|
49
|
+
/** @type {number[]} Recent readings behind that median. */
|
|
50
|
+
#threadBaseReadings = [];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The download limit the processor share last set, bytes per second;
|
|
54
|
+
* Infinity for none, null before the share ran. Said beside what the
|
|
55
|
+
* torrents actually moved.
|
|
56
|
+
*/
|
|
57
|
+
downloadLimit = null;
|
|
58
|
+
|
|
36
59
|
/** The previous reading of the whole machine taken while nothing was encoding. */
|
|
37
60
|
#idleAvailabilitySample = null;
|
|
38
61
|
|
|
@@ -47,16 +70,21 @@ export class HostLoad {
|
|
|
47
70
|
*/
|
|
48
71
|
fileLengthByKey = new Map();
|
|
49
72
|
|
|
50
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* The machine's seconds per megabyte downloaded, spent by the torrent thread
|
|
75
|
+
* (its own processor time divided by the cores, its base draw taken off),
|
|
76
|
+
* once measured. The torrent's work on other threads — libuv's pool, the main
|
|
77
|
+
* thread — is not in it. Read by the processor share and the quality offer.
|
|
78
|
+
*/
|
|
51
79
|
observedTorrentCostPerMegabyte = null;
|
|
52
80
|
|
|
53
81
|
/** @type {number[]} Recent readings behind that median. */
|
|
54
82
|
#torrentCostReadings = [];
|
|
55
83
|
|
|
56
84
|
/**
|
|
57
|
-
* The share of
|
|
58
|
-
* torrents moving nothing — the spending that would have happened anyway
|
|
59
|
-
*
|
|
85
|
+
* The share of the machine this process draws with nothing encoding and the
|
|
86
|
+
* torrents moving nothing — the spending that would have happened anyway.
|
|
87
|
+
* The processor share takes it off what the proxy has.
|
|
60
88
|
*/
|
|
61
89
|
#observedBaseDraw = null;
|
|
62
90
|
|
|
@@ -89,7 +117,7 @@ export class HostLoad {
|
|
|
89
117
|
/**
|
|
90
118
|
* @param {object} host - `liveRunsOf`, `runStateOf`, the readings of `encode/host-load.js` (`readMachineState`, `readProcessCpuSeconds`, `readProxyCpuSeconds`, `readSystemCpu`, `shareOfMachine`), `getSourceStats`, `getTorrentTotals`, `outputs`
|
|
91
119
|
*/
|
|
92
|
-
/** The share of
|
|
120
|
+
/** The share of the machine this process draws doing nothing priced; zero until measured. */
|
|
93
121
|
get baseDraw() {
|
|
94
122
|
return this.#observedBaseDraw ?? 0;
|
|
95
123
|
}
|
|
@@ -113,60 +141,100 @@ export class HostLoad {
|
|
|
113
141
|
}
|
|
114
142
|
|
|
115
143
|
/**
|
|
116
|
-
* What the torrent
|
|
144
|
+
* What the torrent costs this machine per megabyte it downloads, and what
|
|
145
|
+
* this process draws when it is doing nothing priced. One reading of the
|
|
146
|
+
* torrent totals feeds both.
|
|
117
147
|
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
148
|
+
* THE PRICE OF A BYTE is the torrent thread's own processor time against the
|
|
149
|
+
* bytes every torrent downloaded, both read by that thread in one moment, so
|
|
150
|
+
* they cover one interval. It is taken whether or not anything encodes: up to
|
|
151
|
+
* 2.97.7 it was taken only while nothing encoded, and an AVI re-encoded from
|
|
152
|
+
* the first second of its download never got a price, so the processor share
|
|
153
|
+
* never limited the download it was built for (field 2026-10-11: 0 limits on
|
|
154
|
+
* the AVI, 267 on the MKV, torrent-tv/meta#178). The thread's base draw —
|
|
155
|
+
* what it spends in intervals that downloaded nothing — comes off first, with
|
|
156
|
+
* the check of `encode/torrent-cost.js` that what is left is more than the
|
|
157
|
+
* base draw's own scatter; an interval that downloaded nothing is never a
|
|
158
|
+
* price. The torrent's work on other threads is not counted.
|
|
122
159
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
160
|
+
* THE BASE DRAW OF THIS PROCESS is taken as before: only while nothing
|
|
161
|
+
* encodes and nothing downloads, and never across metadata parsing.
|
|
162
|
+
*
|
|
163
|
+
* @param {boolean} nothingEncoding
|
|
126
164
|
*/
|
|
127
|
-
async #learnTorrentCost() {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
165
|
+
async #learnTorrentCost(nothingEncoding) {
|
|
166
|
+
if (this.#readingTorrent) return;
|
|
167
|
+
this.#readingTorrent = true;
|
|
168
|
+
try {
|
|
169
|
+
const activityBefore = this.#host.readMetadataActivity?.();
|
|
170
|
+
const totals = await this.#torrentTotals();
|
|
171
|
+
const now = {
|
|
172
|
+
takenAt: Date.now(),
|
|
173
|
+
cpuSeconds: this.#host.readProxyCpuSeconds(),
|
|
174
|
+
bytes: totals?.downloaded ?? null,
|
|
175
|
+
threadCpuSeconds: totals?.threadCpuSeconds ?? null,
|
|
176
|
+
metadata: this.#host.readMetadataActivity?.()
|
|
177
|
+
};
|
|
178
|
+
this.#learnThreadCost(now);
|
|
179
|
+
if (!nothingEncoding) {
|
|
180
|
+
this.#idleLoadSample = null;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const previous = this.#idleLoadSample;
|
|
184
|
+
this.#idleLoadSample = now;
|
|
185
|
+
if (previous === null || now.bytes === null || previous.bytes === null || now.bytes !== previous.bytes) return;
|
|
186
|
+
// Process CPU includes metadata parsing; an interval holding it is not
|
|
187
|
+
// what this process draws doing nothing.
|
|
188
|
+
if (activityBefore?.active || now.metadata?.active || previous.metadata?.active ||
|
|
189
|
+
activityBefore?.epoch !== now.metadata?.epoch || previous.metadata?.epoch !== now.metadata?.epoch) return;
|
|
190
|
+
const elapsedSec = (now.takenAt - previous.takenAt) / 1000;
|
|
191
|
+
// Divided by the cores: `process.cpuUsage()` adds up every thread, while
|
|
192
|
+
// everything this figure is set against is a share of the whole machine.
|
|
193
|
+
const cpuSeconds = (now.cpuSeconds - previous.cpuSeconds) / Math.max(1, os.cpus().length);
|
|
194
|
+
this.#learnBaseDraw(baseDrawFrom({ cpuSeconds, elapsedSeconds: elapsedSec }));
|
|
195
|
+
} finally {
|
|
196
|
+
this.#readingTorrent = false;
|
|
140
197
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* One interval of the torrent thread: its base draw where it downloaded
|
|
202
|
+
* nothing, a price where it downloaded something, and what it moved against
|
|
203
|
+
* the limit in force, said while anything moves or is limited.
|
|
204
|
+
*
|
|
205
|
+
* @param {{ takenAt: number, bytes: number | null, threadCpuSeconds: number | null }} now
|
|
206
|
+
*/
|
|
207
|
+
#learnThreadCost(now) {
|
|
208
|
+
const previous = this.#torrentSample;
|
|
209
|
+
this.#torrentSample = now;
|
|
210
|
+
if (previous === null || now.bytes === null || previous.bytes === null ||
|
|
211
|
+
now.threadCpuSeconds === null || previous.threadCpuSeconds === null) return;
|
|
145
212
|
const elapsedSec = (now.takenAt - previous.takenAt) / 1000;
|
|
146
213
|
const megabytes = (now.bytes - previous.bytes) / 1e6;
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
214
|
+
// A torrent leaving the pool takes its bytes out of the total, and a new
|
|
215
|
+
// worker starts its clock again: such an interval says nothing.
|
|
216
|
+
if (!(elapsedSec > 0) || megabytes < 0 || now.threadCpuSeconds < previous.threadCpuSeconds) return;
|
|
217
|
+
const cpuSeconds = (now.threadCpuSeconds - previous.threadCpuSeconds) / Math.max(1, os.cpus().length);
|
|
218
|
+
const limit = this.downloadLimit;
|
|
219
|
+
if (megabytes > 0 || Number.isFinite(limit)) {
|
|
220
|
+
logger.info(
|
|
221
|
+
`torrent: downloaded ${(megabytes / elapsedSec).toFixed(2)} MB/s over ${elapsedSec.toFixed(1)}s, ` +
|
|
222
|
+
`limit ${Number.isFinite(limit) ? `${(limit / 1e6).toFixed(2)} MB/s` : "none"}; ` +
|
|
223
|
+
`its thread drew ${Math.round(cpuSeconds / elapsedSec * 100)}% of the machine`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
154
226
|
if (megabytes === 0) {
|
|
155
|
-
|
|
156
|
-
// that interval, it spends whether or not there is a torrent. Measuring
|
|
157
|
-
// it is what lets the next interval be attributed instead of divided
|
|
158
|
-
// whole — see `encode/torrent-cost.js` for the readings that forced this.
|
|
159
|
-
this.#learnBaseDraw(baseDrawFrom({ cpuSeconds, elapsedSeconds: elapsedSec }));
|
|
227
|
+
this.#learnThreadBaseDraw(baseDrawFrom({ cpuSeconds, elapsedSeconds: elapsedSec }));
|
|
160
228
|
return;
|
|
161
229
|
}
|
|
162
230
|
const costPerMegabyte = costPerMegabyteFrom({
|
|
163
231
|
cpuSeconds,
|
|
164
232
|
elapsedSeconds: elapsedSec,
|
|
165
233
|
megabytes,
|
|
166
|
-
baseDraw: this.#
|
|
167
|
-
// How much the draw's own readings disagree, which is how much of
|
|
168
|
-
// interval's remainder means nothing.
|
|
169
|
-
drawScatter: scatterOf(this.#
|
|
234
|
+
baseDraw: this.#observedThreadBaseDraw,
|
|
235
|
+
// How much the base draw's own readings disagree, which is how much of
|
|
236
|
+
// this interval's remainder means nothing.
|
|
237
|
+
drawScatter: scatterOf(this.#threadBaseReadings)
|
|
170
238
|
});
|
|
171
239
|
if (costPerMegabyte === null) {
|
|
172
240
|
return;
|
|
@@ -179,9 +247,31 @@ export class HostLoad {
|
|
|
179
247
|
}
|
|
180
248
|
this.observedTorrentCostPerMegabyte = median;
|
|
181
249
|
logger.info(
|
|
182
|
-
`host-load: the torrent costs ${(median * 1000).toFixed(1)}ms of
|
|
250
|
+
`host-load: the torrent thread costs ${(median * 1000).toFixed(1)}ms of the machine per downloaded MB ` +
|
|
183
251
|
`(median of ${readings.length}, latest ${(costPerMegabyte * 1000).toFixed(1)}ms over ${megabytes.toFixed(1)}MB, ` +
|
|
184
|
-
`base draw ${((this.#
|
|
252
|
+
`its base draw ${((this.#observedThreadBaseDraw ?? 0) * 100).toFixed(1)}% of the machine taken off)`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* What the torrent thread draws in an interval that downloaded nothing.
|
|
258
|
+
*
|
|
259
|
+
* @param {number | null} share - Of the machine, over the interval just read.
|
|
260
|
+
*/
|
|
261
|
+
#learnThreadBaseDraw(share) {
|
|
262
|
+
if (share === null) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const readings = [...this.#threadBaseReadings, share].slice(-READINGS_KEPT);
|
|
266
|
+
this.#threadBaseReadings = readings;
|
|
267
|
+
const median = medianOf(readings);
|
|
268
|
+
if (!movedBeyondScatter(this.#observedThreadBaseDraw, median, readings)) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
this.#observedThreadBaseDraw = median;
|
|
272
|
+
logger.info(
|
|
273
|
+
`host-load: the torrent thread draws ${(median * 100).toFixed(1)}% of the machine while it downloads nothing ` +
|
|
274
|
+
`(median of ${readings.length}, latest ${(share * 100).toFixed(1)}%)`
|
|
185
275
|
);
|
|
186
276
|
}
|
|
187
277
|
|
|
@@ -308,7 +398,7 @@ export class HostLoad {
|
|
|
308
398
|
* Record what this process draws when it is doing none of the work that gets
|
|
309
399
|
* priced.
|
|
310
400
|
*
|
|
311
|
-
* @param {number | null} share - Of
|
|
401
|
+
* @param {number | null} share - Of the machine, over the interval just read.
|
|
312
402
|
* @returns {void}
|
|
313
403
|
*/
|
|
314
404
|
#learnBaseDraw(share) {
|
|
@@ -323,29 +413,31 @@ export class HostLoad {
|
|
|
323
413
|
}
|
|
324
414
|
this.#observedBaseDraw = median;
|
|
325
415
|
logger.info(
|
|
326
|
-
`host-load: this process draws ${(median * 100).toFixed(1)}% of
|
|
416
|
+
`host-load: this process draws ${(median * 100).toFixed(1)}% of the machine with nothing encoding and ` +
|
|
327
417
|
`nothing downloading (median of ${readings.length}, latest ${(share * 100).toFixed(1)}%)`
|
|
328
418
|
);
|
|
329
419
|
}
|
|
330
420
|
|
|
331
421
|
/**
|
|
332
|
-
*
|
|
333
|
-
* asked.
|
|
422
|
+
* The bytes every torrent has downloaded and the torrent thread's processor
|
|
423
|
+
* seconds, read together by that thread, or null when it cannot be asked.
|
|
424
|
+
* Downloaded bytes only: every one is verified against the piece hash and
|
|
425
|
+
* written to the store; a byte sent back to the swarm is neither.
|
|
334
426
|
*
|
|
335
|
-
* @returns {Promise<number | null>}
|
|
427
|
+
* @returns {Promise<{ downloaded: number | null, threadCpuSeconds: number | null } | null>}
|
|
336
428
|
*/
|
|
337
|
-
async #
|
|
429
|
+
async #torrentTotals() {
|
|
338
430
|
if (typeof this.#host.getTorrentTotals !== "function") {
|
|
339
431
|
return null;
|
|
340
432
|
}
|
|
341
433
|
try {
|
|
342
434
|
const totals = await this.#host.getTorrentTotals();
|
|
343
|
-
// Downloaded bytes only. Every one of them is verified against the piece
|
|
344
|
-
// hash and written to the store; a byte sent back to the swarm is neither,
|
|
345
|
-
// and adding the two would price both at whatever the mixture happened to
|
|
346
|
-
// be on the day.
|
|
347
435
|
const downloaded = Number(totals?.downloaded);
|
|
348
|
-
|
|
436
|
+
const threadCpuSeconds = Number(totals?.threadCpuSeconds);
|
|
437
|
+
return {
|
|
438
|
+
downloaded: Number.isFinite(downloaded) ? downloaded : null,
|
|
439
|
+
threadCpuSeconds: Number.isFinite(threadCpuSeconds) ? threadCpuSeconds : null
|
|
440
|
+
};
|
|
349
441
|
} catch {
|
|
350
442
|
return null; // the pool is busy or gone; a reading missed is not a fault
|
|
351
443
|
}
|
|
@@ -375,9 +467,7 @@ export class HostLoad {
|
|
|
375
467
|
} finally {
|
|
376
468
|
this.#reporting = false;
|
|
377
469
|
}
|
|
378
|
-
|
|
379
|
-
await this.#learnTorrentCost();
|
|
380
|
-
}
|
|
470
|
+
await this.#learnTorrentCost(nothingEncoding);
|
|
381
471
|
}
|
|
382
472
|
|
|
383
473
|
/**
|
|
@@ -386,7 +476,7 @@ export class HostLoad {
|
|
|
386
476
|
* in force for ever: it refused every new encoder, and a refused encoder is
|
|
387
477
|
* the only thing that would have taken the next reading.
|
|
388
478
|
*/
|
|
389
|
-
async #learnIdleAvailability() {
|
|
479
|
+
async #learnIdleAvailability(watched, suspended) {
|
|
390
480
|
const system = (await this.#host.readSystemCpu?.()) ?? null;
|
|
391
481
|
const sample = { takenAt: Date.now(), system, proxyCpuSeconds: this.#host.readProxyCpuSeconds() };
|
|
392
482
|
const previous = this.#idleAvailabilitySample;
|
|
@@ -399,7 +489,36 @@ export class HostLoad {
|
|
|
399
489
|
{ takenAt: previous.takenAt, processCpuSeconds: 0, system: previous.system },
|
|
400
490
|
{ takenAt: sample.takenAt, processCpuSeconds: 0, system: sample.system }
|
|
401
491
|
);
|
|
402
|
-
this.#setAvailability(share, previous, sample);
|
|
492
|
+
const proxyShare = this.#setAvailability(share, previous, sample);
|
|
493
|
+
// Said while something is watched, so what is free while every encoder
|
|
494
|
+
// stands is seen beside what it is free for (torrent-tv/meta#178); a proxy
|
|
495
|
+
// nobody watches stays quiet.
|
|
496
|
+
if (share !== null && watched) {
|
|
497
|
+
await this.#sayMachine(share, proxyShare, 0, suspended);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* One line of the machine's reading, the same whether encoders run or stand.
|
|
503
|
+
* `available` is the share OTHER processes leave the proxy, what the offer
|
|
504
|
+
* and the processor share divide; `free` is what nothing used over the
|
|
505
|
+
* interval. Unknown stays `n/a`, never zero.
|
|
506
|
+
*/
|
|
507
|
+
async #sayMachine(share, proxyShare, running, suspended) {
|
|
508
|
+
const machine = await this.#host.readMachineState();
|
|
509
|
+
const asPercent = (value) => (value === null || value === undefined ? "n/a" : `${Math.round(value * 100)}%`);
|
|
510
|
+
const free = Number.isFinite(share.systemShare) ? Math.max(0, 1 - share.systemShare) : null;
|
|
511
|
+
logger.info(
|
|
512
|
+
`host-load: ffmpeg=${asPercent(share.processShare)} proxy=${asPercent(proxyShare)} ` +
|
|
513
|
+
`system=${asPercent(share.systemShare)} ` +
|
|
514
|
+
`iowait=${asPercent(share.iowaitShare)} cpu=${machine.megahertz === null ? "n/a" : `${machine.megahertz}MHz`} ` +
|
|
515
|
+
`temp=${machine.celsius === null ? "n/a" : `${machine.celsius}C`} ` +
|
|
516
|
+
`encoders=${running} running` + (suspended > 0 ? ` +${suspended} suspended` : "") +
|
|
517
|
+
` over=${share.elapsedSec.toFixed(1)}s` +
|
|
518
|
+
// What the offer will multiply a prediction by, in the same line as the
|
|
519
|
+
// readings it comes from.
|
|
520
|
+
` available=${asPercent(this.hostAvailability.share)} free=${asPercent(free)}`
|
|
521
|
+
);
|
|
403
522
|
}
|
|
404
523
|
|
|
405
524
|
/**
|
|
@@ -433,7 +552,8 @@ export class HostLoad {
|
|
|
433
552
|
|
|
434
553
|
/** @returns {Promise<boolean>} True when no encoder is running, which is when the torrent's cost can be taken. */
|
|
435
554
|
async #reportMachineOnce() {
|
|
436
|
-
const
|
|
555
|
+
const outputs = [...this.#host.outputs.values()];
|
|
556
|
+
const encoding = outputs.filter(
|
|
437
557
|
(session) => processCanBeSignalled(this.#host.runStateOf(session))
|
|
438
558
|
);
|
|
439
559
|
const runningNow = encoding.filter((session) => this.#host.runStateOf(session) !== ENCODE_RUN_STATE.SUSPENDED);
|
|
@@ -443,7 +563,8 @@ export class HostLoad {
|
|
|
443
563
|
// in which every encoder was suspended, the torrent's price could have
|
|
444
564
|
// been taken, and none was. What this process spends now is the download,
|
|
445
565
|
// the verification, the piece store and the delivery. Item 7.
|
|
446
|
-
await this.#learnIdleAvailability(
|
|
566
|
+
await this.#learnIdleAvailability(outputs.length > 0,
|
|
567
|
+
encoding.filter((session) => this.#host.runStateOf(session) === ENCODE_RUN_STATE.SUSPENDED).length);
|
|
447
568
|
this.#hostLoadSample = null;
|
|
448
569
|
return true;
|
|
449
570
|
}
|
|
@@ -514,24 +635,12 @@ export class HostLoad {
|
|
|
514
635
|
// they stopped).
|
|
515
636
|
const suspended = encoding.filter((session) => this.#host.runStateOf(session) === ENCODE_RUN_STATE.SUSPENDED).length;
|
|
516
637
|
const running = encoding.length - suspended;
|
|
517
|
-
const machine = await this.#host.readMachineState();
|
|
518
|
-
const asPercent = (value) => (value === null ? "n/a" : `${Math.round(value * 100)}%`);
|
|
519
638
|
// Kept for the quality offer, which predicts from a benchmark taken on a
|
|
520
639
|
// QUIET host: the same reading that is printed here says how much of the
|
|
521
640
|
// machine a new encoder could actually have. Only what nobody has been
|
|
522
641
|
// charged for is subtracted — see `encode/available-share.js`.
|
|
523
642
|
const proxyShare = this.#setAvailability(share, previous, sample);
|
|
524
|
-
|
|
525
|
-
`host-load: ffmpeg=${asPercent(share.processShare)} proxy=${asPercent(proxyShare)} ` +
|
|
526
|
-
`system=${asPercent(share.systemShare)} ` +
|
|
527
|
-
`iowait=${asPercent(share.iowaitShare)} cpu=${machine.megahertz === null ? "n/a" : `${machine.megahertz}MHz`} ` +
|
|
528
|
-
`temp=${machine.celsius === null ? "n/a" : `${machine.celsius}C`} ` +
|
|
529
|
-
`encoders=${running} running` + (suspended > 0 ? ` +${suspended} suspended` : "") +
|
|
530
|
-
` over=${share.elapsedSec.toFixed(1)}s` +
|
|
531
|
-
// What the offer will multiply a prediction by, in the same line as the
|
|
532
|
-
// readings it comes from.
|
|
533
|
-
` available=${asPercent(this.hostAvailability.share)}`
|
|
534
|
-
);
|
|
643
|
+
await this.#sayMachine(share, proxyShare, running, suspended);
|
|
535
644
|
return false;
|
|
536
645
|
}
|
|
537
646
|
|
|
@@ -541,8 +650,9 @@ export class HostLoad {
|
|
|
541
650
|
*
|
|
542
651
|
* A viewer consumes the file at its own byte rate, and every one of those
|
|
543
652
|
* bytes is downloaded, verified and pushed by this process. Priced per
|
|
544
|
-
* megabyte
|
|
545
|
-
*
|
|
653
|
+
* megabyte by the torrent thread's own processor time, which no encoder's
|
|
654
|
+
* reading contains, so the two do not contain each other; the torrent's
|
|
655
|
+
* work on other threads is not in it. Zero while either term is unmeasured
|
|
546
656
|
* — a guess here would refuse rungs on arithmetic nobody performed.
|
|
547
657
|
*
|
|
548
658
|
* @param {HlsSession} session
|
|
@@ -97,7 +97,6 @@ import path from "node:path";
|
|
|
97
97
|
import { contentOf, LocalObservations, PROXY_ROOT } from "../encode/LocalObservations.js";
|
|
98
98
|
import { configurationKeyOf } from "../encode/fingerprint.js";
|
|
99
99
|
import { qualityStateOf } from "../encode/quality/OutputQualityState.js";
|
|
100
|
-
import os from "node:os";
|
|
101
100
|
import { downloadShare } from "../storage/processor-share.js";
|
|
102
101
|
import { ENCODE_RUN_STATE, processCanBeSignalled } from "../encode/encode-run-state.js";
|
|
103
102
|
|
|
@@ -672,10 +671,13 @@ export function wireOutputs({
|
|
|
672
671
|
files.set(key, file);
|
|
673
672
|
}
|
|
674
673
|
const watched = [...files.values()].filter((file) => Number.isFinite(file.bytesPerFilmSecond) && file.bytesPerFilmSecond > 0);
|
|
675
|
-
const share = downloadShare({
|
|
674
|
+
const share = downloadShare({ availableShare: parts.hostLoad.hostAvailability?.share ?? 1,
|
|
676
675
|
baseDraw: parts.hostLoad.baseDraw, costPerMegabyte: parts.hostLoad.observedTorrentCostPerMegabyte, files: watched });
|
|
676
|
+
// Kept beside the readings it is judged against: what the torrents moved
|
|
677
|
+
// over each interval is said next to the limit in force (meta#178).
|
|
678
|
+
parts.hostLoad.downloadLimit = share.bytesPerSecond;
|
|
677
679
|
void Promise.resolve(parts.setDownloadRate(share.bytesPerSecond, `film speed ${share.filmSpeed.toFixed(2)}x ` +
|
|
678
|
-
`over ${watched.length} file(s), ${share.capacity
|
|
680
|
+
`over ${watched.length} file(s), ${Math.round(share.capacity * 100)}% of the machine for the proxy`)).catch(() => {});
|
|
679
681
|
};
|
|
680
682
|
parts.contentionPenalties = contentionPenalties instanceof Map ? contentionPenalties : null;
|
|
681
683
|
// Seconds of film per second when the picture is COPIED, measured at
|
|
@@ -1009,6 +1011,8 @@ export function wireOutputs({
|
|
|
1009
1011
|
openOf: (output) => parts.encodeOrchestrator?.openOf?.(output.outputKey ?? "") ?? null,
|
|
1010
1012
|
urgent: (output, index) => parts.encodeOrchestrator.wantedSegmentsOn(output.outputKey).some(window =>
|
|
1011
1013
|
index >= window.from && index <= window.to && window.urgent === true),
|
|
1014
|
+
// The piece's rank in the output's own map, said with an input's state.
|
|
1015
|
+
rankAt: (output, index) => parts.encodeOrchestrator?.rankAt?.(output.outputKey ?? "", index) ?? null,
|
|
1012
1016
|
log: line => logger.info(line),
|
|
1013
1017
|
changed: (output, result) => {
|
|
1014
1018
|
if (result.kind === "terminal") {
|
|
@@ -2,22 +2,27 @@
|
|
|
2
2
|
* @file How fast the torrents may download, so that downloading does not take
|
|
3
3
|
* the processor the encoders need (torrent-tv/meta#178).
|
|
4
4
|
*
|
|
5
|
-
* Field 2026-10-11, Home Assistant: an AVI re-encoded at 720x400
|
|
6
|
-
* of
|
|
5
|
+
* Field 2026-10-11, Home Assistant: an AVI re-encoded at 720x400 needed 2.3 s
|
|
6
|
+
* of one core per second of film. While the torrent downloaded 4.5-9.8 MB/s —
|
|
7
7
|
* mostly far ahead of the viewer — its thread took 56-63 % of a core, ffmpeg
|
|
8
8
|
* got 176-182 %, below real time, and the viewer saw the spinner; once the
|
|
9
9
|
* download stopped ffmpeg got 216-261 % and there were no stops.
|
|
10
10
|
*
|
|
11
|
-
* THE MODEL
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* the
|
|
11
|
+
* THE MODEL, every term a share of the WHOLE MACHINE. The processor the proxy
|
|
12
|
+
* has, `C`, is the share other processes leave (`encode/available-share.js`)
|
|
13
|
+
* less what this process draws doing nothing priced. Every watched file `f` of
|
|
14
|
+
* every torrent advances at a film speed `s`: its running encoders take `s·k_f`
|
|
15
|
+
* (`k_f` the machine's seconds per second of film, the inverse of the encode
|
|
16
|
+
* speed) and its download `s·w_f·c` (`w_f` its bytes per second of film, `c`
|
|
17
|
+
* the machine's seconds per byte the torrent thread spends). One speed for all
|
|
18
|
+
* is the division under which the slowest file is as fast as it can be:
|
|
18
19
|
*
|
|
19
20
|
* s* = C / Σ_f (k_f + w_f·c), download rate = max(s*, 1) · Σ_f w_f
|
|
20
21
|
*
|
|
22
|
+
* Up to 2.97.7 `C` was counted in cores while `k` and `c` were shares of the
|
|
23
|
+
* machine, so on the four-core addon host the limit was about four times
|
|
24
|
+
* looser than this model.
|
|
25
|
+
*
|
|
21
26
|
* Downloading faster than `s*·w` only piles bytes ahead while the encoders
|
|
22
27
|
* starve; what is fetched first within the rate is the priority map's order.
|
|
23
28
|
* The rate never falls below the films' own byte rate, `Σ w`, because below it
|
|
@@ -31,16 +36,16 @@
|
|
|
31
36
|
|
|
32
37
|
/**
|
|
33
38
|
* @param {object} params
|
|
34
|
-
* @param {number} params.cores
|
|
35
39
|
* @param {number} params.availableShare - The share of the machine other processes leave, 0..1.
|
|
36
|
-
* @param {number} params.baseDraw -
|
|
37
|
-
* @param {number | null} params.costPerMegabyte -
|
|
40
|
+
* @param {number} params.baseDraw - The share of the machine this process draws doing nothing priced.
|
|
41
|
+
* @param {number | null} params.costPerMegabyte - The machine's seconds per megabyte the torrent moves.
|
|
38
42
|
* @param {{ bytesPerFilmSecond: number, encodeCostSec: number }[]} params.files - Watched files.
|
|
39
43
|
* @returns {{ filmSpeed: number, bytesPerSecond: number, capacity: number }}
|
|
40
|
-
* `
|
|
44
|
+
* `capacity` is a share of the machine; `bytesPerSecond` is Infinity where
|
|
45
|
+
* the download is not limited.
|
|
41
46
|
*/
|
|
42
|
-
export function downloadShare({
|
|
43
|
-
const capacity = Math.max(0,
|
|
47
|
+
export function downloadShare({ availableShare, baseDraw, costPerMegabyte, files }) {
|
|
48
|
+
const capacity = Math.max(0, availableShare - baseDraw);
|
|
44
49
|
// A byte whose price is not measured, or costs nothing, takes nothing from
|
|
45
50
|
// the encoders: there is then no share to keep the download to.
|
|
46
51
|
const perByte = Number.isFinite(costPerMegabyte) && costPerMegabyte > 0 ? costPerMegabyte / 1e6 : 0;
|
|
@@ -419,7 +419,11 @@ async function runCommand(command, params, id) {
|
|
|
419
419
|
downloaded += Number.isFinite(gotBytes) ? gotBytes : 0;
|
|
420
420
|
uploaded += Number.isFinite(sentBytes) ? sentBytes : 0;
|
|
421
421
|
}
|
|
422
|
-
|
|
422
|
+
// This thread's own processor time, read in the same moment as the
|
|
423
|
+
// bytes, so the price of a downloaded byte is taken over one interval
|
|
424
|
+
// whether or not anything encodes (torrent-tv/meta#178).
|
|
425
|
+
const usage = process.threadCpuUsage();
|
|
426
|
+
return { downloaded, uploaded, threadCpuSeconds: (usage.user + usage.system) / 1e6 };
|
|
423
427
|
}
|
|
424
428
|
|
|
425
429
|
|