Skip to content

Commit 2e6fdce

Browse files
authored
Merge pull request #1 from genieocode/performance-optimizations-14538468960965277556
Performance Optimizations for RAM and CPU Efficiency
2 parents d4f7b52 + 014fb8e commit 2e6fdce

15 files changed

Lines changed: 256 additions & 119 deletions

File tree

dist/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "nodelink",
3-
"version": "3.8.0-dev.20260422.1",
3+
"version": "3.8.0",
44
"scripts": {
55
"build": "tsc --incremental false",
66
"start": "node --dns-result-order=ipv4first --import tsx src/index.ts",

dist/src/playback/processing/AudioMixer.js

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -63,27 +63,23 @@ export class AudioMixer extends Readable {
6363
mixBuffers(mainPCM, layersPCM) {
6464
if (layersPCM.size === 0 || !this.enabled)
6565
return mainPCM;
66-
const outputBuffer = Buffer.allocUnsafe(mainPCM.length);
66+
const mainLen = mainPCM.length >> 1;
6767
const mainView = this._asInt16Array(mainPCM);
68-
const outputView = this._asInt16Array(outputBuffer);
6968
const activeLayerViews = [];
69+
const layerVolumes = [];
7070
for (const layer of layersPCM.values()) {
71-
activeLayerViews.push({
72-
view: this._asInt16Array(layer.buffer),
73-
volume: layer.volume
74-
});
71+
activeLayerViews.push(this._asInt16Array(layer.buffer));
72+
layerVolumes.push(layer.volume);
7573
}
76-
const mainLen = mainView.length;
7774
const numLayers = activeLayerViews.length;
75+
const outputBuffer = Buffer.allocUnsafe(mainPCM.length);
76+
const outputView = this._asInt16Array(outputBuffer);
7877
for (let i = 0; i < mainLen; i++) {
7978
let sample = mainView[i] ?? 0;
8079
for (let j = 0; j < numLayers; j++) {
81-
const layer = activeLayerViews[j];
82-
if (!layer)
83-
continue;
84-
if (i < layer.view.length) {
85-
sample += ((layer.view[i] ?? 0) * layer.volume) | 0;
86-
}
80+
const layerView = activeLayerViews[j];
81+
const volume = layerVolumes[j];
82+
sample += ((layerView[i] ?? 0) * volume) | 0;
8783
}
8884
outputView[i] = sample < -32768 ? -32768 : sample > 32767 ? 32767 : sample;
8985
}

dist/src/playback/processing/FadeTransformer.js

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -116,22 +116,41 @@ export class FadeTransformer extends Transform {
116116
else {
117117
_useBuffer = true;
118118
}
119-
const step = sampleCount > 1 ? (gainEnd - gainStart) / (sampleCount - 1) : 0;
120-
if (view) {
121-
for (let i = 0; i < view.length; i++) {
122-
const gain = gainStart + step * i;
123-
const sample = view[i] ?? 0;
124-
const value = sample * gain;
125-
view[i] = value < -32768 ? -32768 : value > 32767 ? 32767 : value | 0;
119+
if (gainStart === gainEnd) {
120+
if (view) {
121+
for (let i = 0; i < view.length; i++) {
122+
const sample = view[i] ?? 0;
123+
const value = sample * gainStart;
124+
view[i] = value < -32768 ? -32768 : value > 32767 ? 32767 : value | 0;
125+
}
126+
}
127+
else {
128+
for (let i = 0; i < sampleCount; i++) {
129+
const sample = chunk.readInt16LE(i * 2);
130+
const value = sample * gainStart;
131+
const clamped = value < -32768 ? -32768 : value > 32767 ? 32767 : value | 0;
132+
chunk.writeInt16LE(clamped, i * 2);
133+
}
126134
}
127135
}
128136
else {
129-
for (let i = 0; i < sampleCount; i++) {
130-
const gain = gainStart + step * i;
131-
const sample = chunk.readInt16LE(i * 2);
132-
const value = sample * gain;
133-
const clamped = value < -32768 ? -32768 : value > 32767 ? 32767 : value | 0;
134-
chunk.writeInt16LE(clamped, i * 2);
137+
const step = sampleCount > 1 ? (gainEnd - gainStart) / (sampleCount - 1) : 0;
138+
if (view) {
139+
for (let i = 0; i < view.length; i++) {
140+
const gain = gainStart + step * i;
141+
const sample = view[i] ?? 0;
142+
const value = sample * gain;
143+
view[i] = value < -32768 ? -32768 : value > 32767 ? 32767 : value | 0;
144+
}
145+
}
146+
else {
147+
for (let i = 0; i < sampleCount; i++) {
148+
const gain = gainStart + step * i;
149+
const sample = chunk.readInt16LE(i * 2);
150+
const value = sample * gain;
151+
const clamped = value < -32768 ? -32768 : value > 32767 ? 32767 : value | 0;
152+
chunk.writeInt16LE(clamped, i * 2);
153+
}
135154
}
136155
}
137156
return chunk;

dist/src/playback/processing/LoudnessNormalizer.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const INT16_MAX = 32767;
22
const INT16_MIN = -32768;
33
const MIN_ENERGY = 1e-12;
44
const fround = Math.fround;
5+
const INV_32768 = 1 / 32768;
56
/**
67
* Implements a standard Biquad filter for audio processing.
78
*/
@@ -162,18 +163,20 @@ export class LoudnessNormalizer {
162163
const releaseAlpha = this._releaseAlpha;
163164
const energyAlpha = this._energyAlpha;
164165
const target = this.targetLoudness;
166+
const filters = this.filters;
167+
const numChannels = this.channels;
165168
for (let frameIndex = 0, sampleIndex = 0; frameIndex < frameCount; frameIndex++) {
166169
let energySum = 0.0;
167-
for (let ch = 0; ch < this.channels; ch += 1, sampleIndex += 1) {
168-
const sample = fround((inputView[sampleIndex] ?? 0) / 32768);
169-
const filter = this.filters[ch];
170+
for (let ch = 0; ch < numChannels; ch += 1, sampleIndex += 1) {
171+
const sample = fround((inputView[sampleIndex] ?? 0) * INV_32768);
172+
const filter = filters[ch];
170173
if (filter) {
171174
const filtered = filter.process(sample);
172175
channelBuffer[ch] = filtered;
173176
energySum += filtered * filtered;
174177
}
175178
}
176-
energySum = fround(energySum / this.channels);
179+
energySum = fround(energySum / numChannels);
177180
if (energySum > this._gateThresholdEnergy) {
178181
energyState = fround(energyState * energyAlpha + (1 - energyAlpha) * energySum);
179182
}

dist/src/playback/processing/VolumeTransformer.js

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export class VolumeTransformer extends Transform {
2222
lookaheadBuffer;
2323
lookaheadIndex;
2424
lookaheadFull;
25+
_reusableOutputBuffer = null;
2526
currentVolume;
2627
targetVolume;
2728
startVolume;
@@ -141,9 +142,18 @@ export class VolumeTransformer extends Transform {
141142
if (abs <= this._thresholdValue || this._limitHeadroom <= 0)
142143
return value;
143144
const normalizedOvershoot = (abs - this._thresholdValue) / this._limitHeadroom;
144-
const softened = 1 - Math.exp(-normalizedOvershoot * this.limiterSoftness);
145+
// Fast approximation of 1 - exp(-x) for small x
146+
// 1 - exp(-x) \approx x - x^2/2 + x^3/6
147+
const x = normalizedOvershoot * this.limiterSoftness;
148+
let softened;
149+
if (x < 0.1) {
150+
softened = x * (1 - x * 0.5);
151+
}
152+
else {
153+
softened = 1 - Math.exp(-x);
154+
}
145155
const limited = this._thresholdValue + this._limitHeadroom * softened;
146-
return Math.sign(value) * Math.min(INT16_MAX, limited);
156+
return (value < 0 ? -1 : 1) * (limited > INT16_MAX ? INT16_MAX : limited);
147157
}
148158
_clampToInt16(value) {
149159
if (value >= INT16_MAX)
@@ -186,7 +196,10 @@ export class VolumeTransformer extends Transform {
186196
const gainStep = usableSamples > 1 ? (gainEnd - gainStart) / (usableSamples - 1) : 0;
187197
let gain = gainStart;
188198
if (this.lookaheadSamples > 0) {
189-
const outputBuffer = alignedBufferIfRequired(chunk.length);
199+
if (!this._reusableOutputBuffer || this._reusableOutputBuffer.length < chunk.length) {
200+
this._reusableOutputBuffer = alignedBufferIfRequired(chunk.length);
201+
}
202+
const outputBuffer = this._reusableOutputBuffer.subarray(0, chunk.length);
190203
const outputView = new Int16Array(outputBuffer.buffer, outputBuffer.byteOffset, usableSamples);
191204
if (useBufferOps) {
192205
for (let i = 0; i < usableSamples; i++) {
@@ -202,36 +215,59 @@ export class VolumeTransformer extends Transform {
202215
}
203216
}
204217
else if (view) {
205-
for (let i = 0; i < view.length; i++) {
218+
const len = view.length;
219+
const lookaheadBuffer = this.lookaheadBuffer;
220+
const lookaheadSamples = this.lookaheadSamples;
221+
let lookaheadIndex = this.lookaheadIndex;
222+
for (let i = 0; i < len; i++) {
206223
const rawSample = view[i] ?? 0;
207224
const scaled = rawSample * gain;
208225
const limited = this._applyLimiter(scaled);
209-
const outputSample = this.lookaheadBuffer[this.lookaheadIndex] ?? 0;
210-
this.lookaheadBuffer[this.lookaheadIndex] = limited;
211-
this.lookaheadIndex =
212-
(this.lookaheadIndex + 1) % this.lookaheadSamples;
226+
const outputSample = lookaheadBuffer[lookaheadIndex] ?? 0;
227+
lookaheadBuffer[lookaheadIndex] = limited;
228+
lookaheadIndex = (lookaheadIndex + 1) % lookaheadSamples;
213229
outputView[i] = this._clampToInt16(outputSample);
214230
gain += gainStep;
215231
}
232+
this.lookaheadIndex = lookaheadIndex;
216233
}
217234
if (this.lookaheadIndex === 0)
218235
this.lookaheadFull = true;
219236
return outputBuffer;
220237
}
221238
if (useBufferOps) {
222-
for (let i = 0; i < usableSamples; i++) {
223-
const scaled = chunk.readInt16LE(i * 2) * gain;
224-
const limited = this._applyLimiter(scaled);
225-
chunk.writeInt16LE(this._clampToInt16(limited), i * 2);
226-
gain += gainStep;
239+
if (gainStart === gainEnd) {
240+
for (let i = 0; i < usableSamples; i++) {
241+
const scaled = chunk.readInt16LE(i * 2) * gainStart;
242+
const limited = this._applyLimiter(scaled);
243+
chunk.writeInt16LE(this._clampToInt16(limited), i * 2);
244+
}
245+
}
246+
else {
247+
for (let i = 0; i < usableSamples; i++) {
248+
const scaled = chunk.readInt16LE(i * 2) * gain;
249+
const limited = this._applyLimiter(scaled);
250+
chunk.writeInt16LE(this._clampToInt16(limited), i * 2);
251+
gain += gainStep;
252+
}
227253
}
228254
}
229255
else if (view) {
230-
for (let i = 0; i < view.length; i++) {
231-
const scaled = (view[i] ?? 0) * gain;
232-
const limited = this._applyLimiter(scaled);
233-
view[i] = this._clampToInt16(limited);
234-
gain += gainStep;
256+
const len = view.length;
257+
if (gainStart === gainEnd) {
258+
for (let i = 0; i < len; i++) {
259+
const scaled = (view[i] ?? 0) * gainStart;
260+
const limited = this._applyLimiter(scaled);
261+
view[i] = this._clampToInt16(limited);
262+
}
263+
}
264+
else {
265+
for (let i = 0; i < len; i++) {
266+
const scaled = (view[i] ?? 0) * gain;
267+
const limited = this._applyLimiter(scaled);
268+
view[i] = this._clampToInt16(limited);
269+
gain += gainStep;
270+
}
235271
}
236272
}
237273
return chunk;

dist/src/playback/structs/BufferPool.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@ const parsePositiveIntEnv = (key, fallback) => {
66
const parsed = Number.parseInt(raw, 10);
77
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
88
};
9-
const MAX_POOL_SIZE_BYTES = parsePositiveIntEnv('NODELINK_BUFFER_POOL_MAX_BYTES', 20 * 1024 * 1024 // 20 MB - reduced from 50MB
10-
);
11-
const MAX_BUCKET_ENTRIES = parsePositiveIntEnv('NODELINK_BUFFER_POOL_MAX_BUCKET_ENTRIES', 4 // reduced from 8
9+
const MAX_POOL_SIZE_BYTES = parsePositiveIntEnv('NODELINK_BUFFER_POOL_MAX_BYTES', 64 * 1024 * 1024 // 64 MB
1210
);
11+
const MAX_BUCKET_ENTRIES = parsePositiveIntEnv('NODELINK_BUFFER_POOL_MAX_BUCKET_ENTRIES', 16);
1312
const IDLE_CLEAR_MS = parsePositiveIntEnv('NODELINK_BUFFER_POOL_IDLE_CLEAR_MS', 60000 // 1 min - reduced from 3 min
1413
);
1514
const CLEANUP_INTERVAL = 60000;

dist/src/playback/structs/RingBuffer.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,17 +76,32 @@ export class RingBuffer {
7676
if (bytesToRead === 0)
7777
return null;
7878
const out = Buffer.allocUnsafe(bytesToRead);
79+
this.readTo(out);
80+
return out;
81+
}
82+
/**
83+
* Reads bytes from the ring buffer directly into the target buffer.
84+
* @param target - The target buffer to write into.
85+
* @param targetOffset - Offset in the target buffer.
86+
* @returns The number of bytes actually read.
87+
*/
88+
readTo(target, targetOffset = 0) {
89+
if (!this.buffer)
90+
return 0;
91+
const bytesToRead = Math.min(target.length - targetOffset, this._length);
92+
if (bytesToRead <= 0)
93+
return 0;
7994
const availableAtEnd = this.size - this.readOffset;
8095
if (bytesToRead <= availableAtEnd) {
81-
this.buffer.copy(out, 0, this.readOffset, this.readOffset + bytesToRead);
96+
this.buffer.copy(target, targetOffset, this.readOffset, this.readOffset + bytesToRead);
8297
}
8398
else {
84-
this.buffer.copy(out, 0, this.readOffset, this.size);
85-
this.buffer.copy(out, availableAtEnd, 0, bytesToRead - availableAtEnd);
99+
this.buffer.copy(target, targetOffset, this.readOffset, this.size);
100+
this.buffer.copy(target, targetOffset + availableAtEnd, 0, bytesToRead - availableAtEnd);
86101
}
87102
this.readOffset = (this.readOffset + bytesToRead) % this.size;
88103
this._length -= bytesToRead;
89-
return out;
104+
return bytesToRead;
90105
}
91106
/**
92107
* Skips n bytes in the buffer.

dist/src/workers/main.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1263,10 +1263,10 @@ function startTimers(hibernating = false) {
12631263
players: localPlayers,
12641264
playingPlayers: localPlayingPlayers,
12651265
commandQueueLength: Array.from(guildQueues.values()).reduce((acc, curr) => acc + getHeadQueueLength(curr.queue), 0),
1266-
cpu: { nodelinkLoad },
1267-
eventLoopLag: eluP50,
1268-
eventLoopLagP95: eluP95,
1269-
eventLoopLagP99: eluP99,
1266+
cpu: { nodelinkLoad: Math.round(nodelinkLoad * 100) / 100 },
1267+
eventLoopLag: Math.round(eluP50 * 100) / 100,
1268+
eventLoopLagP95: Math.round(eluP95 * 100) / 100,
1269+
eventLoopLagP99: Math.round(eluP99 * 100) / 100,
12701270
memory: {
12711271
used: mem.heapUsed,
12721272
allocated: mem.heapTotal

src/playback/processing/AudioMixer.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -98,29 +98,29 @@ export class AudioMixer extends Readable {
9898
): Buffer {
9999
if (layersPCM.size === 0 || !this.enabled) return mainPCM
100100

101-
const outputBuffer = Buffer.allocUnsafe(mainPCM.length)
101+
const mainLen = mainPCM.length >> 1
102102
const mainView = this._asInt16Array(mainPCM)
103-
const outputView = this._asInt16Array(outputBuffer)
104103

105-
const activeLayerViews: Array<{ view: Int16Array; volume: number }> = []
104+
const activeLayerViews: Int16Array[] = []
105+
const layerVolumes: number[] = []
106+
106107
for (const layer of layersPCM.values()) {
107-
activeLayerViews.push({
108-
view: this._asInt16Array(layer.buffer),
109-
volume: layer.volume
110-
})
108+
activeLayerViews.push(this._asInt16Array(layer.buffer))
109+
layerVolumes.push(layer.volume)
111110
}
112111

113-
const mainLen = mainView.length
114112
const numLayers = activeLayerViews.length
113+
const outputBuffer = Buffer.allocUnsafe(mainPCM.length)
114+
const outputView = this._asInt16Array(outputBuffer)
115115

116116
for (let i = 0; i < mainLen; i++) {
117117
let sample = mainView[i] ?? 0
118+
118119
for (let j = 0; j < numLayers; j++) {
119-
const layer = activeLayerViews[j]
120-
if (!layer) continue
121-
if (i < layer.view.length) {
122-
sample += ((layer.view[i] ?? 0) * layer.volume) | 0
123-
}
120+
const layerView = activeLayerViews[j] as Int16Array
121+
const volume = layerVolumes[j] as number
122+
123+
sample += ((layerView[i] ?? 0) * volume) | 0
124124
}
125125

126126
outputView[i] = sample < -32768 ? -32768 : sample > 32767 ? 32767 : sample

0 commit comments

Comments
 (0)