From 0db61bb54319997cc619db5d702c477551115139 Mon Sep 17 00:00:00 2001 From: Andrew Madsen Date: Thu, 7 May 2015 12:39:52 -0600 Subject: [PATCH] Issue #82: Added an option to cache the full MIDI responder hierarchy to NS/UIApplication+MIKMIDI. Also _greatly_ improved performance even in the default, uncached mode. --- .../MIKMIDIResponderChainTests.m | 17 ++- ...C1C9286E-763A-4210-B3E7-DF2205A6AA20.plist | 23 ++++ Source/NSUIApplication+MIKMIDI.h | 51 ++++++-- Source/NSUIApplication+MIKMIDI.m | 113 +++++++++++++----- 4 files changed, 160 insertions(+), 44 deletions(-) diff --git a/Framework/MIKMIDI Tests/MIKMIDIResponderChainTests.m b/Framework/MIKMIDI Tests/MIKMIDIResponderChainTests.m index 11e6b001..77efcc1e 100644 --- a/Framework/MIKMIDI Tests/MIKMIDIResponderChainTests.m +++ b/Framework/MIKMIDI Tests/MIKMIDIResponderChainTests.m @@ -52,7 +52,10 @@ - (void)setUp MIKMIDIDummyResponder *sub2sub1 = [[MIKMIDIDummyResponder alloc] initWithMIDIIdentifier:@"Sub2Sub1" subresponders:nil]; MIKMIDIDummyResponder *sub2sub2 = [[MIKMIDIDummyResponder alloc] initWithMIDIIdentifier:@"Sub2Sub2" subresponders:nil]; - MIKMIDIDummyResponder *sub2 = [[MIKMIDIDummyResponder alloc] initWithMIDIIdentifier:@"Sub2" subresponders:@[sub2sub1, sub2sub2]]; + MIKMIDIDummyResponder *sub2sub3sub1 = [[MIKMIDIDummyResponder alloc] initWithMIDIIdentifier:@"Sub2Sub3Sub1" subresponders:nil]; + MIKMIDIDummyResponder *sub2sub3 = [[MIKMIDIDummyResponder alloc] initWithMIDIIdentifier:@"Sub2Sub3" subresponders:@[sub2sub3sub1]]; + MIKMIDIDummyResponder *sub2sub4 = [[MIKMIDIDummyResponder alloc] initWithMIDIIdentifier:@"Sub2Sub4" subresponders:nil]; + MIKMIDIDummyResponder *sub2 = [[MIKMIDIDummyResponder alloc] initWithMIDIIdentifier:@"Sub2" subresponders:@[sub2sub1, sub2sub2, sub2sub3, sub2sub4]]; MIKMIDIDummyResponder *sub3 = [[MIKMIDIDummyResponder alloc] initWithMIDIIdentifier:@"Sub3" subresponders:nil]; @@ -72,6 +75,7 @@ - (void)testRegistration - (void)testUncachedResponderSearchPerformance { NSApplication *app = [NSApplication sharedApplication]; + app.shouldCacheMIKMIDISubresponders = NO; [self measureBlock:^{ for (NSUInteger i=0; i<50000; i++) { [app MIDIResponderWithIdentifier:@"Sub2Sub1"]; @@ -79,4 +83,15 @@ - (void)testUncachedResponderSearchPerformance }]; } +- (void)testCachedResponderSearchPerformance +{ + NSApplication *app = [NSApplication sharedApplication]; + app.shouldCacheMIKMIDISubresponders = YES; + [self measureBlock:^{ + for (NSUInteger i=0; i<50000; i++) { + [app MIDIResponderWithIdentifier:@"Sub2Sub1"]; + } + }]; +} + @end diff --git a/Framework/MIKMIDI.xcodeproj/xcshareddata/xcbaselines/9D4DF1391AAB57430065F004.xcbaseline/C1C9286E-763A-4210-B3E7-DF2205A6AA20.plist b/Framework/MIKMIDI.xcodeproj/xcshareddata/xcbaselines/9D4DF1391AAB57430065F004.xcbaseline/C1C9286E-763A-4210-B3E7-DF2205A6AA20.plist index 7efa07a8..b22dc619 100644 --- a/Framework/MIKMIDI.xcodeproj/xcshareddata/xcbaselines/9D4DF1391AAB57430065F004.xcbaseline/C1C9286E-763A-4210-B3E7-DF2205A6AA20.plist +++ b/Framework/MIKMIDI.xcodeproj/xcshareddata/xcbaselines/9D4DF1391AAB57430065F004.xcbaseline/C1C9286E-763A-4210-B3E7-DF2205A6AA20.plist @@ -4,6 +4,29 @@ classNames + MIKMIDIResponderChainTests + + testCachedResponderSearchPerformance + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.05 + baselineIntegrationDisplayName + Local Baseline + + + testUncachedResponderSearchPerformance + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.94 + baselineIntegrationDisplayName + Local Baseline + + + MIKMIDISequenceTests testMIDIFileReadPerformance diff --git a/Source/NSUIApplication+MIKMIDI.h b/Source/NSUIApplication+MIKMIDI.h index d01dee0d..81284961 100644 --- a/Source/NSUIApplication+MIKMIDI.h +++ b/Source/NSUIApplication+MIKMIDI.h @@ -10,17 +10,17 @@ #if TARGET_OS_IPHONE - #import - #define MIK_APPLICATION_CLASS UIApplication - #define MIK_WINDOW_CLASS UIWindow - #define MIK_VIEW_CLASS UIView +#import +#define MIK_APPLICATION_CLASS UIApplication +#define MIK_WINDOW_CLASS UIWindow +#define MIK_VIEW_CLASS UIView #else - #import - #define MIK_APPLICATION_CLASS NSApplication - #define MIK_WINDOW_CLASS NSWindow - #define MIK_VIEW_CLASS NSView +#import +#define MIK_APPLICATION_CLASS NSApplication +#define MIK_WINDOW_CLASS NSWindow +#define MIK_VIEW_CLASS NSView #endif @@ -64,6 +64,20 @@ */ - (void)unregisterMIDIResponder:(id)responder; +/** + * When subresponder caching is enabled via shouldCacheMIKMIDISubresponders, + * This method will cause the cache to be invalidated and regenerated. If a previously + * registered MIDI responders' subresponders have changed, it can call this method + * to force the cache to be refreshed. + * + * If subresponder caching is disabled (the default), calling this method has no effect, as + * subresponders are dynamically searched on every call to -MIDIResponderWithIdentifier and + * -allMIDIResponders. + * + * @see shouldCacheMIKMIDISubresponders + */ +- (void)refreshRespondersAndSubresponders; + /** * NSApplication (OS X) or UIApplication (iOS) itself implements to methods in the MIKMIDIResponder protocol. * This method determines if any responder in the MIDI responder chain (registered responders and their subresponders) @@ -78,7 +92,7 @@ /** * When this method is invoked with a MIDI command, the application will search its registered MIDI responders, * for responders that respond to the command, then call their -handleMIDICommand: method. - * + * * Call this method from a MIDI source event handler block to automatically dispatch MIDI commands/messages * from that source to all interested registered responders. * @@ -91,7 +105,7 @@ * * @param identifier An NSString instance containing the MIDI identifier to search for. * - * @return An object that conforms to MIKMIDIResponder, or nil if no registered responder for the passed in identifier + * @return An object that conforms to MIKMIDIResponder, or nil if no registered responder for the passed in identifier * could be found. */ - (id)MIDIResponderWithIdentifier:(NSString *)identifier; @@ -103,4 +117,21 @@ */ - (NSSet *)allMIDIResponders; +// Properties + +/** + * When this option is set, the application will cache registered MIDI responders' subresponders. + * Setting this option can greatly improve performance of -MIDIResponderWithIdentifier. However, + * when set, registered responders' -subresponders method cannot dynamically return different results + * e.g. for each MIDI command received. + * + * The entire cache is automatically refreshed anytime a new MIDI responder is registered or unregistered. + * It can also be manually refreshed by calling -refreshRespondersAndSubresponders. + * + * For backwards compatibility the default for this option is NO, or no caching. + * + * @see -refreshRespondersAndSubresponders + */ +@property (nonatomic) BOOL shouldCacheMIKMIDISubresponders; + @end diff --git a/Source/NSUIApplication+MIKMIDI.m b/Source/NSUIApplication+MIKMIDI.m index 9f77a0f8..6d8524d9 100644 --- a/Source/NSUIApplication+MIKMIDI.m +++ b/Source/NSUIApplication+MIKMIDI.m @@ -44,11 +44,22 @@ static BOOL MIKObjectRespondsToMIDICommand(id object, MIKMIDICommand *command) @interface MIKMIDIResponderHierarchyManager : NSObject +// Public +- (void)refreshRespondersAndSubresponders; +- (id)MIDIResponderWithIdentifier:(NSString *)identifier; + +// Properties @property (nonatomic, strong) NSHashTable *registeredMIKMIDIResponders; -@property (nonatomic, strong) NSSet *registeredMIKMIDIRespondersAndSubresponders; +@property (nonatomic, strong, readonly) NSSet *registeredMIKMIDIRespondersAndSubresponders; + +@property (nonatomic, strong) NSHashTable *subrespondersCache; + +@property (nonatomic) BOOL shouldCacheMIKMIDISubresponders; @property (nonatomic, strong, readonly) NSSet *allMIDIResponders; +@property (nonatomic, strong, readonly) NSPredicate *midiIdentifierPredicate; + @end @implementation MIKMIDIResponderHierarchyManager @@ -70,6 +81,8 @@ - (instancetype)init if (self) { NSPointerFunctionsOptions options = [[self class] hashTableOptions]; _registeredMIKMIDIResponders = [[NSHashTable alloc] initWithOptions:options capacity:0]; + + _shouldCacheMIKMIDISubresponders = NO; } return self; } @@ -77,21 +90,33 @@ - (instancetype)init - (void)registerMIDIResponder:(id)responder; { [self.registeredMIKMIDIResponders addObject:responder]; + [self refreshRespondersAndSubresponders]; } - (void)unregisterMIDIResponder:(id)responder; { [self.registeredMIKMIDIResponders addObject:responder]; + [self refreshRespondersAndSubresponders]; } #pragma mark - Public +- (void)refreshRespondersAndSubresponders +{ + self.subrespondersCache = nil; +} + - (id)MIDIResponderWithIdentifier:(NSString *)identifier { NSSet *registeredResponders = self.registeredMIKMIDIRespondersAndSubresponders; - NSPredicate *predicate = [NSPredicate predicateWithFormat:@"MIDIIdentifier LIKE %@", identifier]; - NSSet *results = [registeredResponders filteredSetUsingPredicate:predicate]; - id result = [results anyObject]; + + id result = nil; + for (id responder in registeredResponders) { + if ([[responder MIDIIdentifier] isEqualToString:identifier]) { + result = responder; + break; + } + } #if MIKMIDI_SEARCH_VIEW_HIERARCHY_FOR_RESPONDERS if (!result) { @@ -127,6 +152,10 @@ - (NSSet *)recursiveSubrespondersOfMIDIResponder:(id)responder - (NSSet *)registeredMIKMIDIRespondersAndSubresponders { + if (self.shouldCacheMIKMIDISubresponders && self.subrespondersCache != nil) { + return [self.subrespondersCache setRepresentation]; + } + NSMutableSet *result = [NSMutableSet set]; for (id responder in self.registeredMIKMIDIResponders) { [result unionSet:[self recursiveSubrespondersOfMIDIResponder:responder]]; @@ -136,14 +165,35 @@ - (NSSet *)registeredMIKMIDIRespondersAndSubresponders [result unionSet:[self MIDIRespondersInViewHierarchy]]; #endif + if (self.shouldCacheMIKMIDISubresponders) { + // Load cache with result + NSPointerFunctionsOptions options = [[self class] hashTableOptions]; + self.subrespondersCache = [[NSHashTable alloc] initWithOptions:options capacity:0]; + for (id object in result) { [self.subrespondersCache addObject:object]; } + } + return [result copy]; } ++ (NSSet *)keyPathsForValuesAffectingAllMIDIResponders +{ + return [NSSet setWithObjects:@"registeredMIKMIDIRespondersAndSubresponders", nil]; +} + - (NSSet *)allMIDIResponders { return self.registeredMIKMIDIRespondersAndSubresponders; } +@synthesize midiIdentifierPredicate = _midiIdentifierPredicate; +- (NSPredicate *)midiIdentifierPredicate +{ + if (!_midiIdentifierPredicate) { + _midiIdentifierPredicate = [NSPredicate predicateWithFormat:@"MIDIIdentifier LIKE $MIDIIdentifier"]; + } + return _midiIdentifierPredicate; +} + #pragma mark - Deprecated #if MIKMIDI_SEARCH_VIEW_HIERARCHY_FOR_RESPONDERS @@ -185,29 +235,13 @@ - (NSSet *)MIDIRespondersInViewHierarchy @implementation MIK_APPLICATION_CLASS (MIKMIDI) -+ (MIKMIDIResponderHierarchyManager *)mik_MIDIResponderHierarchyManager -{ - static MIKMIDIResponderHierarchyManager *manager = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - manager = [[MIKMIDIResponderHierarchyManager alloc] init]; - }); - return manager; -} +- (void)registerMIDIResponder:(id)responder; { [self.mikmidi_responderHierarchyManager registerMIDIResponder:responder]; } -- (void)registerMIDIResponder:(id)responder; -{ - [[[self class] mik_MIDIResponderHierarchyManager] registerMIDIResponder:responder]; -} - -- (void)unregisterMIDIResponder:(id)responder; -{ - [[[self class] mik_MIDIResponderHierarchyManager] unregisterMIDIResponder:responder]; -} +- (void)unregisterMIDIResponder:(id)responder; { [self.mikmidi_responderHierarchyManager unregisterMIDIResponder:responder]; } - (BOOL)respondsToMIDICommand:(MIKMIDICommand *)command; { - MIKMIDIResponderHierarchyManager *manager = [[self class] mik_MIDIResponderHierarchyManager]; + MIKMIDIResponderHierarchyManager *manager = self.mikmidi_responderHierarchyManager; NSSet *registeredResponders = [self respondersForCommand:command inResponders:manager.allMIDIResponders]; if ([registeredResponders count]) return YES; @@ -221,7 +255,7 @@ - (BOOL)respondsToMIDICommand:(MIKMIDICommand *)command; - (void)handleMIDICommand:(MIKMIDICommand *)command; { - MIKMIDIResponderHierarchyManager *manager = [[self class] mik_MIDIResponderHierarchyManager]; + MIKMIDIResponderHierarchyManager *manager = self.mikmidi_responderHierarchyManager; NSSet *registeredResponders = [self respondersForCommand:command inResponders:manager.allMIDIResponders]; for (id responder in registeredResponders) { [responder handleMIDICommand:command]; @@ -240,13 +274,11 @@ - (void)handleMIDICommand:(MIKMIDICommand *)command; - (id)MIDIResponderWithIdentifier:(NSString *)identifier; { - return [[[self class] mik_MIDIResponderHierarchyManager] MIDIResponderWithIdentifier:identifier]; + return [self.mikmidi_responderHierarchyManager MIDIResponderWithIdentifier:identifier]; } -- (NSSet *)allMIDIResponders -{ - return [[[self class] mik_MIDIResponderHierarchyManager] allMIDIResponders]; -} +- (NSSet *)allMIDIResponders { return [self.mikmidi_responderHierarchyManager allMIDIResponders]; } +- (void)refreshRespondersAndSubresponders { [self.mikmidi_responderHierarchyManager refreshRespondersAndSubresponders]; } #pragma mark - Private @@ -257,14 +289,29 @@ - (NSSet *)respondersForCommand:(MIKMIDICommand *)command inResponders:(NSSet *) }]]; } -#pragma mark - Deprecated +#pragma mark - Properties -#if MIKMIDI_SEARCH_VIEW_HIERARCHY_FOR_RESPONDERS -- (NSSet *)MIDIRespondersInViewHierarchy +- (MIKMIDIResponderHierarchyManager *)mikmidi_responderHierarchyManager +{ + static MIKMIDIResponderHierarchyManager *manager = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + manager = [[MIKMIDIResponderHierarchyManager alloc] init]; + }); + return manager; +} + ++ (NSSet *)keyPathsForValuesAffectingShouldCacheMIKMIDISubresponders { - return [[[self class] mik_MIDIResponderHierarchyManager] MIDIRespondersInViewHierarchy]; + return [NSSet setWithObject:@"mikmidi_responderHierarchyManager.shouldCacheMIKMIDISubresponders"]; } +- (BOOL)shouldCacheMIKMIDISubresponders { return [self.mikmidi_responderHierarchyManager shouldCacheMIKMIDISubresponders]; } +- (void)setShouldCacheMIKMIDISubresponders:(BOOL)flag { [self.mikmidi_responderHierarchyManager setShouldCacheMIKMIDISubresponders:flag]; } +#pragma mark - Deprecated + +#if MIKMIDI_SEARCH_VIEW_HIERARCHY_FOR_RESPONDERS +- (NSSet *)MIDIRespondersInViewHierarchy { return [self.mikmidi_responderHierarchyManager MIDIRespondersInViewHierarchy]; } #endif // MIKMIDI_SEARCH_VIEW_HIERARCHY_FOR_RESPONDERS @end