Loading debian/changelog +8 −0 Original line number Diff line number Diff line mediadb (20260424+76) unstable; urgency=medium * Fix: remove render_pool_, run FFmpeg inline on HTTP worker threads. Separate render pool caused thread starvation and UI hangs. io_pool_ now exclusively serves prefetch I/O; semaphore provides backpressure. -- Jan Koester <jan.koester@tuxist.de> Thu, 24 Apr 2026 00:00:00 +0200 mediadb (20260424+75) unstable; urgency=medium * Fix: split single io_pool into separate render_pool (CPU-heavy FFmpeg) Loading src/preview.cpp +13 −26 Original line number Diff line number Diff line Loading @@ -695,10 +695,9 @@ std::optional<PreviewResult> FFmpegPreviewer::encode_frame(AVFrame* frame, const PreviewService::PreviewService(BlobCache& cache, MediaBackendApi& db, std::size_t render_threads) : cache_(cache), db_(db), render_pool_(render_threads > 0 ? render_threads : std::max(2u, std::min(8u, std::thread::hardware_concurrency() / 2))), io_pool_(std::max(4u, std::min(16u, std::thread::hardware_concurrency()))), render_sem_(render_pool_.size()) {} render_sem_(render_threads > 0 ? render_threads : std::max(1u, std::thread::hardware_concurrency())) {} std::string PreviewService::make_key(const std::string& media_id, int width, int height, const std::string& fmt, Loading Loading @@ -759,31 +758,19 @@ std::shared_ptr<const BlobValue> PreviewService::get_or_create_streaming(const M ~SemGuard() { self->render_sem_.release(); } } sem_guard{this}; // Run FFmpeg on the io_pool with a timeout. This prevents a stuck // FFmpeg render from blocking the HTTP worker thread indefinitely. // Captures are by value so the lambda is safe if we return on timeout. std::string mid = media.id; std::uint64_t msize = media.size_bytes; std::string mkind = media.media_kind; auto fut = render_pool_.submit([this, mid, msize, mkind, width, height, fmt, t_seconds, duration]() { std::string err; auto res = ffmpeg_.render_streaming(db_, io_pool_, cache_, mid, msize, mkind, width, height, fmt, t_seconds, duration, err); return std::make_pair(std::move(res), std::move(err)); }); auto status = fut.wait_for(std::chrono::seconds(60)); if (status == std::future_status::timeout) { error_out = "preview render timed out after 60s"; return nullptr; // Run FFmpeg inline on the HTTP worker thread. The semaphore above // limits concurrency; io_pool_ handles prefetch I/O independently. std::optional<PreviewResult> result; try { result = ffmpeg_.render_streaming(db_, io_pool_, cache_, media.id, media.size_bytes, media.media_kind, width, height, fmt, t_seconds, duration, error_out); } catch (const std::exception& e) { error_out = std::string("render exception: ") + e.what(); } catch (...) { error_out = "render crashed"; } auto [result, render_err] = fut.get(); error_out = std::move(render_err); if (!result.has_value()) { return nullptr; } Loading src/preview.h +2 −3 Original line number Diff line number Diff line Loading @@ -82,9 +82,8 @@ private: BlobCache& cache_; MediaBackendApi& db_; FFmpegPreviewer ffmpeg_; ThreadPool render_pool_; // CPU-heavy FFmpeg decode/encode — never shares threads with I/O ThreadPool io_pool_; // prefetch I/O only — always has free threads for cluster reads std::counting_semaphore<256> render_sem_; // backpressure: rejects after 30s wait ThreadPool io_pool_; // prefetch I/O only — never blocked by renders std::counting_semaphore<256> render_sem_; // backpressure: limits concurrent renders on HTTP workers std::mutex inflight_mutex_; std::condition_variable inflight_cv_; std::unordered_set<std::string> inflight_; Loading Loading
debian/changelog +8 −0 Original line number Diff line number Diff line mediadb (20260424+76) unstable; urgency=medium * Fix: remove render_pool_, run FFmpeg inline on HTTP worker threads. Separate render pool caused thread starvation and UI hangs. io_pool_ now exclusively serves prefetch I/O; semaphore provides backpressure. -- Jan Koester <jan.koester@tuxist.de> Thu, 24 Apr 2026 00:00:00 +0200 mediadb (20260424+75) unstable; urgency=medium * Fix: split single io_pool into separate render_pool (CPU-heavy FFmpeg) Loading
src/preview.cpp +13 −26 Original line number Diff line number Diff line Loading @@ -695,10 +695,9 @@ std::optional<PreviewResult> FFmpegPreviewer::encode_frame(AVFrame* frame, const PreviewService::PreviewService(BlobCache& cache, MediaBackendApi& db, std::size_t render_threads) : cache_(cache), db_(db), render_pool_(render_threads > 0 ? render_threads : std::max(2u, std::min(8u, std::thread::hardware_concurrency() / 2))), io_pool_(std::max(4u, std::min(16u, std::thread::hardware_concurrency()))), render_sem_(render_pool_.size()) {} render_sem_(render_threads > 0 ? render_threads : std::max(1u, std::thread::hardware_concurrency())) {} std::string PreviewService::make_key(const std::string& media_id, int width, int height, const std::string& fmt, Loading Loading @@ -759,31 +758,19 @@ std::shared_ptr<const BlobValue> PreviewService::get_or_create_streaming(const M ~SemGuard() { self->render_sem_.release(); } } sem_guard{this}; // Run FFmpeg on the io_pool with a timeout. This prevents a stuck // FFmpeg render from blocking the HTTP worker thread indefinitely. // Captures are by value so the lambda is safe if we return on timeout. std::string mid = media.id; std::uint64_t msize = media.size_bytes; std::string mkind = media.media_kind; auto fut = render_pool_.submit([this, mid, msize, mkind, width, height, fmt, t_seconds, duration]() { std::string err; auto res = ffmpeg_.render_streaming(db_, io_pool_, cache_, mid, msize, mkind, width, height, fmt, t_seconds, duration, err); return std::make_pair(std::move(res), std::move(err)); }); auto status = fut.wait_for(std::chrono::seconds(60)); if (status == std::future_status::timeout) { error_out = "preview render timed out after 60s"; return nullptr; // Run FFmpeg inline on the HTTP worker thread. The semaphore above // limits concurrency; io_pool_ handles prefetch I/O independently. std::optional<PreviewResult> result; try { result = ffmpeg_.render_streaming(db_, io_pool_, cache_, media.id, media.size_bytes, media.media_kind, width, height, fmt, t_seconds, duration, error_out); } catch (const std::exception& e) { error_out = std::string("render exception: ") + e.what(); } catch (...) { error_out = "render crashed"; } auto [result, render_err] = fut.get(); error_out = std::move(render_err); if (!result.has_value()) { return nullptr; } Loading
src/preview.h +2 −3 Original line number Diff line number Diff line Loading @@ -82,9 +82,8 @@ private: BlobCache& cache_; MediaBackendApi& db_; FFmpegPreviewer ffmpeg_; ThreadPool render_pool_; // CPU-heavy FFmpeg decode/encode — never shares threads with I/O ThreadPool io_pool_; // prefetch I/O only — always has free threads for cluster reads std::counting_semaphore<256> render_sem_; // backpressure: rejects after 30s wait ThreadPool io_pool_; // prefetch I/O only — never blocked by renders std::counting_semaphore<256> render_sem_; // backpressure: limits concurrent renders on HTTP workers std::mutex inflight_mutex_; std::condition_variable inflight_cv_; std::unordered_set<std::string> inflight_; Loading