@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.
Files changed (50) hide show
  1. package/AGENTS.md +338 -336
  2. package/CHANGELOG.md +2319 -2303
  3. package/docs/encode-architecture.md +55 -19
  4. package/docs/playback-start-forecast.md +13 -5
  5. package/docs/storage-architecture.md +6 -0
  6. package/package.json +1 -1
  7. package/routes/stream/get.js +38 -2
  8. package/server.js +748 -745
  9. package/services/encode/EncodeOrchestrator.js +24 -49
  10. package/services/encode/EncodePlan.js +5 -3
  11. package/services/encode/EncodeRun.js +80 -17
  12. package/services/encode/EncodeRuns.js +1339 -1239
  13. package/services/encode/Renditions.js +2 -1
  14. package/services/encode/RunClock.js +87 -0
  15. package/services/encode/RunProgress.js +1 -6
  16. package/services/encode/encoder-readings.js +26 -28
  17. package/services/encode/piece-completeness.js +46 -0
  18. package/services/encode/quality/EncodeCost.js +1043 -1167
  19. package/services/encode/quality/OutputQualityState.js +1 -5
  20. package/services/encode/quality/QualityController.js +213 -390
  21. package/services/encode/quality/link-budget.js +5 -17
  22. package/services/encode/run-inputs.js +13 -2
  23. package/services/server/SegmentServing.js +3 -1
  24. package/services/server/ViewerRequests.js +2 -7
  25. package/services/server/wire-outputs.js +12 -7
  26. package/services/storage/segment-store/SegmentStore.js +1294 -1255
  27. package/services/torrent/supply-margin.js +7 -5
  28. package/test/auto-quality-step.test.js +75 -29
  29. package/test/decode-cost.test.js +1 -1
  30. package/test/encode-cost.test.js +31 -39
  31. package/test/encode-orchestrator.test.js +15 -8
  32. package/test/encode-plan-viewers.test.js +34 -27
  33. package/test/encode-run.test.js +34 -6
  34. package/test/encoder-readings.test.js +48 -26
  35. package/test/helpers/encode-run.js +113 -9
  36. package/test/link-budget.test.js +4 -5
  37. package/test/manager-surface.test.js +1 -2
  38. package/test/orchestrator-wired.test.js +5 -4
  39. package/test/output-speed.test.js +0 -1
  40. package/test/piece-completeness.test.js +42 -0
  41. package/test/quality-offer.test.js +1 -3
  42. package/test/quality-variants.test.js +1 -1
  43. package/test/run-progress.test.js +2 -3
  44. package/test/same-height-switch.test.js +1 -2
  45. package/test/segment-closed-on-both-branches.test.js +114 -96
  46. package/test/segment-store.test.js +15 -15
  47. package/test/stream-route.test.js +28 -0
  48. package/test/supply-margin.test.js +13 -0
  49. package/test/two-viewers-one-picture.test.js +1 -1
  50. package/test/viewer-outputs.test.js +1 -1
