potential fix for OOM
This commit is contained in:
@@ -121,6 +121,8 @@ Important behavior:
|
|||||||
- Uses bounded branch queues via `createRelayInputBranch`.
|
- Uses bounded branch queues via `createRelayInputBranch`.
|
||||||
- Pauses upstream reading while any branch queue exceeds half of `MAX_RELAY_BRANCH_QUEUE_BYTES`.
|
- Pauses upstream reading while any branch queue exceeds half of `MAX_RELAY_BRANCH_QUEUE_BYTES`.
|
||||||
- Stops playback if any branch queue exceeds `MAX_RELAY_BRANCH_QUEUE_BYTES`.
|
- Stops playback if any branch queue exceeds `MAX_RELAY_BRANCH_QUEUE_BYTES`.
|
||||||
|
- Backpressure accounting must include both chunks queued in JavaScript and bytes already written to ffmpeg stdin while waiting for `drain`. Otherwise fast movie sources can outrun realtime ffmpeg consumption and grow Node heap until OOM.
|
||||||
|
- When waiting for relay capacity, wait only on branches that are actually over the pause threshold. Including already-ready branches in a `Promise.race` can create an immediate-resolution spin loop.
|
||||||
|
|
||||||
Relay mode works best for sequential stream containers such as MPEG-TS/IPTV. It may be less reliable for file formats that require seeking or late metadata, such as some MP4 files.
|
Relay mode works best for sequential stream containers such as MPEG-TS/IPTV. It may be less reliable for file formats that require seeking or late metadata, such as some MP4 files.
|
||||||
|
|
||||||
|
|||||||
@@ -1417,6 +1417,7 @@ function createRelayInputBranch(stream, onError) {
|
|||||||
let accepting = true;
|
let accepting = true;
|
||||||
let ending = false;
|
let ending = false;
|
||||||
let drainPending = false;
|
let drainPending = false;
|
||||||
|
let streamPendingBytes = 0;
|
||||||
let queue = [];
|
let queue = [];
|
||||||
let queueBytes = 0;
|
let queueBytes = 0;
|
||||||
let peakBytes = 0;
|
let peakBytes = 0;
|
||||||
@@ -1424,18 +1425,28 @@ function createRelayInputBranch(stream, onError) {
|
|||||||
|
|
||||||
stream.on('drain', () => {
|
stream.on('drain', () => {
|
||||||
drainPending = false;
|
drainPending = false;
|
||||||
|
streamPendingBytes = 0;
|
||||||
|
updatePeakBytes();
|
||||||
|
notifyWaiters();
|
||||||
flush();
|
flush();
|
||||||
});
|
});
|
||||||
|
|
||||||
stream.on('error', (error) => {
|
stream.on('error', (error) => {
|
||||||
accepting = false;
|
accepting = false;
|
||||||
|
streamPendingBytes = 0;
|
||||||
notifyWaiters();
|
notifyWaiters();
|
||||||
onError(error);
|
onError(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
stream.on('close', () => {
|
||||||
|
accepting = false;
|
||||||
|
streamPendingBytes = 0;
|
||||||
|
notifyWaiters();
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
get queueBytes() {
|
get queueBytes() {
|
||||||
return queueBytes;
|
return getPendingBytes();
|
||||||
},
|
},
|
||||||
get peakBytes() {
|
get peakBytes() {
|
||||||
return peakBytes;
|
return peakBytes;
|
||||||
@@ -1451,9 +1462,9 @@ function createRelayInputBranch(stream, onError) {
|
|||||||
|
|
||||||
queue.push(chunk);
|
queue.push(chunk);
|
||||||
queueBytes += chunk.length;
|
queueBytes += chunk.length;
|
||||||
peakBytes = Math.max(peakBytes, queueBytes);
|
updatePeakBytes();
|
||||||
notifyWaiters();
|
notifyWaiters();
|
||||||
return queueBytes <= MAX_RELAY_BRANCH_QUEUE_BYTES;
|
return getPendingBytes() <= MAX_RELAY_BRANCH_QUEUE_BYTES;
|
||||||
},
|
},
|
||||||
end() {
|
end() {
|
||||||
accepting = false;
|
accepting = false;
|
||||||
@@ -1465,6 +1476,7 @@ function createRelayInputBranch(stream, onError) {
|
|||||||
ending = false;
|
ending = false;
|
||||||
queue = [];
|
queue = [];
|
||||||
queueBytes = 0;
|
queueBytes = 0;
|
||||||
|
streamPendingBytes = 0;
|
||||||
notifyWaiters();
|
notifyWaiters();
|
||||||
|
|
||||||
if (!stream.destroyed) {
|
if (!stream.destroyed) {
|
||||||
@@ -1472,7 +1484,7 @@ function createRelayInputBranch(stream, onError) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
waitForCapacity() {
|
waitForCapacity() {
|
||||||
if (queueBytes <= RELAY_BRANCH_PAUSE_BYTES || !accepting || stream.destroyed) {
|
if (getPendingBytes() <= RELAY_BRANCH_PAUSE_BYTES || !accepting || stream.destroyed) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1484,10 +1496,17 @@ function createRelayInputBranch(stream, onError) {
|
|||||||
|
|
||||||
function writeNow(chunk) {
|
function writeNow(chunk) {
|
||||||
try {
|
try {
|
||||||
drainPending = !stream.write(chunk);
|
if (!stream.write(chunk)) {
|
||||||
|
drainPending = true;
|
||||||
|
streamPendingBytes += chunk.length;
|
||||||
|
updatePeakBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyWaiters();
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
accepting = false;
|
accepting = false;
|
||||||
|
streamPendingBytes = 0;
|
||||||
notifyWaiters();
|
notifyWaiters();
|
||||||
onError(error);
|
onError(error);
|
||||||
return false;
|
return false;
|
||||||
@@ -1498,11 +1517,12 @@ function createRelayInputBranch(stream, onError) {
|
|||||||
while (queue.length > 0 && !drainPending && !stream.destroyed) {
|
while (queue.length > 0 && !drainPending && !stream.destroyed) {
|
||||||
const chunk = queue.shift();
|
const chunk = queue.shift();
|
||||||
queueBytes -= chunk.length;
|
queueBytes -= chunk.length;
|
||||||
notifyWaiters();
|
|
||||||
|
|
||||||
if (!writeNow(chunk)) {
|
if (!writeNow(chunk)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifyWaiters();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ending && queue.length === 0 && !drainPending && !stream.destroyed) {
|
if (ending && queue.length === 0 && !drainPending && !stream.destroyed) {
|
||||||
@@ -1511,7 +1531,7 @@ function createRelayInputBranch(stream, onError) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function notifyWaiters() {
|
function notifyWaiters() {
|
||||||
if (queueBytes > RELAY_BRANCH_PAUSE_BYTES && accepting && !stream.destroyed) {
|
if (getPendingBytes() > RELAY_BRANCH_PAUSE_BYTES && accepting && !stream.destroyed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1522,11 +1542,25 @@ function createRelayInputBranch(stream, onError) {
|
|||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPendingBytes() {
|
||||||
|
return queueBytes + streamPendingBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePeakBytes() {
|
||||||
|
peakBytes = Math.max(peakBytes, getPendingBytes());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForRelayCapacity(branches, shouldStop) {
|
async function waitForRelayCapacity(branches, shouldStop) {
|
||||||
while (!shouldStop() && branches.some((branch) => branch.queueBytes > RELAY_BRANCH_PAUSE_BYTES)) {
|
while (!shouldStop()) {
|
||||||
await Promise.race(branches.map((branch) => branch.waitForCapacity()));
|
const blockedBranches = branches.filter((branch) => branch.queueBytes > RELAY_BRANCH_PAUSE_BYTES);
|
||||||
|
|
||||||
|
if (blockedBranches.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.race(blockedBranches.map((branch) => branch.waitForCapacity()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user