@torrent-tv/proxy 2.80.16 → 2.80.17
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 +36 -2
- package/package.json +1 -1
- package/services/encode/EncodePlan.js +139 -57
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 2.80.17
|
|
2
|
+
|
|
3
|
+
- **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.
|
|
4
|
+
**The shift is under one segment in every measured configuration** — 0.24 of a piece at the addon host's 1080p penalty — so halving was very nearly right, and this is stated rather than dressed up. What the derivation adds that halving never asked is the other half: two encoders under this host's MEASURED contention against one at full speed. At 1080p the penalty for a second is 1.98, it takes very nearly all of the first's speed, and the objective now refuses it and keeps one; with a free second it still places three.
|
|
5
|
+
- **Chore**: `bodies` in the placement arithmetic is `encoders`. A process is not a body, and in Russian the word reads obscenely.
|
|
6
|
+
|
|
1
7
|
## 2.80.16
|
|
2
8
|
|
|
3
9
|
- **Fix**: THE SPINNER CARAVAN, by arithmetic alone, and the two props 2.80.15 carried are gone with it — the threshold that made a move pay for itself, and the exclusion that kept a live run off a deadline-less zone. Both were propping up a comparison that was wrong rather than indifferent. Three quantities were computed wrongly:
|
|
@@ -194,7 +194,7 @@ Not three terms in seconds. One pair per rank the map states, most urgent rank
|
|
|
194
194
|
first, compared position by position:
|
|
195
195
|
|
|
196
196
|
```
|
|
197
|
-
[ late(100), done(100), late(99), done(99), … late(1), done(1) ],
|
|
197
|
+
[ late(100), done(100), late(99), done(99), … late(1), done(1) ], encoders, wasted
|
|
198
198
|
```
|
|
199
199
|
|
|
200
200
|
`late(r)` is how long anybody waits past a deadline at rank `r`; `done(r)` is
|
|
@@ -210,7 +210,7 @@ source of truth about what matters and it already says so — ten ranks on a fil
|
|
|
210
210
|
p100 at the number a viewer is stopped on, doubling zones down to p91 for the far
|
|
211
211
|
tail, p1 for what lies behind them.
|
|
212
212
|
|
|
213
|
-
`
|
|
213
|
+
`encoders` ranks below every rank of the map, so spare capacity cannot buy an
|
|
214
214
|
encoder where the map is indifferent. `wasted` is the swarm's bill for anything
|
|
215
215
|
fetched twice.
|
|
216
216
|
|
|
@@ -267,6 +267,40 @@ doubling ahead of the viewer, one behind — at both one and three runs: the
|
|
|
267
267
|
encoder is placed once, left alone, and moved exactly once, at the viewer's own
|
|
268
268
|
seek.
|
|
269
269
|
|
|
270
|
+
## Where a second encoder joins a stretch
|
|
271
|
+
|
|
272
|
+
Derived, not halved. A stretch of unmade film runs from `from` to `to`; whoever
|
|
273
|
+
is already on it stands at `from` and owes `w` before the piece under it exists;
|
|
274
|
+
a fresh one placed at `x` owes `d` — its start and then a whole piece — and both
|
|
275
|
+
then work at the rate two encoders leave each other, which is measured here.
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
the one already there closes [from, x-1]: w + (x - 1 - from) / r rises with x
|
|
279
|
+
the fresh one closes [x, to]: d + (to - x) / r falls with x
|
|
280
|
+
|
|
281
|
+
x* = (from + to + 1) / 2 + (d - w) * r / 2
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
The midpoint, shifted forward by half the difference of what the two owe, in
|
|
285
|
+
pieces. Halving is the special case `d = w`, which holds when both are fresh.
|
|
286
|
+
|
|
287
|
+
**The shift is under one segment in every measured configuration** — 0.24 of a
|
|
288
|
+
piece at the addon host's 1080p contention — so halving was very nearly right,
|
|
289
|
+
and saying otherwise would be dressing it up.
|
|
290
|
+
|
|
291
|
+
What the derivation adds is the question halving never asked: **is a second
|
|
292
|
+
encoder worth having at all?**
|
|
293
|
+
|
|
294
|
+
```
|
|
295
|
+
one: w + (to - from - 1) / rate
|
|
296
|
+
two: w + (x* - 1 - from) / r
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Nothing is proposed where the second does not win. At 1920x1080 the measured
|
|
300
|
+
penalty for a second encoder on the addon host is 1.98 — it takes very nearly
|
|
301
|
+
all of the first's speed — and the objective keeps one; where a second is free it
|
|
302
|
+
places three.
|
|
303
|
+
|
|
270
304
|
## What is checked
|
|
271
305
|
|
|
272
306
|
`test/one-authority.test.js` holds the shape: one caller of `#startEncodeRun`,
|
package/package.json
CHANGED
|
@@ -168,20 +168,20 @@ export function planEncoders({
|
|
|
168
168
|
const working = live.reduce((best, run) => Math.max(best, run.speedX || 0), 0);
|
|
169
169
|
const rate = segmentSeconds > 0 ? (working > 0 ? working : speedX) / segmentSeconds : 0;
|
|
170
170
|
// How long one piece takes AT THE RATE ACTUALLY IN FORCE. Concurrent encoders
|
|
171
|
-
// slow each other — measured on this host — so an arrangement's own
|
|
171
|
+
// slow each other — measured on this host — so an arrangement's own encoder count
|
|
172
172
|
// decides it, and the delays below are computed per arrangement for that
|
|
173
173
|
// reason. Taken from the unpenalised rate instead, a piece looked cheaper the
|
|
174
|
-
// more
|
|
174
|
+
// more encoders there were, which made extra encoders look free: the plan bought a
|
|
175
175
|
// second encoder where one served, and the arrivals it was compared on were
|
|
176
176
|
// computed at the slower rate all along.
|
|
177
177
|
//
|
|
178
178
|
// `Infinity` where nothing has been measured, which is what "no speed" means
|
|
179
179
|
// and what makes every arrangement equally hopeless rather than equally free.
|
|
180
|
-
const pieceAt = (
|
|
181
|
-
const inForce = rate / contentionPenaltyFor(Math.max(0,
|
|
180
|
+
const pieceAt = (howManyEncoders) => {
|
|
181
|
+
const inForce = rate / contentionPenaltyFor(Math.max(0, howManyEncoders - 1));
|
|
182
182
|
return inForce > 0 ? 1 / inForce : Number.POSITIVE_INFINITY;
|
|
183
183
|
};
|
|
184
|
-
// What a
|
|
184
|
+
// What a encoder costs to take away from where it stands and put somewhere else:
|
|
185
185
|
// its death, the start of another, and the wait for the first bytes there.
|
|
186
186
|
// Taking an encoder somewhere else is stopping this one and waiting for the
|
|
187
187
|
// next to produce. Both halves are measured on this host.
|
|
@@ -240,18 +240,76 @@ export function planEncoders({
|
|
|
240
240
|
// WHAT EACH BODY OWES BEFORE THE PIECE IT STANDS ON EXISTS, priced at the rate
|
|
241
241
|
// the arrangement itself puts in force.
|
|
242
242
|
//
|
|
243
|
-
// Each
|
|
244
|
-
// piece costs — because only there is it known what the
|
|
243
|
+
// Each encoder states its own debt where it is created, as a function of what one
|
|
244
|
+
// piece costs — because only there is it known what the encoder IS, and here only
|
|
245
245
|
// how many of them there are. So there is nothing to dispatch on: no kind, no
|
|
246
246
|
// tag, no case analysis. The count is known before any debt is needed, which
|
|
247
|
-
// is why this is one pass over the
|
|
247
|
+
// is why this is one pass over the encoders rather than a figure computed once
|
|
248
248
|
// outside.
|
|
249
|
-
const priced = (
|
|
250
|
-
const perPiece = pieceAt(
|
|
251
|
-
return
|
|
249
|
+
const priced = (encoders) => {
|
|
250
|
+
const perPiece = pieceAt(encoders.length);
|
|
251
|
+
return encoders.map((encoder) => ({ at: encoder.at, delaySec: encoder.owes(perPiece) }));
|
|
252
252
|
};
|
|
253
253
|
|
|
254
254
|
|
|
255
|
+
// WHERE A SECOND ENCODER MEETS THE ONE ALREADY ON A STRETCH, and whether it is
|
|
256
|
+
// worth having.
|
|
257
|
+
//
|
|
258
|
+
// A stretch of unmade film runs from `from` to `to`. Whoever is already on it
|
|
259
|
+
// stands at `from` and owes `w` before the piece under it exists; a fresh one
|
|
260
|
+
// placed at `x` owes `d` — its start, and then a whole piece — and both then
|
|
261
|
+
// work at the rate two encoders leave each other, which is measured on this
|
|
262
|
+
// host and is not half by assumption.
|
|
263
|
+
//
|
|
264
|
+
// the one already there closes [from, x-1]: w + (x - 1 - from) / r
|
|
265
|
+
// the fresh one closes [x, to]: d + (to - x) / r
|
|
266
|
+
//
|
|
267
|
+
// The first rises with `x` and the second falls, so the stretch is closed
|
|
268
|
+
// soonest where they cross:
|
|
269
|
+
//
|
|
270
|
+
// x* = (from + to + 1) / 2 + (d - w) * r / 2
|
|
271
|
+
//
|
|
272
|
+
// The midpoint, shifted forward by half the difference of what the two owe,
|
|
273
|
+
// expressed in pieces. Halving is the special case `d = w`, which holds when
|
|
274
|
+
// both are fresh — and it was applied to every case, including the common one
|
|
275
|
+
// where `d` is a start plus a piece and `w` is the tail of a piece already
|
|
276
|
+
// being made.
|
|
277
|
+
//
|
|
278
|
+
// AND WHETHER TO SPLIT AT ALL, which halving never asked. Two encoders under
|
|
279
|
+
// contention against one at full speed:
|
|
280
|
+
//
|
|
281
|
+
// one: w + (to - from - 1) / rate
|
|
282
|
+
// two: w + (x* - 1 - from) / r
|
|
283
|
+
//
|
|
284
|
+
// Nothing back where the second does not win. On the addon host at 1920x1080
|
|
285
|
+
// the measured penalty for a second encoder is 1.98 — it takes very nearly
|
|
286
|
+
// all of the first's speed — so there a second almost never pays, and it was
|
|
287
|
+
// being placed regardless.
|
|
288
|
+
const splitAt = (from, to) => {
|
|
289
|
+
const pieces = to - from + 1;
|
|
290
|
+
if (!(pieces > 1) || !(rate > 0)) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
const together = rate / contentionPenaltyFor(live.length);
|
|
294
|
+
if (!(together > 0)) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
const perPieceTogether = 1 / together;
|
|
298
|
+
// What the one already on this stretch owes, or a fresh one's debt where
|
|
299
|
+
// nobody is on it — and then both sides owe the same and the meeting point
|
|
300
|
+
// is the middle, as it should be.
|
|
301
|
+
const onIt = live.find((run) => Number(run.head) === from) ?? null;
|
|
302
|
+
const owedThere = onIt
|
|
303
|
+
? finishesItsPieceIn(onIt, perPieceTogether)
|
|
304
|
+
: firstByteWaitSec + perPieceTogether;
|
|
305
|
+
const owedFresh = firstByteWaitSec + perPieceTogether;
|
|
306
|
+
const meeting = (from + to + 1) / 2 + ((owedFresh - owedThere) * together) / 2;
|
|
307
|
+
const x = Math.min(to, Math.max(from + 1, Math.round(meeting)));
|
|
308
|
+
const withTwo = owedThere + (x - 1 - from) * perPieceTogether;
|
|
309
|
+
const withOne = owedThere + (pieces - 1) / rate;
|
|
310
|
+
return withTwo < withOne ? x : null;
|
|
311
|
+
};
|
|
312
|
+
|
|
255
313
|
// ------------------------------------------------------------------ WHERE
|
|
256
314
|
//
|
|
257
315
|
// A question about the FILM, and about nothing else: which numbers are
|
|
@@ -265,11 +323,12 @@ export function planEncoders({
|
|
|
265
323
|
// EVERY LIVE ENCODER IS PRE-PLACED, because that is what "somebody already
|
|
266
324
|
// gets here in time" means. A number one of them reaches before it is
|
|
267
325
|
// needed is not a position at all; a number none of them reaches is, and
|
|
268
|
-
// needs a
|
|
326
|
+
// needs a encoder brought to it. There is no third case, and in particular no
|
|
269
327
|
// separate question of whether an encoder should drive on or be moved:
|
|
270
328
|
// driving is simply its arrival, and its arrival is priced in one place.
|
|
271
329
|
firstGap: gapFinderFor(coverage, new Set(live), rate, segmentSeconds * refetchSecPerFilmSecond),
|
|
272
|
-
deadlineAt: untilNeeded
|
|
330
|
+
deadlineAt: untilNeeded,
|
|
331
|
+
splitAt
|
|
273
332
|
});
|
|
274
333
|
|
|
275
334
|
// -------------------------------------------------------------------- WHO
|
|
@@ -277,12 +336,12 @@ export function planEncoders({
|
|
|
277
336
|
// ARGMIN OF THE OBJECTIVE, EVALUATED. Not a rule that approximates it.
|
|
278
337
|
//
|
|
279
338
|
// Every way of filling the positions is scored by `latenessOf` and the best is
|
|
280
|
-
// taken. There are at most a handful of positions and a handful of
|
|
339
|
+
// taken. There are at most a handful of positions and a handful of encoders, so
|
|
281
340
|
// the enumeration is exact: no local rule stands in for the objective, and
|
|
282
341
|
// none can therefore disagree with another.
|
|
283
342
|
//
|
|
284
343
|
// Four such rules were written before this and all four had to go — "place
|
|
285
|
-
// where a number is late", "take a
|
|
344
|
+
// where a number is late", "take a encoder that serves nothing", "take one whose
|
|
286
345
|
// work is needed later than this", "drive on or move, by cost". Each looked
|
|
287
346
|
// like a consequence of the model and each approximated it from a different
|
|
288
347
|
// side, so together they contradicted one another and the answer depended on
|
|
@@ -326,21 +385,21 @@ export function planEncoders({
|
|
|
326
385
|
let best = null;
|
|
327
386
|
let bestScore = null;
|
|
328
387
|
for (const arrangement of arrangements) {
|
|
329
|
-
const
|
|
388
|
+
const encoders = [];
|
|
330
389
|
for (let index = 0; index < positions.length; index += 1) {
|
|
331
390
|
const filler = arrangement.fill[index];
|
|
332
391
|
if (filler === null) {
|
|
333
392
|
continue;
|
|
334
393
|
}
|
|
335
394
|
if (filler === "new") {
|
|
336
|
-
// A
|
|
337
|
-
|
|
395
|
+
// A encoder that does not exist yet owes its own start and then the piece.
|
|
396
|
+
encoders.push({ at: positions[index], owes: (piece) => firstByteWaitSec + piece });
|
|
338
397
|
continue;
|
|
339
398
|
}
|
|
340
399
|
const head = Number(filler.head);
|
|
341
400
|
// Left where it stands it owes what is left of the piece under it; taken
|
|
342
401
|
// somewhere else it owes the killing, the start and a whole piece.
|
|
343
|
-
|
|
402
|
+
encoders.push({
|
|
344
403
|
at: positions[index],
|
|
345
404
|
owes: head === positions[index]
|
|
346
405
|
? (piece) => finishesItsPieceIn(filler, piece)
|
|
@@ -350,7 +409,7 @@ export function planEncoders({
|
|
|
350
409
|
// Bodies nobody was given a position for go on working where they stand,
|
|
351
410
|
// and their coverage counts: the file is encoded whole.
|
|
352
411
|
//
|
|
353
|
-
// A
|
|
412
|
+
// A encoder given no end pays a restart the moment anybody is placed inside the
|
|
354
413
|
// road it would drive: where a run stops is fixed when its process starts,
|
|
355
414
|
// so it has to be cut and begun again at its own head. That price was
|
|
356
415
|
// invisible here, and an arrangement was scored as free when it was not.
|
|
@@ -363,14 +422,14 @@ export function planEncoders({
|
|
|
363
422
|
const cutInFront = arrangement.fill.some((filler, index) =>
|
|
364
423
|
filler !== null && positions[index] > head
|
|
365
424
|
&& (endless || positions[index] <= Number(run.to)));
|
|
366
|
-
|
|
425
|
+
encoders.push({
|
|
367
426
|
at: head,
|
|
368
427
|
owes: cutInFront
|
|
369
428
|
? (piece) => moveSec + piece
|
|
370
429
|
: (piece) => finishesItsPieceIn(run, piece)
|
|
371
430
|
});
|
|
372
431
|
}
|
|
373
|
-
const scored = latenessOf(priced(
|
|
432
|
+
const scored = latenessOf(priced(encoders), coverage, wanted, untilNeeded, rate / contentionPenaltyFor(Math.max(0, encoders.length - 1)), refetchPerSegment, segmentSeconds);
|
|
374
433
|
if (bestScore === null || cheaperThan(scored, bestScore)) {
|
|
375
434
|
bestScore = scored;
|
|
376
435
|
best = arrangement;
|
|
@@ -379,13 +438,13 @@ export function planEncoders({
|
|
|
379
438
|
|
|
380
439
|
// A BODY STANDING ON FILM THAT EXISTS is the one arrangement the enumeration
|
|
381
440
|
// above cannot reach: the gap in front of it is nobody's deadline, so it is
|
|
382
|
-
// never a position, and the
|
|
383
|
-
// time. Each such
|
|
441
|
+
// never a position, and the encoder is left to make three hundred pieces a second
|
|
442
|
+
// time. Each such encoder is offered its own first gap and the SAME score decides
|
|
384
443
|
// — moving costs a restart on everything downstream, staying costs the repeat.
|
|
385
444
|
//
|
|
386
445
|
// Offered one at a time rather than folded into the enumeration because the
|
|
387
446
|
// enumeration is exponential in the number of positions, and this is called
|
|
388
|
-
// again on every piece produced. One extra evaluation per
|
|
447
|
+
// again on every piece produced. One extra evaluation per encoder against
|
|
389
448
|
// several thousand arrangements is the difference between arithmetic and a
|
|
390
449
|
// stalled proxy.
|
|
391
450
|
const placement = new Map();
|
|
@@ -395,8 +454,8 @@ export function planEncoders({
|
|
|
395
454
|
placement.set(filler, positions[index]);
|
|
396
455
|
}
|
|
397
456
|
}
|
|
398
|
-
const
|
|
399
|
-
const
|
|
457
|
+
const encodersOf = (override) => {
|
|
458
|
+
const encoders = [];
|
|
400
459
|
for (const run of live) {
|
|
401
460
|
if (override.has(run) && override.get(run) === null) {
|
|
402
461
|
// Asked what the film looks like WITHOUT this one.
|
|
@@ -404,7 +463,7 @@ export function planEncoders({
|
|
|
404
463
|
}
|
|
405
464
|
const at = override.has(run) ? override.get(run) : (placement.get(run) ?? Number(run.head));
|
|
406
465
|
const head = Number(run.head);
|
|
407
|
-
|
|
466
|
+
encoders.push({
|
|
408
467
|
at,
|
|
409
468
|
owes: at === head
|
|
410
469
|
? (piece) => finishesItsPieceIn(run, piece)
|
|
@@ -413,15 +472,15 @@ export function planEncoders({
|
|
|
413
472
|
}
|
|
414
473
|
for (let index = 0; index < positions.length; index += 1) {
|
|
415
474
|
if ((best ? best.fill[index] : null) === "new") {
|
|
416
|
-
|
|
475
|
+
encoders.push({ at: positions[index], owes: (piece) => firstByteWaitSec + piece });
|
|
417
476
|
}
|
|
418
477
|
}
|
|
419
|
-
return priced(
|
|
478
|
+
return priced(encoders);
|
|
420
479
|
};
|
|
421
480
|
const scoreOf = (override) => {
|
|
422
|
-
const
|
|
423
|
-
return latenessOf(
|
|
424
|
-
rate / contentionPenaltyFor(Math.max(0,
|
|
481
|
+
const encoders = encodersOf(override);
|
|
482
|
+
return latenessOf(encoders, coverage, wanted, untilNeeded,
|
|
483
|
+
rate / contentionPenaltyFor(Math.max(0, encoders.length - 1)), refetchPerSegment, segmentSeconds);
|
|
425
484
|
};
|
|
426
485
|
for (const run of live) {
|
|
427
486
|
if (placement.has(run)) {
|
|
@@ -439,16 +498,16 @@ export function planEncoders({
|
|
|
439
498
|
}
|
|
440
499
|
|
|
441
500
|
/**
|
|
442
|
-
* Would the film be worse off without this
|
|
501
|
+
* Would the film be worse off without this encoder? Asked of the same score.
|
|
443
502
|
*
|
|
444
503
|
* @param {object} run
|
|
445
504
|
* @returns {boolean}
|
|
446
505
|
*/
|
|
447
506
|
const worseWithout = (run) => {
|
|
448
|
-
const kept =
|
|
449
|
-
const without =
|
|
450
|
-
const scoreOf_ = (
|
|
451
|
-
rate / contentionPenaltyFor(Math.max(0,
|
|
507
|
+
const kept = encodersOf(new Map());
|
|
508
|
+
const without = encodersOf(new Map([[run, null]]));
|
|
509
|
+
const scoreOf_ = (encoders) => latenessOf(encoders, coverage, wanted, untilNeeded,
|
|
510
|
+
rate / contentionPenaltyFor(Math.max(0, encoders.length - 1)), refetchPerSegment, segmentSeconds);
|
|
452
511
|
return cheaperThan(scoreOf_(kept), scoreOf_(without));
|
|
453
512
|
};
|
|
454
513
|
|
|
@@ -486,7 +545,7 @@ export function planEncoders({
|
|
|
486
545
|
}
|
|
487
546
|
// It stays where it is — unless holding it changes nothing.
|
|
488
547
|
//
|
|
489
|
-
// A
|
|
548
|
+
// A encoder left over from where a viewer used to be goes on costing the
|
|
490
549
|
// machine a process while another encoder already reaches everything it
|
|
491
550
|
// would. The score says so directly: take it away and see. Removing it is
|
|
492
551
|
// refused the moment it makes anything later or leaves film abandoned, so
|
|
@@ -514,7 +573,7 @@ export function planEncoders({
|
|
|
514
573
|
if (action.type !== "keep") {
|
|
515
574
|
continue;
|
|
516
575
|
}
|
|
517
|
-
const without = latenessOf(
|
|
576
|
+
const without = latenessOf(encodersOf(new Map([[run, null]])), coverage, wanted, untilNeeded,
|
|
518
577
|
rate / contentionPenaltyFor(Math.max(0, live.length - 2)), refetchPerSegment, segmentSeconds);
|
|
519
578
|
if (cheapestScore === null || cheaperThan(without, cheapestScore)) {
|
|
520
579
|
cheapestScore = without;
|
|
@@ -635,7 +694,7 @@ export function planEncoders({
|
|
|
635
694
|
* quantity says how many of one are worth one of the other. Adding them would
|
|
636
695
|
* mean choosing that exchange rate, which is inventing a number.
|
|
637
696
|
*
|
|
638
|
-
* @param {{ at: number, delaySec: number }[]}
|
|
697
|
+
* @param {{ at: number, delaySec: number }[]} encoders - Where each encoder would
|
|
639
698
|
* stand, and how long before it produces anything there: nothing where it is
|
|
640
699
|
* already standing, a move or a start otherwise.
|
|
641
700
|
* @param {import("./CoverageMap.js").CoverageMap} coverage
|
|
@@ -649,7 +708,7 @@ export function planEncoders({
|
|
|
649
708
|
* @returns {{ stall: number, ahead: number, whole: number }} Three counts of
|
|
650
709
|
* seconds, compared in that order by {@link cheaperThan}.
|
|
651
710
|
*/
|
|
652
|
-
function latenessOf(
|
|
711
|
+
function latenessOf(encoders, coverage, wanted, untilNeeded, rate, refetchSecPerSegment, segmentSeconds) {
|
|
653
712
|
const first = Math.min(...wanted.map((span) => span.from));
|
|
654
713
|
const last = Math.max(...wanted.map((span) => span.to));
|
|
655
714
|
// What a number nobody reaches at all counts as. The film's own length is the
|
|
@@ -729,8 +788,8 @@ function latenessOf(bodies, coverage, wanted, untilNeeded, rate, refetchSecPerSe
|
|
|
729
788
|
// already made on the way, which costs its own time and the swarm's.
|
|
730
789
|
let soonest = Number.POSITIVE_INFINITY;
|
|
731
790
|
let byWhom = null;
|
|
732
|
-
for (const
|
|
733
|
-
if (
|
|
791
|
+
for (const encoder of encoders) {
|
|
792
|
+
if (encoder.at > index) {
|
|
734
793
|
continue;
|
|
735
794
|
}
|
|
736
795
|
// WHEN THIS BODY REACHES THIS NUMBER. `delaySec` is when it finishes the
|
|
@@ -738,8 +797,8 @@ function latenessOf(bodies, coverage, wanted, untilNeeded, rate, refetchSecPerSe
|
|
|
738
797
|
// to be encoded at `rate`.
|
|
739
798
|
//
|
|
740
799
|
// It used to be `(index - at + 1) / rate` on top of the delay, which
|
|
741
|
-
// charges every
|
|
742
|
-
// fresh
|
|
800
|
+
// charges every encoder a whole piece for the one it is already working on. A
|
|
801
|
+
// fresh encoder does start from nothing, so for it that is right — and it is
|
|
743
802
|
// now inside its own delay. A run 0.8 s into a 0.9 s piece does not, and
|
|
744
803
|
// charging it 0.94 s for that piece is what made moving it look cheaper
|
|
745
804
|
// than leaving it: from #58 it was priced at 1.89 s to reach #59 against
|
|
@@ -747,12 +806,12 @@ function latenessOf(bodies, coverage, wanted, untilNeeded, rate, refetchSecPerSe
|
|
|
747
806
|
// that is nothing but the double charge. Field 2026-09-08: 39 moves in one
|
|
748
807
|
// session, 24 of them between three adjacent numbers, and the picture
|
|
749
808
|
// stood still for 116.7 s.
|
|
750
|
-
const arrival =
|
|
751
|
-
+ (index -
|
|
752
|
-
+ coverage.madeBetween(
|
|
809
|
+
const arrival = encoder.delaySec
|
|
810
|
+
+ (index - encoder.at) / rate
|
|
811
|
+
+ coverage.madeBetween(encoder.at, index) * refetchSecPerSegment;
|
|
753
812
|
if (arrival < soonest) {
|
|
754
813
|
soonest = arrival;
|
|
755
|
-
byWhom =
|
|
814
|
+
byWhom = encoder;
|
|
756
815
|
}
|
|
757
816
|
}
|
|
758
817
|
if (coverage.isReady(index)) {
|
|
@@ -802,7 +861,7 @@ function latenessOf(bodies, coverage, wanted, untilNeeded, rate, refetchSecPerSe
|
|
|
802
861
|
// the swarm's bill, so it cannot buy one where the map is indifferent — and
|
|
803
862
|
// the map IS indifferent about spare capacity, which is what bought an
|
|
804
863
|
// encoder for film nobody waits for.
|
|
805
|
-
|
|
864
|
+
encoders: encoders.length,
|
|
806
865
|
// WHAT THE SWARM PAYS for anything fetched twice, which delays everything.
|
|
807
866
|
wasted: wastedSwarm
|
|
808
867
|
};
|
|
@@ -846,8 +905,8 @@ function cheaperThan(left, right) {
|
|
|
846
905
|
// indifferent, so the machine decides: a process, a reader of the piece store
|
|
847
906
|
// and the swarm's bandwidth are all paid by the viewers the ranks above are
|
|
848
907
|
// about.
|
|
849
|
-
if (left.
|
|
850
|
-
return left.
|
|
908
|
+
if (left.encoders !== right.encoders) {
|
|
909
|
+
return left.encoders < right.encoders;
|
|
851
910
|
}
|
|
852
911
|
return left.wasted < right.wasted;
|
|
853
912
|
}
|
|
@@ -906,7 +965,7 @@ function deadlineReaderFor(windows, segmentSeconds) {
|
|
|
906
965
|
// the viewers, which the map marks with exactly that, was the most urgent
|
|
907
966
|
// material in the file. Everything followed from it: it bought encoders,
|
|
908
967
|
// it took the run standing in front of the viewer because that run was the
|
|
909
|
-
// nearest
|
|
968
|
+
// nearest encoder to it, and it did so again on every pass. Field 2026-09-08:
|
|
910
969
|
// 39 moves in one session, 24 between three adjacent numbers, one viewer
|
|
911
970
|
// on three encoders, and the picture stood still for 116.7 s in three
|
|
912
971
|
// interruptions, the worst of them 91.8 s.
|
|
@@ -1176,7 +1235,20 @@ export function firstUnmetWant(coverage, windows) {
|
|
|
1176
1235
|
* stated want is due now.
|
|
1177
1236
|
* @returns {number[]} Where to start each encoder, ascending.
|
|
1178
1237
|
*/
|
|
1179
|
-
export function placeEncoders({
|
|
1238
|
+
export function placeEncoders({
|
|
1239
|
+
coverage,
|
|
1240
|
+
windows,
|
|
1241
|
+
howMany,
|
|
1242
|
+
firstGap = null,
|
|
1243
|
+
deadlineAt = null,
|
|
1244
|
+
// WHERE TWO ENCODERS SHARE A STRETCH, and whether a second is worth having at
|
|
1245
|
+
// all. Derived by whoever holds the measurements — the rates, the contention
|
|
1246
|
+
// penalty, what a start costs — because this function is positional and holds
|
|
1247
|
+
// none of them. The default is the midpoint, which IS the answer when both are
|
|
1248
|
+
// fresh and owe the same, and is what a caller with nothing measured falls
|
|
1249
|
+
// back on.
|
|
1250
|
+
splitAt = (from, to) => from + Math.floor((to - from + 1) / 2)
|
|
1251
|
+
}) {
|
|
1180
1252
|
if (!(howMany > 0) || windows.length === 0) {
|
|
1181
1253
|
return [];
|
|
1182
1254
|
}
|
|
@@ -1249,8 +1321,18 @@ export function placeEncoders({ coverage, windows, howMany, firstGap = null, dea
|
|
|
1249
1321
|
const to = index === edges.length ? lastOf(windows) : edges[index] - 1;
|
|
1250
1322
|
const room_ = coverage.unmadeRunFrom(from);
|
|
1251
1323
|
if (to >= from && room_ > widest) {
|
|
1252
|
-
|
|
1253
|
-
|
|
1324
|
+
// WHERE THE TWO OF THEM MEET, calculated rather than halved. Halving is
|
|
1325
|
+
// the answer only when both encoders are fresh and owe the same, and it
|
|
1326
|
+
// was applied to every case — one already working is partway through a
|
|
1327
|
+
// piece while a new one owes its whole start, so the point where they
|
|
1328
|
+
// finish together lies further on. Nothing back means a second encoder
|
|
1329
|
+
// does not pay for itself here at all: two under this host's measured
|
|
1330
|
+
// contention against one at full speed, a comparison halving never made.
|
|
1331
|
+
const meet = splitAt(from, Math.min(from + room_ - 1, to));
|
|
1332
|
+
if (meet !== null) {
|
|
1333
|
+
widest = room_;
|
|
1334
|
+
widestFrom = meet;
|
|
1335
|
+
}
|
|
1254
1336
|
}
|
|
1255
1337
|
}
|
|
1256
1338
|
if (widestFrom === null) {
|