From 1c8cc1c4092a5f80accd62ac55a416762f4398ba Mon Sep 17 00:00:00 2001 From: Deivid Soto Date: Tue, 2 Jun 2026 11:51:26 +0200 Subject: [PATCH] perf(stream): run the subtitle/thumbnail prewarm at idle I/O priority MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prewarm's single big read (a ~14 min sequential pass over a 60GB remux to demux subtitles) shares the same disk/NFS as live streaming. Lower the prewarm ffmpeg processes to the Linux IDLE I/O class (ioprio_set) so that background read yields bandwidth to a user who's actually watching — the prewarm slows down under contention instead of starving playback, and runs full speed when the disk is idle. Applied only to the prewarm-only extractors (ExtractSubtitlesVTTMulti, ExtractThumbnailJPEG) via Start → setIdleIOPriority(pid) → Wait; the on-demand /sub + /thumbnail handlers keep normal priority (a user is waiting on those). Linux-only syscall behind a build tag; a no-op stub elsewhere. Best-effort — errors ignored, never required for correctness. Verified: the prewarm ffmpeg shows 'idle' under ionice -p; on-demand stays normal. --- internal/library/mediainfo/ioprio_linux.go | 25 ++++++++++++++++++++++ internal/library/mediainfo/ioprio_other.go | 6 ++++++ internal/library/mediainfo/sidecar.go | 25 +++++++++++++++++----- 3 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 internal/library/mediainfo/ioprio_linux.go create mode 100644 internal/library/mediainfo/ioprio_other.go diff --git a/internal/library/mediainfo/ioprio_linux.go b/internal/library/mediainfo/ioprio_linux.go new file mode 100644 index 0000000..9b1d508 --- /dev/null +++ b/internal/library/mediainfo/ioprio_linux.go @@ -0,0 +1,25 @@ +//go:build linux + +package mediainfo + +import "syscall" + +// Linux I/O priority (ioprio) constants. The 16-bit ioprio value packs a class +// in the top 3 bits (shift 13) and a class-data nibble below it; the IDLE class +// takes no data. +const ( + ioprioWhoProcess = 1 // IOPRIO_WHO_PROCESS + ioprioClassIdle = 3 // IOPRIO_CLASS_IDLE + ioprioClassShift = 13 +) + +// setIdleIOPriority best-effort lowers a process's I/O scheduling class to IDLE, +// so a long background read (the subtitle prewarm of a huge remux — a single +// ~14 min sequential read of a 60GB file over NFS) yields disk/NFS bandwidth to +// foreground work like live streaming. Linux-only; on kernels or filesystems +// that don't honor ioprio this simply has no effect. Errors are intentionally +// ignored — it's an optimization, never required for correctness. +func setIdleIOPriority(pid int) { + ioprio := ioprioClassIdle << ioprioClassShift // IDLE class, data 0 + _, _, _ = syscall.Syscall(syscall.SYS_IOPRIO_SET, uintptr(ioprioWhoProcess), uintptr(pid), uintptr(ioprio)) +} diff --git a/internal/library/mediainfo/ioprio_other.go b/internal/library/mediainfo/ioprio_other.go new file mode 100644 index 0000000..3f84d6a --- /dev/null +++ b/internal/library/mediainfo/ioprio_other.go @@ -0,0 +1,6 @@ +//go:build !linux + +package mediainfo + +// setIdleIOPriority is a no-op on non-Linux platforms (ioprio is Linux-specific). +func setIdleIOPriority(_ int) {} diff --git a/internal/library/mediainfo/sidecar.go b/internal/library/mediainfo/sidecar.go index f5afc3f..15d7074 100644 --- a/internal/library/mediainfo/sidecar.go +++ b/internal/library/mediainfo/sidecar.go @@ -1,6 +1,7 @@ package mediainfo import ( + "bytes" "context" "errors" "fmt" @@ -177,9 +178,17 @@ func ExtractSubtitlesVTTMulti(ctx context.Context, ffmpegPath, mediaPath string, cmd := exec.CommandContext(ctx, ffmpegPath, args...) var stderr strings.Builder cmd.Stderr = &stderr - // A non-zero exit can still leave good per-track files (e.g. one corrupt - // stream), so don't bail on err — read whatever landed and judge by that. - runErr := cmd.Run() + // Run it at IDLE I/O priority: this single ~14 min sequential read of a huge + // remux must not starve live streaming off the same disk/NFS. + var runErr error + if startErr := cmd.Start(); startErr != nil { + runErr = startErr + } else { + setIdleIOPriority(cmd.Process.Pid) + // A non-zero exit can still leave good per-track files (e.g. one corrupt + // stream), so don't bail on err — read whatever landed and judge by that. + runErr = cmd.Wait() + } out := make(map[int][]byte, len(indices)) for idx, f := range tmp { @@ -230,9 +239,15 @@ func ExtractThumbnailJPEG(ctx context.Context, ffmpegPath, mediaPath string, pos "pipe:1", } cmd := exec.CommandContext(ctx, ffmpegPath, args...) - var stderr strings.Builder + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout cmd.Stderr = &stderr - out, err := cmd.Output() + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("ffmpeg thumbnail start: %w", err) + } + setIdleIOPriority(cmd.Process.Pid) // background prewarm yields I/O to live playback + err := cmd.Wait() + out := stdout.Bytes() if err != nil { return nil, fmt.Errorf("ffmpeg thumbnail extract: %w: %s", err, strings.TrimSpace(stderr.String())) }