@@ -141,11 +141,13 @@ export function requiredSpeedFrom(waits) {
141
141
  const lostSec = interruptions
142
142
  .slice(0, last)
143
143
  .reduce((total, one) => total + (one.end - one.start), 0) / 1000;
144
- // A span that is all interruption says the supply delivered nothing at all
145
- // while it was watched. There is no speed that survives that, and saying a
146
- // huge number is less honest than saying it is not a speed question — so the
147
- // largest figure any step is ever compared against is stated and named.
148
- const lostShare = spanSec > 0 ? Math.min(0.99, lostSec / spanSec) : 0;
144
+ // Strictly below one: the span holds at least one gap of positive length
145
+ // (`intervals` above is not empty), and the gaps are the time the supply
146
+ // delivered. So the speed this asks for is finite whatever it measured, and
147
+ // it is not cut down to a figure chosen here: a supply that delivered for a
148
+ // thousandth of the time asks a thousand times realtime, which no re-encode
149
+ // meets and which leaves the copy, as it should.
150
+ const lostShare = spanSec > 0 ? lostSec / spanSec : 0;
149
151
  return {
150
152
  requiredSpeed: 1 / (1 - lostShare),
151
153
  lostShare,
@@ -18,7 +18,7 @@
18
18
 
19
19
  import test from "node:test";
20
20
  import { recordViewerReport } from "../services/viewer/report-intake.js";
21
- import { fakeProcess as fakeEncoder, startRunOn } from "./helpers/encode-run.js";
21
+ import { fakeProcess as fakeEncoder, measureSpeed, startRunOn } from "./helpers/encode-run.js";
22
22
  import assert from "node:assert/strict";
23
23
  import { SourceFile } from "../services/media/SourceFile.js";
24
24
  import { Timeline } from "../services/encode/output/Timeline.js";
@@ -92,7 +92,7 @@ function fakeSession({ dirPath, transcodeVideo = true, cutGrid = transcodeVideo
92
92
  usesExplicitCuts: false,
93
93
  useSyntheticPlaylist: true,
94
94
  playlistText: "#EXTM3U\n",
95
- progress: { state: "running", processedSeconds: 40, startPositionSeconds: 0, speed: "1.0x" }
95
+ progress: { state: "running", processedSeconds: 40, startPositionSeconds: 0 }
96
96
  };
97
97
  }
98
98
 
@@ -162,7 +162,7 @@ function drainingReports(viewer, linkMbps) {
162
162
  * This viewer's page reports a buffer that grows, on the link named.
163
163
  *
164
164
  * @param {object} viewer
165
- * @param {number} linkMbps
165
+ * @param {number | null} linkMbps - Null when the page has not measured it.
166
166
  * @returns {void}
167
167
  */
168
168
  function fillingReports(viewer, linkMbps) {
@@ -195,12 +195,12 @@ test("a picture that cannot be kept up with is asked for as another VARIANT, and
195
195
  });
196
196
 
197
197
  const sizeBefore = `${session.encodeWidth}x${session.encodeHeight}`;
198
- // Sustained sub-realtime, read as a slope: the run has been slow since well
199
- // before the window, and the reading is from this very run.
200
- qualityStateOf(session).budgetSlowSince = Date.now() - 60_000;
201
- qualityStateOf(session).recentSpeed = { speed: 0.7, at: Date.now(), run: [...session.runs][0] };
198
+ // Below realtime over its own working time, and the viewer's buffer runs
199
+ // dry on its trend although their link carries the stream.
200
+ measureSpeed([...session.runs][0], 0.7);
201
+ drainingReports(manager.viewers.get("viewer"), 80);
202
202
 
203
- await manager.quality.runQualityBudgetOnce();
203
+ await manager.quality.noteViewerReported(session.id, "viewer");
204
204
 
205
205
  assert.equal(
206
206
  `${session.encodeWidth}x${session.encodeHeight}`,
@@ -224,10 +224,10 @@ test("a manual viewer is never sent an automatic quality request", async (t) =>
224
224
  await rm(dirPath, { recursive: true, force: true });
225
225
  });
226
226
  manager.viewers.get("viewer").qualityMode = "manual";
227
- qualityStateOf(session).budgetSlowSince = Date.now() - 60_000;
228
- qualityStateOf(session).recentSpeed = { speed: 0.7, at: Date.now(), run: [...session.runs][0] };
227
+ measureSpeed([...session.runs][0], 0.7);
228
+ drainingReports(manager.viewers.get("viewer"), 80);
229
229
 
230
- await manager.quality.runQualityBudgetOnce();
230
+ await manager.quality.noteViewerReported(session.id, "viewer");
231
231
 
232
232
  assert.equal(manager.viewers.get("viewer").qualityAsk, null);
233
233
  const progress = await manager.viewerRequests.getSessionProgress(BASE_ID, "viewer");
@@ -255,8 +255,8 @@ test("the request reaches the browser in the progress report, and stops once the
255
255
  assert.equal(manager.viewers.get("viewer").qualityAsk, null, "and it is let go of, not merely hidden");
256
256
  });
257
257
 
258
- test("a request the player never follows runs out instead of being repeated for the whole film", async (t) => {
259
- const { manager, dirPath } = await managerWithSession();
258
+ test("a request stands while its conditions hold, and the next report that does not ask for it lets it go", async (t) => {
259
+ const { manager, session, dirPath } = await managerWithSession();
260
260
  t.after(async () => {
261
261
  await manager.lifecycle.disposeAll();
262
262
  // The store closes its watch on the directory as it drops the output, and
@@ -265,14 +265,39 @@ test("a request the player never follows runs out instead of being repeated for
265
265
  await rm(dirPath, { recursive: true, force: true });
266
266
  });
267
267
 
268
- // A viewer on a manual pick ignores every request by design, and so does a
269
- // stream with no variants. Neither is an error; both look the same from here.
268
+ // Asked when their buffer was running dry; no chosen time ends it.
270
269
  manager.viewers.get("viewer").qualityAsk = { height: 480, at: Date.now() - 120_000, reason: "measured" };
270
+ const standing = await manager.viewerRequests.getSessionProgress(BASE_ID, "viewer");
271
+ assert.equal(standing.requestedHeight, 480, "two minutes on, it still stands: nothing has said otherwise");
271
272
 
272
- const progress = await manager.viewerRequests.getSessionProgress(BASE_ID, "viewer");
273
+ // Their next report shows a buffer that grows: what it was asked for is gone.
274
+ // The machine has no room to spare, so nothing else is asked in its place.
275
+ measureSpeed([...session.runs][0], 0.9);
276
+ fillingReports(manager.viewers.get("viewer"), null);
277
+ await manager.quality.noteViewerReported(session.id, "viewer");
273
278
 
279
+ const progress = await manager.viewerRequests.getSessionProgress(BASE_ID, "viewer");
274
280
  assert.equal(progress.requestedHeight, 0);
275
- assert.equal(manager.viewers.get("viewer").qualityAsk, null, "said once and let go");
281
+ assert.equal(manager.viewers.get("viewer").qualityAsk, null, "let go by the judgement that did not repeat it");
282
+ });
283
+
284
+ test("a request the next report asks for again stands", async (t) => {
285
+ const { manager, session, dirPath } = await managerWithSession();
286
+ t.after(async () => {
287
+ await manager.lifecycle.disposeAll();
288
+ await new Promise((resolve) => setImmediate(resolve));
289
+ await rm(dirPath, { recursive: true, force: true });
290
+ });
291
+ measureSpeed([...session.runs][0], 0.7);
292
+ drainingReports(manager.viewers.get("viewer"), 80);
293
+ await manager.quality.noteViewerReported(session.id, "viewer");
294
+ const asked = manager.viewers.get("viewer").qualityAsk;
295
+ assert.ok(asked, "the machine cannot keep up and the buffer runs dry");
296
+
297
+ drainingReports(manager.viewers.get("viewer"), 80);
298
+ await manager.quality.noteViewerReported(session.id, "viewer");
299
+
300
+ assert.equal(manager.viewers.get("viewer").qualityAsk, asked, "the same request, not let go and not made again");
276
301
  });
277
302
 
278
303
  test("a COPIED picture is never asked to slow its encoder, because it has none", async (t) => {
@@ -287,10 +312,10 @@ test("a COPIED picture is never asked to slow its encoder, because it has none",
287
312
 
288
313
  // Whatever this reading says, a copy has no encoder to make cheaper: moving
289
314
  // the viewer to a RE-ENCODED rung costs the machine more, not less.
290
- qualityStateOf(session).budgetSlowSince = Date.now() - 60_000;
291
- qualityStateOf(session).recentSpeed = { speed: 0.4, at: Date.now(), run: [...session.runs][0] };
315
+ measureSpeed([...session.runs][0], 0.4);
316
+ drainingReports(manager.viewers.get("viewer"), 80);
292
317
 
293
- await manager.quality.runQualityBudgetOnce();
318
+ await manager.quality.noteViewerReported(session.id, "viewer");
294
319
 
295
320
  assert.equal(manager.viewers.get("viewer").qualityAsk, null, "the copy path's lever is the viewer's link, not the CPU");
296
321
  });
@@ -467,15 +492,18 @@ test("the way BACK UP exists, one rung at a time", async (t) => {
467
492
  await rm(dirPath, { recursive: true, force: true });
468
493
  });
469
494
 
470
- // The viewer is on 480p and the machine has been ahead of realtime for longer
471
- // than the up window.
495
+ // The viewer is on 480p, the machine makes it ahead of realtime over its own
496
+ // working time, and their buffer grows and holds more than another output
497
+ // takes to be ready. No window has to pass: every term is measured.
472
498
  session.variantHeight = 480;
473
499
  session.encodeWidth = 854;
474
500
  session.encodeHeight = 480;
475
- qualityStateOf(session).recentSpeed = { speed: 2.4, at: Date.now(), run: [...session.runs][0] };
476
- qualityStateOf(session).budgetUpSince = Date.now() - 120_000;
501
+ measureSpeed([...session.runs][0], 2.4);
502
+ // Their link is not measured here: what is under test is the room, and a
503
+ // measured link would also weigh a soundtrack this fixture states no rate for.
504
+ fillingReports(manager.viewers.get("viewer"), null);
477
505
 
478
- await manager.quality.runQualityBudgetOnce();
506
+ await manager.quality.noteViewerReported(session.id, "viewer");
479
507
 
480
508
  assert.ok(manager.viewers.get("viewer").qualityAsk, "for most of this project's life there was no step up at all");
481
509
  assert.equal(
@@ -642,8 +670,9 @@ test("a picture seen larger than the rung on screen is asked one rung up, when t
642
670
  session.variantHeight = 480;
643
671
  session.encodeWidth = 854;
644
672
  session.encodeHeight = 480;
645
- qualityStateOf(session).recentSpeed = { speed: 2.4, at: Date.now(), run: [...session.runs][0] };
673
+ measureSpeed([...session.runs][0], 2.4);
646
674
  manager.viewers.get("viewer").noteVisiblePicture({ width: 1920, height: 1080 });
675
+ fillingReports(manager.viewers.get("viewer"), null);
647
676
 
648
677
  await manager.quality.noteViewerReported(session.id, "viewer");
649
678
 
@@ -696,12 +725,29 @@ test("a step down for the machine goes to the rung the picture seen bounds, wher
696
725
  await rm(dirPath, { recursive: true, force: true });
697
726
  });
698
727
  manager.viewers.get("viewer").noteVisiblePicture({ width: 640, height: 360 });
699
- qualityStateOf(session).budgetSlowSince = Date.now() - 60_000;
700
- qualityStateOf(session).recentSpeed = { speed: 0.7, at: Date.now(), run: [...session.runs][0] };
728
+ measureSpeed([...session.runs][0], 0.7);
729
+ drainingReports(manager.viewers.get("viewer"), 80);
701
730
 
702
- await manager.quality.runQualityBudgetOnce();
731
+ await manager.quality.noteViewerReported(session.id, "viewer");
703
732
 
704
733
  const asked = manager.viewers.get("viewer").qualityAsk?.height;
705
734
  assert.ok(asked, "the machine cannot keep up: a step down is asked");
706
735
  assert.ok(asked <= 360, `not a rung between that the screen cannot show (asked for ${asked}p)`);
707
736
  });
737
+
738
+ test("a machine below realtime whose viewer's buffer is filling moves nothing", async (t) => {
739
+ const { manager, session, dirPath } = await managerWithSession();
740
+ t.after(async () => {
741
+ await manager.lifecycle.disposeAll();
742
+ await new Promise((resolve) => setImmediate(resolve));
743
+ await rm(dirPath, { recursive: true, force: true });
744
+ });
745
+ // What the cushion already holds covers the shortfall: no chosen window of
746
+ // slowness decides, the viewer's own buffer trend does.
747
+ measureSpeed([...session.runs][0], 0.7);
748
+ fillingReports(manager.viewers.get("viewer"), 80);
749
+
750
+ await manager.quality.noteViewerReported(session.id, "viewer");
751
+
752
+ assert.ok(!(manager.viewers.get("viewer").qualityAsk?.height < 720), "no step down while the buffer grows");
753
+ });
@@ -355,7 +355,7 @@ test("the OFFER drops the rungs the host cannot hold, and the master keeps addre
355
355
  useSyntheticPlaylist: true,
356
356
  playlistText: "#EXTM3U\n",
357
357
  segmentCount: 100,
358
- progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0, speed: "1.0x" }
358
+ progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0 }
359
359
  };
360
360
  manager.outputs.set(session.id, session);
361
361
 
@@ -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
- progress: { processedSeconds: 0 },
65
+ clock: { at: 1000 },
68
66
  runs: new Set()
69
67
  };
70
- startRunOn(session, { from: 0, speedX: 1 });
68
+ startRunOn(session, { from: 0, clock: session.clock });
71
69
  return session;
72
70
  }
73
71
 
74
72
  /**
75
- * Two readings of one run, far enough apart to be a speed.
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 clock.
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
- session.progress.processedSeconds = 0;
84
- await cost.learnFrom(session);
85
- // The pair is a DELTA, and the first reading has nothing to be a delta from.
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 reading taken while the swarm is short is not filed as the price of this host", async () => {
167
- // A run starved of torrent data reports a speed that measures the swarm. Filed
168
- // as a price it reads as more work per second than the machine has, and every
169
- // quality step is then refused on the download's account.
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, 0.3);
168
+ await watchItRun(cost, session, 8, 9_000);
174
169
 
175
- assert.equal(cost.copyVersionFor("torrent:abc:0"), 0, "nothing was learned about the machine");
176
- assert.equal(qualityStateOf(session).lastAloneSpeed, undefined, "and nothing claims this rung was seen failing");
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("one reading is not a speed", async () => {
187
+ test("a run that has measured nothing teaches nothing", async () => {
193
188
  const { cost } = costOn();
194
189
  const session = sessionProducing({ transcodeVideo: false });
195
- session.progress.processedSeconds = 40;
190
+ const run = [...session.runs][0];
196
191
 
197
- await cost.learnFrom(session);
192
+ await cost.learnFrom(session, run);
198
193
 
199
- assert.equal(cost.copyVersionFor("torrent:abc:0"), 0, "a delta needs two readings of one run");
200
- assert.ok(qualityStateOf(session).learnSample, "and the first is kept to be the other half of the next");
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 pair that straddles a restart measures the seek, not the host", async () => {
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, speedX: 1 });
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: () => 1000,
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: () => 1000,
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.noteSpeed(8);
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.noteSpeed(1);
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.noteSpeed(1);
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.noteSpeed(2);
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.noteSpeed(1);
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].noteSpeed(8);
417
+ measureSpeed(made.runsOn(PICTURE)[0], 8, clock);
411
418
  wants(made, [{ from: 500, to: 530 }]);
412
419
  made.reconcile();
413
420