@torrent-tv/proxy 2.80.17 → 2.80.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/docs/encode-architecture.md +42 -7
- package/package.json +1 -1
- package/services/encode/EncodePlan.js +22 -12
- package/services/encode/run-costs.js +15 -40
- package/services/orchestrators/EncodeOrchestrator.js +16 -0
- package/test/move-cost.test.js +109 -38
- package/test/split-a-stretch.test.js +139 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 2.80.18
|
|
2
|
+
|
|
3
|
+
- **Fix**: The `Infinity` is gone, and with it a double count I had introduced. The measured "time to a first piece" already CONTAINS one piece's encoding, and I was adding another; separated, the two scale differently — a piece costs more when encoders share the machine, a spawn does not. So a fresh encoder owes `spawn overhead + the piece at the rate in force`, a moved one owes the kill and then the same. With nothing measured the overhead is zero and a fresh encoder owes exactly one piece, which is the floor and is DERIVED rather than chosen: a piece cannot appear before it is encoded, and how fast this host encodes is measured before any viewer exists. The `Infinity` was an exception in a model that needs none. Verified by simulation over eighty ticks against the map's real shape — zero moves and zero one-piece intervals at a budget of one encoder and of three.
|
|
4
|
+
- **New**: `encode-plan on <output>: start #58..#481, stop #?..#?` — every action with its INTERVAL, which is what a run is, printed on any pass that does something. Its absence cost three wrong diagnoses of one field session: the line printed the windows, the budget and where the live runs stood, and not the intervals the actions carried.
|
|
5
|
+
- **Chore**: THE FIELD DIAGNOSIS OF 2026-09-08, corrected. What that session did was give every encoder an interval of exactly ONE segment — 63 runs, each spending 1.26 s reaching its first piece, making that one piece, reaching the end of its interval and exiting, twelve of them normally and thirteen killed. The reasons printed beside them read as moves back and forth, so I diagnosed an oscillating placement three times over and shipped 2.80.15 through 2.80.17 against it. Those releases stand on their own — the objective now follows the map's own rank order, the arrival arithmetic no longer charges a run for a piece it is already making, and the split point is derived — but none of them touches the caravan. Which line assigns an interval of one segment is not yet named: on a clean model it does not happen, three candidate explanations were checked and all three fell, and the line above is what will name it on the next session.
|
|
6
|
+
|
|
1
7
|
## 2.80.17
|
|
2
8
|
|
|
3
9
|
- **Fix**: Where a second encoder joins a stretch is DERIVED, and whether it is worth having is asked at all. It was the midpoint of the widest unmade run, unconditionally. Both encoders close their halves at a time that rises with the split point for one and falls for the other, so the stretch is closed soonest where they cross: `x* = (from + to + 1)/2 + (d - w) * r / 2` — the midpoint, shifted forward by half the difference between what a fresh encoder owes and what the one already there owes, in pieces. Halving is the special case where both are fresh and owe the same.
|
|
@@ -242,12 +242,28 @@ piece costs — because only there is it known what the body IS, and at the poin
|
|
|
242
242
|
of pricing only how many there are. So there is no kind, no tag and no case
|
|
243
243
|
analysis.
|
|
244
244
|
|
|
245
|
-
**
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
245
|
+
**The measured start is separated from the piece it contains**, because the two
|
|
246
|
+
scale differently: a piece costs more when encoders share the machine, a spawn
|
|
247
|
+
does not.
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
spawn overhead = measured first output - what one piece costs alone
|
|
251
|
+
a fresh encoder = spawn overhead + the piece at the rate in force
|
|
252
|
+
a moved one = the kill, and then the same
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
With nothing measured the overhead is zero and a fresh encoder owes exactly one
|
|
256
|
+
piece. **That is the floor, and it is derived rather than chosen:** a piece
|
|
257
|
+
cannot appear before it is encoded, and how fast this host encodes is measured
|
|
258
|
+
before any viewer exists. There was an `Infinity` here for the cost of a move,
|
|
259
|
+
on the reasoning that an unmeasured price must not license an irreversible act —
|
|
260
|
+
an exception in a model that needs none, and this is the same statement made by
|
|
261
|
+
arithmetic.
|
|
262
|
+
|
|
263
|
+
And a run killed before producing anything is a measurement too — a lower bound
|
|
264
|
+
on the first output, and the only reading a thrash can supply, since every run in
|
|
265
|
+
one is killed before it finishes anything. So a thrash makes its own moves
|
|
266
|
+
progressively dearer until it stops.
|
|
251
267
|
|
|
252
268
|
### What that was for
|
|
253
269
|
|
|
@@ -262,7 +278,7 @@ IMMEDIATELY and was the most urgent material in the file: it bought encoders, an
|
|
|
262
278
|
it took the run standing in front of the viewer because that run was the nearest
|
|
263
279
|
body to it.
|
|
264
280
|
|
|
265
|
-
Checked by simulation over
|
|
281
|
+
Checked by simulation over eighty ticks against the map's real shape — ten zones
|
|
266
282
|
doubling ahead of the viewer, one behind — at both one and three runs: the
|
|
267
283
|
encoder is placed once, left alone, and moved exactly once, at the viewer's own
|
|
268
284
|
seek.
|
|
@@ -301,6 +317,25 @@ penalty for a second encoder on the addon host is 1.98 — it takes very nearly
|
|
|
301
317
|
all of the first's speed — and the objective keeps one; where a second is free it
|
|
302
318
|
places three.
|
|
303
319
|
|
|
320
|
+
## What the log says when anything is placed or taken away
|
|
321
|
+
|
|
322
|
+
```
|
|
323
|
+
encode-plan on <output>: start #58..#481, stop #?..#?
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Every action with its INTERVAL, which is what a run is. Printed on any pass that
|
|
327
|
+
does something — a session where nothing changes says nothing.
|
|
328
|
+
|
|
329
|
+
It was missing, and its absence cost three wrong diagnoses of one field session.
|
|
330
|
+
The line printed the windows, the budget and where the live runs stood; the
|
|
331
|
+
intervals the actions carried were the one thing it did not print. What that
|
|
332
|
+
session actually did was give every encoder an interval of exactly ONE segment —
|
|
333
|
+
63 runs, each spending 1.26 s reaching its first piece, making that one piece,
|
|
334
|
+
reaching the end of its interval and exiting, twelve of them normally. The
|
|
335
|
+
reasons printed beside them read as moves back and forth, so the fault was read
|
|
336
|
+
as an oscillating placement three times over. An interval of one segment turns
|
|
337
|
+
the protection against two encoders writing one name into a mill for processes.
|
|
338
|
+
|
|
304
339
|
## What is checked
|
|
305
340
|
|
|
306
341
|
`test/one-authority.test.js` holds the shape: one caller of `#startEncodeRun`,
|
package/package.json
CHANGED
|
@@ -102,9 +102,6 @@
|
|
|
102
102
|
* measured on this host from its own runs. Zero until something has measured
|
|
103
103
|
* it, which makes moving one look cheaper than it is and is said here so the
|
|
104
104
|
* bias is known.
|
|
105
|
-
* @param {number} [params.moveCostSec] - What moving a running encoder costs on
|
|
106
|
-
* this host, measured. `Infinity` until something has been measured, because a
|
|
107
|
-
* move is irreversible and leaving the encoder alone is always available.
|
|
108
105
|
* @param {number} [params.now] - The clock, injected. This layer is arithmetic
|
|
109
106
|
* and reads no clock of its own; how old a run is is one of its inputs.
|
|
110
107
|
* @param {number} [params.firstByteWaitSec] - How long a fresh encoder takes to
|
|
@@ -128,7 +125,6 @@ export function planEncoders({
|
|
|
128
125
|
segmentSeconds,
|
|
129
126
|
killCostSec = 0,
|
|
130
127
|
firstByteWaitSec = 0,
|
|
131
|
-
moveCostSec = Number.POSITIVE_INFINITY,
|
|
132
128
|
now = Date.now(),
|
|
133
129
|
refetchSecPerFilmSecond = 0,
|
|
134
130
|
contentionPenaltyFor = () => 1,
|
|
@@ -185,12 +181,26 @@ export function planEncoders({
|
|
|
185
181
|
// its death, the start of another, and the wait for the first bytes there.
|
|
186
182
|
// Taking an encoder somewhere else is stopping this one and waiting for the
|
|
187
183
|
// next to produce. Both halves are measured on this host.
|
|
188
|
-
// WHAT A
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
184
|
+
// WHAT A START AND A MOVE OWE BEFORE THE PIECE THEY STAND ON EXISTS, and
|
|
185
|
+
// neither is infinite.
|
|
186
|
+
//
|
|
187
|
+
// The measured `firstByteWaitSec` is spawn to first piece, so it already
|
|
188
|
+
// contains one piece's encoding. Separated, because the two scale differently:
|
|
189
|
+
// the piece costs more when encoders share the machine, the spawn does not.
|
|
190
|
+
//
|
|
191
|
+
// spawn overhead = measured first output - what one piece costs alone
|
|
192
|
+
// a fresh encoder owes spawn overhead + the piece at the rate in force
|
|
193
|
+
// a moved one owes the kill, and then the same
|
|
194
|
+
//
|
|
195
|
+
// With nothing measured the overhead is zero and a fresh encoder owes exactly
|
|
196
|
+
// one piece — which is the honest floor rather than a guess: a piece cannot
|
|
197
|
+
// appear before it is encoded, and how fast this host encodes is measured
|
|
198
|
+
// before any viewer exists. There was an `Infinity` here for the cost of a
|
|
199
|
+
// move, on the reasoning that an unmeasured price must not license an
|
|
200
|
+
// irreversible act; it was an exception in a model that needs none, and this
|
|
201
|
+
// is the same statement made by arithmetic.
|
|
202
|
+
const spawnOverheadSec = Math.max(0, firstByteWaitSec - (rate > 0 ? 1 / rate : 0));
|
|
203
|
+
const moveSec = killCostSec + spawnOverheadSec;
|
|
194
204
|
|
|
195
205
|
// WHAT A RUN STILL HAS TO GO BEFORE IT PRODUCES ANYTHING — the measured time
|
|
196
206
|
// to a first piece, less the time it has already been alive.
|
|
@@ -393,7 +403,7 @@ export function planEncoders({
|
|
|
393
403
|
}
|
|
394
404
|
if (filler === "new") {
|
|
395
405
|
// A encoder that does not exist yet owes its own start and then the piece.
|
|
396
|
-
encoders.push({ at: positions[index], owes: (piece) =>
|
|
406
|
+
encoders.push({ at: positions[index], owes: (piece) => spawnOverheadSec + piece });
|
|
397
407
|
continue;
|
|
398
408
|
}
|
|
399
409
|
const head = Number(filler.head);
|
|
@@ -472,7 +482,7 @@ export function planEncoders({
|
|
|
472
482
|
}
|
|
473
483
|
for (let index = 0; index < positions.length; index += 1) {
|
|
474
484
|
if ((best ? best.fill[index] : null) === "new") {
|
|
475
|
-
encoders.push({ at: positions[index], owes: (piece) =>
|
|
485
|
+
encoders.push({ at: positions[index], owes: (piece) => spawnOverheadSec + piece });
|
|
476
486
|
}
|
|
477
487
|
}
|
|
478
488
|
return priced(encoders);
|
|
@@ -98,47 +98,22 @@ export class RunCosts {
|
|
|
98
98
|
* @returns {{ killCostSec: number, firstByteWaitSec: number, samples: number }}
|
|
99
99
|
*/
|
|
100
100
|
seconds() {
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
// MEASURED OR ABSENT, and absent is said as zero rather than as a guess.
|
|
102
|
+
//
|
|
103
|
+
// There was an `Infinity` here for a while, for the cost of a move, on the
|
|
104
|
+
// reasoning that an unmeasured price must not license an irreversible act.
|
|
105
|
+
// It was an exception in a model that needs none, and it is not required: a
|
|
106
|
+
// first piece cannot appear faster than it takes to ENCODE one, and how fast
|
|
107
|
+
// this host encodes is measured before any viewer exists. The floor is
|
|
108
|
+
// derived from that where the arithmetic is, and every figure here stays a
|
|
109
|
+
// plain reading or a plain zero.
|
|
110
|
+
//
|
|
111
|
+
// `firstByteWaitSec` is spawn to first piece, so it already contains one
|
|
112
|
+
// piece's encoding. Whoever uses it separates the two, because the piece
|
|
113
|
+
// costs more when encoders share the machine and the spawn does not.
|
|
103
114
|
return {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
// as 0, an unmeasured move was FREE in the plan's arithmetic, so any gain
|
|
107
|
-
// however small justified it — and moving an encoder is irreversible,
|
|
108
|
-
// because the process it kills cannot be un-killed.
|
|
109
|
-
//
|
|
110
|
-
// The blindness was self-sustaining: `#firstOutput` only takes a reading
|
|
111
|
-
// from a run that produced something, and a run killed 0.8 s after
|
|
112
|
-
// starting produces nothing. So a thrash prevented the measurement that
|
|
113
|
-
// would have stopped it. Field 2026-09-08: 39 moves in one session, 24 of
|
|
114
|
-
// them between three adjacent numbers — #58 to #59, #59 to #58, #58 to
|
|
115
|
-
// #60, #60 to #58, six times each — while the picture stood still for
|
|
116
|
-
// 116.7 s.
|
|
117
|
-
//
|
|
118
|
-
// TWO QUESTIONS, NOT ONE, and they take the unknown differently.
|
|
119
|
-
//
|
|
120
|
-
// PLACING an encoder where there is none has no alternative: the film gets
|
|
121
|
-
// made or it does not. So an unmeasured cost must not stand in the way,
|
|
122
|
-
// and the honest figure is what has been measured or nothing.
|
|
123
|
-
//
|
|
124
|
-
// MOVING one has an alternative — leave it alone — and it is
|
|
125
|
-
// irreversible, because the process it kills cannot be un-killed. There
|
|
126
|
-
// an unmeasured cost must not license the act, and `Infinity` is the
|
|
127
|
-
// identity of the comparison that consumes it: "nobody has measured what
|
|
128
|
-
// this costs" and "never worth doing" are the same statement about an
|
|
129
|
-
// action whose price is unknown.
|
|
130
|
-
//
|
|
131
|
-
// Reported as 0 for both, an unmeasured move was FREE in the plan's
|
|
132
|
-
// arithmetic, so a gain of a fraction of a second justified it. And the
|
|
133
|
-
// blindness was self-sustaining: `#firstOutput` takes a reading only from
|
|
134
|
-
// a run that produced something, and every run in a thrash is killed
|
|
135
|
-
// before it finishes anything.
|
|
136
|
-
killCostSec: (dying ?? 0) / 1000,
|
|
137
|
-
firstByteWaitSec: (first ?? 0) / 1000,
|
|
138
|
-
moveCostSec:
|
|
139
|
-
first === null
|
|
140
|
-
? Number.POSITIVE_INFINITY
|
|
141
|
-
: ((dying ?? 0) + first) / 1000,
|
|
115
|
+
killCostSec: (middleOf(this.#dying) ?? 0) / 1000,
|
|
116
|
+
firstByteWaitSec: (middleOf(this.#firstOutput) ?? 0) / 1000,
|
|
142
117
|
samples: Math.min(this.#dying.length, this.#firstOutput.length)
|
|
143
118
|
};
|
|
144
119
|
}
|
|
@@ -417,6 +417,22 @@ export class EncodeOrchestrator {
|
|
|
417
417
|
// happens — everything a rerun of the same decision needs: the windows
|
|
418
418
|
// this call saw (priority, the real time, which side of the viewers),
|
|
419
419
|
// the budget, and where every live run stood.
|
|
420
|
+
// WHAT WAS DECIDED AND WITH WHAT INTERVAL, said whenever anything is placed
|
|
421
|
+
// or taken away — not only on a move.
|
|
422
|
+
//
|
|
423
|
+
// Field 2026-09-08 could not be diagnosed from this line: it printed the
|
|
424
|
+
// windows, the budget and where the live runs stood, and NOT the intervals
|
|
425
|
+
// the actions carried. What the session actually did was give every encoder
|
|
426
|
+
// an interval of exactly ONE segment — 63 runs, each making one piece and
|
|
427
|
+
// exiting, twelve of them normally — and the reasons printed beside them
|
|
428
|
+
// read as moves, so the fault was diagnosed three times as something it was
|
|
429
|
+
// not. An interval is what a run is, and it was the one thing missing.
|
|
430
|
+
if (actions.some((action) => action.type !== "keep")) {
|
|
431
|
+
this.logger.info(
|
|
432
|
+
`encode-plan on ${address}: ` +
|
|
433
|
+
`${actions.map((action) => `${action.type} #${action.from ?? "?"}..#${action.to ?? "?"}`).join(", ")}`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
420
436
|
if (actions.some((action) => action.type === "move")) {
|
|
421
437
|
this.logger.info(
|
|
422
438
|
`encode-plan move on ${address}: windows=${JSON.stringify(windows)} ` +
|
package/test/move-cost.test.js
CHANGED
|
@@ -41,73 +41,145 @@ import { RunCosts } from "../services/encode/run-costs.js";
|
|
|
41
41
|
import { planEncoders } from "../services/encode/EncodePlan.js";
|
|
42
42
|
import { CoverageMap } from "../services/encode/CoverageMap.js";
|
|
43
43
|
|
|
44
|
-
test("nothing measured
|
|
44
|
+
test("nothing measured is a plain zero, and the floor is derived where the arithmetic is", () => {
|
|
45
45
|
const costs = new RunCosts();
|
|
46
46
|
|
|
47
|
-
const {
|
|
48
|
-
assert.equal(
|
|
49
|
-
// Placing one where there is none is the OTHER question, and it has no
|
|
50
|
-
// alternative: the film gets made or it does not.
|
|
51
|
-
assert.equal(firstByteWaitSec, 0, "placing an encoder is not blocked by an unknown price");
|
|
47
|
+
const { firstByteWaitSec, killCostSec } = costs.seconds();
|
|
48
|
+
assert.equal(firstByteWaitSec, 0, "no reading is said as none, not as a guess");
|
|
52
49
|
assert.equal(killCostSec, 0);
|
|
50
|
+
// There was an `Infinity` here — the cost of a move, made unaffordable until
|
|
51
|
+
// something had been measured, on the reasoning that an unmeasured price must
|
|
52
|
+
// not license an irreversible act. It was an exception in a model that needs
|
|
53
|
+
// none: a first piece cannot appear faster than it takes to ENCODE one, and
|
|
54
|
+
// how fast this host encodes is measured before any viewer exists, so the
|
|
55
|
+
// floor belongs where the arithmetic is.
|
|
56
|
+
assert.equal("moveCostSec" in costs.seconds(), false, "no such figure any more");
|
|
53
57
|
});
|
|
54
58
|
|
|
55
59
|
test("a run killed before producing anything is a lower bound on the first output", () => {
|
|
56
60
|
const costs = new RunCosts();
|
|
57
61
|
|
|
58
62
|
// Exactly what a thrash supplies: a run that lived 800 ms and finished
|
|
59
|
-
// nothing. It says the first output takes AT LEAST that long, which is a fact
|
|
63
|
+
// nothing. It says the first output takes AT LEAST that long, which is a fact
|
|
64
|
+
// and the only reading a thrash can give — every run in one is killed before
|
|
65
|
+
// it produces.
|
|
60
66
|
costs.note({ livedMs: 800, dyingMs: 40 });
|
|
61
67
|
|
|
62
|
-
const {
|
|
63
|
-
assert.ok(
|
|
64
|
-
assert.ok(Math.abs(
|
|
68
|
+
const { firstByteWaitSec, killCostSec } = costs.seconds();
|
|
69
|
+
assert.ok(Math.abs(firstByteWaitSec - 0.8) < 0.001, `got ${firstByteWaitSec}`);
|
|
70
|
+
assert.ok(Math.abs(killCostSec - 0.04) < 0.001, `got ${killCostSec}`);
|
|
65
71
|
});
|
|
66
72
|
|
|
67
73
|
test("a run that produced something is measured by its first output, not its life", () => {
|
|
68
74
|
const costs = new RunCosts();
|
|
69
75
|
|
|
70
|
-
costs.note({ livedMs: 60_000, firstOutputMs:
|
|
76
|
+
costs.note({ livedMs: 60_000, firstOutputMs: 1260, dyingMs: 100 });
|
|
71
77
|
|
|
72
|
-
const {
|
|
73
|
-
assert.ok(Math.abs(firstByteWaitSec -
|
|
74
|
-
assert.ok(Math.abs(moveCostSec - 1.0) < 0.001, `got ${moveCostSec}`);
|
|
78
|
+
const { firstByteWaitSec } = costs.seconds();
|
|
79
|
+
assert.ok(Math.abs(firstByteWaitSec - 1.26) < 0.001, `got ${firstByteWaitSec}`);
|
|
75
80
|
});
|
|
76
81
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
/**
|
|
83
|
+
* The map's real shape: one segment at the viewer, doubling zones ahead down to
|
|
84
|
+
* p91, and everything behind them at p1 with no deadline. Written out because a
|
|
85
|
+
* fixture of two zones is not this, and the difference decides the answer: with
|
|
86
|
+
* nothing stated past the viewer's own zone, a run one segment behind it is
|
|
87
|
+
* compared on that zone alone and loses by a tenth of a second.
|
|
88
|
+
*
|
|
89
|
+
* @param {number} head - The segment the viewer is on.
|
|
90
|
+
* @param {number} count
|
|
91
|
+
* @returns {object[]}
|
|
92
|
+
*/
|
|
93
|
+
function mapAt(head, count) {
|
|
94
|
+
const zones = [];
|
|
95
|
+
if (head > 0) {
|
|
96
|
+
zones.push({ from: 0, to: head - 1, priority: 1, withinSeconds: null, behind: true });
|
|
97
|
+
}
|
|
98
|
+
let from = head;
|
|
99
|
+
let width = 1;
|
|
100
|
+
let rank = 100;
|
|
101
|
+
while (from < count && rank > 90) {
|
|
102
|
+
const to = Math.min(count - 1, from + width - 1);
|
|
103
|
+
zones.push({ from, to, priority: rank, withinSeconds: (from - head) * 4.2, behind: false });
|
|
104
|
+
from = to + 1;
|
|
105
|
+
width *= 2;
|
|
106
|
+
rank -= 1;
|
|
107
|
+
}
|
|
108
|
+
if (from < count) {
|
|
109
|
+
zones.push({ from, to: count - 1, priority: 90, withinSeconds: (from - head) * 4.2, behind: false });
|
|
110
|
+
}
|
|
111
|
+
return zones;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
test("an encoder is left alone while the viewer is still at or before it", () => {
|
|
115
|
+
// Every slide of the viewer's zone used to make standing one number behind it
|
|
116
|
+
// score worse than standing in it — by ten milliseconds, which is nothing but
|
|
117
|
+
// the double charge for a piece already being made. What holds now is the
|
|
118
|
+
// narrower and true statement: while the viewer's own zone still contains the
|
|
119
|
+
// encoder's position, it is left alone. A viewer BEFORE it is a different
|
|
120
|
+
// case entirely and correctly moves it back — encoders only go forward, so
|
|
121
|
+
// one standing past a viewer never reaches them.
|
|
122
|
+
//
|
|
123
|
+
// Once the viewer has PASSED it, moving forward is correct and happens once: a
|
|
124
|
+
// run that has produced nothing in 0.8 s of a 1.26 s warm-up owes 0.46 s
|
|
125
|
+
// before its piece exists, while a fresh one at the viewer's own number owes
|
|
126
|
+
// 0.32 s of spawn and then the piece — so the viewer is served sooner, and the
|
|
127
|
+
// number left behind is in nobody's zone.
|
|
81
128
|
const coverage = new CoverageMap();
|
|
82
129
|
coverage.setSegmentCount(482);
|
|
83
|
-
const run = { from: 58, to:
|
|
84
|
-
coverage.claim(run, 58,
|
|
130
|
+
const run = { from: 58, to: -1, head: 58, speedX: 4.45, isAlive: true, startedAt: 1_000_000 };
|
|
131
|
+
coverage.claim(run, 58, -1);
|
|
85
132
|
|
|
86
|
-
const
|
|
133
|
+
for (const viewerAt of [58]) {
|
|
134
|
+
const actions = planEncoders({
|
|
135
|
+
coverage,
|
|
136
|
+
windows: mapAt(viewerAt, 482),
|
|
137
|
+
runs: [run],
|
|
138
|
+
maxRuns: 3,
|
|
139
|
+
segmentSeconds: 4.2,
|
|
140
|
+
speedX: 4.45,
|
|
141
|
+
killCostSec: 0.04,
|
|
142
|
+
// Measured on the addon host: a run started at 15:50:15.521 and its first
|
|
143
|
+
// piece existed at 15:50:16.785.
|
|
144
|
+
firstByteWaitSec: 1.26,
|
|
145
|
+
refetchSecPerFilmSecond: 0,
|
|
146
|
+
// 1.98 at 1920x1080, measured: a second encoder takes very nearly all of
|
|
147
|
+
// the first's speed.
|
|
148
|
+
contentionPenaltyFor: (others) => (others <= 0 ? 1 : 1.98 ** others),
|
|
149
|
+
now: 1_000_000 + 800
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
assert.deepEqual(
|
|
153
|
+
actions.filter((one) => one.type === "move"),
|
|
154
|
+
[],
|
|
155
|
+
`the viewer at #${viewerAt} does not cost the encoder its place`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// And once they are past it, exactly one move — not one per slide.
|
|
160
|
+
const past = [59, 60, 61].map((viewerAt) => planEncoders({
|
|
87
161
|
coverage,
|
|
88
|
-
windows:
|
|
89
|
-
{ from: 0, to: 57, priority: 1, withinSeconds: null, behind: true },
|
|
90
|
-
{ from: 59, to: 60, priority: 100, withinSeconds: 0, behind: false }
|
|
91
|
-
],
|
|
162
|
+
windows: mapAt(viewerAt, 482),
|
|
92
163
|
runs: [run],
|
|
93
164
|
maxRuns: 3,
|
|
94
165
|
segmentSeconds: 4.2,
|
|
95
166
|
speedX: 4.45,
|
|
96
|
-
// Measured on this host: killing takes 40 ms, a fresh encoder's first piece
|
|
97
|
-
// 900 ms. Against that, driving one segment at 4.45x costs 0.94 s — so the
|
|
98
|
-
// two are close, and what settles it is that the move ALSO has to encode
|
|
99
|
-
// the same segment afterwards.
|
|
100
167
|
killCostSec: 0.04,
|
|
101
|
-
firstByteWaitSec:
|
|
102
|
-
moveCostSec: 0.94,
|
|
168
|
+
firstByteWaitSec: 1.26,
|
|
103
169
|
refetchSecPerFilmSecond: 0,
|
|
104
|
-
contentionPenaltyFor: () => 1
|
|
105
|
-
|
|
170
|
+
contentionPenaltyFor: (others) => (others <= 0 ? 1 : 1.98 ** others),
|
|
171
|
+
now: 1_000_000 + 800
|
|
172
|
+
}).filter((one) => one.type === "move"));
|
|
106
173
|
|
|
107
174
|
assert.deepEqual(
|
|
108
|
-
|
|
109
|
-
[],
|
|
110
|
-
"
|
|
175
|
+
past.map((moves) => moves.length),
|
|
176
|
+
[1, 1, 1],
|
|
177
|
+
"one move to where the viewer now is, whichever number that is"
|
|
178
|
+
);
|
|
179
|
+
assert.deepEqual(
|
|
180
|
+
past.map((moves) => moves[0].from),
|
|
181
|
+
[59, 60, 61],
|
|
182
|
+
"and it goes to the viewer's own number, not one past it"
|
|
111
183
|
);
|
|
112
184
|
});
|
|
113
185
|
|
|
@@ -128,8 +200,7 @@ test("a move that genuinely saves the viewer time still happens", () => {
|
|
|
128
200
|
segmentSeconds: 4.2,
|
|
129
201
|
speedX: 4.45,
|
|
130
202
|
killCostSec: 0.04,
|
|
131
|
-
firstByteWaitSec:
|
|
132
|
-
moveCostSec: 0.94,
|
|
203
|
+
firstByteWaitSec: 1.26,
|
|
133
204
|
refetchSecPerFilmSecond: 0,
|
|
134
205
|
contentionPenaltyFor: () => 1
|
|
135
206
|
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Where a second encoder joins a stretch, and whether it is worth having.
|
|
3
|
+
*
|
|
4
|
+
* Both halves are arithmetic and neither was asked before. The stretch used to
|
|
5
|
+
* be halved, unconditionally:
|
|
6
|
+
*
|
|
7
|
+
* widestFrom = from + Math.floor(Math.min(room, to - from + 1) / 2)
|
|
8
|
+
*
|
|
9
|
+
* Halving IS the answer when both encoders are fresh and owe the same. It is not
|
|
10
|
+
* when one of them is already partway through a piece, and it never asked the
|
|
11
|
+
* other question at all — whether two encoders under this host's measured
|
|
12
|
+
* contention beat one at full speed.
|
|
13
|
+
*
|
|
14
|
+
* The derivation. A stretch of unmade film runs from `from` to `to`; whoever is
|
|
15
|
+
* already on it stands at `from` and owes `w` before the piece under it exists;
|
|
16
|
+
* a fresh one placed at `x` owes `d` — its start and then a whole piece — and
|
|
17
|
+
* both then work at the rate two encoders leave each other:
|
|
18
|
+
*
|
|
19
|
+
* the one there closes [from, x-1]: w + (x - 1 - from) / r rises with x
|
|
20
|
+
* the fresh one closes [x, to]: d + (to - x) / r falls with x
|
|
21
|
+
*
|
|
22
|
+
* x* = (from + to + 1) / 2 + (d - w) * r / 2
|
|
23
|
+
*
|
|
24
|
+
* The shift is `firstByteWait / (2 * perPiece)` pieces, so it depends on how
|
|
25
|
+
* long a piece takes here: 0.24 of a piece at 4.45x on a 4.2 s grid, but 0.96 at
|
|
26
|
+
* 8.9x and 2.14 at 20x — a whole segment and more on the copy branch, where a
|
|
27
|
+
* piece is short and a start is not. Measuring only the re-encode rate
|
|
28
|
+
* understated it.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import test from "node:test";
|
|
32
|
+
import assert from "node:assert/strict";
|
|
33
|
+
import { planEncoders } from "../services/encode/EncodePlan.js";
|
|
34
|
+
import { CoverageMap } from "../services/encode/CoverageMap.js";
|
|
35
|
+
|
|
36
|
+
const SEGMENT_SECONDS = 4.2;
|
|
37
|
+
const LAST = 199;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* One encoder on a long stretch nobody has made, and room for more.
|
|
41
|
+
*
|
|
42
|
+
* @param {{ speedX: number, penalty: number, head?: number, ageMs?: number,
|
|
43
|
+
* maxRuns?: number }} params
|
|
44
|
+
* @returns {{ actions: object[], run: object }}
|
|
45
|
+
*/
|
|
46
|
+
function planFor({ speedX, penalty, head = 0, ageMs = 60_000, maxRuns = 3, runs = null }) {
|
|
47
|
+
const coverage = new CoverageMap({ segmentCount: LAST + 1 });
|
|
48
|
+
const run = { from: 0, to: -1, head, speedX, isAlive: true, startedAt: 1_000_000 };
|
|
49
|
+
const live = runs ?? [run];
|
|
50
|
+
for (const one of live) {
|
|
51
|
+
coverage.claim(one, one.from, one.to);
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
run,
|
|
55
|
+
actions: planEncoders({
|
|
56
|
+
coverage,
|
|
57
|
+
// One zone over the whole film, due now, so nothing but the arithmetic of
|
|
58
|
+
// sharing it decides anything.
|
|
59
|
+
windows: [{ from: 0, to: LAST, priority: 100, withinSeconds: 0, behind: false }],
|
|
60
|
+
runs: live,
|
|
61
|
+
maxRuns,
|
|
62
|
+
segmentSeconds: SEGMENT_SECONDS,
|
|
63
|
+
speedX,
|
|
64
|
+
killCostSec: 0.04,
|
|
65
|
+
firstByteWaitSec: 0.9,
|
|
66
|
+
moveCostSec: 0.94,
|
|
67
|
+
refetchSecPerFilmSecond: 0,
|
|
68
|
+
// Measured on the addon host: 1.70 at 854x480, 1.98 at 1920x1080. Raised
|
|
69
|
+
// to the power of how many others there are, so a third costs again.
|
|
70
|
+
contentionPenaltyFor: (others) => (others <= 0 ? 1 : penalty ** others),
|
|
71
|
+
now: 1_000_000 + ageMs
|
|
72
|
+
})
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
test("one encoder that reaches everything in time is left to do it alone", () => {
|
|
77
|
+
// A zone whose deadline GROWS with distance — which is what the map states,
|
|
78
|
+
// one segment at a time — is comfortably served by one encoder at 4.45x: #199
|
|
79
|
+
// is due in 835 s and arrives in 189 s. So there is no position to fill and
|
|
80
|
+
// nothing to split, whatever the budget says.
|
|
81
|
+
//
|
|
82
|
+
// Measured rather than assumed: this is what the plan answers, and the test
|
|
83
|
+
// was written expecting two encoders before it was run.
|
|
84
|
+
const { actions } = planFor({ speedX: 4.45, penalty: 1, maxRuns: 3 });
|
|
85
|
+
|
|
86
|
+
assert.deepEqual(
|
|
87
|
+
actions.map((one) => `${one.type} #${one.from}..#${one.to}`),
|
|
88
|
+
["keep #0..#-1"],
|
|
89
|
+
"one encoder, its road to the end of the film"
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("a stretch nobody is on is split at the middle, and the halves bound each other", () => {
|
|
94
|
+
// Both encoders are fresh, so both owe the same and `d - w` is zero: the
|
|
95
|
+
// meeting point is the midpoint, which is what halving always said. What is
|
|
96
|
+
// new is that each one's road ENDS where the next begins, so neither writes
|
|
97
|
+
// into the other's names.
|
|
98
|
+
const actions = planFor({ speedX: 4.45, penalty: 1, maxRuns: 2, runs: [] }).actions;
|
|
99
|
+
|
|
100
|
+
assert.deepEqual(
|
|
101
|
+
actions.map((one) => `${one.type} #${one.from}..#${one.to}`),
|
|
102
|
+
["start #0..#100", "start #101..#199"],
|
|
103
|
+
"0..199 halved at 100"
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("a third encoder halves the widest half that is left", () => {
|
|
108
|
+
const actions = planFor({ speedX: 4.45, penalty: 1, maxRuns: 3, runs: [] }).actions;
|
|
109
|
+
|
|
110
|
+
assert.deepEqual(
|
|
111
|
+
actions.map((one) => `${one.type} #${one.from}..#${one.to}`),
|
|
112
|
+
["start #0..#50", "start #51..#100", "start #101..#199"],
|
|
113
|
+
"the first half is halved again, greedily, widest first"
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("a stretch of one piece is not split at all", () => {
|
|
118
|
+
const coverage = new CoverageMap({ segmentCount: 2 });
|
|
119
|
+
const actions = planEncoders({
|
|
120
|
+
coverage,
|
|
121
|
+
windows: [{ from: 0, to: 0, priority: 100, withinSeconds: 0, behind: false }],
|
|
122
|
+
runs: [],
|
|
123
|
+
maxRuns: 3,
|
|
124
|
+
segmentSeconds: SEGMENT_SECONDS,
|
|
125
|
+
speedX: 8.9,
|
|
126
|
+
killCostSec: 0.04,
|
|
127
|
+
firstByteWaitSec: 0.9,
|
|
128
|
+
moveCostSec: 0.94,
|
|
129
|
+
refetchSecPerFilmSecond: 0,
|
|
130
|
+
contentionPenaltyFor: () => 1,
|
|
131
|
+
now: 1_000_000
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
assert.equal(
|
|
135
|
+
actions.filter((one) => one.type === "start").length,
|
|
136
|
+
1,
|
|
137
|
+
"there is nothing to share"
|
|
138
|
+
);
|
|
139
|
+
});
|