@torrent-tv/proxy 2.31.0 → 2.33.0
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 +10 -0
- package/assets/calibration/NOTICE.md +49 -16
- package/assets/calibration/cal-h264-1080-hi.mp4 +0 -0
- package/assets/calibration/cal-h264-1080-lo.mp4 +0 -0
- package/assets/calibration/cal-h264-480-hi.mp4 +0 -0
- package/assets/calibration/cal-h264-480-lo.mp4 +0 -0
- package/assets/calibration/cal-h264-720-hi.mp4 +0 -0
- package/assets/calibration/cal-h264-720-lo.mp4 +0 -0
- package/package.json +1 -1
- package/services/available-share.js +89 -0
- package/services/decode-cost-fit.js +331 -0
- package/services/hls-session-manager.js +72 -4
- package/services/hwaccel.js +35 -100
- package/test/available-share.test.js +76 -0
- package/test/decode-cost-fit.test.js +145 -0
- package/test/decode-cost.test.js +347 -347
- package/assets/calibration/cal-1080-hi.mp4 +0 -0
- package/assets/calibration/cal-1080-lo.mp4 +0 -0
- package/assets/calibration/cal-720.mp4 +0 -0
|
@@ -20,6 +20,7 @@ import { logger } from "../utils/logger.js";
|
|
|
20
20
|
import { readKeyframeIndex } from "./container-index/index.js";
|
|
21
21
|
import { readMachineState, readProcessCpuSeconds, readProxyCpuSeconds, readSystemCpu, shareOfMachine } from "./host-load.js";
|
|
22
22
|
import { speedFromReadings } from "./encoder-readings.js";
|
|
23
|
+
import { availableShareFrom, correctForAvailability } from "./available-share.js";
|
|
23
24
|
import {
|
|
24
25
|
ENCODE_RUN_EVENT,
|
|
25
26
|
ENCODE_RUN_STATE,
|
|
@@ -1902,6 +1903,12 @@ export class HlsSessionManager {
|
|
|
1902
1903
|
// software hosts, else the client target). 0 = keep source.
|
|
1903
1904
|
encodeWidth,
|
|
1904
1905
|
encodeHeight,
|
|
1906
|
+
// What the offer predicted this height would do on this machine, so the
|
|
1907
|
+
// field can say what the prediction was worth once the step runs. Null
|
|
1908
|
+
// when the step was never judged — a copied stream needs no encoder and
|
|
1909
|
+
// is never predicted.
|
|
1910
|
+
predictedSpeedWhenOffered: this.lastPredictedByHeight?.get(encodeHeight) ?? null,
|
|
1911
|
+
lastPredictionRatio: null,
|
|
1905
1912
|
// The NAME of this rung, fixed at the height that was asked for. It is
|
|
1906
1913
|
// deliberately not the height being encoded: a viewer who picked 480p on
|
|
1907
1914
|
// a host that then starts them at 360p, or steps down to it later, goes
|
|
@@ -3217,13 +3224,25 @@ export class HlsSessionManager {
|
|
|
3217
3224
|
const proxyShare = Number.isFinite(previous.proxyCpuSeconds)
|
|
3218
3225
|
? (sample.proxyCpuSeconds - previous.proxyCpuSeconds) / (share.elapsedSec * cores)
|
|
3219
3226
|
: null;
|
|
3227
|
+
// Kept for the quality offer, which predicts from a benchmark taken on a
|
|
3228
|
+
// QUIET host: the same reading that is printed here says how much of the
|
|
3229
|
+
// machine a new encoder could actually have. Only what nobody has been
|
|
3230
|
+
// charged for is subtracted — see `available-share.js`.
|
|
3231
|
+
this.hostAvailability = availableShareFrom({
|
|
3232
|
+
systemBusy: share.systemShare,
|
|
3233
|
+
encoderShare: share.processShare,
|
|
3234
|
+
proxyShare
|
|
3235
|
+
});
|
|
3220
3236
|
logger.info(
|
|
3221
3237
|
`host-load: ffmpeg=${asPercent(share.processShare)} proxy=${asPercent(proxyShare)} ` +
|
|
3222
3238
|
`system=${asPercent(share.systemShare)} ` +
|
|
3223
3239
|
`iowait=${asPercent(share.iowaitShare)} cpu=${machine.megahertz === null ? "n/a" : `${machine.megahertz}MHz`} ` +
|
|
3224
3240
|
`temp=${machine.celsius === null ? "n/a" : `${machine.celsius}C`} ` +
|
|
3225
3241
|
`encoders=${running} running` + (suspended > 0 ? ` +${suspended} suspended` : "") +
|
|
3226
|
-
` over=${share.elapsedSec.toFixed(1)}s`
|
|
3242
|
+
` over=${share.elapsedSec.toFixed(1)}s` +
|
|
3243
|
+
// What the offer will multiply a prediction by, in the same line as the
|
|
3244
|
+
// readings it comes from.
|
|
3245
|
+
` available=${asPercent(this.hostAvailability.share)}`
|
|
3227
3246
|
);
|
|
3228
3247
|
}
|
|
3229
3248
|
|
|
@@ -3391,6 +3410,10 @@ export class HlsSessionManager {
|
|
|
3391
3410
|
// although nothing is producing that step any more. The next reading of the
|
|
3392
3411
|
// new encode replaces it.
|
|
3393
3412
|
session.lastAloneSpeed = null;
|
|
3413
|
+
// And so is the prediction it was compared against: it described the step
|
|
3414
|
+
// this session has just left.
|
|
3415
|
+
session.predictedSpeedWhenOffered = this.lastPredictedByHeight?.get(rung.height) ?? null;
|
|
3416
|
+
session.lastPredictionRatio = null;
|
|
3394
3417
|
// Priced the same way the offer and the starting rung are. Choosing the
|
|
3395
3418
|
// preset on the encoder alone treats decoding as free, which is how the
|
|
3396
3419
|
// check and the encode came to disagree in the first place — and here it
|
|
@@ -5444,6 +5467,25 @@ export class HlsSessionManager {
|
|
|
5444
5467
|
// 0.3x reads as 3.33 s of work per second of video, more than the machine
|
|
5445
5468
|
// has — and every other quality step was refused on the download's account.
|
|
5446
5469
|
session.lastAloneSpeed = speed;
|
|
5470
|
+
// What the offer predicted for this very step, against what it then did
|
|
5471
|
+
// with the machine to itself. The prediction is corrected for the share of
|
|
5472
|
+
// the machine that was free at the time, so this ratio is the error that
|
|
5473
|
+
// remains AFTER that correction — which is the only way to tell whether a
|
|
5474
|
+
// stage of roadmap item 3 moved anything. Written when it changes by more
|
|
5475
|
+
// than a tenth, so a steady step says it once rather than every five
|
|
5476
|
+
// seconds.
|
|
5477
|
+
if (Number.isFinite(session.predictedSpeedWhenOffered) && session.predictedSpeedWhenOffered > 0) {
|
|
5478
|
+
const ratio = speed / session.predictedSpeedWhenOffered;
|
|
5479
|
+
const lastSaid = session.lastPredictionRatio;
|
|
5480
|
+
if (!Number.isFinite(lastSaid) || Math.abs(ratio - lastSaid) > 0.1) {
|
|
5481
|
+
session.lastPredictionRatio = ratio;
|
|
5482
|
+
logger.info(
|
|
5483
|
+
`prediction ${session.id.slice(0, 8)} ${session.encodeHeight || "source"}p: ` +
|
|
5484
|
+
`predicted ${session.predictedSpeedWhenOffered.toFixed(2)}x, measured ${speed.toFixed(2)}x ` +
|
|
5485
|
+
`(ratio ${ratio.toFixed(2)}; 1.00 would mean the arithmetic describes this machine)`
|
|
5486
|
+
);
|
|
5487
|
+
}
|
|
5488
|
+
}
|
|
5447
5489
|
if (kind === "audio") {
|
|
5448
5490
|
// What is left after the work that was already accounted for. `null` when
|
|
5449
5491
|
// the subtraction leaves nothing positive, which means the reading says
|
|
@@ -5962,6 +6004,12 @@ export class HlsSessionManager {
|
|
|
5962
6004
|
const kept = [];
|
|
5963
6005
|
/** @type {string[]} */
|
|
5964
6006
|
const dropped = [];
|
|
6007
|
+
// What each height was predicted to do on THIS machine, kept so a session
|
|
6008
|
+
// started at that height can be compared against it once it runs. The
|
|
6009
|
+
// manager holds the last answer, because the offer is computed on the path
|
|
6010
|
+
// that serves every request while a session is created elsewhere.
|
|
6011
|
+
/** @type {Map<number, number | null>} */
|
|
6012
|
+
const predictedByHeight = new Map();
|
|
5965
6013
|
for (const height of heights) {
|
|
5966
6014
|
// The height an encoder is ALREADY producing, and the source's own height
|
|
5967
6015
|
// when the FAMILY serves it by copy — neither has to be predicted,
|
|
@@ -6004,7 +6052,7 @@ export class HlsSessionManager {
|
|
|
6004
6052
|
0,
|
|
6005
6053
|
concurrentCostSec - (runningCostByHeight?.get(height) ?? 0)
|
|
6006
6054
|
);
|
|
6007
|
-
const { speed
|
|
6055
|
+
const { speed } = canSustainOutput({
|
|
6008
6056
|
benchmark,
|
|
6009
6057
|
decodeModel: this.decodeCostModel,
|
|
6010
6058
|
source,
|
|
@@ -6012,11 +6060,24 @@ export class HlsSessionManager {
|
|
|
6012
6060
|
observedDecodeCostSec,
|
|
6013
6061
|
concurrentCostSec: concurrentBesideThis
|
|
6014
6062
|
});
|
|
6015
|
-
|
|
6063
|
+
// The benchmark behind that figure was taken on a QUIET host — one
|
|
6064
|
+
// ffmpeg and nothing else. The machine a step will actually run on is
|
|
6065
|
+
// also running the kernel, the container and whatever else its owner
|
|
6066
|
+
// does, and on the addon host that was measured at 99 % busy with a
|
|
6067
|
+
// quarter of it unattributed. Only the unattributed part is charged
|
|
6068
|
+
// here: our own encoders are already in `concurrentBesideThis` and the
|
|
6069
|
+
// proxy's own work is already priced per megabyte moved.
|
|
6070
|
+
const onThisMachine = correctForAvailability(speed, this.hostAvailability);
|
|
6071
|
+
// Kept against the step's own session, so that when it runs the field
|
|
6072
|
+
// says what the prediction was worth. Without this the only comparison
|
|
6073
|
+
// available is between two figures written minutes apart in different
|
|
6074
|
+
// lines of the log.
|
|
6075
|
+
predictedByHeight.set(height, onThisMachine);
|
|
6076
|
+
if (onThisMachine !== null && onThisMachine >= REALTIME_SPEED_MARGIN) {
|
|
6016
6077
|
kept.push(height);
|
|
6017
6078
|
continue;
|
|
6018
6079
|
}
|
|
6019
|
-
dropped.push(`${height}p=${
|
|
6080
|
+
dropped.push(`${height}p=${onThisMachine === null ? "n/a" : `${onThisMachine.toFixed(2)}x`}`);
|
|
6020
6081
|
}
|
|
6021
6082
|
// Written when the ANSWER changes, not when the answer is recomputed. This
|
|
6022
6083
|
// is asked on the path that serves every playlist, init and segment, and
|
|
@@ -6026,12 +6087,19 @@ export class HlsSessionManager {
|
|
|
6026
6087
|
if (dropped.length > 0) {
|
|
6027
6088
|
const line =
|
|
6028
6089
|
`transcode: not offering ${dropped.join(" ")} — below realtime × ${REALTIME_SPEED_MARGIN} ` +
|
|
6090
|
+
// Said with the figures, because a step refused on a busy machine and
|
|
6091
|
+
// one refused on an idle machine are different facts about the host.
|
|
6092
|
+
(this.hostAvailability?.known
|
|
6093
|
+
? `on a machine with ${Math.round(this.hostAvailability.share * 100)}% to spare `
|
|
6094
|
+
: "") +
|
|
6029
6095
|
`(offering ${kept.map((height) => `${height}p`).join(" ")})`;
|
|
6030
6096
|
if (line !== this.#lastOfferLine) {
|
|
6031
6097
|
this.#lastOfferLine = line;
|
|
6032
6098
|
logger.info(line);
|
|
6033
6099
|
}
|
|
6100
|
+
this.lastPredictedByHeight = predictedByHeight;
|
|
6034
6101
|
} else {
|
|
6102
|
+
this.lastPredictedByHeight = predictedByHeight;
|
|
6035
6103
|
this.#lastOfferLine = "";
|
|
6036
6104
|
}
|
|
6037
6105
|
return kept;
|
package/services/hwaccel.js
CHANGED
|
@@ -24,6 +24,7 @@ import { spawn } from "node:child_process";
|
|
|
24
24
|
import { mkdtempSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
25
25
|
import os from "node:os";
|
|
26
26
|
import path from "node:path";
|
|
27
|
+
import { fitDecodeCost } from "./decode-cost-fit.js";
|
|
27
28
|
import { fileURLToPath } from "node:url";
|
|
28
29
|
import {
|
|
29
30
|
parseFfmpegBitrateKbps,
|
|
@@ -670,45 +671,28 @@ export async function detectTonemapSupport({ ffmpegBin, logger }) {
|
|
|
670
671
|
return supported;
|
|
671
672
|
}
|
|
672
673
|
|
|
673
|
-
/**
|
|
674
|
-
* Solve a 3×3 linear system by Gaussian elimination with partial pivoting.
|
|
675
|
-
*
|
|
676
|
-
* @param {number[][]} rows - Three rows of [c0, c1, c2, rhs].
|
|
677
|
-
* @returns {number[] | null} The three unknowns, or null when singular.
|
|
678
|
-
*/
|
|
679
|
-
function solveLinear3(rows) {
|
|
680
|
-
const m = rows.map((row) => [...row]);
|
|
681
|
-
for (let col = 0; col < 3; col += 1) {
|
|
682
|
-
let pivot = col;
|
|
683
|
-
for (let row = col + 1; row < 3; row += 1) {
|
|
684
|
-
if (Math.abs(m[row][col]) > Math.abs(m[pivot][col])) {
|
|
685
|
-
pivot = row;
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
if (Math.abs(m[pivot][col]) < 1e-12) {
|
|
689
|
-
return null;
|
|
690
|
-
}
|
|
691
|
-
[m[col], m[pivot]] = [m[pivot], m[col]];
|
|
692
|
-
for (let row = 0; row < 3; row += 1) {
|
|
693
|
-
if (row === col) {
|
|
694
|
-
continue;
|
|
695
|
-
}
|
|
696
|
-
const factor = m[row][col] / m[col][col];
|
|
697
|
-
for (let k = col; k < 4; k += 1) {
|
|
698
|
-
m[row][k] -= factor * m[col][k];
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
return [m[0][3] / m[0][0], m[1][3] / m[1][1], m[2][3] / m[2][2]];
|
|
703
|
-
}
|
|
704
674
|
|
|
705
|
-
// The clips the decode cost is
|
|
675
|
+
// The clips the decode cost is fitted from. They ship with the package
|
|
706
676
|
// (`assets/calibration/`), cut from Netflix Open Content "Meridian" (CC-BY 4.0)
|
|
707
677
|
// — real, grainy live action, because a generated `testsrc2` clip decodes 158 %
|
|
708
|
-
// away from a real film where these are 11 % away (measured 2026-08-14).
|
|
709
|
-
//
|
|
710
|
-
//
|
|
711
|
-
|
|
678
|
+
// away from a real film where these are 11 % away (measured 2026-08-14).
|
|
679
|
+
//
|
|
680
|
+
// Three sizes at two bitrates each, with the axes varied INDEPENDENTLY. The set
|
|
681
|
+
// this replaced was three clips for three unknowns, two of them at the same
|
|
682
|
+
// size: an exact system, which cannot fail visibly. On 2026-08-17 it returned
|
|
683
|
+
// `0.007542 × Mpx/s + 0.000000 × Mbit/s + 0.0000 s/s` — the bitrate term and
|
|
684
|
+
// the constant exactly zero — and the prediction on top of it was 1.8-2.2x
|
|
685
|
+
// optimistic. Six points leave three spare, so the fit has a residual, and a
|
|
686
|
+
// term the data does not determine can be refused instead of published as a
|
|
687
|
+
// zero that looks measured. See `assets/calibration/NOTICE.md`.
|
|
688
|
+
const CALIBRATION_CLIPS = [
|
|
689
|
+
"cal-h264-1080-hi.mp4",
|
|
690
|
+
"cal-h264-1080-lo.mp4",
|
|
691
|
+
"cal-h264-720-hi.mp4",
|
|
692
|
+
"cal-h264-720-lo.mp4",
|
|
693
|
+
"cal-h264-480-hi.mp4",
|
|
694
|
+
"cal-h264-480-lo.mp4"
|
|
695
|
+
];
|
|
712
696
|
const CALIBRATION_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "assets", "calibration");
|
|
713
697
|
// How wide the measured window must be before the slope is trusted, and how
|
|
714
698
|
// long to wait for it at most. A second of decoding is thousands of frames on a
|
|
@@ -770,8 +754,8 @@ function parseClipCharacteristics(stderr) {
|
|
|
770
754
|
export async function benchmarkDecodeCost({ ffmpegBin, logger, clipsDir = CALIBRATION_DIR }) {
|
|
771
755
|
const log = logger ?? { info: () => {}, warn: () => {} };
|
|
772
756
|
const startedAllAt = Date.now();
|
|
773
|
-
/** @type {number
|
|
774
|
-
const
|
|
757
|
+
/** @type {Array<{ megapixelsPerSecond: number, megabitsPerSecond: number, costSecondsPerSecond: number }>} */
|
|
758
|
+
const samples = [];
|
|
775
759
|
for (const clip of CALIBRATION_CLIPS) {
|
|
776
760
|
const measured = await measureDecodeSlope(ffmpegBin, path.join(clipsDir, clip));
|
|
777
761
|
if (!measured) {
|
|
@@ -779,21 +763,31 @@ export async function benchmarkDecodeCost({ ffmpegBin, logger, clipsDir = CALIBR
|
|
|
779
763
|
return null;
|
|
780
764
|
}
|
|
781
765
|
const cost = 1 / measured.speed;
|
|
782
|
-
|
|
766
|
+
samples.push({
|
|
767
|
+
megapixelsPerSecond: measured.megapixelsPerSecond,
|
|
768
|
+
megabitsPerSecond: measured.megabitsPerSecond,
|
|
769
|
+
costSecondsPerSecond: cost
|
|
770
|
+
});
|
|
783
771
|
log.info(
|
|
784
772
|
`hwaccel: decode "${clip}" ${measured.megapixelsPerSecond.toFixed(1)} Mpx/s ` +
|
|
785
773
|
`${measured.megabitsPerSecond.toFixed(2)} Mbit/s -> ${measured.speed.toFixed(1)}x ` +
|
|
786
774
|
`(cost ${cost.toFixed(4)} s/s, over ${measured.windowSec.toFixed(1)}s of decoding)`
|
|
787
775
|
);
|
|
788
776
|
}
|
|
789
|
-
const fitted = fitDecodeCost(
|
|
777
|
+
const fitted = fitDecodeCost(samples);
|
|
790
778
|
if (!fitted) {
|
|
791
779
|
log.warn("hwaccel: decode cost could not be fitted to these measurements; decode cost unknown");
|
|
792
780
|
return null;
|
|
793
781
|
}
|
|
794
782
|
log.info(
|
|
795
783
|
`hwaccel: decode cost = ${fitted.pixelTerm.toFixed(6)} × Mpx/s + ${fitted.bitrateTerm.toFixed(6)} × Mbit/s ` +
|
|
796
|
-
`+ ${fitted.constantTerm.toFixed(4)} s/s (${fitted.shape}
|
|
784
|
+
`+ ${fitted.constantTerm.toFixed(4)} s/s (${fitted.shape} from ${fitted.samples} clips, ` +
|
|
785
|
+
`typical disagreement ${fitted.residualRms.toFixed(4)} s/s` +
|
|
786
|
+
// Named rather than implied: a zero in the line above means "not
|
|
787
|
+
// measured" for a dropped term and "measured to be nothing" otherwise,
|
|
788
|
+
// and those are different claims.
|
|
789
|
+
(fitted.dropped.length > 0 ? `, ${fitted.dropped.join(" and ")} not determined by these clips` : "") +
|
|
790
|
+
`, measured in ${((Date.now() - startedAllAt) / 1000).toFixed(1)}s)`
|
|
797
791
|
);
|
|
798
792
|
return { pixelTerm: fitted.pixelTerm, bitrateTerm: fitted.bitrateTerm, constantTerm: fitted.constantTerm };
|
|
799
793
|
}
|
|
@@ -911,65 +905,6 @@ function measureDecodeSlope(ffmpegBin, clipPath) {
|
|
|
911
905
|
});
|
|
912
906
|
}
|
|
913
907
|
|
|
914
|
-
/**
|
|
915
|
-
* Fit the three measurements, and say which shape the data supported.
|
|
916
|
-
*
|
|
917
|
-
* The three-term fit is exact — three points, three unknowns — and is used
|
|
918
|
-
* whenever every term comes out non-negative. A negative term is not a host
|
|
919
|
-
* being odd; it says the difference it was solved from is smaller than the
|
|
920
|
-
* noise between runs, which is what a fast machine produces: measured on a
|
|
921
|
-
* desktop, the 720p clip took LONGER per second than the low-bitrate 1080p one,
|
|
922
|
-
* because process startup is a large share of a decode that takes a second.
|
|
923
|
-
*
|
|
924
|
-
* When that happens the bitrate term — the weak one, and the one solved from a
|
|
925
|
-
* single difference — is dropped and the remaining two are fitted by least
|
|
926
|
-
* squares over all three points. If even the pixel slope comes out non-positive
|
|
927
|
-
* there is no measurable dependence on the source at all, and inventing one is
|
|
928
|
-
* worse than having none: the caller then prices the encoder alone and refuses
|
|
929
|
-
* nothing.
|
|
930
|
-
*
|
|
931
|
-
* @param {number[][]} equations - Rows of [Mpixel/s, Mbit/s, 1, cost].
|
|
932
|
-
* @returns {{ pixelTerm: number, bitrateTerm: number, constantTerm: number, shape: string } | null}
|
|
933
|
-
*/
|
|
934
|
-
function fitDecodeCost(equations) {
|
|
935
|
-
const exact = solveLinear3(equations);
|
|
936
|
-
if (exact && exact[0] > 0 && exact[1] >= 0 && exact[2] >= 0) {
|
|
937
|
-
return { pixelTerm: exact[0], bitrateTerm: exact[1], constantTerm: exact[2], shape: "pixels+bitrate+constant" };
|
|
938
|
-
}
|
|
939
|
-
const count = equations.length;
|
|
940
|
-
const meanPixels = equations.reduce((sum, row) => sum + row[0], 0) / count;
|
|
941
|
-
const meanCost = equations.reduce((sum, row) => sum + row[3], 0) / count;
|
|
942
|
-
let covariance = 0;
|
|
943
|
-
let variance = 0;
|
|
944
|
-
for (const row of equations) {
|
|
945
|
-
covariance += (row[0] - meanPixels) * (row[3] - meanCost);
|
|
946
|
-
variance += (row[0] - meanPixels) ** 2;
|
|
947
|
-
}
|
|
948
|
-
if (!(variance > 0)) {
|
|
949
|
-
return null;
|
|
950
|
-
}
|
|
951
|
-
const pixelTerm = covariance / variance;
|
|
952
|
-
const constantTerm = meanCost - pixelTerm * meanPixels;
|
|
953
|
-
if (pixelTerm > 0 && constantTerm >= 0) {
|
|
954
|
-
return { pixelTerm, bitrateTerm: 0, constantTerm, shape: "pixels+constant" };
|
|
955
|
-
}
|
|
956
|
-
// A negative constant is the line crossing below zero where no clip was
|
|
957
|
-
// measured — every clip is 22 Mpixel/s or more, and nothing here says what a
|
|
958
|
-
// tiny picture costs. Rather than carry a term that would price a small
|
|
959
|
-
// source as free work, fit through the origin: cost proportional to pixels,
|
|
960
|
-
// which is the relationship the measurements do support.
|
|
961
|
-
let weighted = 0;
|
|
962
|
-
let squares = 0;
|
|
963
|
-
for (const row of equations) {
|
|
964
|
-
weighted += row[0] * row[3];
|
|
965
|
-
squares += row[0] ** 2;
|
|
966
|
-
}
|
|
967
|
-
const throughOrigin = squares > 0 ? weighted / squares : 0;
|
|
968
|
-
if (!(throughOrigin > 0)) {
|
|
969
|
-
return null;
|
|
970
|
-
}
|
|
971
|
-
return { pixelTerm: throughOrigin, bitrateTerm: 0, constantTerm: 0, shape: "pixels only" };
|
|
972
|
-
}
|
|
973
908
|
|
|
974
909
|
/**
|
|
975
910
|
* How many times realtime this host can DECODE a source of these
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file What the machine can spare for a new encoder — and what must not be
|
|
3
|
+
* charged twice.
|
|
4
|
+
*
|
|
5
|
+
* The numbers are the addon host's own, measured 2026-08-17 while a 240p step
|
|
6
|
+
* ran at 1.01-1.12x against a prediction of 1.83x.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
|
|
12
|
+
import { availableShareFrom, correctForAvailability } from "../services/available-share.js";
|
|
13
|
+
|
|
14
|
+
test("only the work nobody has been charged for is subtracted", () => {
|
|
15
|
+
// The field reading: the box is saturated, our encoder has more than half of
|
|
16
|
+
// it, and the proxy's own work a fifth.
|
|
17
|
+
const availability = availableShareFrom({ systemBusy: 0.99, encoderShare: 0.56, proxyShare: 0.2 });
|
|
18
|
+
|
|
19
|
+
assert.equal(availability.known, true);
|
|
20
|
+
// 0.99 − 0.76 = 0.23 belongs to the kernel, the container and whatever else
|
|
21
|
+
// the owner runs. Our own encoders are priced by the concurrency arithmetic
|
|
22
|
+
// and the proxy's work per megabyte moved; charging them here as well is the
|
|
23
|
+
// double counting that emptied the quality menu in 2.21.0.
|
|
24
|
+
assert.ok(Math.abs(availability.unattributed - 0.23) < 1e-9, `${availability.unattributed}`);
|
|
25
|
+
assert.ok(Math.abs(availability.share - 0.77) < 1e-9, `${availability.share}`);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("a quiet machine is not corrected at all", () => {
|
|
29
|
+
const availability = availableShareFrom({ systemBusy: 0.05, encoderShare: 0.04, proxyShare: 0.01 });
|
|
30
|
+
assert.equal(availability.unattributed, 0);
|
|
31
|
+
assert.equal(availability.share, 1);
|
|
32
|
+
assert.equal(correctForAvailability(4, availability), 4);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("a host that cannot say says so, and nothing is corrected", () => {
|
|
36
|
+
// Not every host has /proc. An uncorrected figure is honest; a figure
|
|
37
|
+
// corrected by an invented share is not.
|
|
38
|
+
const availability = availableShareFrom({ systemBusy: null, encoderShare: null, proxyShare: null });
|
|
39
|
+
assert.equal(availability.known, false);
|
|
40
|
+
assert.equal(availability.share, 1);
|
|
41
|
+
assert.equal(correctForAvailability(2.5, availability), 2.5, "an unknown machine leaves the prediction alone");
|
|
42
|
+
assert.equal(availableShareFrom().known, false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("readings that overlap by a rounding do not produce a negative machine", () => {
|
|
46
|
+
// The two samples are taken microseconds apart, so our own share can come out
|
|
47
|
+
// fractionally above the system total.
|
|
48
|
+
const availability = availableShareFrom({ systemBusy: 0.60, encoderShare: 0.58, proxyShare: 0.05 });
|
|
49
|
+
assert.equal(availability.unattributed, 0);
|
|
50
|
+
assert.equal(availability.share, 1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("a machine given over entirely to other work leaves nothing", () => {
|
|
54
|
+
const availability = availableShareFrom({ systemBusy: 1, encoderShare: 0, proxyShare: 0 });
|
|
55
|
+
assert.equal(availability.share, 0);
|
|
56
|
+
assert.equal(correctForAvailability(4, availability), 0, "a step cannot be offered on a machine with nothing left");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("the correction is proportional, and only ever downwards", () => {
|
|
60
|
+
const availability = availableShareFrom({ systemBusy: 0.99, encoderShare: 0.56, proxyShare: 0.2 });
|
|
61
|
+
// The field case: a step predicted at 1.83x on a quiet box.
|
|
62
|
+
const corrected = correctForAvailability(1.83, availability);
|
|
63
|
+
assert.ok(Math.abs(corrected - 1.4091) < 0.001, `${corrected}`);
|
|
64
|
+
// And the honest note this test exists to record: 1.41x is still above the
|
|
65
|
+
// 1.01-1.12x that step actually ran at. This correction closes part of the
|
|
66
|
+
// gap, not all of it — which is why the per-step field comparison ships with
|
|
67
|
+
// it rather than after it.
|
|
68
|
+
assert.ok(corrected > 1.12, "the remaining difference is what the field check is for");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("nothing is corrected without a prediction to correct", () => {
|
|
72
|
+
const availability = availableShareFrom({ systemBusy: 0.99, encoderShare: 0.5, proxyShare: 0.2 });
|
|
73
|
+
assert.equal(correctForAvailability(0, availability), 0);
|
|
74
|
+
assert.equal(correctForAvailability(Number.NaN, availability), Number.NaN);
|
|
75
|
+
assert.equal(correctForAvailability(-1, availability), -1);
|
|
76
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The decode-cost fit, and the case it exists to prevent.
|
|
3
|
+
*
|
|
4
|
+
* The failure being pinned is real and dated: on 2026-08-17 three clips for
|
|
5
|
+
* three unknowns returned `0.007542 × Mpx/s + 0.000000 × Mbit/s + 0.0000 s/s`,
|
|
6
|
+
* with the bitrate term and the constant exactly zero, and the prediction built
|
|
7
|
+
* on it was 1.8-2.2x optimistic. An exact system cannot notice that two of its
|
|
8
|
+
* points said the same thing; these tests hold the replacement to noticing.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import test from "node:test";
|
|
13
|
+
|
|
14
|
+
import { decodeCostOf, fitDecodeCost } from "../services/decode-cost-fit.js";
|
|
15
|
+
|
|
16
|
+
const FPS = 24;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A clip's measurement, priced by a known truth so a fit can be checked against
|
|
20
|
+
* the answer it should recover.
|
|
21
|
+
*
|
|
22
|
+
* @param {number} width
|
|
23
|
+
* @param {number} height
|
|
24
|
+
* @param {number} megabitsPerSecond
|
|
25
|
+
* @param {{ pixel: number, bitrate: number, constant: number, noise?: number }} truth
|
|
26
|
+
* @returns {{ megapixelsPerSecond: number, megabitsPerSecond: number, costSecondsPerSecond: number }}
|
|
27
|
+
*/
|
|
28
|
+
function clip(width, height, megabitsPerSecond, truth) {
|
|
29
|
+
const megapixelsPerSecond = (width * height * FPS) / 1e6;
|
|
30
|
+
const cost =
|
|
31
|
+
truth.pixel * megapixelsPerSecond + truth.bitrate * megabitsPerSecond + truth.constant + (truth.noise ?? 0);
|
|
32
|
+
return { megapixelsPerSecond, megabitsPerSecond, costSecondsPerSecond: cost };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The set the clips were cut to: three sizes, two bitrates, varied independently. */
|
|
36
|
+
const SIZES = [
|
|
37
|
+
[1920, 1080],
|
|
38
|
+
[1280, 720],
|
|
39
|
+
[854, 480]
|
|
40
|
+
];
|
|
41
|
+
const BITRATES = [9.5, 1.1];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {{ pixel: number, bitrate: number, constant: number }} truth
|
|
45
|
+
* @param {number[]} [noise] - Per-clip disturbance, in order.
|
|
46
|
+
* @returns {ReturnType<typeof clip>[]}
|
|
47
|
+
*/
|
|
48
|
+
function wellConditionedSet(truth, noise = []) {
|
|
49
|
+
const samples = [];
|
|
50
|
+
let index = 0;
|
|
51
|
+
for (const [width, height] of SIZES) {
|
|
52
|
+
for (const bitrate of BITRATES) {
|
|
53
|
+
samples.push(clip(width, height, bitrate, { ...truth, noise: noise[index] ?? 0 }));
|
|
54
|
+
index += 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return samples;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test("a well-conditioned set recovers every term", () => {
|
|
61
|
+
const truth = { pixel: 0.0055, bitrate: 0.0099, constant: 0.057 };
|
|
62
|
+
const model = fitDecodeCost(wellConditionedSet(truth));
|
|
63
|
+
|
|
64
|
+
assert.ok(model, "six clips over three unknowns must produce a model");
|
|
65
|
+
assert.equal(model.shape, "pixels+bitrate+constant");
|
|
66
|
+
assert.deepEqual(model.dropped, []);
|
|
67
|
+
assert.ok(Math.abs(model.pixelTerm - truth.pixel) < 1e-6, `pixel term ${model.pixelTerm}`);
|
|
68
|
+
assert.ok(Math.abs(model.bitrateTerm - truth.bitrate) < 1e-6, `bitrate term ${model.bitrateTerm}`);
|
|
69
|
+
assert.ok(Math.abs(model.constantTerm - truth.constant) < 1e-6, `constant ${model.constantTerm}`);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("three clips are refused outright — an exact system cannot see its own degeneracy", () => {
|
|
73
|
+
// Exactly the shape that shipped: two clips at the same pixel rate differing
|
|
74
|
+
// only in bitrate, and a third at another size.
|
|
75
|
+
const truth = { pixel: 0.0055, bitrate: 0.0099, constant: 0.057 };
|
|
76
|
+
const three = [clip(1920, 1080, 11.4, truth), clip(1920, 1080, 0.97, truth), clip(1280, 720, 2.25, truth)];
|
|
77
|
+
|
|
78
|
+
assert.equal(
|
|
79
|
+
fitDecodeCost(three),
|
|
80
|
+
null,
|
|
81
|
+
"with no residual there is nothing to notice a degeneracy with, so no model may be published"
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("a term the measurements do not determine is dropped, and said so", () => {
|
|
86
|
+
// A host whose decoding does not depend on bitrate at all: the term is not
|
|
87
|
+
// small, it is absent. What must NOT happen is publishing a zero as though it
|
|
88
|
+
// had been measured.
|
|
89
|
+
const truth = { pixel: 0.0055, bitrate: 0, constant: 0.057 };
|
|
90
|
+
// Deliberately NOT alternating with the bitrate column: an earlier version
|
|
91
|
+
// of this test put positive noise on every high-bitrate clip and negative on
|
|
92
|
+
// every low one, which IS a bitrate signal — the fit found it, correctly, and
|
|
93
|
+
// the test was wrong.
|
|
94
|
+
const noise = [0.004, 0.003, -0.004, 0.002, -0.003, -0.002];
|
|
95
|
+
const model = fitDecodeCost(wellConditionedSet(truth, noise));
|
|
96
|
+
|
|
97
|
+
assert.ok(model);
|
|
98
|
+
assert.ok(model.dropped.includes("bitrate"), `dropped: ${model.dropped.join(",") || "nothing"}`);
|
|
99
|
+
assert.equal(model.bitrateTerm, 0);
|
|
100
|
+
assert.ok(model.shape.includes("pixels"));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("noise in the readings does not become a term", () => {
|
|
104
|
+
// Pure noise around a pixels-only truth, larger than any bitrate effect.
|
|
105
|
+
const truth = { pixel: 0.0055, bitrate: 0, constant: 0 };
|
|
106
|
+
const noise = [0.01, 0.009, -0.012, 0.011, -0.008, -0.01];
|
|
107
|
+
const model = fitDecodeCost(wellConditionedSet(truth, noise));
|
|
108
|
+
|
|
109
|
+
assert.ok(model);
|
|
110
|
+
assert.ok(model.pixelTerm > 0, "the one relationship every reading agrees on survives");
|
|
111
|
+
assert.ok(
|
|
112
|
+
model.dropped.length > 0,
|
|
113
|
+
"and the terms the noise could have supplied are reported as undetermined"
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("without a measurable dependence on the source there is no model", () => {
|
|
118
|
+
// Every clip costs the same regardless of size or bitrate: nothing here says
|
|
119
|
+
// what a bigger picture costs, and inventing it is worse than having none.
|
|
120
|
+
const flat = wellConditionedSet({ pixel: 0, bitrate: 0, constant: 0.3 });
|
|
121
|
+
assert.equal(fitDecodeCost(flat), null);
|
|
122
|
+
assert.equal(fitDecodeCost([]), null);
|
|
123
|
+
assert.equal(fitDecodeCost(null), null);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("readings that are not measurements are left out", () => {
|
|
127
|
+
const truth = { pixel: 0.0055, bitrate: 0.0099, constant: 0.057 };
|
|
128
|
+
const samples = [
|
|
129
|
+
...wellConditionedSet(truth),
|
|
130
|
+
{ megapixelsPerSecond: Number.NaN, megabitsPerSecond: 5, costSecondsPerSecond: 1 },
|
|
131
|
+
{ megapixelsPerSecond: 20, megabitsPerSecond: 5, costSecondsPerSecond: 0 }
|
|
132
|
+
];
|
|
133
|
+
const model = fitDecodeCost(samples);
|
|
134
|
+
assert.ok(model);
|
|
135
|
+
assert.equal(model.samples, 6);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("what a model prices a film at", () => {
|
|
139
|
+
const model = fitDecodeCost(wellConditionedSet({ pixel: 0.0055, bitrate: 0.0099, constant: 0.057 }));
|
|
140
|
+
// The field film: 1080p24 at about 8 Mbit/s.
|
|
141
|
+
const cost = decodeCostOf(model, { megapixelsPerSecond: 49.77, megabitsPerSecond: 8 });
|
|
142
|
+
assert.ok(cost > 0);
|
|
143
|
+
// Which is a decode speed of 1/cost — the figure the quality offer rests on.
|
|
144
|
+
assert.ok(1 / cost > 1 && 1 / cost < 10, `decodes at ${(1 / cost).toFixed(2)}x`);
|
|
145
|
+
});
|