Changeset 201ec06 in Klonkt


Ignore:
Timestamp:
06/28/2026 05:15:48 PM (2 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
a64d705
Parents:
99989f9
Message:

fix(images): cap concurrent ffmpeg (thumbnails) to avoid CPU saturation

A cold-cache image-heavy page fired one ffmpeg per thumbnail at once (a thundering herd),
saturating CPU and making the whole instance slow. Cap concurrent ffmpeg spawns to 3; excess
requests wait briefly for a slot. Applies to local + remote thumbnail generation.

  • services/ThumbnailService.js — acquireSlot/releaseSlot semaphore + runFfmpeg wrapper
File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ThumbnailService.js

    r99989f9 r201ec06  
    2727
    2828let _seq = 0;
     29
     30// Limit concurrent ffmpeg spawns. A cold-cache, image-heavy page fires many thumbnail
     31// requests at once; without a cap each spawns its own ffmpeg → CPU saturation makes the
     32// WHOLE instance slow (the thundering herd). With the cap, excess requests wait briefly
     33// for a slot → bounded CPU, the page still loads (images just appear progressively).
     34const MAX_CONCURRENT = 3;
     35let _active = 0;
     36const _waiters = [];
     37function acquireSlot() {
     38  if (_active < MAX_CONCURRENT) { _active++; return Promise.resolve(); }
     39  return new Promise((resolve) => _waiters.push(resolve));
     40}
     41function releaseSlot() {
     42  const next = _waiters.shift();
     43  if (next) next();      // transfer the slot directly to the next waiter (_active unchanged)
     44  else _active--;
     45}
     46async function runFfmpeg(args) {
     47  await acquireSlot();
     48  try { await execFileP(ffmpegPath, args, { timeout: 20000 }); }
     49  finally { releaseSlot(); }
     50}
    2951
    3052function mediaRoot() {
     
    5779  const tmp = `${cached}.tmp-${process.pid}-${_seq++}`;
    5880  try {
    59     await execFileP(ffmpegPath, [
     81    await runFfmpeg([
    6082      '-hide_banner', '-loglevel', 'error', '-y',
    6183      '-i', orig,
     
    6890      '-f', 'webp',
    6991      tmp,
    70     ], { timeout: 20000 });
     92    ]);
    7193    await fs.promises.rename(tmp, cached);
    7294    return cached;
     
    143165  try {
    144166    await fs.promises.writeFile(tmpIn, buf);
    145     await execFileP(ffmpegPath, [
     167    await runFfmpeg([
    146168      '-hide_banner', '-loglevel', 'error', '-y',
    147169      '-i', tmpIn,
     
    150172      '-c:v', 'libwebp', '-q:v', '82', '-f', 'webp',
    151173      tmpOut,
    152     ], { timeout: 20000 });
     174    ]);
    153175    await fs.promises.rename(tmpOut, cached);
    154176    return cached;
Note: See TracChangeset for help on using the changeset viewer.