@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 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) ], bodies, wasted
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
- `bodies` ranks below every rank of the map, so spare capacity cannot buy an
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.80.16",
3
+ "version": "2.80.17",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -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 body count
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 bodies there were, which made extra bodies look free: the plan bought a
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 = (howManyBodies) => {
181
- const inForce = rate / contentionPenaltyFor(Math.max(0, howManyBodies - 1));
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 body costs to take away from where it stands and put somewhere else:
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 body 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 body IS, and here only
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 bodies rather than a figure computed once
247
+ // is why this is one pass over the encoders rather than a figure computed once
248
248
  // outside.
249
- const priced = (bodies) => {
250
- const perPiece = pieceAt(bodies.length);
251
- return bodies.map((body) => ({ at: body.at, delaySec: body.owes(perPiece) }));
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 body brought to it. There is no third case, and in particular no
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 bodies, so
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 body that serves nothing", "take one whose
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 bodies = [];
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 body that does not exist yet owes its own start and then the piece.
337
- bodies.push({ at: positions[index], owes: (piece) => firstByteWaitSec + piece });
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
- bodies.push({
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 body given no end pays a restart the moment anybody is placed inside the
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
- bodies.push({
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(bodies), coverage, wanted, untilNeeded, rate / contentionPenaltyFor(Math.max(0, bodies.length - 1)), refetchPerSegment, segmentSeconds);
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 body is left to make three hundred pieces a second
383
- // time. Each such body is offered its own first gap and the SAME score decides
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 body against
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 bodiesOf = (override) => {
399
- const bodies = [];
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
- bodies.push({
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
- bodies.push({ at: positions[index], owes: (piece) => firstByteWaitSec + piece });
475
+ encoders.push({ at: positions[index], owes: (piece) => firstByteWaitSec + piece });
417
476
  }
418
477
  }
419
- return priced(bodies);
478
+ return priced(encoders);
420
479
  };
421
480
  const scoreOf = (override) => {
422
- const bodies = bodiesOf(override);
423
- return latenessOf(bodies, coverage, wanted, untilNeeded,
424
- rate / contentionPenaltyFor(Math.max(0, bodies.length - 1)), refetchPerSegment, segmentSeconds);
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 body? Asked of the same score.
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 = bodiesOf(new Map());
449
- const without = bodiesOf(new Map([[run, null]]));
450
- const scoreOf_ = (bodies) => latenessOf(bodies, coverage, wanted, untilNeeded,
451
- rate / contentionPenaltyFor(Math.max(0, bodies.length - 1)), refetchPerSegment, segmentSeconds);
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 body left over from where a viewer used to be goes on costing the
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(bodiesOf(new Map([[run, null]])), coverage, wanted, untilNeeded,
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 }[]} bodies - Where each encoder would
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(bodies, coverage, wanted, untilNeeded, rate, refetchSecPerSegment, segmentSeconds) {
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 body of bodies) {
733
- if (body.at > index) {
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 body a whole piece for the one it is already working on. A
742
- // fresh body does start from nothing, so for it that is right — and it is
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 = body.delaySec
751
- + (index - body.at) / rate
752
- + coverage.madeBetween(body.at, index) * refetchSecPerSegment;
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 = body;
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
- bodies: bodies.length,
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.bodies !== right.bodies) {
850
- return left.bodies < right.bodies;
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 body to it, and it did so again on every pass. Field 2026-09-08:
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({ coverage, windows, howMany, firstGap = null, deadlineAt = null }) {
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
- widest = room_;
1253
- widestFrom = from + Math.floor(Math.min(room_, to - from + 1) / 2);
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) {