Memory Leak during continuous image updates
Hello!
I have encountered a memory leak issue when using the Spot library to develop a program that functions similarly to the magnifier tool in Windows. The program continuously captures a portion of the screen, magnifies it, and displays it in a window. However, over time, the memory usage of the program keeps increasing, leading to a memory leak.
Code Snippet
package main
import (
"image"
"image/color"
"log"
"math/rand"
"runtime"
"time"
"github.com/roblillack/spot"
"github.com/roblillack/spot/ui"
)
func logMemoryUsage() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
log.Printf("TotalAlloc = %v MiB", m.TotalAlloc/1024/1024)
}
func main() {
width, height := 5, 5
zoom := 100
ui.Init()
spot.MountFn(func(ctx *spot.RenderContext) spot.Component {
canvas, SetCanvas := spot.UseState(ctx, image.NewRGBA(image.Rect(0, 0, width*zoom, height*zoom)))
spot.UseEffect(ctx, func() {
go func() {
i := 0
for {
for x := 0; x < width; x++ {
for y := 0; y < height; y++ {
g := uint8(rand.Intn(256))
color := color.RGBA{g, g, g, 255}
for zx := 0; zx < zoom; zx++ {
for zy := 0; zy < zoom; zy++ {
canvas.Set(x*zoom+zx, y*zoom+zy, color)
}
}
}
}
SetCanvas(canvas)
if i%10 == 0 {
logMemoryUsage()
}
i += 1
time.Sleep(300 * time.Millisecond)
}
}()
}, []any{})
return &ui.Window{
Title: "Title",
Width: canvas.Rect.Dx(),
Height: canvas.Rect.Dy(),
Children: []spot.Component{
&ui.Image{
X: 0, Y: 0,
Width: canvas.Rect.Dx(),
Height: canvas.Rect.Dy(),
Image: canvas,
},
},
}
})
ui.Run()
}
Invocation
go build -ldflags "-s -w" && main.exe
Program output
2024/12/06 12:53:20 TotalAlloc = 2 MiB
2024/12/06 12:53:23 TotalAlloc = 22 MiB
2024/12/06 12:53:26 TotalAlloc = 41 MiB
2024/12/06 12:53:29 TotalAlloc = 60 MiB
2024/12/06 12:53:32 TotalAlloc = 79 MiB
2024/12/06 12:53:35 TotalAlloc = 99 MiB
2024/12/06 12:53:39 TotalAlloc = 118 MiB
2024/12/06 12:53:42 TotalAlloc = 137 MiB
2024/12/06 12:53:45 TotalAlloc = 156 MiB
2024/12/06 12:53:48 TotalAlloc = 175 MiB
2024/12/06 12:53:51 TotalAlloc = 194 MiB
2024/12/06 12:53:55 TotalAlloc = 214 MiB
2024/12/06 12:53:58 TotalAlloc = 233 MiB
2024/12/06 12:54:01 TotalAlloc = 252 MiB
2024/12/06 12:54:04 TotalAlloc = 271 MiB
2024/12/06 12:54:07 TotalAlloc = 290 MiB
2024/12/06 12:54:11 TotalAlloc = 309 MiB
2024/12/06 12:54:14 TotalAlloc = 328 MiB
2024/12/06 12:54:17 TotalAlloc = 348 MiB
2024/12/06 12:54:20 TotalAlloc = 367 MiB
Expected Behavior The program should continuously update the displayed image without a significant increase in memory usage.
Actual Behavior The program's memory usage keeps increasing over time, indicating a memory leak.
Additional Information Go version: go1.22.5 windows/amd64 Spot library version: v0.3.2 Operating System: Windows 10
I would appreciate any guidance or fixes to resolve this memory leak issue. Thank you!
To isolate the problem, the provided code uses randomly generated images instead of actual screen captures.
Hey @hungry, thanks for the awesome test case! Alas, by outputting TotalAlloc you will see the total number of bytes accumulated over time, not the amount of bytes currently allocated. Changing this to HeapAlloc should result in a rather constant use of ~1 MiB.
Also to improve memory pressure, you might want to add image.NewRGBA(image.Rect(0, 0, width*zoom, height*zoom)) to a variable what is not re-created (and then discarded) for every render:
emptyImg := image.NewRGBA(image.Rect(0, 0, width*zoom, height*zoom))
ui.Init()
spot.MountFn(func(ctx *spot.RenderContext) spot.Component {
canvas, SetCanvas := spot.UseState(ctx, emptyImg)
spot.UseEffect(ctx, func() {
...
Thanks for the quick reply! I've applied the optimizations you suggest, but as far as I can tell, the problem persists.
Please see this screenshot:
HeapAlloc indeed reports rather small amounts, but Windows Task Manager reports constantly growing size of memory used, which correlates to TotalAlloc. Eventually, the program is unable to allocate more memory and crashes -- that's why I even noticed the problem and started digging into it.
Might be a weird question, but you did not turn of Go's garbage collection using some environment variable, no?
Neither intentionally, nor otherwise:
>SET | grep -i -e "^go"
GOPATH=C:\Users\user\go
I also tried running runtime.GC() manually on each cycle, was of no use.
Hey @hungry, I created a PR which should resolve this issue -- tested it on Linux and macOS. Please give it a try and report back:
- #31
This is awesome! Now memory usage is stable, at least for me and this particular test case. Thanks a lot @roblillack ❤️