diff --git a/dist/package.json b/dist/package.json index 642296c5..5822d219 100644 --- a/dist/package.json +++ b/dist/package.json @@ -1,6 +1,6 @@ { "name": "nodelink", - "version": "3.8.0-dev.20260422.1", + "version": "3.8.0", "scripts": { "build": "tsc --incremental false", "start": "node --dns-result-order=ipv4first --import tsx src/index.ts", diff --git a/dist/src/playback/processing/AudioMixer.js b/dist/src/playback/processing/AudioMixer.js index cfb6f22b..8bb91dae 100644 --- a/dist/src/playback/processing/AudioMixer.js +++ b/dist/src/playback/processing/AudioMixer.js @@ -63,27 +63,23 @@ export class AudioMixer extends Readable { mixBuffers(mainPCM, layersPCM) { if (layersPCM.size === 0 || !this.enabled) return mainPCM; - const outputBuffer = Buffer.allocUnsafe(mainPCM.length); + const mainLen = mainPCM.length >> 1; const mainView = this._asInt16Array(mainPCM); - const outputView = this._asInt16Array(outputBuffer); const activeLayerViews = []; + const layerVolumes = []; for (const layer of layersPCM.values()) { - activeLayerViews.push({ - view: this._asInt16Array(layer.buffer), - volume: layer.volume - }); + activeLayerViews.push(this._asInt16Array(layer.buffer)); + layerVolumes.push(layer.volume); } - const mainLen = mainView.length; const numLayers = activeLayerViews.length; + const outputBuffer = Buffer.allocUnsafe(mainPCM.length); + const outputView = this._asInt16Array(outputBuffer); for (let i = 0; i < mainLen; i++) { let sample = mainView[i] ?? 0; for (let j = 0; j < numLayers; j++) { - const layer = activeLayerViews[j]; - if (!layer) - continue; - if (i < layer.view.length) { - sample += ((layer.view[i] ?? 0) * layer.volume) | 0; - } + const layerView = activeLayerViews[j]; + const volume = layerVolumes[j]; + sample += ((layerView[i] ?? 0) * volume) | 0; } outputView[i] = sample < -32768 ? -32768 : sample > 32767 ? 32767 : sample; } diff --git a/dist/src/playback/processing/FadeTransformer.js b/dist/src/playback/processing/FadeTransformer.js index 1a02eed6..f5c30293 100644 --- a/dist/src/playback/processing/FadeTransformer.js +++ b/dist/src/playback/processing/FadeTransformer.js @@ -116,22 +116,41 @@ export class FadeTransformer extends Transform { else { _useBuffer = true; } - const step = sampleCount > 1 ? (gainEnd - gainStart) / (sampleCount - 1) : 0; - if (view) { - for (let i = 0; i < view.length; i++) { - const gain = gainStart + step * i; - const sample = view[i] ?? 0; - const value = sample * gain; - view[i] = value < -32768 ? -32768 : value > 32767 ? 32767 : value | 0; + if (gainStart === gainEnd) { + if (view) { + for (let i = 0; i < view.length; i++) { + const sample = view[i] ?? 0; + const value = sample * gainStart; + view[i] = value < -32768 ? -32768 : value > 32767 ? 32767 : value | 0; + } + } + else { + for (let i = 0; i < sampleCount; i++) { + const sample = chunk.readInt16LE(i * 2); + const value = sample * gainStart; + const clamped = value < -32768 ? -32768 : value > 32767 ? 32767 : value | 0; + chunk.writeInt16LE(clamped, i * 2); + } } } else { - for (let i = 0; i < sampleCount; i++) { - const gain = gainStart + step * i; - const sample = chunk.readInt16LE(i * 2); - const value = sample * gain; - const clamped = value < -32768 ? -32768 : value > 32767 ? 32767 : value | 0; - chunk.writeInt16LE(clamped, i * 2); + const step = sampleCount > 1 ? (gainEnd - gainStart) / (sampleCount - 1) : 0; + if (view) { + for (let i = 0; i < view.length; i++) { + const gain = gainStart + step * i; + const sample = view[i] ?? 0; + const value = sample * gain; + view[i] = value < -32768 ? -32768 : value > 32767 ? 32767 : value | 0; + } + } + else { + for (let i = 0; i < sampleCount; i++) { + const gain = gainStart + step * i; + const sample = chunk.readInt16LE(i * 2); + const value = sample * gain; + const clamped = value < -32768 ? -32768 : value > 32767 ? 32767 : value | 0; + chunk.writeInt16LE(clamped, i * 2); + } } } return chunk; diff --git a/dist/src/playback/processing/LoudnessNormalizer.js b/dist/src/playback/processing/LoudnessNormalizer.js index 7d3dd9a8..a25824b3 100644 --- a/dist/src/playback/processing/LoudnessNormalizer.js +++ b/dist/src/playback/processing/LoudnessNormalizer.js @@ -2,6 +2,7 @@ const INT16_MAX = 32767; const INT16_MIN = -32768; const MIN_ENERGY = 1e-12; const fround = Math.fround; +const INV_32768 = 1 / 32768; /** * Implements a standard Biquad filter for audio processing. */ @@ -162,18 +163,20 @@ export class LoudnessNormalizer { const releaseAlpha = this._releaseAlpha; const energyAlpha = this._energyAlpha; const target = this.targetLoudness; + const filters = this.filters; + const numChannels = this.channels; for (let frameIndex = 0, sampleIndex = 0; frameIndex < frameCount; frameIndex++) { let energySum = 0.0; - for (let ch = 0; ch < this.channels; ch += 1, sampleIndex += 1) { - const sample = fround((inputView[sampleIndex] ?? 0) / 32768); - const filter = this.filters[ch]; + for (let ch = 0; ch < numChannels; ch += 1, sampleIndex += 1) { + const sample = fround((inputView[sampleIndex] ?? 0) * INV_32768); + const filter = filters[ch]; if (filter) { const filtered = filter.process(sample); channelBuffer[ch] = filtered; energySum += filtered * filtered; } } - energySum = fround(energySum / this.channels); + energySum = fround(energySum / numChannels); if (energySum > this._gateThresholdEnergy) { energyState = fround(energyState * energyAlpha + (1 - energyAlpha) * energySum); } diff --git a/dist/src/playback/processing/VolumeTransformer.js b/dist/src/playback/processing/VolumeTransformer.js index 422e321f..e836b1a1 100644 --- a/dist/src/playback/processing/VolumeTransformer.js +++ b/dist/src/playback/processing/VolumeTransformer.js @@ -22,6 +22,7 @@ export class VolumeTransformer extends Transform { lookaheadBuffer; lookaheadIndex; lookaheadFull; + _reusableOutputBuffer = null; currentVolume; targetVolume; startVolume; @@ -141,9 +142,18 @@ export class VolumeTransformer extends Transform { if (abs <= this._thresholdValue || this._limitHeadroom <= 0) return value; const normalizedOvershoot = (abs - this._thresholdValue) / this._limitHeadroom; - const softened = 1 - Math.exp(-normalizedOvershoot * this.limiterSoftness); + // Fast approximation of 1 - exp(-x) for small x + // 1 - exp(-x) \approx x - x^2/2 + x^3/6 + const x = normalizedOvershoot * this.limiterSoftness; + let softened; + if (x < 0.1) { + softened = x * (1 - x * 0.5); + } + else { + softened = 1 - Math.exp(-x); + } const limited = this._thresholdValue + this._limitHeadroom * softened; - return Math.sign(value) * Math.min(INT16_MAX, limited); + return (value < 0 ? -1 : 1) * (limited > INT16_MAX ? INT16_MAX : limited); } _clampToInt16(value) { if (value >= INT16_MAX) @@ -186,7 +196,10 @@ export class VolumeTransformer extends Transform { const gainStep = usableSamples > 1 ? (gainEnd - gainStart) / (usableSamples - 1) : 0; let gain = gainStart; if (this.lookaheadSamples > 0) { - const outputBuffer = alignedBufferIfRequired(chunk.length); + if (!this._reusableOutputBuffer || this._reusableOutputBuffer.length < chunk.length) { + this._reusableOutputBuffer = alignedBufferIfRequired(chunk.length); + } + const outputBuffer = this._reusableOutputBuffer.subarray(0, chunk.length); const outputView = new Int16Array(outputBuffer.buffer, outputBuffer.byteOffset, usableSamples); if (useBufferOps) { for (let i = 0; i < usableSamples; i++) { @@ -202,36 +215,59 @@ export class VolumeTransformer extends Transform { } } else if (view) { - for (let i = 0; i < view.length; i++) { + const len = view.length; + const lookaheadBuffer = this.lookaheadBuffer; + const lookaheadSamples = this.lookaheadSamples; + let lookaheadIndex = this.lookaheadIndex; + for (let i = 0; i < len; i++) { const rawSample = view[i] ?? 0; const scaled = rawSample * gain; const limited = this._applyLimiter(scaled); - const outputSample = this.lookaheadBuffer[this.lookaheadIndex] ?? 0; - this.lookaheadBuffer[this.lookaheadIndex] = limited; - this.lookaheadIndex = - (this.lookaheadIndex + 1) % this.lookaheadSamples; + const outputSample = lookaheadBuffer[lookaheadIndex] ?? 0; + lookaheadBuffer[lookaheadIndex] = limited; + lookaheadIndex = (lookaheadIndex + 1) % lookaheadSamples; outputView[i] = this._clampToInt16(outputSample); gain += gainStep; } + this.lookaheadIndex = lookaheadIndex; } if (this.lookaheadIndex === 0) this.lookaheadFull = true; return outputBuffer; } if (useBufferOps) { - for (let i = 0; i < usableSamples; i++) { - const scaled = chunk.readInt16LE(i * 2) * gain; - const limited = this._applyLimiter(scaled); - chunk.writeInt16LE(this._clampToInt16(limited), i * 2); - gain += gainStep; + if (gainStart === gainEnd) { + for (let i = 0; i < usableSamples; i++) { + const scaled = chunk.readInt16LE(i * 2) * gainStart; + const limited = this._applyLimiter(scaled); + chunk.writeInt16LE(this._clampToInt16(limited), i * 2); + } + } + else { + for (let i = 0; i < usableSamples; i++) { + const scaled = chunk.readInt16LE(i * 2) * gain; + const limited = this._applyLimiter(scaled); + chunk.writeInt16LE(this._clampToInt16(limited), i * 2); + gain += gainStep; + } } } else if (view) { - for (let i = 0; i < view.length; i++) { - const scaled = (view[i] ?? 0) * gain; - const limited = this._applyLimiter(scaled); - view[i] = this._clampToInt16(limited); - gain += gainStep; + const len = view.length; + if (gainStart === gainEnd) { + for (let i = 0; i < len; i++) { + const scaled = (view[i] ?? 0) * gainStart; + const limited = this._applyLimiter(scaled); + view[i] = this._clampToInt16(limited); + } + } + else { + for (let i = 0; i < len; i++) { + const scaled = (view[i] ?? 0) * gain; + const limited = this._applyLimiter(scaled); + view[i] = this._clampToInt16(limited); + gain += gainStep; + } } } return chunk; diff --git a/dist/src/playback/structs/BufferPool.js b/dist/src/playback/structs/BufferPool.js index 0256b65d..7dbec080 100644 --- a/dist/src/playback/structs/BufferPool.js +++ b/dist/src/playback/structs/BufferPool.js @@ -6,10 +6,9 @@ const parsePositiveIntEnv = (key, fallback) => { const parsed = Number.parseInt(raw, 10); return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; }; -const MAX_POOL_SIZE_BYTES = parsePositiveIntEnv('NODELINK_BUFFER_POOL_MAX_BYTES', 20 * 1024 * 1024 // 20 MB - reduced from 50MB -); -const MAX_BUCKET_ENTRIES = parsePositiveIntEnv('NODELINK_BUFFER_POOL_MAX_BUCKET_ENTRIES', 4 // reduced from 8 +const MAX_POOL_SIZE_BYTES = parsePositiveIntEnv('NODELINK_BUFFER_POOL_MAX_BYTES', 64 * 1024 * 1024 // 64 MB ); +const MAX_BUCKET_ENTRIES = parsePositiveIntEnv('NODELINK_BUFFER_POOL_MAX_BUCKET_ENTRIES', 16); const IDLE_CLEAR_MS = parsePositiveIntEnv('NODELINK_BUFFER_POOL_IDLE_CLEAR_MS', 60000 // 1 min - reduced from 3 min ); const CLEANUP_INTERVAL = 60000; diff --git a/dist/src/playback/structs/RingBuffer.js b/dist/src/playback/structs/RingBuffer.js index 59a565f0..a56c3e0a 100644 --- a/dist/src/playback/structs/RingBuffer.js +++ b/dist/src/playback/structs/RingBuffer.js @@ -76,17 +76,32 @@ export class RingBuffer { if (bytesToRead === 0) return null; const out = Buffer.allocUnsafe(bytesToRead); + this.readTo(out); + return out; + } + /** + * Reads bytes from the ring buffer directly into the target buffer. + * @param target - The target buffer to write into. + * @param targetOffset - Offset in the target buffer. + * @returns The number of bytes actually read. + */ + readTo(target, targetOffset = 0) { + if (!this.buffer) + return 0; + const bytesToRead = Math.min(target.length - targetOffset, this._length); + if (bytesToRead <= 0) + return 0; const availableAtEnd = this.size - this.readOffset; if (bytesToRead <= availableAtEnd) { - this.buffer.copy(out, 0, this.readOffset, this.readOffset + bytesToRead); + this.buffer.copy(target, targetOffset, this.readOffset, this.readOffset + bytesToRead); } else { - this.buffer.copy(out, 0, this.readOffset, this.size); - this.buffer.copy(out, availableAtEnd, 0, bytesToRead - availableAtEnd); + this.buffer.copy(target, targetOffset, this.readOffset, this.size); + this.buffer.copy(target, targetOffset + availableAtEnd, 0, bytesToRead - availableAtEnd); } this.readOffset = (this.readOffset + bytesToRead) % this.size; this._length -= bytesToRead; - return out; + return bytesToRead; } /** * Skips n bytes in the buffer. diff --git a/dist/src/workers/main.js b/dist/src/workers/main.js index 77e0d616..91031c2a 100644 --- a/dist/src/workers/main.js +++ b/dist/src/workers/main.js @@ -1263,10 +1263,10 @@ function startTimers(hibernating = false) { players: localPlayers, playingPlayers: localPlayingPlayers, commandQueueLength: Array.from(guildQueues.values()).reduce((acc, curr) => acc + getHeadQueueLength(curr.queue), 0), - cpu: { nodelinkLoad }, - eventLoopLag: eluP50, - eventLoopLagP95: eluP95, - eventLoopLagP99: eluP99, + cpu: { nodelinkLoad: Math.round(nodelinkLoad * 100) / 100 }, + eventLoopLag: Math.round(eluP50 * 100) / 100, + eventLoopLagP95: Math.round(eluP95 * 100) / 100, + eventLoopLagP99: Math.round(eluP99 * 100) / 100, memory: { used: mem.heapUsed, allocated: mem.heapTotal diff --git a/src/playback/processing/AudioMixer.ts b/src/playback/processing/AudioMixer.ts index ef6b0eb6..5bc129d5 100644 --- a/src/playback/processing/AudioMixer.ts +++ b/src/playback/processing/AudioMixer.ts @@ -98,29 +98,29 @@ export class AudioMixer extends Readable { ): Buffer { if (layersPCM.size === 0 || !this.enabled) return mainPCM - const outputBuffer = Buffer.allocUnsafe(mainPCM.length) + const mainLen = mainPCM.length >> 1 const mainView = this._asInt16Array(mainPCM) - const outputView = this._asInt16Array(outputBuffer) - const activeLayerViews: Array<{ view: Int16Array; volume: number }> = [] + const activeLayerViews: Int16Array[] = [] + const layerVolumes: number[] = [] + for (const layer of layersPCM.values()) { - activeLayerViews.push({ - view: this._asInt16Array(layer.buffer), - volume: layer.volume - }) + activeLayerViews.push(this._asInt16Array(layer.buffer)) + layerVolumes.push(layer.volume) } - const mainLen = mainView.length const numLayers = activeLayerViews.length + const outputBuffer = Buffer.allocUnsafe(mainPCM.length) + const outputView = this._asInt16Array(outputBuffer) for (let i = 0; i < mainLen; i++) { let sample = mainView[i] ?? 0 + for (let j = 0; j < numLayers; j++) { - const layer = activeLayerViews[j] - if (!layer) continue - if (i < layer.view.length) { - sample += ((layer.view[i] ?? 0) * layer.volume) | 0 - } + const layerView = activeLayerViews[j] as Int16Array + const volume = layerVolumes[j] as number + + sample += ((layerView[i] ?? 0) * volume) | 0 } outputView[i] = sample < -32768 ? -32768 : sample > 32767 ? 32767 : sample diff --git a/src/playback/processing/FadeTransformer.ts b/src/playback/processing/FadeTransformer.ts index f878cde2..5ac252ab 100644 --- a/src/playback/processing/FadeTransformer.ts +++ b/src/playback/processing/FadeTransformer.ts @@ -151,23 +151,41 @@ export class FadeTransformer extends Transform implements IFadeTransformer { _useBuffer = true } - const step = sampleCount > 1 ? (gainEnd - gainStart) / (sampleCount - 1) : 0 - - if (view) { - for (let i = 0; i < view.length; i++) { - const gain = gainStart + step * i - const sample = view[i] ?? 0 - const value = sample * gain - view[i] = value < -32768 ? -32768 : value > 32767 ? 32767 : value | 0 + if (gainStart === gainEnd) { + if (view) { + for (let i = 0; i < view.length; i++) { + const sample = view[i] ?? 0 + const value = sample * gainStart + view[i] = value < -32768 ? -32768 : value > 32767 ? 32767 : value | 0 + } + } else { + for (let i = 0; i < sampleCount; i++) { + const sample = chunk.readInt16LE(i * 2) + const value = sample * gainStart + const clamped = + value < -32768 ? -32768 : value > 32767 ? 32767 : value | 0 + chunk.writeInt16LE(clamped, i * 2) + } } } else { - for (let i = 0; i < sampleCount; i++) { - const gain = gainStart + step * i - const sample = chunk.readInt16LE(i * 2) - const value = sample * gain - const clamped = - value < -32768 ? -32768 : value > 32767 ? 32767 : value | 0 - chunk.writeInt16LE(clamped, i * 2) + const step = sampleCount > 1 ? (gainEnd - gainStart) / (sampleCount - 1) : 0 + + if (view) { + for (let i = 0; i < view.length; i++) { + const gain = gainStart + step * i + const sample = view[i] ?? 0 + const value = sample * gain + view[i] = value < -32768 ? -32768 : value > 32767 ? 32767 : value | 0 + } + } else { + for (let i = 0; i < sampleCount; i++) { + const gain = gainStart + step * i + const sample = chunk.readInt16LE(i * 2) + const value = sample * gain + const clamped = + value < -32768 ? -32768 : value > 32767 ? 32767 : value | 0 + chunk.writeInt16LE(clamped, i * 2) + } } } diff --git a/src/playback/processing/LoudnessNormalizer.ts b/src/playback/processing/LoudnessNormalizer.ts index 94fac554..8125e598 100644 --- a/src/playback/processing/LoudnessNormalizer.ts +++ b/src/playback/processing/LoudnessNormalizer.ts @@ -4,6 +4,7 @@ const INT16_MAX = 32767 const INT16_MIN = -32768 const MIN_ENERGY = 1e-12 const fround = Math.fround +const INV_32768 = 1 / 32768 /** * Implements a standard Biquad filter for audio processing. @@ -198,6 +199,9 @@ export class LoudnessNormalizer { const energyAlpha = this._energyAlpha const target = this.targetLoudness + const filters = this.filters + const numChannels = this.channels + for ( let frameIndex = 0, sampleIndex = 0; frameIndex < frameCount; @@ -205,9 +209,9 @@ export class LoudnessNormalizer { ) { let energySum = 0.0 - for (let ch = 0; ch < this.channels; ch += 1, sampleIndex += 1) { - const sample = fround((inputView[sampleIndex] ?? 0) / 32768) - const filter = this.filters[ch] + for (let ch = 0; ch < numChannels; ch += 1, sampleIndex += 1) { + const sample = fround((inputView[sampleIndex] ?? 0) * INV_32768) + const filter = filters[ch] if (filter) { const filtered = filter.process(sample) channelBuffer[ch] = filtered @@ -215,7 +219,7 @@ export class LoudnessNormalizer { } } - energySum = fround(energySum / this.channels) + energySum = fround(energySum / numChannels) if (energySum > this._gateThresholdEnergy) { energyState = fround( diff --git a/src/playback/processing/VolumeTransformer.ts b/src/playback/processing/VolumeTransformer.ts index ca4eedc2..da239ceb 100644 --- a/src/playback/processing/VolumeTransformer.ts +++ b/src/playback/processing/VolumeTransformer.ts @@ -207,10 +207,20 @@ export class VolumeTransformer extends Transform implements IVolumeTransformer { const normalizedOvershoot = (abs - this._thresholdValue) / this._limitHeadroom - const softened = 1 - Math.exp(-normalizedOvershoot * this.limiterSoftness) + + // Fast approximation of 1 - exp(-x) for small x + // 1 - exp(-x) \approx x - x^2/2 + x^3/6 + const x = normalizedOvershoot * this.limiterSoftness + let softened: number + if (x < 0.1) { + softened = x * (1 - x * 0.5) + } else { + softened = 1 - Math.exp(-x) + } + const limited = this._thresholdValue + this._limitHeadroom * softened - return Math.sign(value) * Math.min(INT16_MAX, limited) + return (value < 0 ? -1 : 1) * (limited > INT16_MAX ? INT16_MAX : limited) } private _clampToInt16(value: number): number { @@ -281,19 +291,24 @@ export class VolumeTransformer extends Transform implements IVolumeTransformer { gain += gainStep } } else if (view) { - for (let i = 0; i < view.length; i++) { + const len = view.length + const lookaheadBuffer = this.lookaheadBuffer + const lookaheadSamples = this.lookaheadSamples + let lookaheadIndex = this.lookaheadIndex + + for (let i = 0; i < len; i++) { const rawSample = view[i] ?? 0 const scaled = rawSample * gain const limited = this._applyLimiter(scaled) - const outputSample = this.lookaheadBuffer[this.lookaheadIndex] ?? 0 - this.lookaheadBuffer[this.lookaheadIndex] = limited - this.lookaheadIndex = - (this.lookaheadIndex + 1) % this.lookaheadSamples + const outputSample = lookaheadBuffer[lookaheadIndex] ?? 0 + lookaheadBuffer[lookaheadIndex] = limited + lookaheadIndex = (lookaheadIndex + 1) % lookaheadSamples outputView[i] = this._clampToInt16(outputSample) gain += gainStep } + this.lookaheadIndex = lookaheadIndex } if (this.lookaheadIndex === 0) this.lookaheadFull = true @@ -301,18 +316,35 @@ export class VolumeTransformer extends Transform implements IVolumeTransformer { } if (useBufferOps) { - for (let i = 0; i < usableSamples; i++) { - const scaled = chunk.readInt16LE(i * 2) * gain - const limited = this._applyLimiter(scaled) - chunk.writeInt16LE(this._clampToInt16(limited), i * 2) - gain += gainStep + if (gainStart === gainEnd) { + for (let i = 0; i < usableSamples; i++) { + const scaled = chunk.readInt16LE(i * 2) * gainStart + const limited = this._applyLimiter(scaled) + chunk.writeInt16LE(this._clampToInt16(limited), i * 2) + } + } else { + for (let i = 0; i < usableSamples; i++) { + const scaled = chunk.readInt16LE(i * 2) * gain + const limited = this._applyLimiter(scaled) + chunk.writeInt16LE(this._clampToInt16(limited), i * 2) + gain += gainStep + } } } else if (view) { - for (let i = 0; i < view.length; i++) { - const scaled = (view[i] ?? 0) * gain - const limited = this._applyLimiter(scaled) - view[i] = this._clampToInt16(limited) - gain += gainStep + const len = view.length + if (gainStart === gainEnd) { + for (let i = 0; i < len; i++) { + const scaled = (view[i] ?? 0) * gainStart + const limited = this._applyLimiter(scaled) + view[i] = this._clampToInt16(limited) + } + } else { + for (let i = 0; i < len; i++) { + const scaled = (view[i] ?? 0) * gain + const limited = this._applyLimiter(scaled) + view[i] = this._clampToInt16(limited) + gain += gainStep + } } } diff --git a/src/playback/structs/BufferPool.ts b/src/playback/structs/BufferPool.ts index 3da5e62e..4b1cf6cd 100644 --- a/src/playback/structs/BufferPool.ts +++ b/src/playback/structs/BufferPool.ts @@ -9,11 +9,11 @@ const parsePositiveIntEnv = (key: string, fallback: number): number => { const MAX_POOL_SIZE_BYTES = parsePositiveIntEnv( 'NODELINK_BUFFER_POOL_MAX_BYTES', - 20 * 1024 * 1024 // 20 MB - reduced from 50MB + 64 * 1024 * 1024 // 64 MB ) const MAX_BUCKET_ENTRIES = parsePositiveIntEnv( 'NODELINK_BUFFER_POOL_MAX_BUCKET_ENTRIES', - 4 // reduced from 8 + 16 ) const IDLE_CLEAR_MS = parsePositiveIntEnv( 'NODELINK_BUFFER_POOL_IDLE_CLEAR_MS', diff --git a/src/playback/structs/RingBuffer.ts b/src/playback/structs/RingBuffer.ts index d88b0617..6ce1517a 100644 --- a/src/playback/structs/RingBuffer.ts +++ b/src/playback/structs/RingBuffer.ts @@ -82,18 +82,33 @@ export class RingBuffer { if (bytesToRead === 0) return null const out = Buffer.allocUnsafe(bytesToRead) + this.readTo(out) + return out + } + + /** + * Reads bytes from the ring buffer directly into the target buffer. + * @param target - The target buffer to write into. + * @param targetOffset - Offset in the target buffer. + * @returns The number of bytes actually read. + */ + public readTo(target: Buffer, targetOffset = 0): number { + if (!this.buffer) return 0 + + const bytesToRead = Math.min(target.length - targetOffset, this._length) + if (bytesToRead <= 0) return 0 const availableAtEnd = this.size - this.readOffset if (bytesToRead <= availableAtEnd) { - this.buffer.copy(out, 0, this.readOffset, this.readOffset + bytesToRead) + this.buffer.copy(target, targetOffset, this.readOffset, this.readOffset + bytesToRead) } else { - this.buffer.copy(out, 0, this.readOffset, this.size) - this.buffer.copy(out, availableAtEnd, 0, bytesToRead - availableAtEnd) + this.buffer.copy(target, targetOffset, this.readOffset, this.size) + this.buffer.copy(target, targetOffset + availableAtEnd, 0, bytesToRead - availableAtEnd) } this.readOffset = (this.readOffset + bytesToRead) % this.size this._length -= bytesToRead - return out + return bytesToRead } /** diff --git a/src/workers/main.ts b/src/workers/main.ts index c0473883..b55a78fa 100644 --- a/src/workers/main.ts +++ b/src/workers/main.ts @@ -1713,10 +1713,10 @@ function startTimers(hibernating = false): void { (acc, curr) => acc + getHeadQueueLength(curr.queue), 0 ), - cpu: { nodelinkLoad }, - eventLoopLag: eluP50, - eventLoopLagP95: eluP95, - eventLoopLagP99: eluP99, + cpu: { nodelinkLoad: Math.round(nodelinkLoad * 100) / 100 }, + eventLoopLag: Math.round(eluP50 * 100) / 100, + eventLoopLagP95: Math.round(eluP95 * 100) / 100, + eventLoopLagP99: Math.round(eluP99 * 100) / 100, memory: { used: mem.heapUsed, allocated: mem.heapTotal