Revert "perf(pools): size image/rayon pools to the CFS quota"

This reverts the image-pool migration (commit 5629ba6). The bench
(bench_pool_concurrency / POOL-CONCURRENCY.md) measured the one pool it could
isolate — the thumbnail decode semaphore — and found flat throughput, p99 AND
peak RSS (137 MiB) from K=1..16: shrink-on-load already makes each decode
RAM-cheap, so sizing it to the CFS quota gains nothing measurable. Adding code
without a measured benefit isn't worth it.

Kept: the effective_parallelism() helper (it has a *measured* win in the Tokio
runtime — benches/RUNTIME.md) and the benchmark itself (reusable). The ffmpeg
video fan-out has a plausible a-priori case (one OS process per permit) but is
left as a future, deliberately-measured change rather than shipped on
speculation. Doc updated to record the decision.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JG5yYZ9s868mJwqT2Qz7ez
This commit is contained in:
Claude
2026-06-22 09:04:37 +00:00
parent 013090a14e
commit 18781022be
4 changed files with 28 additions and 25 deletions
+13 -9
View File
@@ -56,15 +56,19 @@ taskset -c 0,1 ./target/release/examples/bench_pool_concurrency # model a 2-co
every core". The flat RSS is exactly that: each concurrent decode's transient every core". The flat RSS is exactly that: each concurrent decode's transient
buffer is small, so 16 in flight cost the same resident memory as 1. buffer is small, so 16 in flight cost the same resident memory as 1.
3. **So the pool migration is a correctness/consistency change, not a perf win.** 3. **Decision: NOT migrated (reverted).** Because the only pool this bench could
It is still worth keeping: it has **no downside** (off-quota `effective == isolate showed zero measured benefit, the `effective_parallelism()` migration
available`, so no change), it unifies pool sizing with the runtime fix behind of the image pools was reverted — adding code without a measured win isn't
one `effective_parallelism()` helper, and it protects the pools this bench did worth it. The `effective_parallelism()` helper stays (it has a *measured*
*not* isolate — the transcode rayon pool (thread stacks) and the ffmpeg video benefit in the Tokio runtime — see `RUNTIME`), so a future, deliberately
fan-out (one OS process per permit), where over-spawning per *host* core under measured case can adopt it per-pool.
a tight quota is genuinely wasteful. But operators should not expect a The one pool with a plausible a-priori argument is the **ffmpeg video
throughput jump from it; the real download/runtime wins are in `BLOB-PREFETCH` fan-out** (one heavyweight OS process per permit — 32 ffmpeg processes for a
and `RUNTIME`. 2-core budget on a many-core host is self-evidently wasteful). That was left
on `available_parallelism()` too, to revisit *with* a measurement if a
high-host-core / low-quota deployment running video thumbnails ever warrants
it. The transcode rayon pool over-sizing only costs parked thread stacks
(negligible).
4. **Honest caveat on scale.** This was run at a 2-core quota on a 4-core host 4. **Honest caveat on scale.** This was run at a 2-core quota on a 4-core host
(K_oversub = 8 ≈ 4×). On a 64-core host under a 2-core quota the host-count (K_oversub = 8 ≈ 4×). On a 64-core host under a 2-core quota the host-count
+3 -3
View File
@@ -363,9 +363,9 @@ impl AppServiceFactory {
if self.config.features.enable_video_thumbnails if self.config.features.enable_video_thumbnails
&& FfmpegVideoFrameService::is_available(&ffmpeg_path) && FfmpegVideoFrameService::is_available(&ffmpeg_path)
{ {
// effective_parallelism respects the CFS quota (--cpus), not let cpus = std::thread::available_parallelism()
// just affinity — so ffmpeg fan-out matches the real core budget. .map(|n| n.get())
let cpus = crate::common::runtime::effective_parallelism(); .unwrap_or(4);
let concurrency = std::env::var("OXICLOUD_VIDEO_THUMBNAIL_CONCURRENCY") let concurrency = std::env::var("OXICLOUD_VIDEO_THUMBNAIL_CONCURRENCY")
.ok() .ok()
.and_then(|v| v.parse::<usize>().ok()) .and_then(|v| v.parse::<usize>().ok())
@@ -31,14 +31,13 @@ pub const MAX_TRANSCODE_SIZE: u64 = 5 * 1024 * 1024;
/// Minimum number of threads in the dedicated transcoding pool /// Minimum number of threads in the dedicated transcoding pool
const MIN_TRANSCODE_THREADS: usize = 2; const MIN_TRANSCODE_THREADS: usize = 2;
/// Compute the number of transcoding threads: half the available CPUs, with a /// Compute the number of transcoding threads: half the available CPUs,
/// floor of `MIN_TRANSCODE_THREADS`. Sized by /// with a floor of `MIN_TRANSCODE_THREADS`. `available_parallelism()`
/// [`effective_parallelism`](crate::common::runtime::effective_parallelism), /// respects cgroup limits (Docker/K8s) and CPU affinity masks.
/// which respects CPU affinity **and** the CFS quota (Docker/K8s `--cpus`) —
/// unlike bare `available_parallelism()`, which ignores the quota and would
/// over-size this CPU-bound pool under a container limit.
fn transcode_thread_count() -> usize { fn transcode_thread_count() -> usize {
let cpus = crate::common::runtime::effective_parallelism(); let cpus = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(MIN_TRANSCODE_THREADS);
(cpus / 2).max(MIN_TRANSCODE_THREADS) (cpus / 2).max(MIN_TRANSCODE_THREADS)
} }
@@ -107,11 +107,8 @@ const STREAM_TO_TEMP_TIMEOUT: Duration = Duration::from_secs(120);
/// concurrency was halved to keep peak RAM in check. Decodes are now DCT-shrunk /// concurrency was halved to keep peak RAM in check. Decodes are now DCT-shrunk
/// to the thumbnail size (~18–25 MB regardless of source resolution), so the RAM /// to the thumbnail size (~18–25 MB regardless of source resolution), so the RAM
/// ceiling no longer forces throttling and we can saturate every core. Override /// ceiling no longer forces throttling and we can saturate every core. Override
/// with `OXICLOUD_THUMBNAIL_DECODE_CONCURRENCY`. Sized by /// with `OXICLOUD_THUMBNAIL_DECODE_CONCURRENCY`. `available_parallelism()`
/// [`effective_parallelism`](crate::common::runtime::effective_parallelism), /// respects cgroup limits (Docker/K8s) and CPU affinity masks.
/// which respects CPU affinity **and** the CFS quota (`--cpus`) — so under a
/// container quota we don't over-permit concurrent CPU-heavy decodes onto cores
/// the scheduler can't give us.
fn max_concurrent_decodes() -> usize { fn max_concurrent_decodes() -> usize {
if let Some(n) = std::env::var(DECODE_CONCURRENCY_ENV) if let Some(n) = std::env::var(DECODE_CONCURRENCY_ENV)
.ok() .ok()
@@ -120,7 +117,10 @@ fn max_concurrent_decodes() -> usize {
{ {
return n; return n;
} }
crate::common::runtime::effective_parallelism().max(2) let cpus = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4);
cpus.max(2)
} }
/// Thumbnail service for generating and caching image thumbnails /// Thumbnail service for generating and caching image thumbnails