diff --git a/api/backend_test.go b/api/backend_test.go index 3bcfbce75c6..ea1903b8daf 100644 --- a/api/backend_test.go +++ b/api/backend_test.go @@ -1926,3 +1926,17 @@ func TestRestoreKeycardAccountAndLogin(t *testing.T) { require.NoError(t, err) require.NotNil(t, acc) } + +func TestReleaseOSMemory(t *testing.T) { + b := NewGethStatusBackend(tt.MustCreateTestLogger()) + + // the first call should succeed + require.NoError(t, b.ReleaseOSMemory()) + + // the second immediate call should be skipped due to rate limiting + require.Error(t, b.ReleaseOSMemory()) + + // reset the lastGCTime to allow the next call + b.lastGCTime = time.Now().Add(-(gcInterval + time.Second)) + require.NoError(t, b.ReleaseOSMemory()) +} diff --git a/api/geth_backend.go b/api/geth_backend.go index 37ddfcfd545..f321988e297 100644 --- a/api/geth_backend.go +++ b/api/geth_backend.go @@ -9,6 +9,8 @@ import ( "math/big" "os" "path/filepath" + "runtime" + "runtime/debug" "strings" "sync" "time" @@ -66,6 +68,10 @@ import ( "github.com/status-im/status-go/walletdatabase" ) +const ( + gcInterval = 10 * time.Second +) + var ( // ErrWhisperClearIdentitiesFailure clearing whisper identities has failed. ErrWhisperClearIdentitiesFailure = errors.New("failed to clear whisper identities") @@ -110,6 +116,9 @@ type GethStatusBackend struct { logger *zap.Logger preLoginLogConfig *logutils.PreLoginLogConfig + + lastGCTime time.Time + lastGCTimeLock sync.Mutex } // NewGethStatusBackend create a new GethStatusBackend instance @@ -3036,3 +3045,44 @@ func (b *GethStatusBackend) SetPreLoginLogLevel(level string) error { } return logutils.OverrideRootLoggerWithConfig(b.preLoginLogConfig.ConvertToLogSettings()) } + +func (b *GethStatusBackend) ReleaseOSMemory() error { + b.lastGCTimeLock.Lock() + defer b.lastGCTimeLock.Unlock() + + // Only run GC if 10 seconds have passed since the last call + if time.Since(b.lastGCTime) < gcInterval { + return errors.New("skipping GC because it was called too recently") + } + b.releaseOSMemory() + b.lastGCTime = time.Now() + return nil +} + +func (b *GethStatusBackend) releaseOSMemory() { + var before, after runtime.MemStats + + // Collect stats before GC + runtime.ReadMemStats(&before) + b.logger.Debug("Before garbage collection", + zap.Uint64("heap_alloc_mb", before.HeapAlloc/1024/1024), + zap.Uint64("heap_objects", before.HeapObjects), + zap.Uint64("sys_mb", before.Sys/1024/1024), + ) + + runtime.GC() + debug.FreeOSMemory() + + // Collect stats after GC + runtime.ReadMemStats(&after) + + // Log results with differences + b.logger.Debug("After garbage collection", + zap.Uint64("heap_alloc_mb", after.HeapAlloc/1024/1024), + zap.Uint64("heap_objects", after.HeapObjects), + zap.Uint64("sys_mb", after.Sys/1024/1024), + zap.Int64("freed_mb", int64(before.HeapAlloc-after.HeapAlloc)/1024/1024), + zap.Int64("objects_freed", int64(before.HeapObjects-after.HeapObjects)), + zap.Int64("released_mb", int64(after.HeapReleased-before.HeapReleased)/1024/1024), + ) +} diff --git a/mobile/status.go b/mobile/status.go index 75ab3807451..30512e974e3 100644 --- a/mobile/status.go +++ b/mobile/status.go @@ -2429,3 +2429,12 @@ func IntendedPanic(message string) string { panic(err) }) } + +func ReleaseOSMemory() string { + return callWithResponse(releaseOSMemory) +} + +func releaseOSMemory() string { + err := statusBackend.ReleaseOSMemory() + return makeJSONResponse(err) +} diff --git a/mobile/status_test.go b/mobile/status_test.go index d18a24313b3..5da47368883 100644 --- a/mobile/status_test.go +++ b/mobile/status_test.go @@ -47,3 +47,10 @@ func TestIntendedPanic(t *testing.T) { IntendedPanic(message) }) } + +// TestReleaseOSMemory is a simple test to ensure the function doesn't panic +func TestReleaseOSMemory(t *testing.T) { + require.NotPanics(t, func() { + ReleaseOSMemory() + }) +}