Jump to content

  • Log In with Google      Sign In   
  • Create Account

Beach ball when adding a massive amount of rows to NSTableView using NSArrayController


Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

  • You cannot reply to this topic
2 replies to this topic

#1 aregee   Members   -  Reputation: 1026

Like
0Likes
Like

Posted 29 March 2014 - 11:59 AM

In my audio player, I am adding a ton of items to a NSTableView through an NSArrayController.

 

Moving the process to an NSOperation with a NSOperationQueue, I hoped to get rid of waiting for a long time for my window to open, and also that my application would be immediately responsive.

 

Here is my NSOperation (.h-file):

#import <Foundation/Foundation.h>
#include "STASearchForMediaProtocol.h"

@interface STASearchForMediaOperation : NSOperation {
    NSString *pathToSearch;
    BOOL searchRecursive;
    NSOperationQueue *ownOperationQueue;
    id <STASearchForMediaProtocol> ourDelegate;
}

- (id)initWithFilePath:(NSString *)path recursiveSearch:(BOOL)recursive usingOperationQueue:(NSOperationQueue *)operationQueue withDelegate:(id)delegate;

@end

Here is my NSOperation (.m-file):

 
#import "STASearchForMediaOperation.h"

@implementation STASearchForMediaOperation

- (id)init {
    self = [super init];
    
    if (self) {
        pathToSearch = nil;
        searchRecursive = NO;
        ownOperationQueue = nil;
        ourDelegate = nil;
    }
    
    return self;
}

- (id)initWithFilePath:(NSString *)path recursiveSearch:(BOOL)recursive usingOperationQueue:(NSOperationQueue *)operationQueue withDelegate:(id)delegate {
    self = [self init];
    
    if (self) {
        pathToSearch = path;
        searchRecursive = recursive;
        ownOperationQueue = operationQueue;
        ourDelegate = delegate;
    }
    
    return self;
}

- (void)searchPathForSong:(NSString *)path {
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSError *error = nil;
    NSArray *paths = [fileManager contentsOfDirectoryAtPath:path error:&error];
    if (error != nil) {
        printf("Error reading path '%s'.", [path UTF8String]);
        return;
    }
    BOOL isDirectory = NO;
    for (NSString *item in paths) {
        NSString *itemPath = [NSString stringWithFormat:@"%@/%@", path, item];
        BOOL fileExistsAtPath = [[NSFileManager defaultManager] fileExistsAtPath:itemPath isDirectory:&isDirectory];
        if (fileExistsAtPath) {
            if (isDirectory)
            {
                //We need our own operation queue to post further searches
                if (searchRecursive && (ownOperationQueue != nil)) {
                    NSOperation *newOperation = [[STASearchForMediaOperation alloc] initWithFilePath:itemPath recursiveSearch:searchRecursive usingOperationQueue:ownOperationQueue withDelegate:ourDelegate];
                    [newOperation addDependency:self];
                    [ownOperationQueue addOperation:newOperation];
                }
            }
        }
        if ([[item pathExtension] isEqualToString:@"mp3"]) {
            NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:2];
            [array addObject:item];
            [array addObject:itemPath];
            if (ourDelegate != nil) {
                [(NSObject *)ourDelegate performSelectorOnMainThread:(@selector(signalMp3FileFound:)) withObject:array waitUntilDone:YES];
            }
        }
        else if ([[item pathExtension] isEqualToString:@"flac"]) {
            NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:2];
            [array addObject:item];
            [array addObject:itemPath];
            if (ourDelegate != nil) {
                [(NSObject *)ourDelegate performSelectorOnMainThread:(@selector(signalFlacFileFound:)) withObject:array waitUntilDone:YES];
            }
        }
        else if ([[item pathExtension] isEqualToString:@"wav"]) {
            NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:2];
            [array addObject:item];
            [array addObject:itemPath];
            if (ourDelegate != nil) {
                [(NSObject *)ourDelegate performSelectorOnMainThread:(@selector(signalWavFileFound:)) withObject:array waitUntilDone:YES];
            }
        }
        else if ([[item pathExtension] isEqualToString:@"aiff"]) {
            NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:2];
            [array addObject:item];
            [array addObject:itemPath];
            if (ourDelegate != nil) {
                [(NSObject *)ourDelegate performSelectorOnMainThread:(@selector(signalAiffFileFound:)) withObject:array waitUntilDone:YES];
            }
        }
        
        if ([self isCancelled]) {
            break;
        }
    }
}

- (void)main {
    @autoreleasepool {
        if (ourDelegate != nil)
        {
            [ourDelegate signalBeginUpdate];
        }
        
        [self searchPathForSong:pathToSearch];
        
        if (ourDelegate != nil)
        {
            [ourDelegate signalEndUpdate];
        }
    }
}

