@torrent-tv/proxy 2.101.2 → 2.101.3

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/bin/cli.js CHANGED
@@ -90,7 +90,6 @@ program
90
90
  .option("--no-port-mapping", "Disable automatic UPnP/NAT-PMP port mapping")
91
91
  .option("--delivery-sink", "Serve /api/delivery-sink, a torrent-free byte stream for delivery testing")
92
92
  .option("--pin-preset <name>", "Encode every software picture at this libx264 preset only, for runs that must be compared at one quality (off by default)")
93
- .option("--input-stretch-bytes <bytes>", "Size every original-input stretch to this many bytes instead of copy rate times open, for runs that compare input sizes; memory, downloaded bytes and the plan still bound it (off by default)")
94
93
  .option("--heap-snapshots", "Write a heap snapshot of a thread whose memory reaches a new high-water; each one stops that thread while it is written (off by default)")
95
94
  .option("--profile-start <seconds>", "Profile the CPU of the main thread and the torrent thread for this many seconds from the first source opened, written beside the state (off by default)")
96
95
  .option(
@@ -400,7 +399,6 @@ try {
400
399
  profileStartSeconds: Number(options.profileStart) || 0,
401
400
  heapSnapshots: options.heapSnapshots === true,
402
401
  pinnedPreset: typeof options.pinPreset === "string" && options.pinPreset.trim() ? options.pinPreset.trim() : null,
403
- inputStretchBytes: Number(options.inputStretchBytes) > 0 ? Math.floor(Number(options.inputStretchBytes)) : null,
404
402
  // Late-bound the same way `webRtcManager` is below: the torrent pool is
405
403
  // built inside `startProxyServer`, before `dataChannelHandler` — the
406
404
  // thing that actually owns a channel to push down — exists.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.101.2",
3
+ "version": "2.101.3",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
package/server.js CHANGED
@@ -88,6 +88,7 @@ import { takeTurns } from "./utils/take-turns.js";
88
88
  import { completedFilesRoot } from "./services/storage/files/CompletedFiles.js";
89
89
  import { startupCpuProfile } from "./services/storage/cpu-profile.js";
90
90
  import { startProcessorReport } from "./services/storage/processor-report.js";
91
+ import { processorBound } from "./services/storage/machine-processor.js";
91
92
 
92
93
  const require = createRequire(import.meta.url);
93
94
  const { version } = require("./package.json");
@@ -134,9 +135,6 @@ export async function startProxyServer({
134
135
  // The one software preset every picture is encoded at, for runs compared at
135
136
  // one quality; null leaves the choice to the machine.
136
137
  pinnedPreset = null,
137
- // The size every original-input stretch is given in place of copy rate
138
- // times open, for runs that compare input sizes; null keeps the rule.
139
- inputStretchBytes = null,
140
138
  // What viewers' connections carry beyond the film, per byte of film; the
141
139
  // transport counts it and is built after this server, so it is late-bound.
142
140
  serviceShare = () => null,
@@ -234,6 +232,10 @@ export async function startProxyServer({
234
232
  let videoEncoder = transcodeAudio
235
233
  ? await detectVideoEncoder({ ffmpegBin, logger })
236
234
  : null;
235
+ // The cores this process may use (a container's quota or CPU set): the
236
+ // readings below divide out a share of those, the unit every later share of
237
+ // the machine is in (torrent-tv/meta#180, phase 3).
238
+ const processorCores = processorBound().cores;
237
239
  // WHAT THIS HOST DOES, measured before any viewer exists. Every one of these
238
240
  // was gated on the chosen encoder being SOFTWARE, and two of the three are
239
241
  // not about the encoder at all: a host with a GPU decodes in software just
@@ -246,7 +248,7 @@ export async function startProxyServer({
246
248
  // (the rungs are timed on raw frames), but the order keeps the two figures
247
249
  // side by side in the log.
248
250
  const decodeCostModel = transcodeAudio
249
- ? await benchmarkDecodeCost({ ffmpegBin, logger })
251
+ ? await benchmarkDecodeCost({ ffmpegBin, logger, processorCores })
250
252
  : null;
251
253
  // What a second job costs on this host. Measured because the budget adds
252
254
  // independent prices and this host says two jobs that each fit alone do not
@@ -267,16 +269,16 @@ export async function startProxyServer({
267
269
  // the processor and the threads, the device and its driver — which is what
268
270
  // decides whether anything kept from an earlier run still applies.
269
271
  const fingerprint = videoEncoder
270
- ? await readHostFingerprint({ ffmpegBin, encoder: videoEncoder })
272
+ ? await readHostFingerprint({ ffmpegBin, encoder: videoEncoder, processorCores })
271
273
  : null;
272
274
  /** @type {Record<string, object[]>} */
273
275
  const calibratedModes = {};
274
276
  if (videoEncoder) {
275
277
  calibratedModes[videoEncoder.kind] =
276
- (await calibrateEncoder({ ffmpegBin, encoder: videoEncoder, logger })).modes;
278
+ (await calibrateEncoder({ ffmpegBin, encoder: videoEncoder, logger, processorCores })).modes;
277
279
  if (videoEncoder.kind !== "software") {
278
280
  calibratedModes.software =
279
- (await calibrateEncoder({ ffmpegBin, encoder: softwareDescriptor(), logger })).modes;
281
+ (await calibrateEncoder({ ffmpegBin, encoder: softwareDescriptor(), logger, processorCores })).modes;
280
282
  // A device that passed detection's own test and then none of whose modes
281
283
  // passed the product's is not used: detection encodes a synthetic pattern
282
284
  // with arguments of its own, and it is the product's arguments a viewer
@@ -857,7 +859,6 @@ export async function startProxyServer({
857
859
  let sourcePreparation;
858
860
  const outputParts = wireOutputs({
859
861
  pinnedPreset,
860
- inputStretchBytes,
861
862
  indexMemory,
862
863
  serviceShare,
863
864
  readMetadataActivity: () => containerOrchestrator.activity(),
@@ -62,20 +62,10 @@ export class EncodeInputs {
62
62
  * @type {(output: object, from: number, to: number, input: object) => void}
63
63
  */
64
64
  #stretchCopied;
65
- /**
66
- * A stretch size the operator named for comparison runs
67
- * (`--input-stretch-bytes`), in place of copy rate times open; null keeps
68
- * the measured rule. Only the target changes: memory, downloaded bytes and
69
- * the plan's bound still end a stretch (torrent-tv/meta#182).
70
- *
71
- * @type {number | null}
72
- */
73
- #fixedStretchBytes;
74
65
 
75
66
  constructor({ resolve, readRanges, heldRanges = null, reviseBudget, changed, failed, capacity = () => null, urgent = () => true,
76
- rankAt = () => null, openOf = () => null, log = () => {}, now = Date.now, stretchCopied = () => {}, stretchBytes = null }) {
67
+ rankAt = () => null, openOf = () => null, log = () => {}, now = Date.now, stretchCopied = () => {} }) {
77
68
  this.#stretchCopied = stretchCopied;
78
- this.#fixedStretchBytes = Number.isSafeInteger(stretchBytes) && stretchBytes > 0 ? stretchBytes : null;
79
69
  this.#openOf = openOf;
80
70
  this.#resolve = resolve;
81
71
  this.#read = readRanges;
@@ -354,10 +344,6 @@ export class EncodeInputs {
354
344
  #sizingFor(output) {
355
345
  const rate = middleOf(this.#copyRates);
356
346
  const open = this.#openOf(output);
357
- if (this.#fixedStretchBytes !== null) {
358
- return { bytes: this.#fixedStretchBytes, openMeasured: true, openSeconds: open?.seconds ?? null,
359
- copyBytesPerMs: rate, fixed: true };
360
- }
361
347
  return rate === null || !open ? null : { bytes: Math.floor(rate * open.seconds * 1000),
362
348
  openMeasured: open.measured, openSeconds: open.seconds, copyBytesPerMs: rate };
363
349
  }
@@ -621,7 +607,7 @@ export class EncodeInputs {
621
607
  if (result.kind === "result") {
622
608
  this.#log(`encode input selection of ${addressOf(output)}: ${JSON.stringify({
623
609
  from, requestedTo: request.to, serves, floor, targetEnd, chosenEnd: end,
624
- targetBytes: target, targetFixed: sizing?.fixed === true, minimumBytes: minimum, inputBytes: result.bytes,
610
+ targetBytes: target, minimumBytes: minimum, inputBytes: result.bytes,
625
611
  copyBytesPerMs: sizing?.copyBytesPerMs ?? null, openSeconds: sizing?.openSeconds ?? null,
626
612
  openMeasured: sizing?.openMeasured ?? null, reservations, probes
627
613
  })}`);
@@ -41,6 +41,19 @@
41
41
  * `share`, what other processes leave, stays for the download's share of the
42
42
  * proxy's processor (`processor-share.js`), which divides the proxy's room
43
43
  * between its own download and its encoders and must not lose that room twice.
44
+ *
45
+ * THE WHOLE MACHINE IS WHAT THIS PROCESS MAY USE (torrent-tv/meta#180, phase
46
+ * 3). `/proc/stat` and `os.cpus()` describe the host; a container's CPU quota
47
+ * (`cpu.max`, Docker `--cpus`) or a CPU set (`--cpuset-cpus`) gives the proxy
48
+ * fewer cores than that. Where such a bound is smaller than the host, both
49
+ * shares are fractions of the bound instead: the encoders get what the host's
50
+ * other work leaves, but never more than the bound less the proxy's own draw.
51
+ * Measured as fractions of the host, a 1.5-core quota on an 8-core host read
52
+ * a busy neighbour as lost room the quota never had, and the proxy's own draw
53
+ * as an eighth of what it takes from the quota. The startup measurements are
54
+ * divided by the same fraction (`host-load.js` `freeShareDuring`), so a cost
55
+ * filed from either stays in one unit. Without a bound smaller than the host,
56
+ * nothing changes.
44
57
  */
45
58
 
46
59
  /**
@@ -52,10 +65,17 @@
52
65
  * @param {number | null} reading.encoderShare - Our own ffmpeg processes.
53
66
  * @param {number | null} reading.proxyShare - The proxy process itself, every
54
67
  * thread of it.
55
- * @returns {{ share: number, forEncoders: number, unattributed: number, known: boolean }}
68
+ * @param {number | null} [reading.hostCores] - The cores the three shares
69
+ * above are fractions of.
70
+ * @param {number | null} [reading.boundCores] - The cores this process may use
71
+ * (`storage/machine-processor.js` `processorBound`); counts only when
72
+ * smaller than `hostCores`.
73
+ * @returns {{ share: number, forEncoders: number, unattributed: number, known: boolean, boundCores?: number }}
56
74
  * `share` is what other processes leave; `forEncoders` what they and the
57
- * proxy leave. `known` is false when the host does not report its own load —
58
- * then both are 1 and the caller must say it is uncorrected rather than pretend.
75
+ * proxy leave; both of `boundCores` where it is given, else of the whole
76
+ * host. `unattributed` stays a fraction of the host. `known` is false when
77
+ * the host does not report its own load — then both are 1 and the caller
78
+ * must say it is uncorrected rather than pretend.
59
79
  */
60
80
  export function availableShareFrom(reading = {}) {
61
81
  const systemBusy = finite(reading.systemBusy);
@@ -65,11 +85,26 @@ export function availableShareFrom(reading = {}) {
65
85
  return { share: 1, forEncoders: 1, unattributed: 0, known: false };
66
86
  }
67
87
  const encoders = finite(reading.encoderShare) ?? 0;
68
- const ours = encoders + (finite(reading.proxyShare) ?? 0);
88
+ const proxy = finite(reading.proxyShare) ?? 0;
89
+ const ours = encoders + proxy;
69
90
  // Rounding, and the two readings being taken microseconds apart, can put our
70
91
  // own share fractionally above the system total. Below zero is not a
71
92
  // measurement of anything.
72
93
  const unattributed = clamp(systemBusy - ours, 0, 1);
94
+ const hostCores = finite(reading.hostCores);
95
+ const boundCores = finite(reading.boundCores);
96
+ if (hostCores !== null && boundCores !== null && boundCores > 0 && boundCores < hostCores) {
97
+ // In cores: the host's other work takes from the host, the proxy's own
98
+ // draw from the bound as well.
99
+ const leftOnHost = hostCores - clamp(systemBusy - encoders, 0, 1) * hostCores;
100
+ return {
101
+ share: clamp(Math.min(boundCores, hostCores - unattributed * hostCores) / boundCores, 0, 1),
102
+ forEncoders: clamp(Math.min(boundCores - proxy * hostCores, leftOnHost) / boundCores, 0, 1),
103
+ unattributed,
104
+ known: true,
105
+ boundCores
106
+ };
107
+ }
73
108
  return { share: clamp(1 - unattributed, 0, 1), forEncoders: clamp(1 - (systemBusy - encoders), 0, 1),
74
109
  unattributed, known: true };
75
110
  }
@@ -185,6 +185,8 @@ async function checkModeProducesCorrectSegments({ ffmpegBin, encoder, rung, segm
185
185
  * @param {{ info: (m: string) => void, warn: (m: string) => void }} [params.logger]
186
186
  * @param {number} [params.segmentDurationSec]
187
187
  * @param {Array<{ width: number, height: number }>} [params.frames]
188
+ * @param {number | null} [params.processorCores] - The cores this process may
189
+ * use, which the readings' share of the machine is of (`measureEncodeSlope`).
188
190
  * @param {object} [params.measure] - Replaces the three readings, for a check
189
191
  * that must not run ffmpeg: `check`, `rawFrames`, `speed`. `speed` answers a
190
192
  * number, or `{ speed, freeShare }` as `measureEncodeSlope` does.
@@ -196,12 +198,13 @@ export async function calibrateEncoder({
196
198
  logger,
197
199
  segmentDurationSec = 4,
198
200
  frames = CALIBRATION_FRAMES,
201
+ processorCores = null,
199
202
  measure = null
200
203
  }) {
201
204
  const log = logger ?? { info: () => {}, warn: () => {} };
202
205
  const check = measure?.check ?? checkModeProducesCorrectSegments;
203
206
  const rawFrames = measure?.rawFrames ?? ((frame) => decodeToRawFrames(ffmpegBin, log, frame));
204
- const speed = measure?.speed ?? ((rung, rawPath, frame) => measureEncodeSlope(ffmpegBin, encoder, rung, rawPath, frame));
207
+ const speed = measure?.speed ?? ((rung, rawPath, frame) => measureEncodeSlope(ffmpegBin, encoder, rung, rawPath, frame, processorCores));
205
208
  const release = measure?.release ?? ((rawPath) => {
206
209
  try {
207
210
  rmSync(path.dirname(rawPath), { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
@@ -22,7 +22,10 @@
22
22
  * 2. the libx264 build — the version string x264 writes into every stream it
23
23
  * produces (`x264 - core N rNNNN hash`), read from one encoded frame;
24
24
  * 3. the processor model and the thread count the encoder is given
25
- * (`CPU_THREADS`, which is the core count);
25
+ * (`CPU_THREADS`, which is the core count), and the cores this process may
26
+ * use where a container's CPU quota or CPU set makes them fewer: a speed
27
+ * filed as a share of those cores (`available-share.js`) does not hold
28
+ * under another quota (torrent-tv/meta#180);
26
29
  * 4. for a device-backed encoder, the device model and its driver: NVIDIA's
27
30
  * own tool where it exists, and otherwise what the kernel publishes under
28
31
  * `/sys` for the render node or the video device.
@@ -116,7 +119,9 @@ export function configurationKeyOf(fingerprint, encoder) {
116
119
  ...common,
117
120
  `x264=${fingerprint?.x264 ?? UNKNOWN}`,
118
121
  `cpu=${fingerprint?.cpu ?? UNKNOWN}`,
119
- `threads=${fingerprint?.threads ?? UNKNOWN}`
122
+ `threads=${fingerprint?.threads ?? UNKNOWN}`,
123
+ // Only where it binds, so a host without a quota keeps its key.
124
+ ...(Number.isFinite(fingerprint?.boundCores) ? [`bound=${fingerprint.boundCores}`] : [])
120
125
  ].join(";");
121
126
  }
122
127
  const device = fingerprint?.devices?.[encoder?.kind] ?? null;
@@ -137,6 +142,8 @@ export function configurationKeyOf(fingerprint, encoder) {
137
142
  * @property {string} x264
138
143
  * @property {string} cpu
139
144
  * @property {number} threads
145
+ * @property {number | null} [boundCores] - The cores this process may use,
146
+ * where fewer than the host's; absent or null otherwise.
140
147
  * @property {Record<string, DeviceFingerprint>} devices - By encoder kind.
141
148
  */
142
149
 
@@ -288,10 +295,10 @@ async function nvidiaDevice() {
288
295
  * Read this host's fingerprint. Always resolves: an unreadable part is
289
296
  * `unknown`, never a refusal to start.
290
297
  *
291
- * @param {{ ffmpegBin: string, encoder?: { kind: string, device: string | null } | null }} params
298
+ * @param {{ ffmpegBin: string, encoder?: { kind: string, device: string | null } | null, processorCores?: number | null }} params
292
299
  * @returns {Promise<HostFingerprint>}
293
300
  */
294
- export async function readHostFingerprint({ ffmpegBin, encoder = null }) {
301
+ export async function readHostFingerprint({ ffmpegBin, encoder = null, processorCores = null }) {
295
302
  const versionText = await capture(ffmpegBin, ["-hide_banner", "-version"], 5000);
296
303
  // `-hide_banner` does not hide the version line itself: it is the answer.
297
304
  const ffmpeg = parseFfmpegVersion(versionText ? versionText.toString("utf8") : "");
@@ -317,6 +324,7 @@ export async function readHostFingerprint({ ffmpegBin, encoder = null }) {
317
324
  x264,
318
325
  cpu: os.cpus()[0]?.model?.trim() || UNKNOWN,
319
326
  threads: CPU_THREADS,
327
+ boundCores: Number.isFinite(processorCores) && processorCores < os.cpus().length ? processorCores : null,
320
328
  devices
321
329
  };
322
330
  }
@@ -189,14 +189,14 @@ export function shareOfMachine(before, after, cores = os.cpus().length) {
189
189
  * One reading of everything above, to be compared against the next.
190
190
  *
191
191
  * @param {number | null} pid - The encoder's process, when one is running.
192
- * @returns {Promise<{ takenAt: number, processCpuSeconds: number | null, system: CpuTotals | null }>}
192
+ * @returns {Promise<{ takenAt: number, processCpuSeconds: number | null, system: CpuTotals | null, proxyCpuSeconds: number }>}
193
193
  */
194
194
  export async function sampleHost(pid) {
195
195
  const [processCpuSeconds, system] = await Promise.all([
196
196
  pid === null ? Promise.resolve(null) : readProcessCpuSeconds(pid),
197
197
  readSystemCpu()
198
198
  ]);
199
- return { takenAt: Date.now(), processCpuSeconds, system };
199
+ return { takenAt: Date.now(), processCpuSeconds, system, proxyCpuSeconds: readProxyCpuSeconds() };
200
200
  }
201
201
 
202
202
  /**
@@ -215,12 +215,22 @@ export async function sampleHost(pid) {
215
215
  * Null when the host does not report its load (no `/proc`), or a reading was
216
216
  * refused; the measurement is then used as it was read, and says so.
217
217
  *
218
- * @param {{ takenAt: number, processCpuSeconds: number | null, system: object | null } | null} before
219
- * @param {{ takenAt: number, processCpuSeconds: number | null, system: object | null } | null} after
218
+ * Where this process may use fewer cores than the host has (a container's CPU
219
+ * quota or CPU set, `boundCores`), the share is of those cores, as the share a
220
+ * running encoder is filed with (`available-share.js`): what the host's other
221
+ * work leaves, but never more than the bound less this process's own draw.
222
+ * Otherwise the measurement, which ran inside the bound, was divided by a
223
+ * share of the whole host, and a busy neighbour made it read faster than the
224
+ * bound allows (torrent-tv/meta#180, phase 3).
225
+ *
226
+ * @param {{ takenAt: number, processCpuSeconds: number | null, system: object | null, proxyCpuSeconds?: number | null } | null} before
227
+ * @param {{ takenAt: number, processCpuSeconds: number | null, system: object | null, proxyCpuSeconds?: number | null } | null} after
220
228
  * @param {number} [cores]
229
+ * @param {number | null} [boundCores] - The cores this process may use; counts
230
+ * only when fewer than `cores`.
221
231
  * @returns {number | null} Between 0 and 1.
222
232
  */
223
- export function freeShareDuring(before, after, cores = os.cpus().length) {
233
+ export function freeShareDuring(before, after, cores = os.cpus().length, boundCores = null) {
224
234
  if (!before || !after) {
225
235
  return null;
226
236
  }
@@ -229,10 +239,28 @@ export function freeShareDuring(before, after, cores = os.cpus().length) {
229
239
  return null;
230
240
  }
231
241
  const others = Math.min(1, Math.max(0, shares.systemShare - shares.processShare));
232
- const free = 1 - others;
242
+ const free = Number.isFinite(boundCores) && boundCores > 0 && boundCores < cores
243
+ ? Math.min(1, (cores - others * cores) / boundCores, (boundCores - proxyCoresBetween(before, after)) / boundCores)
244
+ : 1 - others;
233
245
  return free > 0 ? free : null;
234
246
  }
235
247
 
248
+ /**
249
+ * The cores this process drew between two readings; 0 where either has no
250
+ * reading of it.
251
+ *
252
+ * @param {{ takenAt: number, proxyCpuSeconds?: number | null }} before
253
+ * @param {{ takenAt: number, proxyCpuSeconds?: number | null }} after
254
+ * @returns {number}
255
+ */
256
+ function proxyCoresBetween(before, after) {
257
+ const elapsedSec = (after.takenAt - before.takenAt) / 1000;
258
+ if (!(elapsedSec > 0) || !Number.isFinite(before.proxyCpuSeconds) || !Number.isFinite(after.proxyCpuSeconds)) {
259
+ return 0;
260
+ }
261
+ return Math.max(0, (after.proxyCpuSeconds - before.proxyCpuSeconds) / elapsedSec);
262
+ }
263
+
236
264
  /**
237
265
  * How much CPU THIS process has used, across every thread it owns.
238
266
  *
@@ -679,13 +679,13 @@ export async function benchmarkContention({ ffmpegBin, logger, clipsDir = CALIBR
679
679
  return penalties;
680
680
  }
681
681
 
682
- export async function benchmarkDecodeCost({ ffmpegBin, logger, clipsDir = CALIBRATION_DIR }) {
682
+ export async function benchmarkDecodeCost({ ffmpegBin, logger, clipsDir = CALIBRATION_DIR, processorCores = null }) {
683
683
  const log = logger ?? { info: () => {}, warn: () => {} };
684
684
  const startedAllAt = Date.now();
685
685
  /** @type {Record<string, { pixelTerm: number, bitrateTerm: number, constantTerm: number }>} */
686
686
  const families = {};
687
687
  for (const [family, clips] of Object.entries(CALIBRATION_SETS)) {
688
- const fitted = await fitOneFamily({ ffmpegBin, log, clipsDir, family, clips });
688
+ const fitted = await fitOneFamily({ ffmpegBin, log, clipsDir, family, clips, processorCores });
689
689
  if (fitted) {
690
690
  families[family] = fitted;
691
691
  }
@@ -721,10 +721,10 @@ export async function benchmarkDecodeCost({ ffmpegBin, logger, clipsDir = CALIBR
721
721
  /**
722
722
  * Fit one codec family's decode cost from its own clips.
723
723
  *
724
- * @param {{ ffmpegBin: string, log: { info: Function, warn: Function }, clipsDir: string, family: string, clips: string[] }} params
724
+ * @param {{ ffmpegBin: string, log: { info: Function, warn: Function }, clipsDir: string, family: string, clips: string[], processorCores?: number | null }} params
725
725
  * @returns {Promise<{ pixelTerm: number, bitrateTerm: number, constantTerm: number } | null>}
726
726
  */
727
- async function fitOneFamily({ ffmpegBin, log, clipsDir, family, clips }) {
727
+ async function fitOneFamily({ ffmpegBin, log, clipsDir, family, clips, processorCores = null }) {
728
728
  const startedAllAt = Date.now();
729
729
  /** @type {Array<{ megapixelsPerSecond: number, megabitsPerSecond: number, costSecondsPerSecond: number }>} */
730
730
  const samples = [];
@@ -745,7 +745,7 @@ async function fitOneFamily({ ffmpegBin, log, clipsDir, family, clips }) {
745
745
  }
746
746
  for (const [index, clip] of clips.entries()) {
747
747
  const stream = streams[index];
748
- const measured = stream ? await decodePipedStream(ffmpegBin, stream, log) : null;
748
+ const measured = stream ? await decodePipedStream(ffmpegBin, stream, log, processorCores) : null;
749
749
  if (!measured?.speed) {
750
750
  log.warn(
751
751
  `hwaccel: decode benchmark "${clip}" said nothing; ${family} not measured` +
@@ -998,9 +998,11 @@ function runCapturingStderr(ffmpegBin, args, timeoutMs) {
998
998
  *
999
999
  * @param {string} ffmpegBin
1000
1000
  * @param {{ bytes: Buffer, demuxer: string, megapixelsPerSecond: number, megabitsPerSecond: number, fps: number }} stream
1001
+ * @param {{ info: Function, warn: Function }} [log]
1002
+ * @param {number | null} [processorCores] - The cores this process may use, for `freeShareDuring`.
1001
1003
  * @returns {Promise<{ speed: number, windowSec: number, megapixelsPerSecond: number, megabitsPerSecond: number } | null>}
1002
1004
  */
1003
- function decodePipedStream(ffmpegBin, stream, log = { info: () => {}, warn: () => {} }) {
1005
+ function decodePipedStream(ffmpegBin, stream, log = { info: () => {}, warn: () => {} }, processorCores = null) {
1004
1006
  return new Promise((resolve) => {
1005
1007
  const args = [
1006
1008
  "-hide_banner", "-loglevel", "error", "-nostats",
@@ -1047,7 +1049,7 @@ function decodePipedStream(ffmpegBin, stream, log = { info: () => {}, warn: () =
1047
1049
  const hostAfter = hostBefore !== null && child?.pid && child.exitCode === null
1048
1050
  ? await sampleHost(child.pid).catch(() => null)
1049
1051
  : null;
1050
- const freeShare = freeShareDuring(hostBefore === null ? null : await hostBefore, hostAfter);
1052
+ const freeShare = freeShareDuring(hostBefore === null ? null : await hostBefore, hostAfter, undefined, processorCores);
1051
1053
  try {
1052
1054
  child?.stdin?.destroy();
1053
1055
  } catch {
@@ -1418,9 +1420,13 @@ export function slopeOf(samples, minimumWindowSec = ENCODE_BENCHMARK_WINDOW_SEC)
1418
1420
  * A hardware encoder is limited by its device and not by the cores, so its
1419
1421
  * reading is kept as read.
1420
1422
  *
1423
+ * `processorCores` is the cores this process may use (a container's quota or
1424
+ * CPU set); the share divided out is of those where they are fewer than the
1425
+ * host's (`freeShareDuring`, torrent-tv/meta#180).
1426
+ *
1421
1427
  * @returns {Promise<{ speed: number, measuredSpeed: number, freeShare: number | null } | null>}
1422
1428
  */
1423
- export function measureEncodeSlope(ffmpegBin, encoder, rung, rawFramesPath, frame = { width: BENCHMARK_REF_W, height: BENCHMARK_REF_H }) {
1429
+ export function measureEncodeSlope(ffmpegBin, encoder, rung, rawFramesPath, frame = { width: BENCHMARK_REF_W, height: BENCHMARK_REF_H }, processorCores = null) {
1424
1430
  const cpuBound = encoder?.kind === "software";
1425
1431
  return new Promise((resolve) => {
1426
1432
  const args = [
@@ -1470,7 +1476,7 @@ export function measureEncodeSlope(ffmpegBin, encoder, rung, rawFramesPath, fram
1470
1476
  resolve(null);
1471
1477
  return;
1472
1478
  }
1473
- const freeShare = freeShareDuring(before, after);
1479
+ const freeShare = freeShareDuring(before, after, undefined, processorCores);
1474
1480
  resolve({ speed: freeShare === null ? value : value / freeShare, measuredSpeed: value, freeShare });
1475
1481
  });
1476
1482
  };
@@ -593,6 +593,7 @@ export class HostLoad {
593
593
  // What the offer will multiply a prediction by, in the same line as the
594
594
  // readings it comes from.
595
595
  ` available=${asPercent(this.hostAvailability.share)} for-encoders=${asPercent(this.hostAvailability.forEncoders)}` +
596
+ (Number.isFinite(this.hostAvailability.boundCores) ? ` of ${this.hostAvailability.boundCores.toFixed(2)} cores` : "") +
596
597
  ` free=${asPercent(free)}` + processor + late
597
598
  );
598
599
  }
@@ -664,7 +665,7 @@ export class HostLoad {
664
665
  /**
665
666
  * @param {{ elapsedSec: number, processShare: number | null, systemShare: number | null, rejected?: string[] } | null} share
666
667
  * @param {{ proxyCpuSeconds: number | null }} previous
667
- * @param {{ proxyCpuSeconds: number | null }} sample
668
+ * @param {{ proxyCpuSeconds: number | null, processor?: { bound?: { cores: number } | null } | null }} sample
668
669
  * @returns {number | null} The proxy's own share of the machine, for the log.
669
670
  */
670
671
  #setAvailability(share, previous, sample) {
@@ -682,10 +683,14 @@ export class HostLoad {
682
683
  const proxyShare = Number.isFinite(previous.proxyCpuSeconds)
683
684
  ? (sample.proxyCpuSeconds - previous.proxyCpuSeconds) / (share.elapsedSec * cores)
684
685
  : null;
686
+ // Of the cores this process may use where that is fewer than the host's
687
+ // (torrent-tv/meta#180, phase 3).
685
688
  this.hostAvailability = availableShareFrom({
686
689
  systemBusy: share.systemShare,
687
690
  encoderShare: share.processShare,
688
- proxyShare
691
+ proxyShare,
692
+ hostCores: cores,
693
+ boundCores: sample.processor?.bound?.cores ?? null
689
694
  });
690
695
  return proxyShare;
691
696
  }
@@ -197,9 +197,6 @@ export function wireOutputs({
197
197
  // A software preset every picture is encoded at, for runs compared at one
198
198
  // quality (`--pin-preset`); null leaves the choice to the machine.
199
199
  pinnedPreset = null,
200
- // A size every original-input stretch is given in place of copy rate
201
- // times open (`--input-stretch-bytes`); null keeps the rule.
202
- inputStretchBytes = null,
203
200
  // What every encoder this host may use was qualified and measured to do at
204
201
  // startup, by kind (`encode/calibration.js`). Takes the place of the one
205
202
  // array above, which a test may still hand in on its own.
@@ -1117,7 +1114,6 @@ export function wireOutputs({
1117
1114
  readRanges: readEncodeRanges,
1118
1115
  heldRanges: readEncodeHeldRanges,
1119
1116
  stretchCopied,
1120
- stretchBytes: inputStretchBytes,
1121
1117
  reviseBudget: () => parts.machineBudget.revise("memory", "admitted encode inputs"),
1122
1118
  capacity: () => parts.machineBudget.capacityOf("memory"),
1123
1119
  // How long this output's input takes to open, from the one owner of start
@@ -1141,9 +1137,6 @@ export function wireOutputs({
1141
1137
  parts.serving.invalidateWaits(output);
1142
1138
  }
1143
1139
  }) : null);
1144
- if (parts.encodeInputs && inputStretchBytes) {
1145
- logger.info(`encode input: every original-input stretch sized to ${inputStretchBytes} bytes (--input-stretch-bytes)`);
1146
- }
1147
1140
  // One owner of the disk, and the list of what takes it lives with the owner.
1148
1141
  parts.machineBudget = wireMachineBudget({
1149
1142
  segmentStore: parts.segmentStore,