Addresses items raised by the multi-agent code review of the 0.9.9 HW accel + first-start work: - EncoderProfile now carries DecodeHwAccel so the demuxer `-hwaccel` flag and the encoder argv derive from a single resolved profile. Adding a new backend can no longer leave the two switches out of sync. - VAAPI no longer passes `-hwaccel_output_format vaapi`. That option pinned decoded frames to GPU memory, but the filter chain (scale, format, setparams) runs on CPU and would fail with "impossible to convert between formats". Frames now decode HW + flow on CPU; the encoder uploads back to GPU. Pre-existing bug, never reported because no one had VAAPI auto-detected in practice. - readyMax field comment + name: documented that it's a COUNT (segments ready), not an index. The semantics were correct but the comment read "highest index" which made `idx < readyMax` look like an off-by-one to reviewers. - probe_cache background janitor: 5-minute sweeper that drops expired entries even when no lookup retouches the key. Lookup-only eviction was fine for small libraries but unbounded for users who browse and abandon thousands of files within a TTL window. Lazy + sync.Once. - probe_cache TTL eviction now re-checks under the write lock so a concurrent re-insert isn't accidentally evicted. - probe_cache size-change test now Chtimes the file back to its original mtime so only `size` differs between store and lookup keys — properly exercises the size-check path. - New TestProbeCache_SweepDropsExpired covers the janitor sweep. - CHANGELOG: backfilled missing compare links 0.6.4 → 0.9.9. - Stale "line ~1119" reference in VideoToolbox comment dropped; the bitrate block moved a few lines and the comment was already wrong.
141 lines
3.9 KiB
Go
141 lines
3.9 KiB
Go
package engine
|
|
|
|
import (
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// probeCacheTTL is how long a cached probe stays usable. The cache key
|
|
// already incorporates mtime + size, so the TTL is a defense against
|
|
// runaway memory growth from stale paths, not a freshness guarantee — a
|
|
// rename + recreate at the same inode (rare) would still be caught by the
|
|
// mtime delta.
|
|
const probeCacheTTL = 30 * time.Minute
|
|
|
|
// probeCacheJanitorInterval is how often the background sweeper wakes to
|
|
// drop expired entries. Lookup-time eviction handles hot paths, but a
|
|
// user who browses 5k files and then stops would leak entries until each
|
|
// is individually re-touched. 5 min ≈ 6 sweeps per TTL window — enough
|
|
// to keep memory bounded without burning CPU.
|
|
const probeCacheJanitorInterval = 5 * time.Minute
|
|
|
|
type probeCacheEntry struct {
|
|
probe *StreamProbe
|
|
expires time.Time
|
|
}
|
|
|
|
type probeCacheKey struct {
|
|
path string
|
|
mtime int64 // ModTime().UnixNano()
|
|
size int64
|
|
}
|
|
|
|
var (
|
|
probeCacheMu sync.RWMutex
|
|
probeCache = make(map[probeCacheKey]probeCacheEntry)
|
|
probeCacheJanitor sync.Once
|
|
)
|
|
|
|
// startProbeCacheJanitor launches the background sweeper exactly once per
|
|
// process. Lazy — fired on first storeProbeCache. Drops expired entries
|
|
// every probeCacheJanitorInterval. Idempotent (sync.Once).
|
|
func startProbeCacheJanitor() {
|
|
probeCacheJanitor.Do(func() {
|
|
go func() {
|
|
ticker := time.NewTicker(probeCacheJanitorInterval)
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
sweepProbeCache(time.Now())
|
|
}
|
|
}()
|
|
})
|
|
}
|
|
|
|
// sweepProbeCache removes every entry whose expiry is at or before `now`.
|
|
// Exposed for tests; production code calls it indirectly via the janitor
|
|
// goroutine.
|
|
func sweepProbeCache(now time.Time) int {
|
|
probeCacheMu.Lock()
|
|
defer probeCacheMu.Unlock()
|
|
removed := 0
|
|
for k, e := range probeCache {
|
|
if !now.Before(e.expires) {
|
|
delete(probeCache, k)
|
|
removed++
|
|
}
|
|
}
|
|
return removed
|
|
}
|
|
|
|
// lookupProbeCache returns the cached StreamProbe for the given path if its
|
|
// mtime + size still match the value recorded at insert time, AND the cache
|
|
// entry hasn't expired. Any stat failure / mismatch returns (nil, false) so
|
|
// the caller falls through to a fresh ffprobe run.
|
|
func lookupProbeCache(path string) (*StreamProbe, bool) {
|
|
fi, err := os.Stat(path)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
key := probeCacheKey{
|
|
path: path,
|
|
mtime: fi.ModTime().UnixNano(),
|
|
size: fi.Size(),
|
|
}
|
|
probeCacheMu.RLock()
|
|
entry, ok := probeCache[key]
|
|
probeCacheMu.RUnlock()
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
if time.Now().After(entry.expires) {
|
|
// Re-check under the write lock so a concurrent re-insert (same key,
|
|
// fresh expiry) isn't accidentally evicted.
|
|
probeCacheMu.Lock()
|
|
if cur, stillThere := probeCache[key]; stillThere && time.Now().After(cur.expires) {
|
|
delete(probeCache, key)
|
|
}
|
|
probeCacheMu.Unlock()
|
|
return nil, false
|
|
}
|
|
return entry.probe, true
|
|
}
|
|
|
|
// storeProbeCache stashes a fresh probe result under the (path, mtime, size)
|
|
// key. A subsequent ffprobe-skipping HIT requires the file to still have the
|
|
// same mtime + size — anything else (re-encoded, renamed+recreated at the
|
|
// same path, truncated) misses and triggers a re-probe.
|
|
func storeProbeCache(path string, probe *StreamProbe) {
|
|
fi, err := os.Stat(path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
key := probeCacheKey{
|
|
path: path,
|
|
mtime: fi.ModTime().UnixNano(),
|
|
size: fi.Size(),
|
|
}
|
|
probeCacheMu.Lock()
|
|
probeCache[key] = probeCacheEntry{
|
|
probe: probe,
|
|
expires: time.Now().Add(probeCacheTTL),
|
|
}
|
|
probeCacheMu.Unlock()
|
|
// Lazy janitor — fires once per process. No-op after first call.
|
|
startProbeCacheJanitor()
|
|
}
|
|
|
|
// ResetProbeCache clears the in-memory probe cache. Test-only.
|
|
func ResetProbeCache() {
|
|
probeCacheMu.Lock()
|
|
probeCache = make(map[probeCacheKey]probeCacheEntry)
|
|
probeCacheMu.Unlock()
|
|
}
|
|
|
|
// ProbeCacheSize returns the number of entries currently cached. Exposed
|
|
// for diagnostics + tests.
|
|
func ProbeCacheSize() int {
|
|
probeCacheMu.RLock()
|
|
defer probeCacheMu.RUnlock()
|
|
return len(probeCache)
|
|
}
|