perf(stream): run the subtitle/thumbnail prewarm at idle I/O priority
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.
This commit is contained in:
parent
8a47132f15
commit
1c8cc1c409
3 changed files with 51 additions and 5 deletions
25
internal/library/mediainfo/ioprio_linux.go
Normal file
25
internal/library/mediainfo/ioprio_linux.go
Normal file
|
|
@ -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))
|
||||||
|
}
|
||||||
6
internal/library/mediainfo/ioprio_other.go
Normal file
6
internal/library/mediainfo/ioprio_other.go
Normal file
|
|
@ -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) {}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package mediainfo
|
package mediainfo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -177,9 +178,17 @@ func ExtractSubtitlesVTTMulti(ctx context.Context, ffmpegPath, mediaPath string,
|
||||||
cmd := exec.CommandContext(ctx, ffmpegPath, args...)
|
cmd := exec.CommandContext(ctx, ffmpegPath, args...)
|
||||||
var stderr strings.Builder
|
var stderr strings.Builder
|
||||||
cmd.Stderr = &stderr
|
cmd.Stderr = &stderr
|
||||||
// A non-zero exit can still leave good per-track files (e.g. one corrupt
|
// Run it at IDLE I/O priority: this single ~14 min sequential read of a huge
|
||||||
// stream), so don't bail on err — read whatever landed and judge by that.
|
// remux must not starve live streaming off the same disk/NFS.
|
||||||
runErr := cmd.Run()
|
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))
|
out := make(map[int][]byte, len(indices))
|
||||||
for idx, f := range tmp {
|
for idx, f := range tmp {
|
||||||
|
|
@ -230,9 +239,15 @@ func ExtractThumbnailJPEG(ctx context.Context, ffmpegPath, mediaPath string, pos
|
||||||
"pipe:1",
|
"pipe:1",
|
||||||
}
|
}
|
||||||
cmd := exec.CommandContext(ctx, ffmpegPath, args...)
|
cmd := exec.CommandContext(ctx, ffmpegPath, args...)
|
||||||
var stderr strings.Builder
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
cmd.Stderr = &stderr
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ffmpeg thumbnail extract: %w: %s", err, strings.TrimSpace(stderr.String()))
|
return nil, fmt.Errorf("ffmpeg thumbnail extract: %w: %s", err, strings.TrimSpace(stderr.String()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue