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
- 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).
- Send ten
GetStats requests over obs-websocket.
grep -c 'no pool in place' /tmp/obs.txt → 1 980 new lines (198 per request). Ten GetVersion, GetStreamStatus, GetRecordStatus or GetRecordDirectory add 0.
- 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)
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
GetStatsanswer leaves about 200 Foundation objects in memory that are never released. At oneGetStatsper 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:
GetStatscallsos_get_free_disk_space()foravailableDiskSpace; on macOS that isos_get_free_space()inlibobs/util/platform-cocoa.m, which builds anNSURL, anNSArrayof keys and theNSDictionaryfrom-[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
mastertoday.Steps to Reproduce
OBJC_DEBUG_MISSING_POOLS=YES /Applications/OBS.app/Contents/MacOS/OBS 2> /tmp/obs.txt(any profile whose recording path exists — the default~/Moviesdoes; with a non-existent path the lookup fails early and allocates almost nothing).GetStatsrequests over obs-websocket.grep -c 'no pool in place' /tmp/obs.txt→ 1 980 new lines (198 per request). TenGetVersion,GetStreamStatus,GetRecordStatusorGetRecordDirectoryadd 0.GetStats(one at a time): resident size 228 → 340 MB, and it stays there;heapon the process shows +594 000 objects, mostly__NSArrayI_Transfer,__NSCFString,NSConcreteData,NSPathStore2,__NSDictionaryM;vmmap --summaryputs the growth inMALLOC_NANO.Class breakdown per ten requests, from the diagnostic:
Anything else we should know?
The fix is an
@autoreleasepoolaround the body ofos_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):The same applies to any other Cocoa code reached from obs-websocket's thread;
os_get_free_spaceis the oneGetStatshits every time. Glad to open this as a PR if that is preferred.— filed by the Rigger project (an application built on OBS)