I've been working to track this down for 2 days now and I'm just taking a long shot to see if anyone might have a new idea for me.
My cgo-based bindings library seems to have unbounded RSS memory growth, which I have been able to reduce to the smallest benchmark test and even pinpoint the exact call, but the reason behind it still eludes me.
The context is that I have a struct in C++ that will store a const char* for the last exception that was thrown, as a strdup() copy which gets cleaned up properly each time.
typedef struct _HandleContext {
HandleId handle;
const char* last_error;
} _HandleContext;
const char* getLastError(_HandleContext* ctx);
On the Go side, I have a function for lastError() to return the last error value
func (c *Config) lastError() error {
err := C.getLastError(c.ptr)
if err == nil {
return nil
}
e := C.GoString(err)
if e == "" {
return nil
}
runtime.KeepAlive(c)
// return nil // <- would result in no memory growth
return errors.New(e) // <- results in memory growth
}
What I am seeing in my benchmark test is that the RSS grows something like 20MB a second, yet the GODEBUG=gctrace=1 and the pprof memory profile don't reflect this memory usage at all, aside from showing a hotspot (in pprof) being the GoString() call:
gc 4 @4.039s 0%: 0.006+0.14+0.003 ms clock, 0.024+0.10/0.039/0.070+0.014 ms cpu, 4->4->0 MB, 5 MB goal, 4 P
gc 5 @6.857s 0%: 0.003+0.20+0.004 ms clock, 0.015+0.069/0.025/0.15+0.016 ms cpu, 4->4->0 MB, 5 MB goal, 4 P
...
gc 26 @69.498s 0%: 0.005+0.12+0.003 ms clock, 0.021+0.10/0.044/0.093+0.014 ms cpu, 4->4->0 MB, 5 MB goal, 4 P
// 800MB RSS usage
gc 27 @71.824s 0%: 0.006+2.2+0.003 ms clock, 0.025+0.063/0.058/0.11+0.014 ms cpu, 4->4->0 MB, 5 MB goal, 4 P
(pprof) top10
Showing nodes accounting for 46083.69kB, 100% of 46083.69kB total
Showing top 10 nodes out of 19
flat flat% sum% cum cum%
30722.34kB 66.67% 66.67% 30722.34kB 66.67% <...>._Cfunc_GoStringN (inline)
7168.11kB 15.55% 82.22% 7168.11kB 15.55% errors.New (inline)
3073.16kB 6.67% 88.89% 46083.69kB 100% <...>.testLeak
1536.02kB 3.33% 92.22% 1536.02kB 3.33% <...>.(*DisplayTransform).SetInputColorSpace.func1
1024.02kB 2.22% 94.44% 1024.02kB 2.22% <...>.(*Config).NumViews.func1
1024.02kB 2.22% 96.67% 1024.02kB 2.22% <...>.(*Config).View.func1
512.01kB 1.11% 97.78% 512.01kB 1.11% <...>.(*DisplayTransform).SetView.func1
512.01kB 1.11% 98.89% 512.01kB 1.11% <...>._Cfunc_GoString (inline)
512.01kB 1.11% 100% 512.01kB 1.11% <...>.newProcessor (inline)
0 0% 100% 512.01kB 1.11% <...>.(*Config).ColorSpaceNameByIndex
Regardless of whether I ignore the error return value in my test, it grows. If I return nil instead of errors.New(e), it will stay around 20MB RSS.
I MUST be doing something stupid, but I can't see any reason for the memory growth based on returning this string wrapped in an error. At first I thought I was leaking in C/C++ but it a led to this one factor on the Go side. Any tips would help greatly, since I have tried debug GC output, pprof reports, valgrind, address sanitizer, and refactoring the entire memory management of my C bindings layer.
Justin