spot icon indicating copy to clipboard operation
spot copied to clipboard

Memory Leak during continuous image updates

Open hungry opened this issue 1 year ago • 8 comments

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!

hungry avatar Dec 06 '24 10:12 hungry

To isolate the problem, the provided code uses randomly generated images instead of actual screen captures.

hungry avatar Dec 06 '24 10:12 hungry

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() {
        ...

roblillack avatar Dec 06 '24 13:12 roblillack

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: Screenshot 2024-12-06 170015

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.

hungry avatar Dec 06 '24 14:12 hungry

Might be a weird question, but you did not turn of Go's garbage collection using some environment variable, no?

roblillack avatar Dec 06 '24 16:12 roblillack

Neither intentionally, nor otherwise:

>SET | grep -i -e "^go"
GOPATH=C:\Users\user\go

hungry avatar Dec 06 '24 16:12 hungry

I also tried running runtime.GC() manually on each cycle, was of no use.

hungry avatar Dec 06 '24 16:12 hungry

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

roblillack avatar Dec 07 '24 14:12 roblillack

This is awesome! Now memory usage is stable, at least for me and this particular test case. Thanks a lot @roblillack ❤️

hungry avatar Dec 07 '24 15:12 hungry