From 6d221846df2dc834b42f7fc41a3c988eb2c91c35 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Fri, 17 Jul 2026 01:28:35 +0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20perf:=20Performance=20and=20Memo?= =?UTF-8?q?ry=20optimization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/render/cache.go | 65 +++++++++++++++++++++++++++------- internal/render/render_test.go | 42 +++++++++++++++++----- internal/tsf/format.go | 3 ++ internal/tsf/mmap_other.go | 2 ++ internal/tsf/mmap_unix.go | 9 +++++ 5 files changed, 100 insertions(+), 21 deletions(-) diff --git a/internal/render/cache.go b/internal/render/cache.go index 7106de1..e030b39 100644 --- a/internal/render/cache.go +++ b/internal/render/cache.go @@ -2,7 +2,9 @@ package render import ( "bytes" + "compress/flate" "container/list" + "io" "sync" "sync/atomic" "time" @@ -37,13 +39,48 @@ type Cache struct { } type cacheEntry struct { - key cacheKey - ascii []byte - cost int64 + key cacheKey + data []byte + origLen int + cost int64 } -func entryCost(_ cacheKey, ascii []byte) int64 { - return int64(cap(ascii)) + 160 +func entryCost(_ cacheKey, data []byte) int64 { + 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 { @@ -82,15 +119,18 @@ func (c *Cache) get(key cacheKey) ([]byte, bool) { } shard := c.shard(key) shard.mu.Lock() - defer shard.mu.Unlock() el, ok := shard.entries[key] if !ok { + shard.mu.Unlock() c.misses.Add(1) return nil, false } - c.hits.Add(1) 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) { @@ -98,7 +138,8 @@ func (c *Cache) put(key cacheKey, ascii []byte) { return } shard := c.shard(key) - cost := entryCost(key, ascii) + compressed := compressAscii(ascii) + cost := entryCost(key, compressed) if cost > shard.maxBytes { c.rejections.Add(1) return @@ -108,7 +149,8 @@ func (c *Cache) put(key cacheKey, ascii []byte) { if _, ok := shard.entries[key]; ok { 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 c.size.Add(cost) for shard.size > shard.maxBytes { @@ -219,9 +261,6 @@ func (r *Renderer) Render(index, width, height int, keepAspectRatio bool, tier C } putPixBuf(pix) - if r.cache != nil && cap(ascii) > len(ascii)+len(ascii)/4 { - ascii = bytes.Clone(ascii) - } r.cache.put(key, ascii) if r.cache != nil { r.cache.renders.Add(1) diff --git a/internal/render/render_test.go b/internal/render/render_test.go index 71a7b60..890fdf8 100644 --- a/internal/render/render_test.go +++ b/internal/render/render_test.go @@ -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) { 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) for i := range 5 { - c.put(key(i), bytes.Repeat([]byte("x"), 1000)) + c.put(key(i), payload) } if 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) { c := NewCache(0) if c != nil { @@ -119,7 +145,7 @@ func TestRenderCacheDisabled(t *testing.T) { func TestRenderCacheRejectsOversizedEntry(t *testing.T) { c := NewCache(256) - c.put(cacheKey{}, bytes.Repeat([]byte("x"), 10_000)) + c.put(cacheKey{}, incompressible(10_000)) if _, ok := c.get(cacheKey{}); ok { 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) value := make([]byte, 1, 4096) cache.put(cacheKey{}, value) - if _, ok := cache.get(cacheKey{}); ok { - t.Fatal("cache accepted an entry whose backing allocation exceeds its budget") + if _, ok := cache.get(cacheKey{}); !ok { + t.Fatal("small value should be cached regardless of its backing capacity") } - if cache.Stats().Rejections != 1 { - t.Fatalf("rejections = %d, want 1", cache.Stats().Rejections) + if got := cache.size.Load(); got > 512 { + t.Fatalf("size = %d, want <= 512", got) } } diff --git a/internal/tsf/format.go b/internal/tsf/format.go index dc3469d..023e895 100644 --- a/internal/tsf/format.go +++ b/internal/tsf/format.go @@ -112,6 +112,9 @@ func Load(filename string) (*FramesContainer, error) { if off != len(raw) { return nil, invalid() } + + file.dropResident() + data := &FramesContainer{ColorFrames: colorFrames, FPS: fps} frameFileOwners.Store(data, file) owned = true diff --git a/internal/tsf/mmap_other.go b/internal/tsf/mmap_other.go index 6fdd9d1..3b12eed 100644 --- a/internal/tsf/mmap_other.go +++ b/internal/tsf/mmap_other.go @@ -8,3 +8,5 @@ func readFrameFile(filename string) (*frameFile, error) { data, err := os.ReadFile(filename) return &frameFile{data: data}, err } + +func (f *frameFile) dropResident() {} diff --git a/internal/tsf/mmap_unix.go b/internal/tsf/mmap_unix.go index 3e32f71..fdef072 100644 --- a/internal/tsf/mmap_unix.go +++ b/internal/tsf/mmap_unix.go @@ -28,6 +28,8 @@ func readFrameFile(filename string) (*frameFile, error) { data, err := os.ReadFile(filename) return &frameFile{data: data}, err } + + _ = syscall.Madvise(data, syscall.MADV_RANDOM) return &frameFile{ data: data, cleanup: func() error { @@ -35,3 +37,10 @@ func readFrameFile(filename string) (*frameFile, error) { }, }, nil } + +func (f *frameFile) dropResident() { + if f == nil || f.cleanup == nil || len(f.data) == 0 { + return + } + _ = syscall.Madvise(f.data, syscall.MADV_DONTNEED) +}