@end

My protocol for the delegate:

@protocol STASearchForMediaProtocol <NSObject>

@required
- (void)signalBeginUpdate;
- (void)signalEndUpdate;
- (void)signalMp3FileFound:(id)object;
- (void)signalFlacFileFound:(id)object;
- (void)signalWavFileFound:(id)object;
- (void)signalAiffFileFound:(id)object;

@end

My app delegate methods that are being used for this purpose:

- (void)signalBeginUpdate {
    if (updateCount == 0) {
        [tableViewRef beginUpdates];
    }
    
    updateCount++;
}

- (void)signalEndUpdate {
    updateCount--;
    
    if (updateCount == 0) {
        [tableViewRef endUpdates];
    }
}

- (void)signalMp3FileFound:(id)object {
    @autoreleasepool {
        NSArray *items = (NSArray *)object;
        [self addSongWithArtist:@"Music" album:@"MP3" song:(NSString *)[items objectAtIndex:0] fileName:(NSString *)[items objectAtIndex:1]];
    }
}

- (void)signalFlacFileFound:(id)object {
    @autoreleasepool {
        NSArray *items = (NSArray *)object;
        [self addSongWithArtist:@"Music" album:@"FLAC" song:(NSString *)[items objectAtIndex:0] fileName:(NSString *)[items objectAtIndex:1]];
    }
}

- (void)signalWavFileFound:(id)object {
    @autoreleasepool {
        NSArray *items = (NSArray *)object;
        [self addSongWithArtist:@"Music" album:@"WAV" song:(NSString *)[items objectAtIndex:0] fileName:(NSString *)[items objectAtIndex:1]];
    }
}

- (void)signalAiffFileFound:(id)object {
    @autoreleasepool {
        NSArray *items = (NSArray *)object;
        [self addSongWithArtist:@"Music" album:@"AIFF" song:(NSString *)[items objectAtIndex:0] fileName:(NSString *)[items objectAtIndex:1]];
    }
}

The 'addSongWithArtist:album:song:fileName:' method:

- (void)addSongWithArtist:(NSString *)artist album:(NSString *)album song:(NSString *)song fileName:(NSString *)fileName {
    internalIDCounter++;

    [ArrayControllerRef addObject:[NSMutableDictionary dictionaryWithObjectsAndKeys:artist, @"Artist", album, @"Album", song, @"Song", fileName, @"fileName", [NSNumber numberWithUnsignedLong:internalIDCounter], @"iID", nil]];
}

How I initiate the NSOperation:

    updateCount = 0;
    fileSearchQueue = [[NSOperationQueue alloc] init];
    [fileSearchQueue setName:@"File search operation queue"];
    
    STASearchForMediaOperation *searchForMedia = [[STASearchForMediaOperation alloc] initWithFilePath:@"/Volumes/Music/UNSORT" recursiveSearch:YES usingOperationQueue:fileSearchQueue withDelegate:self];
    [fileSearchQueue addOperation:searchForMedia];

My problem is...

 

As the code is right now: I still have to wait for the process to complete before the window opens. With beach ball...  It takes an awful long time...  I never waited long enough to see the app really start.

 

If I change 'waitUntilDone' to 'NO', I get the window straight away as I want, but with a little period of 'hiccups' with beach ball and waiting.  I also get a 'NSLog'-message:

 

"CoreAnimation: warning, deleted thread with uncommitted CATransaction; set CA_DEBUG_TRANSACTIONS=1 in environment to log backtraces."

 

It is also random how many songs will actually be in my NSTableView.  Often it has a lot of empty rows.

 

I guess this is because the threads gets deleted before my table view manages to retrieve the data since I don't 'waitUntilDone'.

 

Does anyone have a suggestion:

1. How to fix my solution?

2. A better solution to solve my problem?

 



Sponsor:

#2 aregee   Members   -  Reputation: 1026

Like
0Likes
Like

Posted 29 March 2014 - 02:35 PM

I did a little change.  Instead of spawning a new operation for each directory recursion, I just did the same recursion on the same thread.  It might not have been a good way to do this anyway.  This made the whole process much much faster, and I didn't get the suspected deadlock-situation I got sometimes any more either.  I still get the message about: 

 

"CoreAnimation: warning, deleted thread with uncommitted CATransaction; set CA_DEBUG_TRANSACTIONS=1 in environment to log backtraces."

 

I am not sure yet why exactly the mechanism why that happens yet, but I will continue to investigate.

 

No-one has any ideas?



#3 aregee   Members   -  Reputation: 1026

Like
1Likes
Like

Posted 29 March 2014 - 02:59 PM

Never mind...  I forgot to call the beginUpdate and endUpdate on the main thread...






Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.



PARTNERS