@torrent-tv/proxy 2.89.3 → 2.89.4
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/AGENTS.md +338 -336
- package/CHANGELOG.md +12 -0
- package/docs/encode-architecture.md +55 -19
- package/docs/playback-start-forecast.md +13 -5
- package/docs/storage-architecture.md +6 -0
- package/package.json +1 -1
- package/routes/stream/get.js +38 -2
- package/server.js +748 -745
- package/services/encode/EncodeOrchestrator.js +24 -49
- package/services/encode/EncodePlan.js +5 -3
- package/services/encode/EncodeRun.js +80 -17
- package/services/encode/EncodeRuns.js +1339 -1239
- package/services/encode/Renditions.js +2 -1
- package/services/encode/RunClock.js +87 -0
- package/services/encode/RunProgress.js +1 -6
- package/services/encode/encoder-readings.js +26 -28
- package/services/encode/piece-completeness.js +46 -0
- package/services/encode/quality/EncodeCost.js +1043 -1167
- package/services/encode/quality/OutputQualityState.js +1 -5
- package/services/encode/quality/QualityController.js +213 -390
- package/services/encode/quality/link-budget.js +5 -17
- package/services/encode/run-inputs.js +13 -2
- package/services/server/SegmentServing.js +3 -1
- package/services/server/ViewerRequests.js +2 -7
- package/services/server/wire-outputs.js +12 -7
- package/services/storage/segment-store/SegmentStore.js +46 -29
- package/services/torrent/supply-margin.js +7 -5
- package/test/auto-quality-step.test.js +75 -29
- package/test/decode-cost.test.js +1 -1
- package/test/encode-cost.test.js +31 -39
- package/test/encode-orchestrator.test.js +15 -8
- package/test/encode-plan-viewers.test.js +34 -27
- package/test/encode-run.test.js +34 -6
- package/test/encoder-readings.test.js +48 -26
- package/test/helpers/encode-run.js +113 -9
- package/test/link-budget.test.js +4 -5
- package/test/manager-surface.test.js +1 -2
- package/test/orchestrator-wired.test.js +5 -4
- package/test/output-speed.test.js +0 -1
- package/test/piece-completeness.test.js +42 -0
- package/test/quality-offer.test.js +1 -3
- package/test/quality-variants.test.js +1 -1
- package/test/run-progress.test.js +2 -3
- package/test/same-height-switch.test.js +1 -2
- package/test/segment-store.test.js +15 -15
- package/test/stream-route.test.js +28 -0
- package/test/supply-margin.test.js +13 -0
- package/test/two-viewers-one-picture.test.js +1 -1
- package/test/viewer-outputs.test.js +1 -1
package/test/encode-cost.test.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import test from "node:test";
|
|
11
11
|
import assert from "node:assert/strict";
|
|
12
12
|
import { EncodeCost } from "../services/encode/quality/EncodeCost.js";
|
|
13
|
-
import { startRunOn } from "./helpers/encode-run.js";
|
|
13
|
+
import { measureSpeed, startRunOn } from "./helpers/encode-run.js";
|
|
14
14
|
import { outputSpec } from "./helpers/output-spec.js";
|
|
15
15
|
import { runStateOf } from "../services/encode/encode-run-state.js";
|
|
16
16
|
import { qualityStateOf } from "../services/encode/quality/OutputQualityState.js";
|
|
@@ -39,10 +39,8 @@ function costOn(readings = {}) {
|
|
|
39
39
|
runningEncoders: () => readings.runningEncoders?.() ?? 0,
|
|
40
40
|
encodersRunningNow: () => 0,
|
|
41
41
|
torrentCostSecFor: () => 0,
|
|
42
|
-
boundBy: async () => readings.boundBy ?? "cpu",
|
|
43
42
|
runsFor: (session) => [...(session.runs ?? [])],
|
|
44
|
-
stateFor: (session) => runStateOf(session.runs)
|
|
45
|
-
progressFor: (session) => session.progress ?? null
|
|
43
|
+
stateFor: (session) => runStateOf(session.runs)
|
|
46
44
|
});
|
|
47
45
|
return { cost, asked: () => asked, host };
|
|
48
46
|
}
|
|
@@ -64,30 +62,28 @@ function sessionProducing(what = {}) {
|
|
|
64
62
|
}),
|
|
65
63
|
file: { key: what.key ?? "torrent:abc:0", name: "film.mkv" },
|
|
66
64
|
output: { encodeWidth: 0, encodeHeight: 0, outputFps: 25, softwarePreset: null },
|
|
67
|
-
|
|
65
|
+
clock: { at: 1000 },
|
|
68
66
|
runs: new Set()
|
|
69
67
|
};
|
|
70
|
-
startRunOn(session, { from: 0,
|
|
68
|
+
startRunOn(session, { from: 0, clock: session.clock });
|
|
71
69
|
return session;
|
|
72
70
|
}
|
|
73
71
|
|
|
74
72
|
/**
|
|
75
|
-
*
|
|
73
|
+
* The session's run measures its speed, and is learned from as the product
|
|
74
|
+
* learns: once the run has a reading.
|
|
76
75
|
*
|
|
77
76
|
* @param {EncodeCost} cost
|
|
78
77
|
* @param {object} session
|
|
79
|
-
* @param {number} speedX - Seconds of film produced per second of
|
|
78
|
+
* @param {number} speedX - Seconds of film produced per second of the run's own work.
|
|
79
|
+
* @param {number} [waitedMs] - How long the run's input waits for the swarm
|
|
80
|
+
* between two closed pieces.
|
|
80
81
|
* @returns {Promise<void>}
|
|
81
82
|
*/
|
|
82
|
-
async function watchItRun(cost, session, speedX) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// Backdated rather than waited for: the window a speed may be read over is
|
|
87
|
-
// seconds, and a test that sleeps through it measures the clock.
|
|
88
|
-
qualityStateOf(session).learnSample.takenAt -= 10_000;
|
|
89
|
-
session.progress.processedSeconds = 10 * speedX;
|
|
90
|
-
await cost.learnFrom(session);
|
|
83
|
+
async function watchItRun(cost, session, speedX, waitedMs = 0) {
|
|
84
|
+
const run = [...session.runs][0];
|
|
85
|
+
measureSpeed(run, speedX, session.clock, waitedMs);
|
|
86
|
+
await cost.learnFrom(session, run);
|
|
91
87
|
}
|
|
92
88
|
|
|
93
89
|
test("the host is asked at the moment of the question, not when this was built", () => {
|
|
@@ -163,17 +159,16 @@ test("a copy is priced from what it was seen doing", async () => {
|
|
|
163
159
|
);
|
|
164
160
|
});
|
|
165
161
|
|
|
166
|
-
test("a
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
const { cost } = costOn({ boundBy: "download" });
|
|
162
|
+
test("the time a run's input waited for the swarm is not in the price of this host", async () => {
|
|
163
|
+
// Field 2026-10-01: a copy waited 32.93 s and 43.91 s for two pieces and read
|
|
164
|
+
// 0.21x over the clock. Over its own working time it is the machine's speed.
|
|
165
|
+
const { cost } = costOn();
|
|
171
166
|
const session = sessionProducing({ transcodeVideo: false });
|
|
172
167
|
|
|
173
|
-
await watchItRun(cost, session,
|
|
168
|
+
await watchItRun(cost, session, 8, 9_000);
|
|
174
169
|
|
|
175
|
-
assert.equal(
|
|
176
|
-
assert.equal(
|
|
170
|
+
assert.equal(qualityStateOf(session).lastAloneSpeed, 8, "the speed is the run's own, not the swarm's");
|
|
171
|
+
assert.equal(cost.copyVersionFor("torrent:abc:0"), 1, "and it is filed as what copying costs here");
|
|
177
172
|
});
|
|
178
173
|
|
|
179
174
|
test("a picture's reading taken beside another encoder is not filed", async () => {
|
|
@@ -189,30 +184,27 @@ test("a picture's reading taken beside another encoder is not filed", async () =
|
|
|
189
184
|
assert.equal(cost.copyVersionFor("torrent:abc:0"), 0);
|
|
190
185
|
});
|
|
191
186
|
|
|
192
|
-
test("
|
|
187
|
+
test("a run that has measured nothing teaches nothing", async () => {
|
|
193
188
|
const { cost } = costOn();
|
|
194
189
|
const session = sessionProducing({ transcodeVideo: false });
|
|
195
|
-
|
|
190
|
+
const run = [...session.runs][0];
|
|
196
191
|
|
|
197
|
-
await cost.learnFrom(session);
|
|
192
|
+
await cost.learnFrom(session, run);
|
|
198
193
|
|
|
199
|
-
assert.equal(
|
|
200
|
-
assert.
|
|
194
|
+
assert.equal(run.speedReading, null, "a speed needs two closed pieces of one run");
|
|
195
|
+
assert.equal(cost.copyVersionFor("torrent:abc:0"), 0);
|
|
201
196
|
});
|
|
202
197
|
|
|
203
|
-
test("a
|
|
198
|
+
test("a restarted run starts its own reckoning, so a seek is never read as speed", async () => {
|
|
199
|
+
// Twenty minutes of film against five seconds of work is a seek, and filed as
|
|
200
|
+
// a price it admits every step there is. The reading belongs to the run that
|
|
201
|
+
// made it, so a new run at another place has none until its own pieces close.
|
|
204
202
|
const { cost } = costOn();
|
|
205
203
|
const session = sessionProducing({ transcodeVideo: false });
|
|
206
|
-
await cost.learnFrom(session);
|
|
207
|
-
qualityStateOf(session).learnSample.takenAt -= 10_000;
|
|
208
|
-
// A new run, at another place in the film: twenty minutes of film against five
|
|
209
|
-
// seconds of clock is a seek, and filed as a price it admits every step there
|
|
210
|
-
// is.
|
|
211
204
|
session.runs = new Set();
|
|
212
|
-
startRunOn(session, { from: 300,
|
|
213
|
-
session.progress.processedSeconds = 1200;
|
|
205
|
+
const moved = startRunOn(session, { from: 300, clock: session.clock });
|
|
214
206
|
|
|
215
|
-
await cost.learnFrom(session);
|
|
207
|
+
await cost.learnFrom(session, moved);
|
|
216
208
|
|
|
217
209
|
assert.equal(cost.copyVersionFor("torrent:abc:0"), 0);
|
|
218
210
|
});
|
|
@@ -12,6 +12,7 @@ import test from "node:test";
|
|
|
12
12
|
import assert from "node:assert/strict";
|
|
13
13
|
import { EventEmitter } from "node:events";
|
|
14
14
|
import { EncodeRun } from "../services/encode/EncodeRun.js";
|
|
15
|
+
import { measureSpeed } from "./helpers/encode-run.js";
|
|
15
16
|
import { ENCODE_EXIT } from "../services/encode/encode-exit.js";
|
|
16
17
|
import { SoftwareEncoder } from "../services/encode/SoftwareEncoder.js";
|
|
17
18
|
import { EncodeOrchestrator } from "../services/encode/EncodeOrchestrator.js";
|
|
@@ -33,10 +34,16 @@ const RUN_COSTS = { killCostSec: 0, firstByteWaitSec: 0.12 };
|
|
|
33
34
|
|
|
34
35
|
const PICTURE = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
|
|
35
36
|
|
|
37
|
+
/** What every run of these tests reads as now; a measurement advances it. */
|
|
38
|
+
const clock = { at: 1000 };
|
|
39
|
+
|
|
36
40
|
class FakeProcess extends EventEmitter {
|
|
37
41
|
constructor() {
|
|
38
42
|
super();
|
|
39
43
|
this.pid = 1;
|
|
44
|
+
// The progress channel and the channel closed pieces are named on.
|
|
45
|
+
this.stdout = new EventEmitter();
|
|
46
|
+
this.stdio = [null, this.stdout, null, new EventEmitter()];
|
|
40
47
|
this.signals = [];
|
|
41
48
|
}
|
|
42
49
|
|
|
@@ -123,7 +130,7 @@ function orchestrator({ maxRuns = 2 } = {}) {
|
|
|
123
130
|
buildArgs: () => ["-i", "in", "out"],
|
|
124
131
|
spawn: () => process_,
|
|
125
132
|
logger: { info: (line) => lines.push(line), warn: (line) => lines.push(line) },
|
|
126
|
-
now: () =>
|
|
133
|
+
now: () => clock.at,
|
|
127
134
|
onEnded: (ended) => made.noteEnded(ended)
|
|
128
135
|
});
|
|
129
136
|
// Filed under the run itself: a run has no name, so a test that has to
|
|
@@ -146,7 +153,7 @@ function orchestrator({ maxRuns = 2 } = {}) {
|
|
|
146
153
|
buildArgs: () => ["-i", "in", "out"],
|
|
147
154
|
spawn: () => process_,
|
|
148
155
|
logger: { info: (line) => lines.push(line), warn: (line) => lines.push(line) },
|
|
149
|
-
now: () =>
|
|
156
|
+
now: () => clock.at,
|
|
150
157
|
onEnded: (ended) => made.noteEnded(ended)
|
|
151
158
|
});
|
|
152
159
|
processes.set(run, process_);
|
|
@@ -210,7 +217,7 @@ test("an encoder already working covers what it will reach in time", () => {
|
|
|
210
217
|
// wanted at all.
|
|
211
218
|
const { made, buildRun } = orchestrator();
|
|
212
219
|
const first = buildRun({ from: 0, to: -1 });
|
|
213
|
-
first
|
|
220
|
+
measureSpeed(first, 8, clock);
|
|
214
221
|
made.adopt(PICTURE, first);
|
|
215
222
|
made.noteProduced(PICTURE, 0);
|
|
216
223
|
made.noteProduced(PICTURE, 1);
|
|
@@ -240,7 +247,7 @@ test("somebody stopped where no encoder can arrive in time is served, and the sc
|
|
|
240
247
|
// seconds anybody spends looking at a spinner — outranks it.
|
|
241
248
|
const { made, buildRun } = orchestrator();
|
|
242
249
|
const first = buildRun({ from: 0, to: -1 });
|
|
243
|
-
first
|
|
250
|
+
measureSpeed(first, 1, clock);
|
|
244
251
|
made.adopt(PICTURE, first);
|
|
245
252
|
made.noteProduced(PICTURE, 0);
|
|
246
253
|
made.noteProduced(PICTURE, 1);
|
|
@@ -260,7 +267,7 @@ test("two encoders on one output never share a segment number", () => {
|
|
|
260
267
|
// what that neighbour makes sooner.
|
|
261
268
|
const { made, buildRun } = orchestrator();
|
|
262
269
|
const first = buildRun({ from: 0, to: -1 });
|
|
263
|
-
first
|
|
270
|
+
measureSpeed(first, 1, clock);
|
|
264
271
|
made.adopt(PICTURE, first);
|
|
265
272
|
made.noteProduced(PICTURE, 0);
|
|
266
273
|
wants(made, [{ from: 200, to: 230, withinSeconds: 0 }]);
|
|
@@ -294,7 +301,7 @@ test("a second viewer far behind is served, at whatever the score says is soones
|
|
|
294
301
|
wants(made, [{ from: 500, to: 530, withinSeconds: 0 }]);
|
|
295
302
|
made.reconcile();
|
|
296
303
|
for (const run of made.runsOn(PICTURE)) {
|
|
297
|
-
run
|
|
304
|
+
measureSpeed(run, 2, clock);
|
|
298
305
|
}
|
|
299
306
|
wants(made, [
|
|
300
307
|
{ from: 500, to: 530, withinSeconds: 0 },
|
|
@@ -359,7 +366,7 @@ test("a run that meets material made elsewhere is moved past it", () => {
|
|
|
359
366
|
for (let index = 102; index <= 150; index += 1) {
|
|
360
367
|
made.coverageOf(PICTURE).markReady(index);
|
|
361
368
|
}
|
|
362
|
-
first
|
|
369
|
+
measureSpeed(first, 1, clock);
|
|
363
370
|
made.reconcile();
|
|
364
371
|
const runs = made.runsOn(PICTURE);
|
|
365
372
|
assert.equal(runs.length, 1, "one encoder, moved rather than joined by another");
|
|
@@ -407,7 +414,7 @@ test("the swarm limits the encoders, whatever the processor allows", () => {
|
|
|
407
414
|
const { made, lines } = orchestrator({ maxRuns: 2 });
|
|
408
415
|
wants(made, [{ from: 100, to: 130 }]);
|
|
409
416
|
made.reconcile();
|
|
410
|
-
made.runsOn(PICTURE)[0]
|
|
417
|
+
measureSpeed(made.runsOn(PICTURE)[0], 8, clock);
|
|
411
418
|
wants(made, [{ from: 500, to: 530 }]);
|
|
412
419
|
made.reconcile();
|
|
413
420
|
|
|
@@ -28,6 +28,7 @@ import test from "node:test";
|
|
|
28
28
|
import assert from "node:assert/strict";
|
|
29
29
|
import { EventEmitter } from "node:events";
|
|
30
30
|
import { EncodeRun } from "../services/encode/EncodeRun.js";
|
|
31
|
+
import { measureSpeed } from "./helpers/encode-run.js";
|
|
31
32
|
import { SoftwareEncoder } from "../services/encode/SoftwareEncoder.js";
|
|
32
33
|
import { EncodeOrchestrator } from "../services/encode/EncodeOrchestrator.js";
|
|
33
34
|
import { mapForViewer, mergeMaps, runsOf } from "../services/viewer/PriorityMap.js";
|
|
@@ -52,10 +53,16 @@ const SEGMENT_SECONDS = 4;
|
|
|
52
53
|
const FILM_SECONDS = 4000;
|
|
53
54
|
const SEGMENTS = FILM_SECONDS / SEGMENT_SECONDS;
|
|
54
55
|
|
|
56
|
+
/** What every run of these tests reads as now; a measurement advances it. */
|
|
57
|
+
const clock = { at: 1000 };
|
|
58
|
+
|
|
55
59
|
class FakeProcess extends EventEmitter {
|
|
56
60
|
constructor() {
|
|
57
61
|
super();
|
|
58
62
|
this.pid = 1;
|
|
63
|
+
// The progress channel and the channel closed pieces are named on.
|
|
64
|
+
this.stdout = new EventEmitter();
|
|
65
|
+
this.stdio = [null, this.stdout, null, new EventEmitter()];
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
kill(signal) {
|
|
@@ -75,7 +82,7 @@ function orchestrator({ maxRuns = 3 } = {}) {
|
|
|
75
82
|
buildArgs: () => ["-i", "in", "out"],
|
|
76
83
|
spawn: () => new FakeProcess(),
|
|
77
84
|
logger: { info() {}, warn() {} },
|
|
78
|
-
now: () =>
|
|
85
|
+
now: () => clock.at,
|
|
79
86
|
onEnded: (ended) => made.noteEnded(ended)
|
|
80
87
|
});
|
|
81
88
|
made = new EncodeOrchestrator({
|
|
@@ -209,7 +216,7 @@ test("one viewer playing: the encoder keeping up buys no second one", () => {
|
|
|
209
216
|
watches("one", { atSeconds: 400 });
|
|
210
217
|
made.reconcile();
|
|
211
218
|
const [run] = made.runsOn(PICTURE);
|
|
212
|
-
run
|
|
219
|
+
measureSpeed(run, 6, clock);
|
|
213
220
|
for (let index = 100; index < 130; index += 1) {
|
|
214
221
|
made.noteProduced(PICTURE, index);
|
|
215
222
|
}
|
|
@@ -225,7 +232,7 @@ test("one viewer seeking far ahead: an encoder is placed there", () => {
|
|
|
225
232
|
const { made, watches } = orchestrator();
|
|
226
233
|
watches("one", { atSeconds: 400 });
|
|
227
234
|
made.reconcile();
|
|
228
|
-
made.runsOn(PICTURE)[0]
|
|
235
|
+
measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
|
|
229
236
|
made.noteProduced(PICTURE, 100);
|
|
230
237
|
|
|
231
238
|
// The peak JUMPS. Nothing about the old place is wanted now.
|
|
@@ -243,7 +250,7 @@ test("one viewer paused: nothing is late, so no encoder is added", () => {
|
|
|
243
250
|
const { made, watches } = orchestrator();
|
|
244
251
|
watches("one", { atSeconds: 400 });
|
|
245
252
|
made.reconcile();
|
|
246
|
-
made.runsOn(PICTURE)[0]
|
|
253
|
+
measureSpeed(made.runsOn(PICTURE)[0], 1, clock);
|
|
247
254
|
made.noteProduced(PICTURE, 100);
|
|
248
255
|
|
|
249
256
|
watches("one", { atSeconds: 404, playing: false });
|
|
@@ -295,7 +302,7 @@ test("two viewers far apart get an encoder each", () => {
|
|
|
295
302
|
const { made, watches } = orchestrator();
|
|
296
303
|
watches("one", { atSeconds: 400 });
|
|
297
304
|
made.reconcile();
|
|
298
|
-
made.runsOn(PICTURE)[0]
|
|
305
|
+
measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
|
|
299
306
|
made.noteProduced(PICTURE, 100);
|
|
300
307
|
|
|
301
308
|
watches("two", { atSeconds: 3000 });
|
|
@@ -314,12 +321,12 @@ test("two viewers: one seeking does not take the other's encoder", () => {
|
|
|
314
321
|
// long one takes to reach a number is the whole comparison. Real life
|
|
315
322
|
// measures it from the first run; a test that stated both viewers before any
|
|
316
323
|
// run existed would be asking the plan to buy a process on no evidence.
|
|
317
|
-
made.runsOn(PICTURE)[0]
|
|
324
|
+
measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
|
|
318
325
|
made.noteProduced(PICTURE, 100);
|
|
319
326
|
watches("two", { atSeconds: 3000 });
|
|
320
327
|
made.reconcile();
|
|
321
328
|
for (const run of made.runsOn(PICTURE)) {
|
|
322
|
-
run
|
|
329
|
+
measureSpeed(run, 6, clock);
|
|
323
330
|
}
|
|
324
331
|
assert.ok(made.runsOn(PICTURE).length >= 2, "one each to begin with");
|
|
325
332
|
|
|
@@ -342,7 +349,7 @@ test("two viewers: one pausing leaves the other served", () => {
|
|
|
342
349
|
watches("two", { atSeconds: 3000 });
|
|
343
350
|
made.reconcile();
|
|
344
351
|
for (const run of made.runsOn(PICTURE)) {
|
|
345
|
-
run
|
|
352
|
+
measureSpeed(run, 1, clock);
|
|
346
353
|
}
|
|
347
354
|
|
|
348
355
|
watches("two", { atSeconds: 3000, playing: false });
|
|
@@ -359,7 +366,7 @@ test("two viewers: the one who leaves takes nothing from the one who stays", ()
|
|
|
359
366
|
watches("two", { atSeconds: 3000 });
|
|
360
367
|
made.reconcile();
|
|
361
368
|
for (const run of made.runsOn(PICTURE)) {
|
|
362
|
-
run
|
|
369
|
+
measureSpeed(run, 1, clock);
|
|
363
370
|
}
|
|
364
371
|
|
|
365
372
|
leaves("two");
|
|
@@ -379,7 +386,7 @@ test("three viewers far apart get an encoder each when the machine affords it",
|
|
|
379
386
|
watches("three", { atSeconds: 3600 });
|
|
380
387
|
made.reconcile();
|
|
381
388
|
for (const run of made.runsOn(PICTURE)) {
|
|
382
|
-
run
|
|
389
|
+
measureSpeed(run, 6, clock);
|
|
383
390
|
}
|
|
384
391
|
made.reconcile();
|
|
385
392
|
|
|
@@ -402,7 +409,7 @@ test("three viewers, a machine that affords two: the budget binds, not the map",
|
|
|
402
409
|
watches("three", { atSeconds: 3600 });
|
|
403
410
|
made.reconcile();
|
|
404
411
|
for (const run of made.runsOn(PICTURE)) {
|
|
405
|
-
run
|
|
412
|
+
measureSpeed(run, 1, clock);
|
|
406
413
|
}
|
|
407
414
|
made.reconcile();
|
|
408
415
|
|
|
@@ -415,13 +422,13 @@ test("three viewers: one seeks onto another, and the two of them share", () => {
|
|
|
415
422
|
const { made, watches } = orchestrator({ maxRuns: 3 });
|
|
416
423
|
watches("one", { atSeconds: 400 });
|
|
417
424
|
made.reconcile();
|
|
418
|
-
made.runsOn(PICTURE)[0]
|
|
425
|
+
measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
|
|
419
426
|
made.noteProduced(PICTURE, 100);
|
|
420
427
|
watches("two", { atSeconds: 2000 });
|
|
421
428
|
watches("three", { atSeconds: 3600 });
|
|
422
429
|
made.reconcile();
|
|
423
430
|
for (const run of made.runsOn(PICTURE)) {
|
|
424
|
-
run
|
|
431
|
+
measureSpeed(run, 6, clock);
|
|
425
432
|
}
|
|
426
433
|
made.reconcile();
|
|
427
434
|
assert.equal(made.runsOn(PICTURE).length, 3);
|
|
@@ -438,7 +445,7 @@ test("three viewers: all paused, and no encoder is added for any of them", () =>
|
|
|
438
445
|
const { made, watches } = orchestrator({ maxRuns: 3 });
|
|
439
446
|
watches("one", { atSeconds: 400 });
|
|
440
447
|
made.reconcile();
|
|
441
|
-
made.runsOn(PICTURE)[0]
|
|
448
|
+
measureSpeed(made.runsOn(PICTURE)[0], 1, clock);
|
|
442
449
|
const before = made.runsOn(PICTURE).length;
|
|
443
450
|
|
|
444
451
|
watches("one", { atSeconds: 400, playing: false });
|
|
@@ -461,7 +468,7 @@ test("the plan is a function of the state: the same state twice gives the same a
|
|
|
461
468
|
watches("two", { atSeconds: 2000 });
|
|
462
469
|
made.reconcile();
|
|
463
470
|
for (const run of made.runsOn(PICTURE)) {
|
|
464
|
-
run
|
|
471
|
+
measureSpeed(run, 1, clock);
|
|
465
472
|
}
|
|
466
473
|
made.reconcile();
|
|
467
474
|
const first = placements(made);
|
|
@@ -483,7 +490,7 @@ test("exactly realtime arrives exactly on time, and no second encoder is bought"
|
|
|
483
490
|
const { made, watches } = orchestrator({ maxRuns: 3 });
|
|
484
491
|
watches("one", { atSeconds: 400 });
|
|
485
492
|
made.reconcile();
|
|
486
|
-
made.runsOn(PICTURE)[0]
|
|
493
|
+
measureSpeed(made.runsOn(PICTURE)[0], 1, clock);
|
|
487
494
|
made.noteProduced(PICTURE, 100);
|
|
488
495
|
made.reconcile();
|
|
489
496
|
|
|
@@ -507,7 +514,7 @@ test("below realtime, whether a second encoder helps is arithmetic, and here it
|
|
|
507
514
|
const { made, watches, leaves } = orchestrator({ maxRuns: 3 });
|
|
508
515
|
watches("one", { atSeconds: 400 });
|
|
509
516
|
made.reconcile();
|
|
510
|
-
made.runsOn(PICTURE)[0]
|
|
517
|
+
measureSpeed(made.runsOn(PICTURE)[0], 0.5, clock);
|
|
511
518
|
made.noteProduced(PICTURE, 100);
|
|
512
519
|
made.reconcile();
|
|
513
520
|
|
|
@@ -531,7 +538,7 @@ test("two viewers arriving together on a cold output get one encoder, then are m
|
|
|
531
538
|
assert.ok(made.runsOn(PICTURE).length >= 1, "at least one, until something is measured");
|
|
532
539
|
assert.equal(onTheStretchOf(made, 100), 1, "and not two on the same place");
|
|
533
540
|
|
|
534
|
-
made.runsOn(PICTURE)[0]
|
|
541
|
+
measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
|
|
535
542
|
made.noteProduced(PICTURE, 100);
|
|
536
543
|
made.reconcile();
|
|
537
544
|
assert.ok(placements(made).includes(750), "and now the far one is served too");
|
|
@@ -542,7 +549,7 @@ test("an encoder comfortably faster than realtime is left to do the whole stretc
|
|
|
542
549
|
const { made, watches } = orchestrator({ maxRuns: 3 });
|
|
543
550
|
watches("one", { atSeconds: 400 });
|
|
544
551
|
made.reconcile();
|
|
545
|
-
made.runsOn(PICTURE)[0]
|
|
552
|
+
measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
|
|
546
553
|
made.noteProduced(PICTURE, 100);
|
|
547
554
|
made.reconcile();
|
|
548
555
|
|
|
@@ -561,7 +568,7 @@ test("a swarm that feeds one encoder moves it to whoever is late, rather than se
|
|
|
561
568
|
const [run] = made.runsOn(PICTURE);
|
|
562
569
|
// 6x against a swarm charging 0.25 s per second of film: one encoder already
|
|
563
570
|
// takes one and a half of what is delivered, so the budget is one.
|
|
564
|
-
run
|
|
571
|
+
measureSpeed(run, 6, clock);
|
|
565
572
|
made.refetchSecPerFilmSecond = () => 0.25;
|
|
566
573
|
made.noteProduced(PICTURE, 100);
|
|
567
574
|
|
|
@@ -583,7 +590,7 @@ test("one viewer seeking back into film that exists is served from it, with no e
|
|
|
583
590
|
const { made, watches } = orchestrator();
|
|
584
591
|
watches("one", { atSeconds: 400 });
|
|
585
592
|
made.reconcile();
|
|
586
|
-
made.runsOn(PICTURE)[0]
|
|
593
|
+
measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
|
|
587
594
|
for (let index = 100; index <= 160; index += 1) {
|
|
588
595
|
made.noteProduced(PICTURE, index);
|
|
589
596
|
}
|
|
@@ -616,7 +623,7 @@ test("one viewer seeking back into film nobody has gets an encoder there", () =>
|
|
|
616
623
|
const { made, watches } = orchestrator();
|
|
617
624
|
watches("one", { atSeconds: 3000 });
|
|
618
625
|
made.reconcile();
|
|
619
|
-
made.runsOn(PICTURE)[0]
|
|
626
|
+
measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
|
|
620
627
|
made.noteProduced(PICTURE, 750);
|
|
621
628
|
|
|
622
629
|
watches("one", { atSeconds: 400 });
|
|
@@ -631,14 +638,14 @@ test("two viewers: one seeks back onto film the other already had made", () => {
|
|
|
631
638
|
const { made, watches } = orchestrator({ maxRuns: 3 });
|
|
632
639
|
watches("one", { atSeconds: 400 });
|
|
633
640
|
made.reconcile();
|
|
634
|
-
made.runsOn(PICTURE)[0]
|
|
641
|
+
measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
|
|
635
642
|
for (let index = 100; index <= 200; index += 1) {
|
|
636
643
|
made.noteProduced(PICTURE, index);
|
|
637
644
|
}
|
|
638
645
|
watches("two", { atSeconds: 3000 });
|
|
639
646
|
made.reconcile();
|
|
640
647
|
for (const run of made.runsOn(PICTURE)) {
|
|
641
|
-
run
|
|
648
|
+
measureSpeed(run, 6, clock);
|
|
642
649
|
}
|
|
643
650
|
const before = made.runsOn(PICTURE).length;
|
|
644
651
|
|
|
@@ -662,7 +669,7 @@ test("three viewers: one forward, one back, one paused", () => {
|
|
|
662
669
|
watches("three", { atSeconds: 3600 });
|
|
663
670
|
made.reconcile();
|
|
664
671
|
for (const run of made.runsOn(PICTURE)) {
|
|
665
|
-
run
|
|
672
|
+
measureSpeed(run, 6, clock);
|
|
666
673
|
}
|
|
667
674
|
made.reconcile();
|
|
668
675
|
|
|
@@ -690,7 +697,7 @@ test("a viewer scrubbing back and forth does not accumulate encoders", () => {
|
|
|
690
697
|
const { made, watches } = orchestrator({ maxRuns: 3 });
|
|
691
698
|
watches("one", { atSeconds: 400 });
|
|
692
699
|
made.reconcile();
|
|
693
|
-
made.runsOn(PICTURE)[0]
|
|
700
|
+
measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
|
|
694
701
|
made.noteProduced(PICTURE, 100);
|
|
695
702
|
made.reconcile();
|
|
696
703
|
const settled = placements(made);
|
|
@@ -698,7 +705,7 @@ test("a viewer scrubbing back and forth does not accumulate encoders", () => {
|
|
|
698
705
|
watches("one", { atSeconds: 2000 });
|
|
699
706
|
made.reconcile();
|
|
700
707
|
for (const run of made.runsOn(PICTURE)) {
|
|
701
|
-
run
|
|
708
|
+
measureSpeed(run, 6, clock);
|
|
702
709
|
}
|
|
703
710
|
watches("one", { atSeconds: 404 });
|
|
704
711
|
made.reconcile();
|
package/test/encode-run.test.js
CHANGED
|
@@ -146,13 +146,41 @@ test("a process that could not be started ends like any other failure", () => {
|
|
|
146
146
|
assert.match(ends[0].because, /ENOENT/);
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
-
test("
|
|
150
|
-
const {
|
|
149
|
+
test("its speed is film made between closed pieces over its own working time", () => {
|
|
150
|
+
const clock = { at: 1000 };
|
|
151
|
+
const process_ = new FakeProcess();
|
|
152
|
+
process_.stdout = new EventEmitter();
|
|
153
|
+
process_.stdio = [null, process_.stdout, null, new EventEmitter()];
|
|
154
|
+
const readings = [];
|
|
155
|
+
const run = new EncodeRun({
|
|
156
|
+
address: "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy",
|
|
157
|
+
encoder: new SoftwareEncoder(),
|
|
158
|
+
from: 0,
|
|
159
|
+
to: 9,
|
|
160
|
+
buildArgs: () => [],
|
|
161
|
+
spawn: () => process_,
|
|
162
|
+
logger: { info() {}, warn() {} },
|
|
163
|
+
now: () => clock.at,
|
|
164
|
+
onSpeedMeasured: (reading) => readings.push(reading)
|
|
165
|
+
});
|
|
151
166
|
assert.equal(run.speedX, 0, "nothing measured yet");
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
167
|
+
const report = (seconds) => process_.stdout.emit("data", `out_time_ms=${seconds * 1_000_000}\n`);
|
|
168
|
+
const closed = (name) => process_.stdio[3].emit("data", `${name}\n`);
|
|
169
|
+
report(0);
|
|
170
|
+
closed("a");
|
|
171
|
+
clock.at += 1000;
|
|
172
|
+
report(4);
|
|
173
|
+
closed("b"); // "a" is published: the reckoning starts here
|
|
174
|
+
assert.equal(run.speedX, 0, "one closed piece measures nothing");
|
|
175
|
+
// The input waits for the swarm for 30 s; that is not the run's work.
|
|
176
|
+
run.inputWaitBegins();
|
|
177
|
+
clock.at += 30_000;
|
|
178
|
+
run.inputWaitEnds();
|
|
179
|
+
clock.at += 2000;
|
|
180
|
+
report(12);
|
|
181
|
+
closed("c"); // "b" is published
|
|
182
|
+
assert.equal(run.speedX, 4, "8 s of film over 2 s of work");
|
|
183
|
+
assert.deepEqual(readings, [{ speed: 4, at: 34_000 }], "told once, with when");
|
|
156
184
|
});
|
|
157
185
|
|
|
158
186
|
test("a segment number behind its own start is not its to claim", () => {
|
|
@@ -1,46 +1,68 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file What two
|
|
2
|
+
* @file What two work samples of a running encoder say about its speed.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* which filed as the price of copying would refuse rungs on a measurement of a
|
|
9
|
-
* pause.
|
|
4
|
+
* The cases this exists for: a copy whose cumulative speed counts the time it
|
|
5
|
+
* was stopped, and a run whose input waited for the swarm (field 2026-10-01: a
|
|
6
|
+
* copy read 0.21x after waiting 32.93 s and 43.91 s for two pieces). A work
|
|
7
|
+
* sample states the run's own working time, so neither enters the speed.
|
|
10
8
|
*/
|
|
11
9
|
|
|
12
10
|
import assert from "node:assert/strict";
|
|
13
11
|
import test from "node:test";
|
|
14
12
|
|
|
15
|
-
import {
|
|
13
|
+
import { RunClock } from "../services/encode/RunClock.js";
|
|
14
|
+
import { speedFromWork } from "../services/encode/encoder-readings.js";
|
|
16
15
|
|
|
17
|
-
const
|
|
16
|
+
const sample = (at, producedSeconds, workingMs) => ({ at, producedSeconds, workingMs });
|
|
18
17
|
|
|
19
|
-
test("
|
|
20
|
-
assert.equal(
|
|
18
|
+
test("film made over the run's own work is the speed", () => {
|
|
19
|
+
assert.equal(speedFromWork(sample(10_000, 100, 5_000), sample(15_000, 140, 10_000)), 8);
|
|
21
20
|
});
|
|
22
21
|
|
|
23
|
-
test("a stretch
|
|
24
|
-
assert.equal(
|
|
22
|
+
test("a stretch with nothing made says nothing", () => {
|
|
23
|
+
assert.equal(speedFromWork(sample(10_000, 100, 5_000), sample(20_000, 100, 15_000)), null);
|
|
25
24
|
});
|
|
26
25
|
|
|
27
|
-
test("a run that
|
|
28
|
-
|
|
29
|
-
assert.equal(speedFromReadings(at(10, 100), at(20, 100), 3), null);
|
|
26
|
+
test("a run that went backwards says nothing rather than a negative speed", () => {
|
|
27
|
+
assert.equal(speedFromWork(sample(10_000, 200, 5_000), sample(20_000, 100, 15_000)), null);
|
|
30
28
|
});
|
|
31
29
|
|
|
32
|
-
test("a
|
|
33
|
-
assert.equal(
|
|
30
|
+
test("a missing sample is not an answer", () => {
|
|
31
|
+
assert.equal(speedFromWork(null, sample(20_000, 100, 1_000)), null);
|
|
32
|
+
assert.equal(speedFromWork(sample(10_000, 100, 1_000), null), null);
|
|
34
33
|
});
|
|
35
34
|
|
|
36
|
-
test("
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
test("waiting for the input and being stopped are not working time", () => {
|
|
36
|
+
let now = 0;
|
|
37
|
+
const clock = new RunClock({ now: () => now });
|
|
38
|
+
now = 2_000;
|
|
39
|
+
clock.inputWaitBegins();
|
|
40
|
+
now = 34_930; // the field's 32.93 s wait for one piece
|
|
41
|
+
clock.inputWaitEnds();
|
|
42
|
+
now = 36_000;
|
|
43
|
+
clock.stopped();
|
|
44
|
+
now = 50_000;
|
|
45
|
+
clock.continued();
|
|
46
|
+
now = 51_000;
|
|
47
|
+
assert.equal(clock.workingMs(), 2_000 + 1_070 + 1_000);
|
|
39
48
|
});
|
|
40
49
|
|
|
41
|
-
test("
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
test("two inputs waiting at once, or a wait while stopped, are idle once", () => {
|
|
51
|
+
let now = 0;
|
|
52
|
+
const clock = new RunClock({ now: () => now });
|
|
53
|
+
clock.inputWaitBegins();
|
|
54
|
+
now = 1_000;
|
|
55
|
+
clock.inputWaitBegins();
|
|
56
|
+
clock.stopped();
|
|
57
|
+
now = 3_000;
|
|
58
|
+
clock.inputWaitEnds();
|
|
59
|
+
clock.continued();
|
|
60
|
+
now = 4_000;
|
|
61
|
+
clock.inputWaitEnds();
|
|
62
|
+
now = 5_000;
|
|
63
|
+
assert.equal(clock.workingMs(), 1_000);
|
|
64
|
+
// An ongoing wait is idle up to the moment asked.
|
|
65
|
+
clock.inputWaitBegins();
|
|
66
|
+
now = 6_000;
|
|
67
|
+
assert.equal(clock.workingMs(), 1_000);
|
|
46
68
|
});
|