mirror of
https://github.com/YuzuZensai/TrollSSH.git
synced 2026-09-13 19:49:04 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee9d36dd4f
|
||
|
|
6d221846df
|
||
|
|
facdb35d5d
|
@@ -31,6 +31,9 @@ MAX_TERMINAL_CELLS=256000
|
|||||||
|
|
||||||
# Memory budget in MB for the rendered-frame cache (0 disables caching).
|
# Memory budget in MB for the rendered-frame cache (0 disables caching).
|
||||||
RENDER_CACHE_MB=256
|
RENDER_CACHE_MB=256
|
||||||
|
# Compress cached frames (flate). Cuts cache RAM ~2x but decompresses on every
|
||||||
|
# cache hit, so CPU rises with concurrent sessions. Off = zero-copy reads.
|
||||||
|
RENDER_CACHE_COMPRESS=false
|
||||||
|
|
||||||
# Go soft memory limit; set below your container limit to avoid OOM kills.
|
# Go soft memory limit; set below your container limit to avoid OOM kills.
|
||||||
GOMEMLIMIT=1GiB
|
GOMEMLIMIT=1GiB
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ type Config struct {
|
|||||||
MaxTerminalCells int
|
MaxTerminalCells int
|
||||||
SessionTimeout time.Duration
|
SessionTimeout time.Duration
|
||||||
RenderCacheMB int
|
RenderCacheMB int
|
||||||
|
RenderCacheCompress bool
|
||||||
BrightnessThreshold int
|
BrightnessThreshold int
|
||||||
Charset string
|
Charset string
|
||||||
Invert bool
|
Invert bool
|
||||||
@@ -133,6 +134,7 @@ func Load() Config {
|
|||||||
MaxTerminalCells: envInt("MAX_TERMINAL_CELLS", 500*512, 1, maxInt),
|
MaxTerminalCells: envInt("MAX_TERMINAL_CELLS", 500*512, 1, maxInt),
|
||||||
SessionTimeout: envDurationMs("SESSION_TIMEOUT", 10*time.Minute),
|
SessionTimeout: envDurationMs("SESSION_TIMEOUT", 10*time.Minute),
|
||||||
RenderCacheMB: envInt("RENDER_CACHE_MB", 256, 0, maxInt),
|
RenderCacheMB: envInt("RENDER_CACHE_MB", 256, 0, maxInt),
|
||||||
|
RenderCacheCompress: envBool("RENDER_CACHE_COMPRESS", false),
|
||||||
BrightnessThreshold: envInt("BRIGHTNESS_THRESHOLD", 40, 0, 100),
|
BrightnessThreshold: envInt("BRIGHTNESS_THRESHOLD", 40, 0, 100),
|
||||||
Charset: envString("CHARSET", "detailed"),
|
Charset: envString("CHARSET", "detailed"),
|
||||||
Invert: envBool("INVERT", false),
|
Invert: envBool("INVERT", false),
|
||||||
|
|||||||
+61
-15
@@ -2,7 +2,9 @@ package render
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"compress/flate"
|
||||||
"container/list"
|
"container/list"
|
||||||
|
"io"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@@ -27,6 +29,7 @@ type cacheShard struct {
|
|||||||
|
|
||||||
type Cache struct {
|
type Cache struct {
|
||||||
shards []cacheShard
|
shards []cacheShard
|
||||||
|
compress bool
|
||||||
size atomic.Int64
|
size atomic.Int64
|
||||||
hits atomic.Uint64
|
hits atomic.Uint64
|
||||||
misses atomic.Uint64
|
misses atomic.Uint64
|
||||||
@@ -37,21 +40,56 @@ type Cache struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type cacheEntry struct {
|
type cacheEntry struct {
|
||||||
key cacheKey
|
key cacheKey
|
||||||
ascii []byte
|
data []byte
|
||||||
cost int64
|
origLen int
|
||||||
|
cost int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func entryCost(_ cacheKey, ascii []byte) int64 {
|
func entryCost(_ cacheKey, data []byte) int64 {
|
||||||
return int64(cap(ascii)) + 160
|
return int64(cap(data)) + 160
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCache(maxBytes int64) *Cache {
|
var flateWriters = sync.Pool{New: func() any {
|
||||||
|
w, _ := flate.NewWriter(io.Discard, 1)
|
||||||
|
return w
|
||||||
|
}}
|
||||||
|
|
||||||
|
type flateReader interface {
|
||||||
|
io.Reader
|
||||||
|
flate.Resetter
|
||||||
|
}
|
||||||
|
|
||||||
|
var flateReaders = sync.Pool{New: func() any {
|
||||||
|
return flate.NewReader(bytes.NewReader(nil)).(flateReader)
|
||||||
|
}}
|
||||||
|
|
||||||
|
func compressAscii(src []byte) []byte {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.Grow(len(src)/3 + 64)
|
||||||
|
w := flateWriters.Get().(*flate.Writer)
|
||||||
|
w.Reset(&buf)
|
||||||
|
_, _ = w.Write(src)
|
||||||
|
_ = w.Close()
|
||||||
|
flateWriters.Put(w)
|
||||||
|
return bytes.Clone(buf.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func decompressAscii(src []byte, origLen int) []byte {
|
||||||
|
r := flateReaders.Get().(flateReader)
|
||||||
|
_ = r.Reset(bytes.NewReader(src), nil)
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, origLen))
|
||||||
|
_, _ = io.Copy(buf, r)
|
||||||
|
flateReaders.Put(r)
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCache(maxBytes int64, compress bool) *Cache {
|
||||||
if maxBytes <= 0 {
|
if maxBytes <= 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
shardCount := int(min(int64(16), max(int64(1), maxBytes/(1<<20))))
|
shardCount := int(min(int64(16), max(int64(1), maxBytes/(1<<20))))
|
||||||
cache := &Cache{shards: make([]cacheShard, shardCount)}
|
cache := &Cache{shards: make([]cacheShard, shardCount), compress: compress}
|
||||||
for i := range cache.shards {
|
for i := range cache.shards {
|
||||||
cache.shards[i] = cacheShard{
|
cache.shards[i] = cacheShard{
|
||||||
maxBytes: maxBytes / int64(shardCount),
|
maxBytes: maxBytes / int64(shardCount),
|
||||||
@@ -82,15 +120,21 @@ func (c *Cache) get(key cacheKey) ([]byte, bool) {
|
|||||||
}
|
}
|
||||||
shard := c.shard(key)
|
shard := c.shard(key)
|
||||||
shard.mu.Lock()
|
shard.mu.Lock()
|
||||||
defer shard.mu.Unlock()
|
|
||||||
el, ok := shard.entries[key]
|
el, ok := shard.entries[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
shard.mu.Unlock()
|
||||||
c.misses.Add(1)
|
c.misses.Add(1)
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
c.hits.Add(1)
|
|
||||||
shard.order.MoveToBack(el)
|
shard.order.MoveToBack(el)
|
||||||
return el.Value.(*cacheEntry).ascii, true
|
entry := el.Value.(*cacheEntry)
|
||||||
|
data, origLen := entry.data, entry.origLen
|
||||||
|
shard.mu.Unlock()
|
||||||
|
c.hits.Add(1)
|
||||||
|
if !c.compress {
|
||||||
|
return data, true
|
||||||
|
}
|
||||||
|
return decompressAscii(data, origLen), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) put(key cacheKey, ascii []byte) {
|
func (c *Cache) put(key cacheKey, ascii []byte) {
|
||||||
@@ -98,7 +142,11 @@ func (c *Cache) put(key cacheKey, ascii []byte) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
shard := c.shard(key)
|
shard := c.shard(key)
|
||||||
cost := entryCost(key, ascii)
|
data, origLen := ascii, 0
|
||||||
|
if c.compress {
|
||||||
|
data, origLen = compressAscii(ascii), len(ascii)
|
||||||
|
}
|
||||||
|
cost := entryCost(key, data)
|
||||||
if cost > shard.maxBytes {
|
if cost > shard.maxBytes {
|
||||||
c.rejections.Add(1)
|
c.rejections.Add(1)
|
||||||
return
|
return
|
||||||
@@ -108,7 +156,8 @@ func (c *Cache) put(key cacheKey, ascii []byte) {
|
|||||||
if _, ok := shard.entries[key]; ok {
|
if _, ok := shard.entries[key]; ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
shard.entries[key] = shard.order.PushBack(&cacheEntry{key: key, ascii: ascii, cost: cost})
|
entry := &cacheEntry{key: key, data: data, origLen: origLen, cost: cost}
|
||||||
|
shard.entries[key] = shard.order.PushBack(entry)
|
||||||
shard.size += cost
|
shard.size += cost
|
||||||
c.size.Add(cost)
|
c.size.Add(cost)
|
||||||
for shard.size > shard.maxBytes {
|
for shard.size > shard.maxBytes {
|
||||||
@@ -219,9 +268,6 @@ func (r *Renderer) Render(index, width, height int, keepAspectRatio bool, tier C
|
|||||||
}
|
}
|
||||||
putPixBuf(pix)
|
putPixBuf(pix)
|
||||||
|
|
||||||
if r.cache != nil && cap(ascii) > len(ascii)+len(ascii)/4 {
|
|
||||||
ascii = bytes.Clone(ascii)
|
|
||||||
}
|
|
||||||
r.cache.put(key, ascii)
|
r.cache.put(key, ascii)
|
||||||
if r.cache != nil {
|
if r.cache != nil {
|
||||||
r.cache.renders.Add(1)
|
r.cache.renders.Add(1)
|
||||||
|
|||||||
@@ -43,27 +43,35 @@ func BenchmarkRenderGray(b *testing.B) { benchRender(b, ColorTierNone, 120,
|
|||||||
func BenchmarkRenderTrueBig(b *testing.B) { benchRender(b, ColorTierTrueColor, 240, 70) }
|
func BenchmarkRenderTrueBig(b *testing.B) { benchRender(b, ColorTierTrueColor, 240, 70) }
|
||||||
|
|
||||||
func BenchmarkRenderCachedParallel(b *testing.B) {
|
func BenchmarkRenderCachedParallel(b *testing.B) {
|
||||||
fc := loadBenchSet(b)
|
for _, compress := range []bool{false, true} {
|
||||||
r := NewRenderer(0, fc.ColorFrames, Options{
|
name := "uncompressed"
|
||||||
BrightnessThreshold: 40,
|
if compress {
|
||||||
Charset: "detailed",
|
name = "compressed"
|
||||||
}, NewCache(8<<20))
|
|
||||||
frame, err := r.Render(0, 120, 40, false, ColorTierTrueColor)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
b.SetBytes(int64(len(frame)))
|
|
||||||
b.ReportAllocs()
|
|
||||||
var failures atomic.Uint64
|
|
||||||
b.ResetTimer()
|
|
||||||
b.RunParallel(func(pb *testing.PB) {
|
|
||||||
for pb.Next() {
|
|
||||||
if _, err := r.Render(0, 120, 40, false, ColorTierTrueColor); err != nil {
|
|
||||||
failures.Add(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
b.Run(name, func(b *testing.B) {
|
||||||
if failures.Load() != 0 {
|
fc := loadBenchSet(b)
|
||||||
b.Fatalf("render failures: %d", failures.Load())
|
r := NewRenderer(0, fc.ColorFrames, Options{
|
||||||
|
BrightnessThreshold: 40,
|
||||||
|
Charset: "detailed",
|
||||||
|
}, NewCache(8<<20, compress))
|
||||||
|
frame, err := r.Render(0, 120, 40, false, ColorTierTrueColor)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
b.SetBytes(int64(len(frame)))
|
||||||
|
b.ReportAllocs()
|
||||||
|
var failures atomic.Uint64
|
||||||
|
b.ResetTimer()
|
||||||
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
|
for pb.Next() {
|
||||||
|
if _, err := r.Render(0, 120, 40, false, ColorTierTrueColor); err != nil {
|
||||||
|
failures.Add(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if failures.Load() != 0 {
|
||||||
|
b.Fatalf("render failures: %d", failures.Load())
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ func TestRenderConcurrentSameKey(t *testing.T) {
|
|||||||
r := NewRenderer(0, [][]byte{jpegBuf.Bytes()}, Options{
|
r := NewRenderer(0, [][]byte{jpegBuf.Bytes()}, Options{
|
||||||
BrightnessThreshold: 40,
|
BrightnessThreshold: 40,
|
||||||
Charset: "standard",
|
Charset: "standard",
|
||||||
}, NewCache(1<<20))
|
}, NewCache(1<<20, false))
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
results := make([][]byte, 32)
|
results := make([][]byte, 32)
|
||||||
@@ -88,12 +88,25 @@ func TestRenderConcurrentSameKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func incompressible(n int) []byte {
|
||||||
|
b := make([]byte, n)
|
||||||
|
x := uint32(0x9e3779b9)
|
||||||
|
for i := range b {
|
||||||
|
x ^= x << 13
|
||||||
|
x ^= x >> 17
|
||||||
|
x ^= x << 5
|
||||||
|
b[i] = byte(x)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
func TestRenderCacheEvictsByBytes(t *testing.T) {
|
func TestRenderCacheEvictsByBytes(t *testing.T) {
|
||||||
key := func(index int) cacheKey { return cacheKey{index: index} }
|
key := func(index int) cacheKey { return cacheKey{index: index} }
|
||||||
budget := 3 * entryCost(key(0), bytes.Repeat([]byte("x"), 1000))
|
payload := incompressible(1000)
|
||||||
c := NewCache(budget)
|
budget := 3 * entryCost(key(0), compressAscii(payload))
|
||||||
|
c := NewCache(budget, true)
|
||||||
for i := range 5 {
|
for i := range 5 {
|
||||||
c.put(key(i), bytes.Repeat([]byte("x"), 1000))
|
c.put(key(i), payload)
|
||||||
}
|
}
|
||||||
if c.size.Load() > budget {
|
if c.size.Load() > budget {
|
||||||
t.Errorf("size %d exceeds budget %d", c.size.Load(), budget)
|
t.Errorf("size %d exceeds budget %d", c.size.Load(), budget)
|
||||||
@@ -106,8 +119,23 @@ func TestRenderCacheEvictsByBytes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRenderCacheRoundTrips(t *testing.T) {
|
||||||
|
for _, compress := range []bool{false, true} {
|
||||||
|
c := NewCache(1<<20, compress)
|
||||||
|
want := incompressible(4096)
|
||||||
|
c.put(cacheKey{}, want)
|
||||||
|
got, ok := c.get(cacheKey{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("compress=%v: entry should be cached", compress)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, want) {
|
||||||
|
t.Fatalf("compress=%v: entry does not match original", compress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRenderCacheDisabled(t *testing.T) {
|
func TestRenderCacheDisabled(t *testing.T) {
|
||||||
c := NewCache(0)
|
c := NewCache(0, false)
|
||||||
if c != nil {
|
if c != nil {
|
||||||
t.Fatal("zero budget should disable the cache")
|
t.Fatal("zero budget should disable the cache")
|
||||||
}
|
}
|
||||||
@@ -118,8 +146,8 @@ func TestRenderCacheDisabled(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderCacheRejectsOversizedEntry(t *testing.T) {
|
func TestRenderCacheRejectsOversizedEntry(t *testing.T) {
|
||||||
c := NewCache(256)
|
c := NewCache(256, true)
|
||||||
c.put(cacheKey{}, bytes.Repeat([]byte("x"), 10_000))
|
c.put(cacheKey{}, incompressible(10_000))
|
||||||
if _, ok := c.get(cacheKey{}); ok {
|
if _, ok := c.get(cacheKey{}); ok {
|
||||||
t.Error("entry larger than budget should not be cached")
|
t.Error("entry larger than budget should not be cached")
|
||||||
}
|
}
|
||||||
@@ -128,15 +156,15 @@ func TestRenderCacheRejectsOversizedEntry(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderCacheAccountsRetainedCapacity(t *testing.T) {
|
func TestRenderCacheAccountsCompressedSize(t *testing.T) {
|
||||||
cache := NewCache(512)
|
cache := NewCache(512, true)
|
||||||
value := make([]byte, 1, 4096)
|
value := make([]byte, 1, 4096)
|
||||||
cache.put(cacheKey{}, value)
|
cache.put(cacheKey{}, value)
|
||||||
if _, ok := cache.get(cacheKey{}); ok {
|
if _, ok := cache.get(cacheKey{}); !ok {
|
||||||
t.Fatal("cache accepted an entry whose backing allocation exceeds its budget")
|
t.Fatal("small value should be cached regardless of its backing capacity")
|
||||||
}
|
}
|
||||||
if cache.Stats().Rejections != 1 {
|
if got := cache.size.Load(); got > 512 {
|
||||||
t.Fatalf("rejections = %d, want 1", cache.Stats().Rejections)
|
t.Fatalf("size = %d, want <= 512", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ func clampTermSize(cols, rows, maxDimension, maxCells, quantum int) (int, int) {
|
|||||||
func New(deps ServerDeps) *Server {
|
func New(deps ServerDeps) *Server {
|
||||||
cfg := deps.Config
|
cfg := deps.Config
|
||||||
|
|
||||||
cache := render.NewCache(int64(cfg.RenderCacheMB) << 20)
|
cache := render.NewCache(int64(cfg.RenderCacheMB)<<20, cfg.RenderCacheCompress)
|
||||||
sets := make([]frameSet, len(deps.VideoSets))
|
sets := make([]frameSet, len(deps.VideoSets))
|
||||||
for i, data := range deps.VideoSets {
|
for i, data := range deps.VideoSets {
|
||||||
sets[i] = frameSet{
|
sets[i] = frameSet{
|
||||||
@@ -447,17 +447,36 @@ type termSize struct {
|
|||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
updated time.Time
|
updated time.Time
|
||||||
|
timer *time.Timer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *termSize) set(w, h, maxDimension, maxCells int, force bool) {
|
func (t *termSize) set(w, h, maxDimension, maxCells int, force bool) {
|
||||||
|
width, height := clampTermSize(w, h, maxDimension, maxCells, terminalSizeQuantum)
|
||||||
|
|
||||||
t.mu.Lock()
|
t.mu.Lock()
|
||||||
if !force && time.Since(t.updated) < resizeDebounce {
|
defer t.mu.Unlock()
|
||||||
t.mu.Unlock()
|
|
||||||
return
|
if !force {
|
||||||
|
if remaining := resizeDebounce - time.Since(t.updated); remaining > 0 {
|
||||||
|
if t.timer != nil {
|
||||||
|
t.timer.Stop()
|
||||||
|
}
|
||||||
|
t.timer = time.AfterFunc(remaining, func() {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
t.width, t.height = width, height
|
||||||
|
t.updated = time.Now()
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
t.width, t.height = clampTermSize(w, h, maxDimension, maxCells, terminalSizeQuantum)
|
|
||||||
|
if t.timer != nil {
|
||||||
|
t.timer.Stop()
|
||||||
|
t.timer = nil
|
||||||
|
}
|
||||||
|
t.width, t.height = width, height
|
||||||
t.updated = time.Now()
|
t.updated = time.Now()
|
||||||
t.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *termSize) get() (int, int) {
|
func (t *termSize) get() (int, int) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package sshserver
|
|||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
@@ -102,6 +103,26 @@ func TestTermSizeDebouncesResize(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTermSizeAppliesFinalResizeAfterDebounce(t *testing.T) {
|
||||||
|
size := &termSize{}
|
||||||
|
size.set(80, 24, 512, 500*512, true)
|
||||||
|
|
||||||
|
// Rapid burst of resize events, as happens during an interactive drag-resize.
|
||||||
|
size.set(100, 40, 512, 500*512, false)
|
||||||
|
size.set(150, 60, 512, 500*512, false)
|
||||||
|
size.set(200, 100, 512, 500*512, false)
|
||||||
|
|
||||||
|
if w, h := size.get(); w != 80 || h != 24 {
|
||||||
|
t.Fatalf("size changed before debounce elapsed: %dx%d", w, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(resizeDebounce + 50*time.Millisecond)
|
||||||
|
|
||||||
|
if w, h := size.get(); w != 200 || h != 100 {
|
||||||
|
t.Fatalf("final resize was not applied after debounce: got %dx%d, want 200x100", w, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestClampTermSize(t *testing.T) {
|
func TestClampTermSize(t *testing.T) {
|
||||||
w, h := clampTermSize(1000, 500, 512, 65536, 4)
|
w, h := clampTermSize(1000, 500, 512, 65536, 4)
|
||||||
if w < 1 || h < 1 || w > 512 || h > 512 || w*h > 65536 {
|
if w < 1 || h < 1 || w > 512 || h > 512 || w*h > 65536 {
|
||||||
|
|||||||
@@ -112,6 +112,9 @@ func Load(filename string) (*FramesContainer, error) {
|
|||||||
if off != len(raw) {
|
if off != len(raw) {
|
||||||
return nil, invalid()
|
return nil, invalid()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
file.dropResident()
|
||||||
|
|
||||||
data := &FramesContainer{ColorFrames: colorFrames, FPS: fps}
|
data := &FramesContainer{ColorFrames: colorFrames, FPS: fps}
|
||||||
frameFileOwners.Store(data, file)
|
frameFileOwners.Store(data, file)
|
||||||
owned = true
|
owned = true
|
||||||
|
|||||||
@@ -8,3 +8,5 @@ func readFrameFile(filename string) (*frameFile, error) {
|
|||||||
data, err := os.ReadFile(filename)
|
data, err := os.ReadFile(filename)
|
||||||
return &frameFile{data: data}, err
|
return &frameFile{data: data}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *frameFile) dropResident() {}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ func readFrameFile(filename string) (*frameFile, error) {
|
|||||||
data, err := os.ReadFile(filename)
|
data, err := os.ReadFile(filename)
|
||||||
return &frameFile{data: data}, err
|
return &frameFile{data: data}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = syscall.Madvise(data, syscall.MADV_RANDOM)
|
||||||
return &frameFile{
|
return &frameFile{
|
||||||
data: data,
|
data: data,
|
||||||
cleanup: func() error {
|
cleanup: func() error {
|
||||||
@@ -35,3 +37,10 @@ func readFrameFile(filename string) (*frameFile, error) {
|
|||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *frameFile) dropResident() {
|
||||||
|
if f == nil || f.cleanup == nil || len(f.data) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = syscall.Madvise(f.data, syscall.MADV_DONTNEED)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user