mirror of
https://github.com/YuzuZensai/TrollSSH.git
synced 2026-09-13 17:39:09 +00:00
🚀 perf: Performance and Memory optimization
This commit is contained in:
+52
-13
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,3 +8,5 @@ func readFrameFile(filename string) (*frameFile, error) {
|
||||
data, err := os.ReadFile(filename)
|
||||
return &frameFile{data: data}, err
|
||||
}
|
||||
|
||||
func (f *frameFile) dropResident() {}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user