perf: in-memory PCM cache + skip InlineVolume at vol 1.0
- PCM-Dateien werden beim ersten Abspielen in den RAM geladen (Map<string, Buffer>) - Nachfolgende Plays lesen aus RAM statt Disk -> eliminiert I/O-Latenz - InlineVolume Transform wird bei Volume 1.0 uebersprungen (unnoetige Sample-Verarbeitung) - Fallback createReadStream mit 256KB highWaterMark fuer schnelleres Buffering - Memory-Cache-Limit konfigurierbar via PCM_CACHE_MAX_MB env (default 512MB) - Cache-Invalidierung bei Quelldatei-Aenderungen
This commit is contained in:
parent
cde29698ca
commit
901f0bf1dd
1 changed files with 1602 additions and 1555 deletions
|
|
@ -24,7 +24,7 @@ import sodium from 'libsodium-wrappers';
|
|||
import nacl from 'tweetnacl';
|
||||
// Streaming externer Plattformen entfernt – nur MP3-URLs werden noch unterstützt
|
||||
import child_process from 'node:child_process';
|
||||
import { PassThrough } from 'node:stream';
|
||||
import { PassThrough, Readable } from 'node:stream';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
|
@ -158,6 +158,35 @@ const NORMALIZE_TP = String(process.env.NORMALIZE_TP ?? '-1.5');
|
|||
const NORM_CACHE_DIR = path.join(SOUNDS_DIR, '.norm-cache');
|
||||
fs.mkdirSync(NORM_CACHE_DIR, { recursive: true });
|
||||
|
||||
// In-Memory PCM Cache: gecachte PCM-Dateien werden beim ersten Abspielen in den RAM geladen.
|
||||
// Danach kein Disk-I/O mehr bei Cache-Hits -> nahezu instant playback.
|
||||
const pcmMemoryCache = new Map<string, Buffer>();
|
||||
const PCM_MEMORY_CACHE_MAX_MB = Number(process.env.PCM_CACHE_MAX_MB ?? '512');
|
||||
let pcmMemoryCacheBytes = 0;
|
||||
|
||||
function getPcmFromMemory(cachedPath: string): Buffer | null {
|
||||
const buf = pcmMemoryCache.get(cachedPath);
|
||||
if (buf) return buf;
|
||||
// Erste Anfrage: von Disk in RAM laden
|
||||
try {
|
||||
const data = fs.readFileSync(cachedPath);
|
||||
const newTotal = pcmMemoryCacheBytes + data.byteLength;
|
||||
if (newTotal <= PCM_MEMORY_CACHE_MAX_MB * 1024 * 1024) {
|
||||
pcmMemoryCache.set(cachedPath, data);
|
||||
pcmMemoryCacheBytes = newTotal;
|
||||
}
|
||||
return data;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
function invalidatePcmMemory(cachedPath: string): void {
|
||||
const buf = pcmMemoryCache.get(cachedPath);
|
||||
if (buf) {
|
||||
pcmMemoryCacheBytes -= buf.byteLength;
|
||||
pcmMemoryCache.delete(cachedPath);
|
||||
}
|
||||
}
|
||||
|
||||
/** Erzeugt einen Cache-Key aus dem relativen Pfad (Slash-normalisiert, Sonderzeichen escaped) */
|
||||
function normCacheKey(filePath: string): string {
|
||||
const rel = path.relative(SOUNDS_DIR, filePath).replace(/\\/g, '/');
|
||||
|
|
@ -172,7 +201,7 @@ function getNormCachePath(filePath: string): string | null {
|
|||
try {
|
||||
const srcMtime = fs.statSync(filePath).mtimeMs;
|
||||
const cacheMtime = fs.statSync(cacheFile).mtimeMs;
|
||||
if (srcMtime > cacheMtime) { try { fs.unlinkSync(cacheFile); } catch {} return null; }
|
||||
if (srcMtime > cacheMtime) { try { fs.unlinkSync(cacheFile); } catch {} invalidatePcmMemory(cacheFile); return null; }
|
||||
} catch { return null; }
|
||||
return cacheFile;
|
||||
}
|
||||
|
|
@ -381,9 +410,19 @@ async function playFilePath(guildId: string, channelId: string, filePath: string
|
|||
if (NORMALIZE_ENABLE) {
|
||||
const cachedPath = getNormCachePath(filePath);
|
||||
if (cachedPath) {
|
||||
// Cache-Hit: gecachte PCM-Datei als Stream lesen (kein ffmpeg, instant)
|
||||
const pcmStream = fs.createReadStream(cachedPath);
|
||||
// Cache-Hit: PCM aus RAM lesen (kein Disk-I/O, instant)
|
||||
const pcmBuf = getPcmFromMemory(cachedPath);
|
||||
if (pcmBuf) {
|
||||
const useInline = useVolume !== 1;
|
||||
resource = createAudioResource(Readable.from(pcmBuf), {
|
||||
inlineVolume: useInline,
|
||||
inputType: StreamType.Raw
|
||||
});
|
||||
} else {
|
||||
// Fallback: Stream von Disk
|
||||
const pcmStream = fs.createReadStream(cachedPath, { highWaterMark: 256 * 1024 });
|
||||
resource = createAudioResource(pcmStream, { inlineVolume: true, inputType: StreamType.Raw });
|
||||
}
|
||||
} else {
|
||||
// Cache-Miss: ffmpeg streamen (sofortige Wiedergabe) UND gleichzeitig in Cache schreiben
|
||||
const cacheFile = path.join(NORM_CACHE_DIR, normCacheKey(filePath));
|
||||
|
|
@ -402,6 +441,14 @@ async function playFilePath(guildId: string, channelId: string, filePath: string
|
|||
playerStream.end();
|
||||
cacheWrite.end();
|
||||
console.log(`${new Date().toISOString()} | Loudnorm cached: ${path.basename(filePath)}`);
|
||||
// In Memory-Cache laden fuer naechsten Aufruf
|
||||
try {
|
||||
const buf = fs.readFileSync(cacheFile);
|
||||
if (pcmMemoryCacheBytes + buf.byteLength <= PCM_MEMORY_CACHE_MAX_MB * 1024 * 1024) {
|
||||
pcmMemoryCache.set(cacheFile, buf);
|
||||
pcmMemoryCacheBytes += buf.byteLength;
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
ff.on('error', () => { try { fs.unlinkSync(cacheFile); } catch {} });
|
||||
ff.on('close', (code) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue