@torrent-tv/proxy 2.89.3 → 2.89.5
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 +2319 -2303
- 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 +1294 -1255
- 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-closed-on-both-branches.test.js +114 -96
- 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
|
@@ -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
|
});
|
|
@@ -15,6 +15,79 @@
|
|
|
15
15
|
|
|
16
16
|
import { EncodeRun } from "../../services/encode/EncodeRun.js";
|
|
17
17
|
|
|
18
|
+
/** The working time between two closed pieces of a test measurement. */
|
|
19
|
+
const MEASURED_STEP_MS = 1000;
|
|
20
|
+
|
|
21
|
+
/** Closed pieces in a test measurement: the last two give the reading. */
|
|
22
|
+
const MEASURED_STEPS = 3;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The clock each run built by `startRunOn` reads: standing while a measurement
|
|
26
|
+
* is said, the wall clock otherwise. @type {WeakMap<EncodeRun, { at: number | null }>}
|
|
27
|
+
*/
|
|
28
|
+
const wallClocks = new WeakMap();
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Make a run measure a speed the way it does in the product: progress reports
|
|
32
|
+
* and the names of closed pieces on the encoder's own channels, one second of
|
|
33
|
+
* the run's own work apart. A closed piece is published when the next name
|
|
34
|
+
* arrives, and each publication is one reading against the one before, so the
|
|
35
|
+
* last of three steps reads exactly `speedX` whatever the run measured before.
|
|
36
|
+
*
|
|
37
|
+
* @param {EncodeRun} run - Its process exposes `stdout` and `stdio[3]`.
|
|
38
|
+
* @param {number} speedX - Film seconds per second of the run's work.
|
|
39
|
+
* @param {{ at: number } | null} [clock] - What the run's `now` reads; advanced
|
|
40
|
+
* here. Absent for a run built by `startRunOn` without one: its clock is
|
|
41
|
+
* stood just behind the wall clock for the measurement and released after,
|
|
42
|
+
* so the reading is dated now.
|
|
43
|
+
* @param {number} [waitedMs] - How long the run's input waits for the swarm in
|
|
44
|
+
* each step, on top of its second of work.
|
|
45
|
+
* @returns {void}
|
|
46
|
+
*/
|
|
47
|
+
export function measureSpeed(run, speedX, clock = null, waitedMs = 0) {
|
|
48
|
+
if (clock === null) {
|
|
49
|
+
const wall = wallClocks.get(run);
|
|
50
|
+
if (!wall) {
|
|
51
|
+
throw new TypeError("measureSpeed needs the clock the run reads");
|
|
52
|
+
}
|
|
53
|
+
// Never behind the run's start or its own last reading: a step back in
|
|
54
|
+
// time is no work.
|
|
55
|
+
wall.at = Math.max(
|
|
56
|
+
Date.now() - (MEASURED_STEP_MS + waitedMs) * MEASURED_STEPS,
|
|
57
|
+
run.startedAt,
|
|
58
|
+
run.speedReading?.at ?? Number.NEGATIVE_INFINITY
|
|
59
|
+
);
|
|
60
|
+
measureSpeed(run, speedX, wall, waitedMs);
|
|
61
|
+
wall.at = null;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const { stdout, stdio } = run.process;
|
|
65
|
+
let produced = run.progress.processedSeconds - run.progress.startPositionSeconds;
|
|
66
|
+
for (let step = 0; step < MEASURED_STEPS; step += 1) {
|
|
67
|
+
if (waitedMs > 0) {
|
|
68
|
+
run.inputWaitBegins();
|
|
69
|
+
clock.at += waitedMs;
|
|
70
|
+
run.inputWaitEnds();
|
|
71
|
+
}
|
|
72
|
+
clock.at += MEASURED_STEP_MS;
|
|
73
|
+
produced += (speedX * MEASURED_STEP_MS) / 1000;
|
|
74
|
+
say(stdout, `out_time_ms=${Math.round(produced * 1_000_000)}\n`);
|
|
75
|
+
say(stdio[3], `measured-${step}\n`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {{ write?: (text: string) => void, emit?: (event: string, text: string) => void }} channel
|
|
81
|
+
* @param {string} text
|
|
82
|
+
*/
|
|
83
|
+
function say(channel, text) {
|
|
84
|
+
if (typeof channel.write === "function") {
|
|
85
|
+
channel.write(text);
|
|
86
|
+
} else {
|
|
87
|
+
channel.emit("data", text);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
18
91
|
/**
|
|
19
92
|
* A child process that records what was done to it.
|
|
20
93
|
*
|
|
@@ -26,6 +99,8 @@ import { EncodeRun } from "../../services/encode/EncodeRun.js";
|
|
|
26
99
|
export function fakeProcess({ pid = null, exitsWhenKilled = true } = {}) {
|
|
27
100
|
/** @type {Map<string, (...args: unknown[]) => void>} */
|
|
28
101
|
const listeners = new Map();
|
|
102
|
+
const stdout = channel();
|
|
103
|
+
const closedPieces = channel();
|
|
29
104
|
return {
|
|
30
105
|
pid,
|
|
31
106
|
killed: false,
|
|
@@ -33,8 +108,10 @@ export function fakeProcess({ pid = null, exitsWhenKilled = true } = {}) {
|
|
|
33
108
|
signalCode: null,
|
|
34
109
|
/** Every signal it was sent, in order. */
|
|
35
110
|
signals: [],
|
|
36
|
-
stdout
|
|
111
|
+
stdout,
|
|
37
112
|
stderr: { on() {} },
|
|
113
|
+
// The descriptor the encoder names its closed pieces on (`-segment_list pipe:3`).
|
|
114
|
+
stdio: [null, stdout, null, closedPieces],
|
|
38
115
|
on(event, handler) {
|
|
39
116
|
const kept = listeners.get(event) ?? [];
|
|
40
117
|
kept.push(handler);
|
|
@@ -71,6 +148,24 @@ export function fakeProcess({ pid = null, exitsWhenKilled = true } = {}) {
|
|
|
71
148
|
};
|
|
72
149
|
}
|
|
73
150
|
|
|
151
|
+
/**
|
|
152
|
+
* A readable side of a pipe that a test writes into.
|
|
153
|
+
*
|
|
154
|
+
* @returns {{ on: (event: string, handler: (chunk: string) => void) => void, write: (text: string) => void }}
|
|
155
|
+
*/
|
|
156
|
+
function channel() {
|
|
157
|
+
/** @type {((chunk: string) => void)[]} */
|
|
158
|
+
const handlers = [];
|
|
159
|
+
return {
|
|
160
|
+
on(event, handler) {
|
|
161
|
+
if (event === "data") handlers.push(handler);
|
|
162
|
+
},
|
|
163
|
+
write(text) {
|
|
164
|
+
for (const handler of handlers) handler(text);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
74
169
|
/** A logger that says nothing, for tests that are not about the log. */
|
|
75
170
|
export const silentLogger = { info() {}, warn() {}, error() {} };
|
|
76
171
|
|
|
@@ -100,14 +195,17 @@ export function startRunOn(session, options = {}) {
|
|
|
100
195
|
producing = true,
|
|
101
196
|
lastSegmentIndex = null,
|
|
102
197
|
usesExplicitCuts = false,
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
// for
|
|
109
|
-
|
|
198
|
+
// THE SPEED IT HAS MEASURED: film made over its own working time between
|
|
199
|
+
// two closed pieces. Left at zero, the plan has no speed to compute an
|
|
200
|
+
// arrival from, every arrangement of encoders is equally hopeless, they all
|
|
201
|
+
// tie, and the encoder is taken away for changing nothing.
|
|
202
|
+
speedX = 0,
|
|
203
|
+
// What the run reads as now, for a test that drives time itself. Absent,
|
|
204
|
+
// the run reads the wall clock, which stands still only while a
|
|
205
|
+
// measurement is said.
|
|
206
|
+
clock: given = null
|
|
110
207
|
} = options;
|
|
208
|
+
const clock = given ?? { at: null };
|
|
111
209
|
const run = new EncodeRun({
|
|
112
210
|
address: session.outputKey ?? session.id ?? "output",
|
|
113
211
|
encoder: { name: "libx264", kind: "software" },
|
|
@@ -118,9 +216,15 @@ export function startRunOn(session, options = {}) {
|
|
|
118
216
|
logger: silentLogger,
|
|
119
217
|
lastSegmentIndex: () => lastSegmentIndex,
|
|
120
218
|
usesExplicitCuts,
|
|
219
|
+
now: () => clock.at ?? Date.now(),
|
|
121
220
|
because: "a test asked for it"
|
|
122
221
|
});
|
|
123
|
-
|
|
222
|
+
if (given === null) {
|
|
223
|
+
wallClocks.set(run, clock);
|
|
224
|
+
}
|
|
225
|
+
if (speedX > 0) {
|
|
226
|
+
measureSpeed(run, speedX, given);
|
|
227
|
+
}
|
|
124
228
|
if (producing) {
|
|
125
229
|
// What moves a run out of starting is its first segment, in the product as
|
|
126
230
|
// here — so a run that is producing has made one, and its head stands one
|
package/test/link-budget.test.js
CHANGED
|
@@ -9,7 +9,6 @@ import test from "node:test";
|
|
|
9
9
|
import assert from "node:assert/strict";
|
|
10
10
|
import {
|
|
11
11
|
admissionRank,
|
|
12
|
-
LINK_SAFETY,
|
|
13
12
|
LINK_VERDICT,
|
|
14
13
|
linkCouldCarry,
|
|
15
14
|
loadOf,
|
|
@@ -67,11 +66,11 @@ test("a load is as trustworthy as its least trustworthy part", () => {
|
|
|
67
66
|
assert.ok(Math.abs(loadOf(known(3), known(0.128)).totalMbps - 3.128) < 1e-9, "the sound is added to the picture");
|
|
68
67
|
});
|
|
69
68
|
|
|
70
|
-
test("the sound is counted
|
|
71
|
-
// The picture alone fits the
|
|
72
|
-
//
|
|
69
|
+
test("the sound is counted with the picture against the whole measured link", () => {
|
|
70
|
+
// The picture alone fits the link exactly; with the soundtrack the viewer
|
|
71
|
+
// hears, it does not. No share of the link is held back.
|
|
73
72
|
const link = 10;
|
|
74
|
-
const picture = known(link
|
|
73
|
+
const picture = known(link);
|
|
75
74
|
assert.equal(linkCouldCarry(link, loadOf(picture, null)).verdict, LINK_VERDICT.FITS, "without sound it would pass");
|
|
76
75
|
assert.equal(
|
|
77
76
|
linkCouldCarry(link, loadOf(picture, known(AUDIO_TRANSCODE_KBPS / 1000))).verdict,
|
|
@@ -142,7 +142,7 @@ function fakeSession({ id = SESSION_ID } = {}) {
|
|
|
142
142
|
useSyntheticPlaylist: true,
|
|
143
143
|
playlistText: "#EXTM3U\n",
|
|
144
144
|
segmentCount: 10,
|
|
145
|
-
progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0
|
|
145
|
+
progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0 }
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
148
|
|
|
@@ -182,7 +182,6 @@ test("the progress report keeps every figure it carries today", async () => {
|
|
|
182
182
|
"percent",
|
|
183
183
|
"remainingSeconds",
|
|
184
184
|
"segmentDurationSec",
|
|
185
|
-
"speed",
|
|
186
185
|
"currentHeight",
|
|
187
186
|
"offeredHeights",
|
|
188
187
|
"requestedHeight",
|
|
@@ -18,7 +18,7 @@ import { wireOutputs } from "../services/server/wire-outputs.js";
|
|
|
18
18
|
import { SegmentStore } from "../services/storage/segment-store/SegmentStore.js";
|
|
19
19
|
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
20
20
|
import { qualityStateOf } from "../services/encode/quality/OutputQualityState.js";
|
|
21
|
-
import { startRunOn } from "./helpers/encode-run.js";
|
|
21
|
+
import { measureSpeed, startRunOn } from "./helpers/encode-run.js";
|
|
22
22
|
import { outputSpec } from "./helpers/output-spec.js";
|
|
23
23
|
|
|
24
24
|
const KEY = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
|
|
@@ -65,9 +65,10 @@ function sessionOn({ manager, id, dirPath, encodeStartIndex = 0, runEndIndex = -
|
|
|
65
65
|
const run = startRunOn(session, { from: encodeStartIndex, to: runEndIndex });
|
|
66
66
|
manager.encodeOrchestrator.adopt(KEY, run);
|
|
67
67
|
session.testRun = run;
|
|
68
|
-
// A speed is
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
// A speed is the run's own reading, measured from its closed pieces.
|
|
69
|
+
if (speed > 0) {
|
|
70
|
+
measureSpeed(run, speed);
|
|
71
|
+
}
|
|
71
72
|
}
|
|
72
73
|
return session;
|
|
73
74
|
}
|