macOS: every obs-websocket GetStats leaks ~200 autoreleased objects (os_get_free_space has no autorelease pool on the websocket thread) — ~2 MB/min at one poll per second

Open Beginner friendly
#13,918 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
1/5
Estimated time
Under an hour
Newbie friendliness
92/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
objective-c

Research direction

Start in libobs/util/platform-cocoa.m at os_get_free_space, then review how GetStats reaches it on the obs-websocket thread. Reproduce with OBJC_DEBUG_MISSING_POOLS=YES and repeated GetStats requests; done means the diagnostic reports no missing pools and repeated polling no longer causes unbounded resident-memory growth.

Written by the indexing model from the issue text.

Description

Operating System Info

macOS 15

Other OS

macOS 15.7.9 (24G830), Intel (x86_64 build)

OBS Studio Version

32.2.2

OBS Studio Version (Other)

No response

OBS Studio Log URL

Not needed for this one: nothing is logged; the reproduction is a websocket client and the process's resident size. Happy to attach a log on request.

OBS Studio Crash Log URL

No response

Expected Behavior

An obs-websocket client that polls GetStats (a stats dashboard, a controller, any client that watches fps/skipped frames) does not make OBS's memory grow over time.

Current Behavior

On macOS, every GetStats answer leaves about 200 Foundation objects in memory that are never released. At one GetStats per second OBS grows by roughly 2 MB per minute for as long as the client is connected — linear, never plateaus. Two hours of polling: +280 MB resident.

Cause, from the source: GetStats calls os_get_free_disk_space() for availableDiskSpace; on macOS that is os_get_free_space() in libobs/util/platform-cocoa.m, which builds an NSURL, an NSArray of keys and the NSDictionary from -[NSURL resourceValuesForKeys:error:] (and everything that dictionary holds). All of it is autoreleased. obs-websocket answers the request on its own thread, which has no autorelease pool, so the objects are never released ("autoreleased with no pool in place - just leaking", in the runtime's words). The main thread has a pool and is unaffected, which is why the UI never shows this.

The function exists in the same form on master today.

Steps to Reproduce
  1. Launch OBS with the runtime's diagnostic for this exact condition: OBJC_DEBUG_MISSING_POOLS=YES /Applications/OBS.app/Contents/MacOS/OBS 2> /tmp/obs.txt (any profile whose recording path exists — the default ~/Movies does; with a non-existent path the lookup fails early and allocates almost nothing).
  2. Send ten GetStats requests over obs-websocket.
  3. grep -c 'no pool in place' /tmp/obs.txt1 980 new lines (198 per request). Ten GetVersion, GetStreamStatus, GetRecordStatus or GetRecordDirectory add 0.
  4. Send 3 000 GetStats (one at a time): resident size 228 → 340 MB, and it stays there; heap on the process shows +594 000 objects, mostly __NSArrayI_Transfer, __NSCFString, NSConcreteData, NSPathStore2, __NSDictionaryM; vmmap --summary puts the growth in MALLOC_NANO.

Class breakdown per ten requests, from the diagnostic:

 880 __NSArrayI_Transfer
 532 __NSCFString
 411 NSConcreteData
 118 NSPathStore2
  20 __NSDictionaryM
Anything else we should know?

The fix is an @autoreleasepool around the body of os_get_free_space (the other Cocoa helpers in that file already do this, e.g. os_request_high_performance). With it, the same ten requests add 0 diagnostic lines and 3 000 requests add about 1 MB. Patch against 32.2.2 (applies to master):

--- a/libobs/util/platform-cocoa.m
+++ b/libobs/util/platform-cocoa.m
@@ -361,22 +361,30 @@ uint64_t os_get_sys_free_size(void)
 int64_t os_get_free_space(const char *path)
 {
     if (path) {
-        NSURL *fileURL = [NSURL fileURLWithPath:@(path)];
-
-        NSArray *availableCapacityKeys = @[
-            NSURLVolumeAvailableCapacityKey, NSURLVolumeAvailableCapacityForImportantUsageKey,
-            NSURLVolumeAvailableCapacityForOpportunisticUsageKey
-        ];
-
-        NSDictionary *values = [fileURL resourceValuesForKeys:availableCapacityKeys error:nil];
-
-        NSNumber *availableImportantSpace = values[NSURLVolumeAvailableCapacityForImportantUsageKey];
-        NSNumber *availableSpace = values[NSURLVolumeAvailableCapacityKey];
-
-        if (availableImportantSpace.longValue > 0) {
-            return availableImportantSpace.longValue;
-        } else {
-            return availableSpace.longValue;
+        /* Callers are not only the main thread: obs-websocket answers
+         * GetStats on its own thread, which has no autorelease pool, and
+         * every object autoreleased here (the URL, the key array, the
+         * resource dictionary and all it holds) would then live until that
+         * thread exits — about 200 objects per call, 2 MB a minute at one
+         * poll a second. */
+        @autoreleasepool {
+            NSURL *fileURL = [NSURL fileURLWithPath:@(path)];
+
+            NSArray *availableCapacityKeys = @[
+                NSURLVolumeAvailableCapacityKey, NSURLVolumeAvailableCapacityForImportantUsageKey,
+                NSURLVolumeAvailableCapacityForOpportunisticUsageKey
+            ];
+
+            NSDictionary *values = [fileURL resourceValuesForKeys:availableCapacityKeys error:nil];
+
+            NSNumber *availableImportantSpace = values[NSURLVolumeAvailableCapacityForImportantUsageKey];
+            NSNumber *availableSpace = values[NSURLVolumeAvailableCapacityKey];
+
+            if (availableImportantSpace.longValue > 0) {
+                return availableImportantSpace.longValue;
+            } else {
+                return availableSpace.longValue;
+            }
         }
     }
 

The same applies to any other Cocoa code reached from obs-websocket's thread; os_get_free_space is the one GetStats hits every time. Glad to open this as a PR if that is preferred.

— filed by the Rigger project (an application built on OBS)

Dominant language
C
Stars
76.4k
Forks
10.2k
Avg merge
4d 23h
Merged PRs (30d)
12

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from obsproject/obs-studio

All issues in obsproject/obs-studio

Similar issues

More C issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.