2 Commits
Author SHA1 Message Date
yuzu 6d221846df 🚀 perf: Performance and Memory optimization 2026-07-17 01:28:35 +07:00
yuzu facdb35d5d 🐛 fix: apply final resize after debounce instead of dropping it 2026-07-17 00:19:59 +07:00
7 changed files with 145 additions and 26 deletions
+50 -11
View File
@@ -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"
@@ -38,12 +40,47 @@ type Cache struct {
type cacheEntry struct { type cacheEntry struct {
key cacheKey key cacheKey
ascii []byte data []byte
origLen int
cost int64 cost int64
} }
func entryCost(_ cacheKey, ascii []byte) int64 { func entryCost(_ cacheKey, data []byte) int64 {
return int64(cap(ascii)) + 160 return int64(cap(data)) + 160
}
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) *Cache { func NewCache(maxBytes int64) *Cache {
@@ -82,15 +119,18 @@ 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)
return decompressAscii(data, origLen), true
} }
func (c *Cache) put(key cacheKey, ascii []byte) { func (c *Cache) put(key cacheKey, ascii []byte) {
@@ -98,7 +138,8 @@ func (c *Cache) put(key cacheKey, ascii []byte) {
return return
} }
shard := c.shard(key) shard := c.shard(key)
cost := entryCost(key, ascii) compressed := compressAscii(ascii)
cost := entryCost(key, compressed)
if cost > shard.maxBytes { if cost > shard.maxBytes {
c.rejections.Add(1) c.rejections.Add(1)
return return
@@ -108,7 +149,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: compressed, origLen: len(ascii), 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 +261,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)
+34 -8
View File
@@ -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)
budget := 3 * entryCost(key(0), compressAscii(payload))
c := NewCache(budget) c := NewCache(budget)
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,6 +119,19 @@ func TestRenderCacheEvictsByBytes(t *testing.T) {
} }
} }
func TestRenderCacheRoundTrips(t *testing.T) {
c := NewCache(1 << 20)
want := incompressible(4096)
c.put(cacheKey{}, want)
got, ok := c.get(cacheKey{})
if !ok {
t.Fatal("entry should be cached")
}
if !bytes.Equal(got, want) {
t.Fatal("decompressed entry does not match original")
}
}
func TestRenderCacheDisabled(t *testing.T) { func TestRenderCacheDisabled(t *testing.T) {
c := NewCache(0) c := NewCache(0)
if c != nil { if c != nil {
@@ -119,7 +145,7 @@ func TestRenderCacheDisabled(t *testing.T) {
func TestRenderCacheRejectsOversizedEntry(t *testing.T) { func TestRenderCacheRejectsOversizedEntry(t *testing.T) {
c := NewCache(256) c := NewCache(256)
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 +154,15 @@ func TestRenderCacheRejectsOversizedEntry(t *testing.T) {
} }
} }
func TestRenderCacheAccountsRetainedCapacity(t *testing.T) { func TestRenderCacheAccountsCompressedSize(t *testing.T) {
cache := NewCache(512) cache := NewCache(512)
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)
} }
} }
+23 -4
View File
@@ -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()
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 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) {
+21
View File
@@ -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 {
+3
View File
@@ -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
+2
View File
@@ -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() {}
+9
View File
@@ -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)
}