0

I'm using the following simple Objective-C program to receive messages from another process and display alerts, using an NSPasteboard as the means of communication between the 2 processes. The program works for me, but I've observed that the program is consuming huge amounts of RAM, regardless of whether the partner process is posting new messages to the pasteboard and the inner alert block is entered. It produces 1G swapfiles in /private/var/vm/ every few minutes while it runs until I kill it.

I ran the program through Xcode Instruments, and I am seeing that [NSPasteboard canReadObjectForClasses:options:] is creating persistent __NSArrayI objects every time it is called, which is the source of the RAM bloat. Can anyone else confirm this? Is this a bug in [NSPasteboard canReadObjectForClasses:options:], or should I just be performing this task differently?

EDIT:
I originally forgot to enable ARC. Even with ARC enabled however, memory usage growth still greatly outpaces automatic memory release, and the swapfiles are still being created over time. Does anyone have suggestions on how I can rewrite this script to use a better convention than while(true) so that [NSPasteboard canReadObjectForClasses:options:] is called less? It also still strikes me as odd that this AppKit function isn't automatically releasing the __NSArrayI objects it creates and assumes ARC will take care of it; is this common practice for such a library?

#import <Foundation/Foundation.h>
#import <CoreFoundation/CoreFoundation.h>
#import <AppKit/Appkit.h>

int main(void) {
    NSPasteboard *pb = [NSPasteboard pasteboardWithName:@"alertBoard"];
    NSArray *clsss = @[[NSString class]];
    while(true) {
        if ([pb canReadObjectForClasses:clsss options:nil]) {
            NSArray *contents = [pb readObjectsForClasses:@[[NSString class]] options: nil];
            CFStringRef msg  = (__bridge CFStringRef) [contents firstObject];
            [pb clearContents];

            CFUserNotificationDisplayNotice(2, 3, NULL, NULL, NULL, CFSTR("Alert"), msg, NULL);
        }   
    }   
    return 0;
}

I'm compiling the program like so g++ -framework Foundation -framework CoreFoundation -framework AppKit -fobjc-arc alertDaemon.m -o alertDaemon. The problem occurs regardless of the other process, so running the program for a few minutes is all that's needed to observe swapfile creation.

eckenrod
  • 519
  • 4
  • 17
  • you already know how to run with the Instruments profiler. Why not use the "leaks" template in Instruments which will report leaks. You can also use address sanitizer from Xcode, which can be enabled via Product -> Scheme -> Diagnostics – Brad Allred Sep 12 '18 at 21:33
  • 1
    Also, I notice you aren't using ARC. Why is that? there is no reason not to use ARC for new development that comes to my mind. – Brad Allred Sep 12 '18 at 21:35
  • @BradAllred thank you for pointing out that I needed to enable ARC - I did not know it needed to be explicitly enabled with `-fobjc-arc`. Even with ARC enabled however, memory usage growth still outpaces automatic memory release. It seems that I need to rewrite this program and remove the `while(true)` block so that it calls `[pb canReadObjectForClasses:clsss options:nil]` less. Do you have any suggestions? Also, the Leak Checks instrument does not report any leaks regardless of whether ARC is enabled. – eckenrod Sep 13 '18 at 12:08
  • you cant "outpace" ARC, its not a garbage collector. Use an actual runloop (`NSRunLoop` or `CFRunLoop`) rather than `while (true)`. you can try an `@autorelese` block; you probably have a lot of autoreleased "temporaries" that are never getting drained. – Brad Allred Sep 13 '18 at 15:02
  • The leak in `canReadObjectForClasses:options:` [still exists](https://developer.apple.com/forums/thread/703804), running under ARC, and seems to occur specifically with private NSPasteboards. Does anybody have a solution to that? I can't avoid checking it like the OP ended up doing, e.g. when validating a user interface item. Wrapping it in an autorelease block doesn't seem to prevent the leak. Not sure I see how NSRunLoop could help. `-releaseGlobally` wraps up the leak but of course that clears the pasteboard in question which is not ideal when validating if there's objects on it for paste. – Billy Gray Apr 15 '22 at 14:08

1 Answers1

0

I came up with a solution with uses [NSPasteboard changeCount] to check if the pasteboard has been written to instead of [NSPasteboard canReadObjectForClasses:options:]. Now the program uses a consistent, small amount of memory instead of eating up RAM. Now I just need to figure out how to use fewer CPU cycles, though better solutions than using continuous polling to check an NSPasteboard are scare.

#import <Foundation/Foundation.h>
#import <CoreFoundation/CoreFoundation.h>
#import <AppKit/Appkit.h>

int main(void) {
    NSPasteboard *pb = [NSPasteboard pasteboardWithName:@"alertBoard"];
    NSArray *clsss = @[[NSString class]];
    int chngCnt = [pb changeCount];
    while(true) {
        if (chngCnt != [pb changeCount]) {
            NSArray *contents = [pb readObjectsForClasses:@[[NSString class]] options: nil];
            CFStringRef msg  = (__bridge CFStringRef) [contents firstObject];
            chngCnt = [pb clearContents];

            CFUserNotificationDisplayNotice(2, 3, NULL, NULL, NULL, CFSTR("Alert"), msg, NULL);
        }   
    }   
    return 0;
}
eckenrod
  • 519
  • 4
  • 17