@torrent-tv/proxy 2.80.5 → 2.80.7
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 +36 -0
- package/package.json +1 -1
- package/routes/api/delivery-sink/get.js +8 -4
- package/server.js +415 -403
- package/services/data-channel-handler.js +87 -25
- package/services/delivery-probe.js +38 -5
- package/services/encode/CoverageMap.js +77 -4
- package/services/encode/EncodePlan.js +1025 -358
- package/services/encode/EncodeRun.js +19 -1
- package/services/encode/SegmentDemand.js +0 -0
- package/services/encode/SegmentStore.js +53 -1
- package/services/encode/open-piece.js +22 -1
- package/services/encode/run-command.js +12 -2
- package/services/hls-session-manager.js +38 -158
- package/services/hwaccel.js +182 -54
- package/services/orchestrators/EncodeOrchestrator.js +118 -89
- package/services/output/LiveOutputs.js +233 -213
- package/services/output/Timeline.js +333 -256
- package/services/piece-store/shared-piece-store.js +13 -9
- package/services/priority/PriorityMap.js +262 -108
- package/services/priority/PriorityOrchestrator.js +31 -6
- package/services/quality/EncodeCost.js +555 -500
- package/services/torrent-pool.js +9 -4
- package/test/encode-orchestrator.test.js +195 -65
- package/test/encode-plan-viewers.test.js +719 -0
- package/test/encode-plan.test.js +174 -81
- package/test/open-piece.test.js +40 -0
- package/test/output-speed.test.js +86 -0
- package/test/piece-store-eviction.test.js +26 -0
- package/test/priority-map-download.test.js +25 -7
- package/test/priority-map.test.js +134 -83
- package/test/seek-landing.test.js +109 -76
- package/test/segment-demand.test.js +54 -56
- package/test/wedge-certainty.test.js +3 -3
|
@@ -1,257 +1,450 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file How many encoders there should be on one output, and where each of them
|
|
3
|
-
* belongs — decided from numbers alone.
|
|
4
|
-
*
|
|
5
|
-
* The decision is separated from carrying it out on purpose. Every rule below
|
|
6
|
-
* was previously a condition somewhere inside an eleven-thousand-line file,
|
|
7
|
-
* reachable only by starting a real ffmpeg, and each of them was written for
|
|
8
|
-
* one viewer:
|
|
9
|
-
*
|
|
10
|
-
* - a run was placed at the position of whoever asked, and never at the first
|
|
11
|
-
* thing missing, so a viewer moving into a stretch already on disk restarted
|
|
12
|
-
* an encoder to make it a second time;
|
|
13
|
-
* - a run had no end at all — neither `-to` nor `-t` appeared anywhere — so it
|
|
14
|
-
* ran until something killed it, and two runs on one output could not exist
|
|
15
|
-
* without writing over each other;
|
|
16
|
-
* - nothing stopped a run that had caught up with material somebody else had
|
|
17
|
-
* already made.
|
|
18
|
-
*
|
|
19
|
-
* The rule this file exists to express, stated by the user 2026-09-04:
|
|
20
|
-
*
|
|
21
|
-
* > Viewers are always independent and always reuse what can be reused. The
|
|
22
|
-
* > number of encoders is however many are needed; how many are needed follows
|
|
23
|
-
* > from which sets of output parameters are wanted and where the viewers stand
|
|
24
|
-
* > inside each. Segments produced by ANY encoder are available to ANY viewer,
|
|
25
|
-
* > and which viewer asked never enters the question.
|
|
26
|
-
*
|
|
27
|
-
* So no name of a viewer reaches this file. It is given what is wanted, what
|
|
28
|
-
* exists, what is being made, and what the machine can afford.
|
|
29
|
-
*
|
|
30
|
-
* **A viewer decides the ORDER the map is walked in and, through the budget, how
|
|
31
|
-
* many processes walk it. Nothing else.** Stated by the user 2026-09-05, and it
|
|
32
|
-
* is the rule the rest of this file now follows: while a file is being encoded
|
|
33
|
-
* it is encoded WHOLE, in the order the map dictates. Who wants which segment
|
|
34
|
-
* decides which gap is closed first, never whether a run may go on living.
|
|
35
|
-
*
|
|
36
|
-
* **A run is therefore never stopped for standing outside a viewer's window.**
|
|
37
|
-
* It used to be, and the two decisions that produced that were in direct
|
|
38
|
-
* contradiction — measured in the field 2026-09-05 on a viewer watching an
|
|
39
|
-
* episode:
|
|
40
|
-
*
|
|
41
|
-
* 1. this file commanded a start inside the window, at #46;
|
|
42
|
-
* 2. `planRunInterval` in the session manager moved the start to #78, because
|
|
43
|
-
* it counted a suspended run's claim as reaching `head + look-ahead`;
|
|
44
|
-
* 3. this file then saw a run at #78 against a window of [27, 57], found no
|
|
45
|
-
* overlap, and killed it as "nothing it was given is wanted";
|
|
46
|
-
* 4. neither coverage nor demand had changed, so the same start was commanded
|
|
47
|
-
* again — 350-700ms per cycle, dozens of times, no segment ever produced,
|
|
48
|
-
* the viewer's picture stopped for 125 seconds.
|
|
49
|
-
*
|
|
50
|
-
* Both of those other authorities are gone (roadmap item 76, step 5). What is
|
|
51
|
-
* left is this file, and the only reasons it stops a run are: nobody is
|
|
52
|
-
* watching the output at all; the machine affords fewer processes; or there is
|
|
53
|
-
* nothing left unmade anywhere in the track.
|
|
54
|
-
*
|
|
55
|
-
* **A run's end comes from the coverage**, never from a window: it runs until
|
|
56
|
-
* it meets material somebody else has made or is making, or until the end of
|
|
57
|
-
* the film.
|
|
58
|
-
*/
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* One encoder that is running now.
|
|
62
|
-
*
|
|
63
|
-
* @typedef {object} LiveRun
|
|
64
|
-
* @property {string} id
|
|
65
|
-
* @property {number} from - The first number it was given.
|
|
66
|
-
* @property {number} to - The last number it was given, inclusive.
|
|
67
|
-
* @property {number} head - The next number it will produce. Its position.
|
|
68
|
-
* @property {number} speedX - Measured encode speed against realtime, from
|
|
69
|
-
* ffmpeg's own progress. Zero or less means nothing has measured it yet, and
|
|
70
|
-
* then no comparison involving its speed can be made.
|
|
71
|
-
*/
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* What a viewer is waiting for. Which viewer is deliberately absent.
|
|
75
|
-
*
|
|
76
|
-
* @typedef {object} WantedSpan
|
|
77
|
-
* @property {number} from
|
|
78
|
-
* @property {number} to
|
|
79
|
-
*/
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* @typedef {{ type: "start", from: number, to: number, because: string }
|
|
83
|
-
* | { type: "move", run: object, from: number, to: number, because: string }
|
|
84
|
-
* | { type: "stop", run: object, because: string }
|
|
85
|
-
* | { type: "keep", run: object, from: number, to: number }} PlanAction
|
|
86
|
-
*/
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Decide what to do with the encoders on one output.
|
|
90
|
-
*
|
|
91
|
-
* @param {object} params
|
|
92
|
-
* @param {import("./CoverageMap.js").CoverageMap} params.coverage - What has
|
|
93
|
-
* been made and what is being made.
|
|
94
|
-
* @param {WantedSpan[]} params.windows - What viewers are waiting for, one
|
|
95
|
-
* window each. Empty means nobody is watching this output.
|
|
96
|
-
* @param {LiveRun[]} params.runs - The encoders running on it now.
|
|
97
|
-
* @param {number} params.maxRuns - How many encoders this machine can afford on
|
|
98
|
-
* this output. Comes from the same arithmetic that decides the quality offer;
|
|
99
|
-
* it is measured per host and never chosen here.
|
|
100
|
-
* @param {number} params.segmentSeconds - How much film one segment holds.
|
|
101
|
-
* @param {number} params.
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
// WHICH IS CHEAPER, AND BOTH SIDES COUNTED WHOLE.
|
|
181
|
-
//
|
|
182
|
-
// Driving through material that exists costs this run's own encode time for
|
|
183
|
-
// it, and costs the swarm the same bytes a second time — the film has to be
|
|
184
|
-
// fetched again to be encoded again.
|
|
185
|
-
//
|
|
186
|
-
// Moving costs the death of this run, the start of another, and the wait
|
|
187
|
-
// for the first bytes at the new position. The last of those is the largest
|
|
188
|
-
// in the field and the one nothing measures yet; while it is unmeasured it
|
|
189
|
-
// counts as zero, which makes moving look cheaper than it is.
|
|
190
|
-
//
|
|
191
|
-
// A run whose speed nothing has measured yet cannot be compared at all, and
|
|
192
|
-
// then it is KEPT. Moving costs a known amount for an unknown gain, and a
|
|
193
|
-
// fresh run has produced nothing, so taking its work away is certainly a
|
|
194
|
-
// loss. This used to answer the other way, and every just-started run was
|
|
195
|
-
// moved the moment anything ahead of it was covered — which, once the whole
|
|
196
|
-
// film ahead had been made, was always.
|
|
197
|
-
// Both sides have to be known for the comparison to mean anything. The
|
|
198
|
-
// encoder's own speed is one; what the swarm charges to fetch the same
|
|
199
|
-
// bytes again is the other, and where nothing has measured it the sum is
|
|
200
|
-
// not a cost but a fragment of one. Answering from a fragment biases the
|
|
201
|
-
// decision one way — towards moving, since the missing term is on the
|
|
202
|
-
// driving side — so an unknown term is a reason to keep, exactly as an
|
|
203
|
-
// unmeasured speed is.
|
|
204
|
-
const known = run.speedX > 0 && refetchSecPerFilmSecond > 0;
|
|
205
|
-
const refetchSec = coveredAhead * segmentSeconds * refetchSecPerFilmSecond;
|
|
206
|
-
const driveSec = known ? (coveredAhead * segmentSeconds) / run.speedX + refetchSec : null;
|
|
207
|
-
const moveSec = restartCostSec + killCostSec + firstByteWaitSec;
|
|
208
|
-
if (driveSec === null || driveSec <= moveSec) {
|
|
209
|
-
surviving.add(run);
|
|
210
|
-
keeps.push({ type: "keep", run, from: run.head, to: run.to });
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const free = coverage.freeRunFrom(gap, run);
|
|
215
|
-
surviving.add(run);
|
|
216
|
-
moves.push({
|
|
217
|
-
type: "move",
|
|
218
|
-
run,
|
|
219
|
-
from: gap,
|
|
220
|
-
to: endOfStretch(gap, free),
|
|
221
|
-
because:
|
|
222
|
-
`driving through ${coveredAhead} covered segment(s) costs ${driveSec.toFixed(2)}s ` +
|
|
223
|
-
`(encode ${((coveredAhead * segmentSeconds) / run.speedX).toFixed(2)}s + ` +
|
|
224
|
-
`refetch ${refetchSec.toFixed(2)}s) against ${moveSec.toFixed(2)}s to move ` +
|
|
225
|
-
`(kill ${killCostSec.toFixed(2)}s + start ${restartCostSec.toFixed(2)}s + ` +
|
|
226
|
-
`first bytes ${firstByteWaitSec.toFixed(2)}s)`
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// 3. Gaps somebody is waiting for that nobody is making, IN THE ORDER THE
|
|
231
|
-
// DEMAND MAP PUTS THEM: most urgent zone first, and within one zone the
|
|
232
|
-
// lowest number, because that is where a viewer is stopped. The budget
|
|
233
|
-
// rarely stretches to every gap, so which one is taken first is the whole
|
|
234
|
-
// of what a viewer's presence decides.
|
|
235
|
-
const budget = Math.max(0, maxRuns - surviving.size);
|
|
236
|
-
const alreadyPlanned = new Set(moves.map((action) => /** @type {{from:number}} */ (action).from));
|
|
237
|
-
for (const from of placeEncoders({
|
|
1
|
+
/**
|
|
2
|
+
* @file How many encoders there should be on one output, and where each of them
|
|
3
|
+
* belongs — decided from numbers alone.
|
|
4
|
+
*
|
|
5
|
+
* The decision is separated from carrying it out on purpose. Every rule below
|
|
6
|
+
* was previously a condition somewhere inside an eleven-thousand-line file,
|
|
7
|
+
* reachable only by starting a real ffmpeg, and each of them was written for
|
|
8
|
+
* one viewer:
|
|
9
|
+
*
|
|
10
|
+
* - a run was placed at the position of whoever asked, and never at the first
|
|
11
|
+
* thing missing, so a viewer moving into a stretch already on disk restarted
|
|
12
|
+
* an encoder to make it a second time;
|
|
13
|
+
* - a run had no end at all — neither `-to` nor `-t` appeared anywhere — so it
|
|
14
|
+
* ran until something killed it, and two runs on one output could not exist
|
|
15
|
+
* without writing over each other;
|
|
16
|
+
* - nothing stopped a run that had caught up with material somebody else had
|
|
17
|
+
* already made.
|
|
18
|
+
*
|
|
19
|
+
* The rule this file exists to express, stated by the user 2026-09-04:
|
|
20
|
+
*
|
|
21
|
+
* > Viewers are always independent and always reuse what can be reused. The
|
|
22
|
+
* > number of encoders is however many are needed; how many are needed follows
|
|
23
|
+
* > from which sets of output parameters are wanted and where the viewers stand
|
|
24
|
+
* > inside each. Segments produced by ANY encoder are available to ANY viewer,
|
|
25
|
+
* > and which viewer asked never enters the question.
|
|
26
|
+
*
|
|
27
|
+
* So no name of a viewer reaches this file. It is given what is wanted, what
|
|
28
|
+
* exists, what is being made, and what the machine can afford.
|
|
29
|
+
*
|
|
30
|
+
* **A viewer decides the ORDER the map is walked in and, through the budget, how
|
|
31
|
+
* many processes walk it. Nothing else.** Stated by the user 2026-09-05, and it
|
|
32
|
+
* is the rule the rest of this file now follows: while a file is being encoded
|
|
33
|
+
* it is encoded WHOLE, in the order the map dictates. Who wants which segment
|
|
34
|
+
* decides which gap is closed first, never whether a run may go on living.
|
|
35
|
+
*
|
|
36
|
+
* **A run is therefore never stopped for standing outside a viewer's window.**
|
|
37
|
+
* It used to be, and the two decisions that produced that were in direct
|
|
38
|
+
* contradiction — measured in the field 2026-09-05 on a viewer watching an
|
|
39
|
+
* episode:
|
|
40
|
+
*
|
|
41
|
+
* 1. this file commanded a start inside the window, at #46;
|
|
42
|
+
* 2. `planRunInterval` in the session manager moved the start to #78, because
|
|
43
|
+
* it counted a suspended run's claim as reaching `head + look-ahead`;
|
|
44
|
+
* 3. this file then saw a run at #78 against a window of [27, 57], found no
|
|
45
|
+
* overlap, and killed it as "nothing it was given is wanted";
|
|
46
|
+
* 4. neither coverage nor demand had changed, so the same start was commanded
|
|
47
|
+
* again — 350-700ms per cycle, dozens of times, no segment ever produced,
|
|
48
|
+
* the viewer's picture stopped for 125 seconds.
|
|
49
|
+
*
|
|
50
|
+
* Both of those other authorities are gone (roadmap item 76, step 5). What is
|
|
51
|
+
* left is this file, and the only reasons it stops a run are: nobody is
|
|
52
|
+
* watching the output at all; the machine affords fewer processes; or there is
|
|
53
|
+
* nothing left unmade anywhere in the track.
|
|
54
|
+
*
|
|
55
|
+
* **A run's end comes from the coverage**, never from a window: it runs until
|
|
56
|
+
* it meets material somebody else has made or is making, or until the end of
|
|
57
|
+
* the film.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* One encoder that is running now.
|
|
62
|
+
*
|
|
63
|
+
* @typedef {object} LiveRun
|
|
64
|
+
* @property {string} id
|
|
65
|
+
* @property {number} from - The first number it was given.
|
|
66
|
+
* @property {number} to - The last number it was given, inclusive.
|
|
67
|
+
* @property {number} head - The next number it will produce. Its position.
|
|
68
|
+
* @property {number} speedX - Measured encode speed against realtime, from
|
|
69
|
+
* ffmpeg's own progress. Zero or less means nothing has measured it yet, and
|
|
70
|
+
* then no comparison involving its speed can be made.
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* What a viewer is waiting for. Which viewer is deliberately absent.
|
|
75
|
+
*
|
|
76
|
+
* @typedef {object} WantedSpan
|
|
77
|
+
* @property {number} from
|
|
78
|
+
* @property {number} to
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @typedef {{ type: "start", from: number, to: number, because: string }
|
|
83
|
+
* | { type: "move", run: object, from: number, to: number, because: string }
|
|
84
|
+
* | { type: "stop", run: object, because: string }
|
|
85
|
+
* | { type: "keep", run: object, from: number, to: number }} PlanAction
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Decide what to do with the encoders on one output.
|
|
90
|
+
*
|
|
91
|
+
* @param {object} params
|
|
92
|
+
* @param {import("./CoverageMap.js").CoverageMap} params.coverage - What has
|
|
93
|
+
* been made and what is being made.
|
|
94
|
+
* @param {WantedSpan[]} params.windows - What viewers are waiting for, one
|
|
95
|
+
* window each. Empty means nobody is watching this output.
|
|
96
|
+
* @param {LiveRun[]} params.runs - The encoders running on it now.
|
|
97
|
+
* @param {number} params.maxRuns - How many encoders this machine can afford on
|
|
98
|
+
* this output. Comes from the same arithmetic that decides the quality offer;
|
|
99
|
+
* it is measured per host and never chosen here.
|
|
100
|
+
* @param {number} params.segmentSeconds - How much film one segment holds.
|
|
101
|
+
* @param {number} [params.killCostSec] - How long stopping an encoder takes,
|
|
102
|
+
* measured on this host from its own runs. Zero until something has measured
|
|
103
|
+
* it, which makes moving one look cheaper than it is and is said here so the
|
|
104
|
+
* bias is known.
|
|
105
|
+
* @param {number} [params.firstByteWaitSec] - How long a fresh encoder takes to
|
|
106
|
+
* produce anything: process start, opening the input, and the first piece.
|
|
107
|
+
* Measured the same way. It replaced a constant of 0.12 s taken from one
|
|
108
|
+
* host and charged to every other.
|
|
109
|
+
* @param {(others: number) => number} [params.contentionPenaltyFor] - How much
|
|
110
|
+
* slower ONE encoder runs with that many others beside it, measured on this
|
|
111
|
+
* host. Without it every extra process looks free, and the score then wants an
|
|
112
|
+
* encoder per piece: at exactly realtime each next piece is marginally late
|
|
113
|
+
* however many are running, so another one always seemed to help a little.
|
|
114
|
+
* Unmeasured is 1, and then the budget is the only thing bounding the count.
|
|
115
|
+
* @returns {PlanAction[]} Stops first, then moves, then starts, so that a plan
|
|
116
|
+
* carried out in order never holds two encoders where it means to hold one.
|
|
117
|
+
*/
|
|
118
|
+
export function planEncoders({
|
|
119
|
+
coverage,
|
|
120
|
+
windows,
|
|
121
|
+
runs,
|
|
122
|
+
maxRuns,
|
|
123
|
+
segmentSeconds,
|
|
124
|
+
killCostSec = 0,
|
|
125
|
+
firstByteWaitSec = 0,
|
|
126
|
+
refetchSecPerFilmSecond = 0,
|
|
127
|
+
contentionPenaltyFor = () => 1,
|
|
128
|
+
speedX = 0
|
|
129
|
+
}) {
|
|
130
|
+
/** @type {PlanAction[]} */
|
|
131
|
+
const stops = [];
|
|
132
|
+
/** @type {PlanAction[]} */
|
|
133
|
+
const moves = [];
|
|
134
|
+
/** @type {PlanAction[]} */
|
|
135
|
+
const starts = [];
|
|
136
|
+
/** @type {PlanAction[]} */
|
|
137
|
+
const keeps = [];
|
|
138
|
+
|
|
139
|
+
const wanted = Array.isArray(windows) ? windows : [];
|
|
140
|
+
const live = Array.isArray(runs) ? runs : [];
|
|
141
|
+
|
|
142
|
+
// Nobody is watching this output: every encoder on it is making segments for
|
|
143
|
+
// no one. This is the case a look-ahead cannot answer, because look-ahead
|
|
144
|
+
// asks how far AHEAD of a viewer a run is and there is no viewer.
|
|
145
|
+
if (wanted.length === 0) {
|
|
146
|
+
for (const run of live) {
|
|
147
|
+
stops.push({ type: "stop", run, because: "nobody is watching this output" });
|
|
148
|
+
}
|
|
149
|
+
return stops;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// How far a search for a gap needs to look: past the furthest thing anybody
|
|
153
|
+
// is waiting for there is nothing to decide about.
|
|
154
|
+
const demandTo = Math.max(...wanted.map((span) => span.to));
|
|
155
|
+
const untilNeeded = deadlineReaderFor(wanted, segmentSeconds);
|
|
156
|
+
// Segments produced per second, from the fastest measured encoder here.
|
|
157
|
+
// Seconds of film per second, divided by the film one piece holds.
|
|
158
|
+
//
|
|
159
|
+
// A RUN WORKING ON THIS OUTPUT OUTRANKS THE BENCHMARK. The startup figure is
|
|
160
|
+
// what this host does on reference clips; a run here is what it does on THIS
|
|
161
|
+
// material, and that is the more specific statement. Taken as a floor instead
|
|
162
|
+
// — the larger of the two — an encoder reporting half realtime was scored as
|
|
163
|
+
// though it ran at twice, and nothing was ever late.
|
|
164
|
+
const working = live.reduce((best, run) => Math.max(best, run.speedX || 0), 0);
|
|
165
|
+
const rate = segmentSeconds > 0 ? (working > 0 ? working : speedX) / segmentSeconds : 0;
|
|
166
|
+
// What a body costs to take away from where it stands and put somewhere else:
|
|
167
|
+
// its death, the start of another, and the wait for the first bytes there.
|
|
168
|
+
// Taking an encoder somewhere else is stopping this one and waiting for the
|
|
169
|
+
// next to produce. Both halves are measured on this host.
|
|
170
|
+
const moveSec = killCostSec + firstByteWaitSec;
|
|
171
|
+
|
|
172
|
+
// ------------------------------------------------------------------ WHERE
|
|
173
|
+
//
|
|
174
|
+
// A question about the FILM, and about nothing else: which numbers are
|
|
175
|
+
// missing, when each is needed, how fast this machine encodes, how many
|
|
176
|
+
// processes it can hold. No encoder that happens to be running enters it,
|
|
177
|
+
// which is why it can be answered by arithmetic.
|
|
178
|
+
const positions = placeEncoders({
|
|
238
179
|
coverage,
|
|
239
180
|
windows: wanted,
|
|
240
|
-
howMany:
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
181
|
+
howMany: maxRuns,
|
|
182
|
+
// EVERY LIVE ENCODER IS PRE-PLACED, because that is what "somebody already
|
|
183
|
+
// gets here in time" means. A number one of them reaches before it is
|
|
184
|
+
// needed is not a position at all; a number none of them reaches is, and
|
|
185
|
+
// needs a body brought to it. There is no third case, and in particular no
|
|
186
|
+
// separate question of whether an encoder should drive on or be moved:
|
|
187
|
+
// driving is simply its arrival, and its arrival is priced in one place.
|
|
188
|
+
firstGap: gapFinderFor(coverage, new Set(live), rate, segmentSeconds * refetchSecPerFilmSecond),
|
|
189
|
+
deadlineAt: untilNeeded
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// -------------------------------------------------------------------- WHO
|
|
193
|
+
//
|
|
194
|
+
// ARGMIN OF THE OBJECTIVE, EVALUATED. Not a rule that approximates it.
|
|
195
|
+
//
|
|
196
|
+
// Every way of filling the positions is scored by `latenessOf` and the best is
|
|
197
|
+
// taken. There are at most a handful of positions and a handful of bodies, so
|
|
198
|
+
// the enumeration is exact: no local rule stands in for the objective, and
|
|
199
|
+
// none can therefore disagree with another.
|
|
200
|
+
//
|
|
201
|
+
// Four such rules were written before this and all four had to go — "place
|
|
202
|
+
// where a number is late", "take a body that serves nothing", "take one whose
|
|
203
|
+
// work is needed later than this", "drive on or move, by cost". Each looked
|
|
204
|
+
// like a consequence of the model and each approximated it from a different
|
|
205
|
+
// side, so together they contradicted one another and the answer depended on
|
|
206
|
+
// which ran first.
|
|
207
|
+
/** One decision per live encoder, so none can be decided twice. @type {Map<object, PlanAction>} */
|
|
208
|
+
const decided = new Map();
|
|
209
|
+
const room = Math.max(0, maxRuns - live.length);
|
|
210
|
+
const refetchPerSegment = segmentSeconds * refetchSecPerFilmSecond;
|
|
211
|
+
const startSec = firstByteWaitSec;
|
|
212
|
+
|
|
213
|
+
let arrangements = [{ fill: [], used: new Set(), fresh: 0 }];
|
|
214
|
+
for (let index = 0; index < positions.length; index += 1) {
|
|
215
|
+
const next = [];
|
|
216
|
+
for (const arrangement of arrangements) {
|
|
217
|
+
next.push({ fill: [...arrangement.fill, null], used: arrangement.used, fresh: arrangement.fresh });
|
|
218
|
+
// A FRESH PROCESS IS OFFERED BEFORE ANY WORKING BODY, so that when the two
|
|
219
|
+
// score the same the working one is left alone. Taking it is free in the
|
|
220
|
+
// arithmetic — its output stays on disk — but it is not free in fact: the
|
|
221
|
+
// run it belongs to has a position, a warm input and a measured speed, and
|
|
222
|
+
// all three are thrown away for nothing.
|
|
223
|
+
if (arrangement.fresh < room) {
|
|
224
|
+
next.push({
|
|
225
|
+
fill: [...arrangement.fill, "new"],
|
|
226
|
+
used: arrangement.used,
|
|
227
|
+
fresh: arrangement.fresh + 1
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
for (const run of live) {
|
|
231
|
+
if (arrangement.used.has(run)) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
next.push({
|
|
235
|
+
fill: [...arrangement.fill, run],
|
|
236
|
+
used: new Set([...arrangement.used, run]),
|
|
237
|
+
fresh: arrangement.fresh
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
arrangements = next;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let best = null;
|
|
245
|
+
let bestScore = null;
|
|
246
|
+
for (const arrangement of arrangements) {
|
|
247
|
+
const bodies = [];
|
|
248
|
+
for (let index = 0; index < positions.length; index += 1) {
|
|
249
|
+
const filler = arrangement.fill[index];
|
|
250
|
+
if (filler === null) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (filler === "new") {
|
|
254
|
+
bodies.push({ at: positions[index], delaySec: startSec });
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const head = Number(filler.head);
|
|
258
|
+
bodies.push({
|
|
259
|
+
at: positions[index],
|
|
260
|
+
delaySec: head === positions[index] ? 0 : moveSec
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
// Bodies nobody was given a position for go on working where they stand,
|
|
264
|
+
// and their coverage counts: the file is encoded whole.
|
|
265
|
+
//
|
|
266
|
+
// A body given no end pays a restart the moment anybody is placed inside the
|
|
267
|
+
// road it would drive: where a run stops is fixed when its process starts,
|
|
268
|
+
// so it has to be cut and begun again at its own head. That price was
|
|
269
|
+
// invisible here, and an arrangement was scored as free when it was not.
|
|
270
|
+
for (const run of live) {
|
|
271
|
+
if (arrangement.used.has(run)) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const head = Number(run.head);
|
|
275
|
+
const endless = Number(run.to) < Number(run.from);
|
|
276
|
+
const cutInFront = arrangement.fill.some((filler, index) =>
|
|
277
|
+
filler !== null && positions[index] > head
|
|
278
|
+
&& (endless || positions[index] <= Number(run.to)));
|
|
279
|
+
bodies.push({ at: head, delaySec: cutInFront ? moveSec : 0 });
|
|
280
|
+
}
|
|
281
|
+
const scored = latenessOf(bodies, coverage, wanted, untilNeeded, rate / contentionPenaltyFor(Math.max(0, bodies.length - 1)), refetchPerSegment, segmentSeconds);
|
|
282
|
+
if (bestScore === null || cheaperThan(scored, bestScore)) {
|
|
283
|
+
bestScore = scored;
|
|
284
|
+
best = arrangement;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// A BODY STANDING ON FILM THAT EXISTS is the one arrangement the enumeration
|
|
289
|
+
// above cannot reach: the gap in front of it is nobody's deadline, so it is
|
|
290
|
+
// never a position, and the body is left to make three hundred pieces a second
|
|
291
|
+
// time. Each such body is offered its own first gap and the SAME score decides
|
|
292
|
+
// — moving costs a restart on everything downstream, staying costs the repeat.
|
|
293
|
+
//
|
|
294
|
+
// Offered one at a time rather than folded into the enumeration because the
|
|
295
|
+
// enumeration is exponential in the number of positions, and this is called
|
|
296
|
+
// again on every piece produced. One extra evaluation per body against
|
|
297
|
+
// several thousand arrangements is the difference between arithmetic and a
|
|
298
|
+
// stalled proxy.
|
|
299
|
+
const placement = new Map();
|
|
300
|
+
for (let index = 0; index < positions.length; index += 1) {
|
|
301
|
+
const filler = best ? best.fill[index] : null;
|
|
302
|
+
if (filler && filler !== "new") {
|
|
303
|
+
placement.set(filler, positions[index]);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const bodiesOf = (override) => {
|
|
307
|
+
const bodies = [];
|
|
308
|
+
for (const run of live) {
|
|
309
|
+
if (override.has(run) && override.get(run) === null) {
|
|
310
|
+
// Asked what the film looks like WITHOUT this one.
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
const at = override.has(run) ? override.get(run) : (placement.get(run) ?? Number(run.head));
|
|
314
|
+
const head = Number(run.head);
|
|
315
|
+
bodies.push({ at, delaySec: at === head ? 0 : moveSec });
|
|
316
|
+
}
|
|
317
|
+
for (let index = 0; index < positions.length; index += 1) {
|
|
318
|
+
if ((best ? best.fill[index] : null) === "new") {
|
|
319
|
+
bodies.push({ at: positions[index], delaySec: startSec });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return bodies;
|
|
323
|
+
};
|
|
324
|
+
const scoreOf = (override) => {
|
|
325
|
+
const bodies = bodiesOf(override);
|
|
326
|
+
return latenessOf(bodies, coverage, wanted, untilNeeded,
|
|
327
|
+
rate / contentionPenaltyFor(Math.max(0, bodies.length - 1)), refetchPerSegment, segmentSeconds);
|
|
328
|
+
};
|
|
329
|
+
for (const run of live) {
|
|
330
|
+
if (placement.has(run)) {
|
|
244
331
|
continue;
|
|
245
332
|
}
|
|
246
|
-
|
|
333
|
+
const gap = coverage.firstGapFrom(run.head, undefined, run);
|
|
334
|
+
if (gap === null || gap === Number(run.head)) {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
const asIs = scoreOf(new Map());
|
|
338
|
+
const moved = scoreOf(new Map([[run, gap]]));
|
|
339
|
+
if (cheaperThan(moved, asIs)) {
|
|
340
|
+
placement.set(run, gap);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Would the film be worse off without this body? Asked of the same score.
|
|
346
|
+
*
|
|
347
|
+
* @param {object} run
|
|
348
|
+
* @returns {boolean}
|
|
349
|
+
*/
|
|
350
|
+
const worseWithout = (run) => {
|
|
351
|
+
const kept = bodiesOf(new Map());
|
|
352
|
+
const without = bodiesOf(new Map([[run, null]]));
|
|
353
|
+
const scoreOf_ = (bodies) => latenessOf(bodies, coverage, wanted, untilNeeded,
|
|
354
|
+
rate / contentionPenaltyFor(Math.max(0, bodies.length - 1)), refetchPerSegment, segmentSeconds);
|
|
355
|
+
return cheaperThan(scoreOf_(kept), scoreOf_(without));
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const stretchAt = (from) => endOfStretch(from, Math.min(
|
|
359
|
+
coverage.unmadeRunFrom(from),
|
|
360
|
+
coverage.freeRunFrom(from, new Set(live))
|
|
361
|
+
));
|
|
362
|
+
|
|
363
|
+
for (let index = 0; index < positions.length; index += 1) {
|
|
364
|
+
if ((best ? best.fill[index] : null) !== "new") {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const at = positions[index];
|
|
247
368
|
starts.push({
|
|
248
369
|
type: "start",
|
|
249
|
-
from,
|
|
250
|
-
to:
|
|
251
|
-
because: `#${
|
|
370
|
+
from: at,
|
|
371
|
+
to: stretchAt(at),
|
|
372
|
+
because: `#${at} is wanted and nobody reaches it in time`
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
for (const run of live) {
|
|
377
|
+
const head = Number(run.head);
|
|
378
|
+
const at = placement.has(run) ? placement.get(run) : head;
|
|
379
|
+
if (at !== head) {
|
|
380
|
+
decided.set(run, {
|
|
381
|
+
type: "move",
|
|
382
|
+
run,
|
|
383
|
+
from: at,
|
|
384
|
+
to: stretchAt(at),
|
|
385
|
+
because: `standing at #${head} scores worse than standing at #${at}, counting ` +
|
|
386
|
+
"both how late the film would be and the work that would be done twice"
|
|
387
|
+
});
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
// It stays where it is — unless holding it changes nothing.
|
|
391
|
+
//
|
|
392
|
+
// A body left over from where a viewer used to be goes on costing the
|
|
393
|
+
// machine a process while another encoder already reaches everything it
|
|
394
|
+
// would. The score says so directly: take it away and see. Removing it is
|
|
395
|
+
// refused the moment it makes anything later or leaves film abandoned, so
|
|
396
|
+
// this cannot quietly drop the encoder somebody is waiting on.
|
|
397
|
+
if (!placement.has(run) && !worseWithout(run)) {
|
|
398
|
+
stops.push({
|
|
399
|
+
type: "stop",
|
|
400
|
+
run,
|
|
401
|
+
because: "the film is no worse off without it"
|
|
402
|
+
});
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
decided.set(run, { type: "keep", run, from: head, to: run.to });
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// THE MACHINE'S LIMIT BINDS, whatever the map wants. It is measured — the
|
|
409
|
+
// processor, the swarm and the piece store each give a figure and the smallest
|
|
410
|
+
// wins — and an encoder over it is one the host cannot feed. Which of them
|
|
411
|
+
// goes is the same question as any other here: the one the film misses least,
|
|
412
|
+
// by the same score.
|
|
413
|
+
while (decided.size + starts.length > maxRuns) {
|
|
414
|
+
let cheapest = null;
|
|
415
|
+
let cheapestScore = null;
|
|
416
|
+
for (const [run, action] of decided) {
|
|
417
|
+
if (action.type !== "keep") {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
const without = latenessOf(bodiesOf(new Map([[run, null]])), coverage, wanted, untilNeeded,
|
|
421
|
+
rate / contentionPenaltyFor(Math.max(0, live.length - 2)), refetchPerSegment, segmentSeconds);
|
|
422
|
+
if (cheapestScore === null || cheaperThan(without, cheapestScore)) {
|
|
423
|
+
cheapestScore = without;
|
|
424
|
+
cheapest = run;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (cheapest === null) {
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
decided.delete(cheapest);
|
|
431
|
+
placement.delete(cheapest);
|
|
432
|
+
stops.push({
|
|
433
|
+
type: "stop",
|
|
434
|
+
run: cheapest,
|
|
435
|
+
because: `the machine holds ${maxRuns} encoder(s) on this output and this is the one ` +
|
|
436
|
+
"the film misses least"
|
|
252
437
|
});
|
|
253
438
|
}
|
|
254
439
|
|
|
440
|
+
for (const action of decided.values()) {
|
|
441
|
+
if (action.type === "keep") {
|
|
442
|
+
keeps.push(action);
|
|
443
|
+
} else {
|
|
444
|
+
moves.push(action);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
255
448
|
// ONE ENCODER'S WORK ENDS WHERE THE NEXT ONE'S BEGINS.
|
|
256
449
|
//
|
|
257
450
|
// A free stretch may run to the end of the track, and an encoder given all of
|
|
@@ -266,61 +459,513 @@ export function planEncoders({
|
|
|
266
459
|
// The bound is taken from the NEXT ENCODER'S START, not from a band edge: a
|
|
267
460
|
// band edge travels with the viewer, so every step forward would leave a
|
|
268
461
|
// sliver just past the previous encoder and buy an encoder for it.
|
|
269
|
-
|
|
462
|
+
// A RUN THAT IS STAYING IS IN THE SORT TOO, because its road can be taken.
|
|
463
|
+
//
|
|
464
|
+
// It used to be left out, on the reading that a run staying put keeps what it
|
|
465
|
+
// was given. That is true of the stretch it was GIVEN and false of the road it
|
|
466
|
+
// will actually drive: a run with no end carries no `-to` and walks to the end
|
|
467
|
+
// of the film, so an encoder placed in front of it writes the same names.
|
|
468
|
+
const placed = [...moves, ...starts, ...keeps].sort(
|
|
270
469
|
(left, right) => /** @type {any} */ (left).from - /** @type {any} */ (right).from
|
|
271
470
|
);
|
|
272
471
|
for (let index = 0; index < placed.length - 1; index += 1) {
|
|
273
|
-
const here = /** @type {{ from: number, to: number }} */ (placed[index]);
|
|
472
|
+
const here = /** @type {{ type: string, run?: object, from: number, to: number }} */ (placed[index]);
|
|
274
473
|
const next = /** @type {{ from: number }} */ (placed[index + 1]);
|
|
275
|
-
if (here.to
|
|
276
|
-
|
|
474
|
+
if (here.to >= 0 && here.to < next.from) {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
here.to = next.from - 1;
|
|
478
|
+
if (here.type !== "keep") {
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
// SHORTENING A LIVE RUN'S ROAD MEANS STOPPING IT, not merely writing a
|
|
482
|
+
// smaller number down. Where a run's end goes is fixed when its process
|
|
483
|
+
// starts, so one that was given none keeps producing past any bound decided
|
|
484
|
+
// later and would write one piece into the new encoder's road — two
|
|
485
|
+
// processes on one name, which is the collision this whole pass exists to
|
|
486
|
+
// prevent. So it ends here and begins again at its own head with a real end;
|
|
487
|
+
// the viewer in front pays a restart, which is a cost this file already
|
|
488
|
+
// prices rather than a interruption nobody counted.
|
|
489
|
+
keeps.splice(keeps.indexOf(here), 1);
|
|
490
|
+
moves.push({
|
|
491
|
+
type: "move",
|
|
492
|
+
run: here.run,
|
|
493
|
+
from: here.from,
|
|
494
|
+
to: here.to,
|
|
495
|
+
because:
|
|
496
|
+
`an encoder is needed at #${next.from}, which this run would reach only by ` +
|
|
497
|
+
"encoding through; it takes the road up to there and ends by itself"
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return [...stops, ...moves, ...starts, ...keeps];
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* THE OBJECTIVE, as a value that can be compared.
|
|
508
|
+
*
|
|
509
|
+
* THREE COUNTS OF SECONDS, COMPARED IN ORDER. The order is the user's, stated
|
|
510
|
+
* 2026-09-06, and a later count decides only where the earlier ones tie:
|
|
511
|
+
*
|
|
512
|
+
* 1. SECONDS ANYBODY SPENDS LOOKING AT A SPINNER. Nothing outranks it, at any
|
|
513
|
+
* size. Walked forward in film order rather than summed piece by piece: a
|
|
514
|
+
* viewer who is stopped is not watching, so a wait moves every deadline
|
|
515
|
+
* behind it by its own length;
|
|
516
|
+
*
|
|
517
|
+
* 2. WHEN THE FILM IN FRONT OF THE VIEWERS IS FINISHED — the last piece of it to
|
|
518
|
+
* be made, whichever encoder makes it. A stretch no encoder will ever reach
|
|
519
|
+
* counts as never, which is what stops the front being abandoned;
|
|
520
|
+
*
|
|
521
|
+
* 3. WHEN THE WHOLE FILE IS FINISHED — the film behind the viewers included,
|
|
522
|
+
* plus what the swarm pays to fetch anything a second time. Film nobody is
|
|
523
|
+
* waiting for still has value: a viewer seeking back into a part that exists
|
|
524
|
+
* starts playing at once, and seeking back is what people do in the first
|
|
525
|
+
* minutes while they find their place. So spare capacity goes to finishing
|
|
526
|
+
* the file. This is where "the file is encoded WHOLE" lives; it used to be a
|
|
527
|
+
* penalty for film below the lowest encoder, which said the same thing as a
|
|
528
|
+
* patch and said it about one edge of the track only.
|
|
529
|
+
*
|
|
530
|
+
* WHY AN ENCODER MAY STAND BEHIND A VIEWER while film in front is still unmade:
|
|
531
|
+
* encoders work at the same time, so one in front and one behind can finish the
|
|
532
|
+
* file sooner than two in front. Where nobody is stalled and the front is closed
|
|
533
|
+
* just as fast, the file being done sooner is the answer — count 3 deciding a
|
|
534
|
+
* tie in 1 and 2, which is exactly what the order is for.
|
|
535
|
+
*
|
|
536
|
+
* WHY THE COUNTS ARE COMPARED AND NOT ADDED: seconds of somebody waiting and
|
|
537
|
+
* seconds until a distant stretch exists are not the same thing, and no measured
|
|
538
|
+
* quantity says how many of one are worth one of the other. Adding them would
|
|
539
|
+
* mean choosing that exchange rate, which is inventing a number.
|
|
540
|
+
*
|
|
541
|
+
* @param {{ at: number, delaySec: number }[]} bodies - Where each encoder would
|
|
542
|
+
* stand, and how long before it produces anything there: nothing where it is
|
|
543
|
+
* already standing, a move or a start otherwise.
|
|
544
|
+
* @param {import("./CoverageMap.js").CoverageMap} coverage
|
|
545
|
+
* @param {WantedSpan[]} wanted
|
|
546
|
+
* @param {(index: number) => number} untilNeeded
|
|
547
|
+
* @param {number} rate - Segments per second. Always a real figure: this host
|
|
548
|
+
* measures what it encodes at on startup, before any viewer exists, and every
|
|
549
|
+
* run that works then refines it. There is no "unmeasured" case to answer.
|
|
550
|
+
* @param {number} refetchSecPerSegment
|
|
551
|
+
* @param {number} segmentSeconds
|
|
552
|
+
* @returns {{ stall: number, ahead: number, whole: number }} Three counts of
|
|
553
|
+
* seconds, compared in that order by {@link cheaperThan}.
|
|
554
|
+
*/
|
|
555
|
+
function latenessOf(bodies, coverage, wanted, untilNeeded, rate, refetchSecPerSegment, segmentSeconds) {
|
|
556
|
+
const first = Math.min(...wanted.map((span) => span.from));
|
|
557
|
+
const last = Math.max(...wanted.map((span) => span.to));
|
|
558
|
+
// What a number nobody reaches at all counts as. The film's own length is the
|
|
559
|
+
// honest bound — nothing can be later than never — and a finite figure is what
|
|
560
|
+
// lets two hopeless arrangements still be told apart by the rest of the sum.
|
|
561
|
+
const never = (last + 1) * segmentSeconds;
|
|
562
|
+
|
|
563
|
+
// WHICH SIDE OF THE VIEWERS a piece is on. The map states it; nothing here
|
|
564
|
+
// works it out from positions, and nothing here knows where a viewer stands.
|
|
565
|
+
//
|
|
566
|
+
// It was read off the deadline before — no time stated meant behind — and that
|
|
567
|
+
// is true only of a viewer who is playing. A paused viewer has no times
|
|
568
|
+
// anywhere, so their whole film read as behind them, "ahead before behind"
|
|
569
|
+
// had nothing to compare, and the encoder was free to wander to the start of
|
|
570
|
+
// the file. Which side a stretch is on and how soon it is wanted are two
|
|
571
|
+
// different facts, and the map states both.
|
|
572
|
+
const isBehind = (at) => {
|
|
573
|
+
let behind = false;
|
|
574
|
+
for (const span of wanted) {
|
|
575
|
+
if (at < span.from || at > span.to) {
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (span.behind !== true) {
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
behind = true;
|
|
582
|
+
}
|
|
583
|
+
return behind;
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
// EVERY COUNT IS OVER THE FILM, NOT OVER THE ENCODERS. When a piece is made
|
|
587
|
+
// depends on which encoder reaches it soonest, and the encoder that reaches
|
|
588
|
+
// film in front of the viewers may well be standing behind them.
|
|
589
|
+
//
|
|
590
|
+
// Counted over the encoders instead — each charged to the side it stands on —
|
|
591
|
+
// the score had a hole that swallowed everything: an arrangement with every
|
|
592
|
+
// encoder BEHIND the viewers had nothing charged to the film in front, so its
|
|
593
|
+
// second term was zero, which is the best value there is. The plan then
|
|
594
|
+
// abandoned the film in front of a viewer and put both encoders at the start
|
|
595
|
+
// of the file, which is the opposite of the rule it is supposed to obey.
|
|
596
|
+
//
|
|
597
|
+
// AND THE WAITING IS WALKED FORWARD, not summed piece by piece.
|
|
598
|
+
//
|
|
599
|
+
// Not a sum of each piece's own lateness. A viewer who is stopped is not
|
|
600
|
+
// watching, so everything after the piece they are stopped on is needed that
|
|
601
|
+
// much later too: one wait moves every deadline behind it by its own length.
|
|
602
|
+
//
|
|
603
|
+
// Summed independently instead, the far tail of a long file outvoted the film
|
|
604
|
+
// under the viewer's feet — measured, and it placed the only encoder at #114
|
|
605
|
+
// while the viewer stood at #100, because thirteen pieces of certain waiting
|
|
606
|
+
// "cost" less than 886 distant pieces arriving a little later. Walking the
|
|
607
|
+
// clock forward makes that trade impossible: abandoning the near film delays
|
|
608
|
+
// the far film by at least as much.
|
|
609
|
+
let stalled = 0;
|
|
610
|
+
let tardiness = 0;
|
|
611
|
+
let aheadDone = 0;
|
|
612
|
+
let behindDone = 0;
|
|
613
|
+
let wastedSwarm = 0;
|
|
614
|
+
for (let index = first; index <= last; index += 1) {
|
|
615
|
+
// Which encoder gets to this piece first, and when. One standing on it is
|
|
616
|
+
// already there; one behind it must work its way up, re-making anything
|
|
617
|
+
// already made on the way, which costs its own time and the swarm's.
|
|
618
|
+
let soonest = Number.POSITIVE_INFINITY;
|
|
619
|
+
let byWhom = null;
|
|
620
|
+
for (const body of bodies) {
|
|
621
|
+
if (body.at > index) {
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
const arrival = body.delaySec
|
|
625
|
+
+ (index - body.at + 1) / rate
|
|
626
|
+
+ coverage.madeBetween(body.at, index) * refetchSecPerSegment;
|
|
627
|
+
if (arrival < soonest) {
|
|
628
|
+
soonest = arrival;
|
|
629
|
+
byWhom = body;
|
|
630
|
+
}
|
|
277
631
|
}
|
|
632
|
+
if (coverage.isReady(index)) {
|
|
633
|
+
// It exists. Nobody waits for it and nothing is owed — but whoever passes
|
|
634
|
+
// over it makes it a second time, and the swarm fetches its bytes again.
|
|
635
|
+
if (byWhom !== null) {
|
|
636
|
+
wastedSwarm += refetchSecPerSegment;
|
|
637
|
+
}
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
// A piece nobody is working towards arrives never. There is no third case:
|
|
641
|
+
// the host measures what it encodes at, and what it copies at, before any
|
|
642
|
+
// viewer exists, so a speed is always a real number and an arrival can
|
|
643
|
+
// always be computed.
|
|
644
|
+
// Nothing arrives later than never, which is the bound the film's own length
|
|
645
|
+
// gives. It is a definition rather than a guard: it also makes the score
|
|
646
|
+
// total on a host whose startup measured nothing at all, where every arrival
|
|
647
|
+
// is beyond reckoning and every arrangement is therefore equally hopeless.
|
|
648
|
+
const when = byWhom === null ? never : Math.min(soonest, never);
|
|
649
|
+
if (isBehind(index)) {
|
|
650
|
+
behindDone = Math.max(behindDone, when);
|
|
651
|
+
} else {
|
|
652
|
+
aheadDone = Math.max(aheadDone, when);
|
|
653
|
+
}
|
|
654
|
+
const deadline = untilNeeded(index);
|
|
655
|
+
if (Number.isFinite(deadline)) {
|
|
656
|
+
const due = deadline + stalled;
|
|
657
|
+
const waited = Math.max(0, when - due);
|
|
658
|
+
tardiness += waited;
|
|
659
|
+
stalled += waited;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return {
|
|
664
|
+
// 1. SECONDS ANYBODY SPENDS LOOKING AT A SPINNER. Nothing outranks it.
|
|
665
|
+
stall: tardiness,
|
|
666
|
+
// 2. WHEN THE FILM IN FRONT OF THEM IS DONE — the last piece of it to be
|
|
667
|
+
// made, whichever encoder makes it. Film nobody reaches counts as never,
|
|
668
|
+
// which is what stops the front being abandoned.
|
|
669
|
+
ahead: aheadDone,
|
|
670
|
+
// 3. WHEN THE WHOLE FILE IS DONE — the film behind included, and the swarm's
|
|
671
|
+
// price for anything fetched twice, which delays everything.
|
|
672
|
+
whole: Math.max(aheadDone, behindDone) + wastedSwarm
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Is the first arrangement cheaper than the second?
|
|
678
|
+
*
|
|
679
|
+
* Three things in order, stated by the user 2026-09-06: nobody stares at a
|
|
680
|
+
* spinner; then the film in front of the viewers is finished soonest; then the
|
|
681
|
+
* whole file is. A later one decides only where the earlier ones tie.
|
|
682
|
+
*
|
|
683
|
+
* That order is why an encoder may stand BEHIND a viewer while film in front is
|
|
684
|
+
* still unmade: encoders work at the same time, so one in front and one behind
|
|
685
|
+
* can finish the file sooner than two in front — and where nobody is stalled and
|
|
686
|
+
* the front is closed just as fast, the file being done sooner is the answer.
|
|
687
|
+
*
|
|
688
|
+
* @param {{ stall: number, ahead: number, whole: number }} left
|
|
689
|
+
* @param {{ stall: number, ahead: number, whole: number }} right
|
|
690
|
+
* @returns {boolean}
|
|
691
|
+
*/
|
|
692
|
+
function cheaperThan(left, right) {
|
|
693
|
+
if (left.stall !== right.stall) {
|
|
694
|
+
return left.stall < right.stall;
|
|
695
|
+
}
|
|
696
|
+
if (left.ahead !== right.ahead) {
|
|
697
|
+
return left.ahead < right.ahead;
|
|
698
|
+
}
|
|
699
|
+
return left.whole < right.whole;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* How long until a number is needed, read off the map.
|
|
704
|
+
*
|
|
705
|
+
* The map states it per stretch, for the stretch's NEAR EDGE, because a stretch
|
|
706
|
+
* is met at its beginning. Every number inside is needed no sooner than that, so
|
|
707
|
+
* taking the stretch's figure for all of them is the safe reading: it can only
|
|
708
|
+
* make the filling earlier than it has to be, never later.
|
|
709
|
+
*
|
|
710
|
+
* Where the map says nothing, nobody is coming and nothing can be late.
|
|
711
|
+
*
|
|
712
|
+
* Inside a stretch the time GROWS with the distance, because a viewer covers a
|
|
713
|
+
* second of film in a second: the number `n` places past the near edge is
|
|
714
|
+
* reached `n` segments of film later. Taking the near edge's figure for every
|
|
715
|
+
* number inside instead makes a whole stretch due at once — measured while
|
|
716
|
+
* building this: the first stretch is as wide as the measured allowance, so its
|
|
717
|
+
* far end was demanded instantly and an encoder was placed on a number another
|
|
718
|
+
* one was already writing.
|
|
719
|
+
*
|
|
720
|
+
* @param {WantedSpan[]} windows
|
|
721
|
+
* @param {number} segmentSeconds - How much film one number holds.
|
|
722
|
+
* @returns {(index: number) => number}
|
|
723
|
+
*/
|
|
724
|
+
/** @param {WantedSpan[]} windows */
|
|
725
|
+
function firstOf(windows) {
|
|
726
|
+
return Math.min(...windows.map((span) => span.from));
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/** @param {WantedSpan[]} windows */
|
|
730
|
+
function lastOf(windows) {
|
|
731
|
+
return Math.max(...windows.map((span) => span.to));
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function deadlineReaderFor(windows, segmentSeconds) {
|
|
735
|
+
const perSegment = segmentSeconds > 0 ? segmentSeconds : 0;
|
|
736
|
+
return (index) => {
|
|
737
|
+
let soonest = Number.POSITIVE_INFINITY;
|
|
738
|
+
for (const span of windows) {
|
|
739
|
+
if (index < span.from || index > span.to) {
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
const stated = /** @type {{ withinSeconds?: number }} */ (span).withinSeconds;
|
|
743
|
+
// A stretch stated with no time is somebody waiting at its near edge: that
|
|
744
|
+
// is what stating one means. The rest of it grows with the distance, the
|
|
745
|
+
// same as a stated one — read as due all at once instead, a window as wide
|
|
746
|
+
// as a viewer's cushion demanded its far end instantly and bought an
|
|
747
|
+
// encoder to stand beside one already working.
|
|
748
|
+
const within = stated === undefined ? 0 : Number(stated);
|
|
749
|
+
if (!Number.isFinite(within)) {
|
|
750
|
+
// Stated as no time at all: nobody is coming here.
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
const here = within + (index - span.from) * perSegment;
|
|
754
|
+
if (here < soonest) {
|
|
755
|
+
soonest = here;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return soonest;
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* WHERE ENCODERS BELONG, from the model rather than from a list of cases.
|
|
764
|
+
*
|
|
765
|
+
* The problem this solves, stated exactly:
|
|
766
|
+
*
|
|
767
|
+
* - the track is a line of segment numbers; `M` are the ones not made;
|
|
768
|
+
* - each `x` carries a DEADLINE `D(x)`, the seconds until somebody needs it.
|
|
769
|
+
* That is what the priority map is a reading of — a viewer moving forward
|
|
770
|
+
* covers a second of film in a second, so the time until they are at `x` is
|
|
771
|
+
* the distance to it. `Infinity` where nobody is coming;
|
|
772
|
+
* - an encoder is a SEQUENTIAL producer: placed at `a`, it delivers `a + j` at
|
|
773
|
+
* time `(j + 1) / r`, where `r` is segments per second, measured. It cannot
|
|
774
|
+
* skip, so its whole schedule follows from where it starts;
|
|
775
|
+
* - the machine affords `k` of them, measured.
|
|
776
|
+
*
|
|
777
|
+
* Two consequences fall out and need no rule of their own. Placements
|
|
778
|
+
* `a_1 < ... < a_k` PARTITION the line: encoder `i` is useful only on
|
|
779
|
+
* `[a_i, a_{i+1})`, because past that its neighbour got there first. And a
|
|
780
|
+
* segment served by encoder `i` arrives at `(x - a_i + 1) / r`, which is
|
|
781
|
+
* therefore also the answer to "when would the encoder already placed before it
|
|
782
|
+
* get here" — the second half of the comparison, and the half that was missing.
|
|
783
|
+
*
|
|
784
|
+
* `x` is LATE when it arrives after `D(x)`. The objective is no late segments;
|
|
785
|
+
* where `k` does not stretch to that, lateness beginning as far to the right as
|
|
786
|
+
* possible.
|
|
787
|
+
*
|
|
788
|
+
* THE ALGORITHM is first-fit, left to right:
|
|
789
|
+
*
|
|
790
|
+
* for each missing x with a finite deadline, ascending:
|
|
791
|
+
* if some encoder already placed at a satisfies (x - a + 1)/r <= D(x):
|
|
792
|
+
* it covers x
|
|
793
|
+
* else:
|
|
794
|
+
* place an encoder at x
|
|
795
|
+
*
|
|
796
|
+
* A LIVE run enters as an encoder already placed at its own head. There is no
|
|
797
|
+
* special case for it.
|
|
798
|
+
*
|
|
799
|
+
* WHY IT IS OPTIMAL. The leftmost missing number with a finite deadline must be
|
|
800
|
+
* covered by somebody. An encoder placed exactly on it delivers it at the
|
|
801
|
+
* earliest time any placement can, `1/r`, and covers the longest suffix any
|
|
802
|
+
* placement can — starting further left only re-makes material and arrives
|
|
803
|
+
* later, starting further right does not cover it at all. So the greedy choice
|
|
804
|
+
* is never worse than any other, and the usual exchange argument carries it to
|
|
805
|
+
* the whole line. This is the known result for FIXED-ORDER scheduling with
|
|
806
|
+
* deadlines, where first-fit is optimal at unit processing times, and a segment
|
|
807
|
+
* is one unit. General machine minimisation with release times and deadlines is
|
|
808
|
+
* NP-hard; this case is polynomial because the order is forced and each machine
|
|
809
|
+
* covers a contiguous stretch.
|
|
810
|
+
*
|
|
811
|
+
* WHAT WAS TRIED FIRST AND WAS WRONG, kept because each looked reasonable:
|
|
812
|
+
*
|
|
813
|
+
* - `(h - p) * s / (1 - s)`, how long a run stays in front of a viewer moving
|
|
814
|
+
* forward. It answers a different question: a viewer stopped with an empty
|
|
815
|
+
* buffer needs the segment now, and at exactly realtime that formula says
|
|
816
|
+
* "for ever" while the viewer waits thirteen minutes;
|
|
817
|
+
* - whether a run's head lies inside a wanted band — which ties an encoder to
|
|
818
|
+
* whoever is standing there, and this layer must never know that;
|
|
819
|
+
* - the run's head as a barrier, everything above it placeable. It has no time
|
|
820
|
+
* in it at all, so it cannot tell two segments ahead from two hundred.
|
|
821
|
+
*
|
|
822
|
+
* Each was a case, not a model. The deadline is the model.
|
|
823
|
+
*
|
|
824
|
+
* @param {import("./CoverageMap.js").CoverageMap} coverage
|
|
825
|
+
* @param {Set<object>} surviving - Runs that will still be alive, as encoders
|
|
826
|
+
* already placed at their own heads.
|
|
827
|
+
* @param {number} rate - Segments produced per second by one encoder, measured.
|
|
828
|
+
* Zero when nothing has measured it, and then no arrival time can be computed
|
|
829
|
+
* and every claimed number is left alone.
|
|
830
|
+
* @returns {(at: number, bound: number, deadlineAt: (index: number) => number, alsoPlaced?: number[]) => number | null}
|
|
831
|
+
*/
|
|
832
|
+
function gapFinderFor(coverage, surviving, rate, refetchSecPerSegment = 0) {
|
|
833
|
+
/** Encoders already placed: where each stands, and how far its road runs. */
|
|
834
|
+
const placed = [];
|
|
835
|
+
for (const run of surviving) {
|
|
836
|
+
const head = Number(/** @type {{ head?: number }} */ (run).head);
|
|
837
|
+
placed.push({
|
|
838
|
+
at: Number.isFinite(head) ? head : Number(/** @type {{ from: number }} */ (run).from),
|
|
839
|
+
// A live run's road, so that placing inside it can be priced. A run given
|
|
840
|
+
// no end drives to the end of the film, which is what makes the price real.
|
|
841
|
+
// A run given no end drives to the end of the film, which is what makes
|
|
842
|
+
// the price of cutting in front of it real. Written out rather than
|
|
843
|
+
// imported: this file depends on nothing, and that is what lets it be
|
|
844
|
+
// exercised with plain values alone.
|
|
845
|
+
to: Number(/** @type {{ to: number }} */ (run).to) < Number(/** @type {{ from: number }} */ (run).from)
|
|
846
|
+
? Number.POSITIVE_INFINITY
|
|
847
|
+
: Number(/** @type {{ to: number }} */ (run).to)
|
|
848
|
+
});
|
|
278
849
|
}
|
|
850
|
+
return (at, bound, deadlineAt, alsoPlaced) => {
|
|
851
|
+
const start = Number.isInteger(at) && at > 0 ? at : 0;
|
|
852
|
+
const last = Number.isInteger(bound) ? bound : -1;
|
|
853
|
+
// THE LATE NUMBER THAT IS DUE SOONEST, not the leftmost one.
|
|
854
|
+
//
|
|
855
|
+
// With room for every placement the two are the same answer. With a budget
|
|
856
|
+
// that binds they are not, and the objective decides: lateness pushed as far
|
|
857
|
+
// to the right as possible means the soonest deadline is served first. A
|
|
858
|
+
// walk by number gave the one machine to a viewer due in ten minutes while
|
|
859
|
+
// another stood waiting with an empty buffer.
|
|
860
|
+
//
|
|
861
|
+
// Ties go to the smaller number, so the answer does not depend on the order
|
|
862
|
+
// the map happens to be in.
|
|
863
|
+
let best = null;
|
|
864
|
+
let bestDue = Number.POSITIVE_INFINITY;
|
|
865
|
+
for (let index = start; index <= last; index += 1) {
|
|
866
|
+
if (coverage.isReady(index)) {
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
const deadline = deadlineAt(index);
|
|
870
|
+
if (!Number.isFinite(deadline)) {
|
|
871
|
+
// NOBODY IS COMING HERE, so nothing can be late — but the film is still
|
|
872
|
+
// wanted, and this is where spare capacity goes. The number is proposed;
|
|
873
|
+
// whether an encoder is actually spent on it is the score's answer, and
|
|
874
|
+
// the score puts anything anybody is waiting for first.
|
|
875
|
+
return index;
|
|
876
|
+
}
|
|
877
|
+
// When would the SOONEST of those already placed get here? Encoders placed
|
|
878
|
+
// EARLIER IN THIS PASS count: the first one placed for a viewer covers the
|
|
879
|
+
// stretch in front of them, and without counting it the walk placed a
|
|
880
|
+
// second and a third on the very next numbers — three processes a segment
|
|
881
|
+
// apart for one person, which is the waste this model exists to refuse.
|
|
882
|
+
let soonest = Number.POSITIVE_INFINITY;
|
|
883
|
+
for (const a of [...placed.map((live) => live.at), ...(alsoPlaced ?? [])]) {
|
|
884
|
+
if (a > index) {
|
|
885
|
+
// Standing past it. Encoders only move forward, so it never will.
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
if (a === index) {
|
|
889
|
+
// Standing ON it. No placement is faster than the one already made.
|
|
890
|
+
soonest = 0;
|
|
891
|
+
break;
|
|
892
|
+
}
|
|
893
|
+
// WHEN THIS BODY GETS HERE, and both terms of it.
|
|
894
|
+
//
|
|
895
|
+
// Its own encoding of everything between, and the swarm's price for the
|
|
896
|
+
// film it would fetch a SECOND time — every number between that is
|
|
897
|
+
// already made, it makes again. That second term is why "should this
|
|
898
|
+
// encoder drive on or be moved" is not a question of its own: an
|
|
899
|
+
// encoder with three hundred made pieces in front of it is simply slow
|
|
900
|
+
// to arrive, and the model compares arrivals. Asked separately it was a
|
|
901
|
+
// second authority over the same encoder, and the two disagreed.
|
|
902
|
+
const arrival = (index - a + 1) / rate
|
|
903
|
+
+ coverage.madeBetween(a, index) * refetchSecPerSegment;
|
|
904
|
+
if (arrival < soonest) {
|
|
905
|
+
soonest = arrival;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
if (soonest <= deadline) {
|
|
909
|
+
// Somebody gets here in time. Nothing to decide.
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
// IT IS LATE, AND THAT IS ALL THIS DECIDES. Whether filling it is worth
|
|
913
|
+
// the price is not asked here: this only proposes candidates, and the
|
|
914
|
+
// score decides how many of them are taken and by whom. Asked here as
|
|
915
|
+
// well, it was a second cost model beside the objective — with its own
|
|
916
|
+
// idea of what a process costs — and the two disagreed at exactly
|
|
917
|
+
// realtime, where every next piece is marginally late and each looked
|
|
918
|
+
// worth its own encoder.
|
|
919
|
+
if (deadline < bestDue) {
|
|
920
|
+
best = index;
|
|
921
|
+
bestDue = deadline;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return best;
|
|
925
|
+
};
|
|
926
|
+
}
|
|
279
927
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
*
|
|
300
|
-
* is
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
return lowest;
|
|
323
|
-
}
|
|
928
|
+
/**
|
|
929
|
+
* The last number of a stretch that begins at `from` and is `length` long.
|
|
930
|
+
*
|
|
931
|
+
* `-1` when the length is not finite, which is this layer's word for a run with
|
|
932
|
+
* no end: the film's length is not known, so there is nothing to stop it at, and
|
|
933
|
+
* a number invented here would be an end nobody measured.
|
|
934
|
+
*
|
|
935
|
+
* @param {number} from
|
|
936
|
+
* @param {number} length
|
|
937
|
+
* @returns {number}
|
|
938
|
+
*/
|
|
939
|
+
function endOfStretch(from, length) {
|
|
940
|
+
return Number.isFinite(length) ? from + Math.max(1, length) - 1 : -1;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* The lowest number a viewer is waiting for that is not ready — what the plan
|
|
945
|
+
* is judged by.
|
|
946
|
+
*
|
|
947
|
+
* Not used to decide anything: it is the figure a log line carries, so that a
|
|
948
|
+
* plan that keeps producing while a viewer waits is visible rather than
|
|
949
|
+
* inferred.
|
|
950
|
+
*
|
|
951
|
+
* @param {import("./CoverageMap.js").CoverageMap} coverage
|
|
952
|
+
* @param {WantedSpan[]} windows
|
|
953
|
+
* @returns {number | null}
|
|
954
|
+
*/
|
|
955
|
+
export function firstUnmetWant(coverage, windows) {
|
|
956
|
+
let lowest = null;
|
|
957
|
+
for (const span of windows ?? []) {
|
|
958
|
+
for (let at = span.from; at <= span.to; at += 1) {
|
|
959
|
+
if (!coverage.isReady(at)) {
|
|
960
|
+
if (lowest === null || at < lowest) {
|
|
961
|
+
lowest = at;
|
|
962
|
+
}
|
|
963
|
+
break;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return lowest;
|
|
968
|
+
}
|
|
324
969
|
|
|
325
970
|
/**
|
|
326
971
|
* Where to put the encoders this machine can afford.
|
|
@@ -351,87 +996,109 @@ export function firstUnmetWant(coverage, windows) {
|
|
|
351
996
|
* @param {number} params.howMany - What the machine affords.
|
|
352
997
|
* @param {number} params.speedX - Measured. Zero when nothing has measured it,
|
|
353
998
|
* and then only the first requirement can be served.
|
|
999
|
+
* @param {(at: number, bound: number, deadlineAt: (index: number) => number, placed: number[]) => number | null} [params.firstGap] -
|
|
1000
|
+
* Where a gap may be opened. Defaults to the map's own answer; the plan hands
|
|
1001
|
+
* in one that also counts a number a live run has claimed but will not reach
|
|
1002
|
+
* before it is needed, which is the only way anybody beyond a working encoder
|
|
1003
|
+
* is served.
|
|
1004
|
+
* @param {(index: number) => number} [params.deadlineAt] - Seconds until that
|
|
1005
|
+
* number is needed. `Infinity` where nobody is coming. Absent means every
|
|
1006
|
+
* stated want is due now.
|
|
354
1007
|
* @returns {number[]} Where to start each encoder, ascending.
|
|
355
1008
|
*/
|
|
356
|
-
export function placeEncoders({ coverage, windows, howMany,
|
|
1009
|
+
export function placeEncoders({ coverage, windows, howMany, firstGap = null, deadlineAt = null }) {
|
|
357
1010
|
if (!(howMany > 0) || windows.length === 0) {
|
|
358
1011
|
return [];
|
|
359
1012
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const
|
|
1013
|
+
// NOW when the caller says nothing. A stated want with no time is somebody
|
|
1014
|
+
// waiting on it — that is what stating one means — so the honest reading is
|
|
1015
|
+
// that it is due. `Infinity` is a statement in its own right and has to be
|
|
1016
|
+
// made: it says nobody is coming.
|
|
1017
|
+
const untilNeeded = deadlineAt ?? (() => 0);
|
|
1018
|
+
/** Where this pass has placed so far — each one covers what it can reach. */
|
|
1019
|
+
const placedHere = [];
|
|
1020
|
+
const gapAt = firstGap
|
|
1021
|
+
? (at, bound) => firstGap(at, bound, untilNeeded, placedHere)
|
|
1022
|
+
: (at, bound) => coverage.firstGapFrom(at, bound);
|
|
1023
|
+
|
|
365
1024
|
|
|
1025
|
+
// CANDIDATES COME FROM THE PRIORITY MAP, IN THE ORDER THE MAP STATES.
|
|
1026
|
+
//
|
|
1027
|
+
// The map already answers every question that was being re-derived here. Its
|
|
1028
|
+
// ranks say what matters most — the number a viewer is stopped on, then what
|
|
1029
|
+
// is in front of them band by band, then the rest of the track, and last of
|
|
1030
|
+
// all what lies behind them. A pause flattens those ranks; a seek moves them;
|
|
1031
|
+
// a second viewer merges into them. So walking the map in its own order is
|
|
1032
|
+
// what "ahead before behind" means, and nothing here has to work out where the
|
|
1033
|
+
// viewers are.
|
|
1034
|
+
//
|
|
1035
|
+
// It was not read that way. This walked the film by number and proposed
|
|
1036
|
+
// whatever was late, then a second pass divided the leftovers — an order of
|
|
1037
|
+
// its own invention, which put the beginning of the file before the film in
|
|
1038
|
+
// front of a viewer and, at one point, proposed #0, #1 and #2 as three
|
|
1039
|
+
// separate places.
|
|
1040
|
+
//
|
|
1041
|
+
// One candidate per zone: the first number in it nobody has and nobody
|
|
1042
|
+
// reaches in time. Zones with no deadline can have nothing late in them, so
|
|
1043
|
+
// there it is simply the first number nobody has — which is how spare capacity
|
|
1044
|
+
// comes to finish the file.
|
|
366
1045
|
/** @type {number[]} */
|
|
367
1046
|
const places = [];
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
1047
|
+
const byRank = [...windows].sort(
|
|
1048
|
+
(left, right) => (right.priority ?? 0) - (left.priority ?? 0) || left.from - right.from
|
|
1049
|
+
);
|
|
1050
|
+
for (const zone of byRank) {
|
|
1051
|
+
if (places.length >= howMany) {
|
|
1052
|
+
break;
|
|
372
1053
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
// Nothing ahead is missing. Whatever is left anywhere is divided equally.
|
|
380
|
-
return divideEqually(coverage, byUrgency, howMany, places);
|
|
1054
|
+
const at = gapAt(zone.from, zone.to);
|
|
1055
|
+
if (at === null || places.includes(at)) {
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
places.push(at);
|
|
1059
|
+
placedHere.push(at);
|
|
381
1060
|
}
|
|
382
1061
|
|
|
383
|
-
|
|
1062
|
+
// AND WHERE TO SPLIT WHAT IS LEFT, for the capacity the map has not spent.
|
|
1063
|
+
//
|
|
1064
|
+
// The search above proposes only what is LATE, so once one encoder covers a
|
|
1065
|
+
// zone in time that zone proposes nothing more — and a machine that holds four
|
|
1066
|
+
// ran one. Finishing a contiguous stretch soonest with several machines of the
|
|
1067
|
+
// same speed means dividing it between them, which is where these come from:
|
|
1068
|
+
// the widest run of film between two encoders, split.
|
|
1069
|
+
//
|
|
1070
|
+
// Proposing is not spending. The score decides whether another process is
|
|
1071
|
+
// worth it, and its first term — how late the film is — always outranks its
|
|
1072
|
+
// second, so this can never take capacity from somebody waiting.
|
|
384
1073
|
while (places.length < howMany) {
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
1074
|
+
const edges = [...places].sort((left, right) => left - right);
|
|
1075
|
+
let widestFrom = null;
|
|
1076
|
+
let widest = 0;
|
|
1077
|
+
for (let index = 0; index <= edges.length; index += 1) {
|
|
1078
|
+
const from = index === 0 ? firstOf(windows) : edges[index - 1] + 1;
|
|
1079
|
+
const to = index === edges.length ? lastOf(windows) : edges[index] - 1;
|
|
1080
|
+
const room_ = coverage.unmadeRunFrom(from);
|
|
1081
|
+
if (to >= from && room_ > widest) {
|
|
1082
|
+
widest = room_;
|
|
1083
|
+
widestFrom = from + Math.floor(Math.min(room_, to - from + 1) / 2);
|
|
1084
|
+
}
|
|
391
1085
|
}
|
|
392
|
-
|
|
393
|
-
if (next === null || next <= previous) {
|
|
1086
|
+
if (widestFrom === null) {
|
|
394
1087
|
break;
|
|
395
1088
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
return divideEqually(coverage, byUrgency, howMany, places);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Spread whatever encoders are left over the film that is still missing.
|
|
403
|
-
*
|
|
404
|
-
* Equal shares, because equal shares finish together: any other division is
|
|
405
|
-
* finished when its longest share is, which is later.
|
|
406
|
-
*
|
|
407
|
-
* @param {import("./CoverageMap.js").CoverageMap} coverage
|
|
408
|
-
* @param {WantedSpan[]} byUrgency
|
|
409
|
-
* @param {number} howMany
|
|
410
|
-
* @param {number[]} places
|
|
411
|
-
* @returns {number[]}
|
|
412
|
-
*/
|
|
413
|
-
function divideEqually(coverage, byUrgency, howMany, places) {
|
|
414
|
-
for (const span of byUrgency) {
|
|
415
|
-
if (places.length >= howMany) {
|
|
1089
|
+
const at = coverage.firstGapFrom(widestFrom, lastOf(windows));
|
|
1090
|
+
if (at === null || places.includes(at)) {
|
|
416
1091
|
break;
|
|
417
1092
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
continue;
|
|
421
|
-
}
|
|
422
|
-
// Where this band's missing film would be cut if the encoders left over
|
|
423
|
-
// shared it. One share per encoder, and the first share is the gap itself.
|
|
424
|
-
const left = howMany - places.length;
|
|
425
|
-
const width = Math.max(1, Math.floor((span.to - gap + 1) / left));
|
|
426
|
-
for (let at = gap; at <= span.to && places.length < howMany; at += width) {
|
|
427
|
-
const found = coverage.firstGapFrom(at, span.to);
|
|
428
|
-
if (found === null) {
|
|
429
|
-
break;
|
|
430
|
-
}
|
|
431
|
-
if (!places.includes(found)) {
|
|
432
|
-
places.push(found);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
1093
|
+
places.push(at);
|
|
1094
|
+
placedHere.push(at);
|
|
435
1095
|
}
|
|
436
|
-
|
|
1096
|
+
|
|
1097
|
+
// These are CANDIDATES, not decisions. What is late proposes first, because
|
|
1098
|
+
// that is what a viewer feels; what is merely unmade proposes after it. The
|
|
1099
|
+
// score decides which of them are worth a process, and a paused viewer, who
|
|
1100
|
+
// states no deadline at all, therefore still leaves the file being finished
|
|
1101
|
+
// rather than the machine falling idle.
|
|
1102
|
+
return places.sort((left, right) => left - right);
|
|
437
1103
|
}
|
|
1104
|
+
|