Skip to content

Commit

Permalink
Issue #82: Added an option to cache the full MIDI responder hierarchy…
Browse files Browse the repository at this point in the history
… to NS/UIApplication+MIKMIDI. Also _greatly_ improved performance even in the default, uncached mode.
  • Loading branch information
Andrew Madsen committed May 7, 2015
1 parent 5dba0e0 commit 0db61bb
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 44 deletions.
17 changes: 16 additions & 1 deletion Framework/MIKMIDI Tests/MIKMIDIResponderChainTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand All @@ -72,11 +75,23 @@ - (void)testRegistration
- (void)testUncachedResponderSearchPerformance
{
NSApplication *app = [NSApplication sharedApplication];
app.shouldCacheMIKMIDISubresponders = NO;
[self measureBlock:^{
for (NSUInteger i=0; i<50000; i++) {
[app MIDIResponderWithIdentifier:@"Sub2Sub1"];
}
}];
}

- (void)testCachedResponderSearchPerformance
{
NSApplication *app = [NSApplication sharedApplication];
app.shouldCacheMIKMIDISubresponders = YES;
[self measureBlock:^{
for (NSUInteger i=0; i<50000; i++) {
[app MIDIResponderWithIdentifier:@"Sub2Sub1"];
}
}];
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,29 @@
<dict>
<key>classNames</key>
<dict>
<key>MIKMIDIResponderChainTests</key>
<dict>
<key>testCachedResponderSearchPerformance</key>
<dict>
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>0.05</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
<key>testUncachedResponderSearchPerformance</key>
<dict>
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>0.94</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
</dict>
<key>MIKMIDISequenceTests</key>
<dict>
<key>testMIDIFileReadPerformance</key>
Expand Down
51 changes: 41 additions & 10 deletions Source/NSUIApplication+MIKMIDI.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@

#if TARGET_OS_IPHONE

#import <UIKit/UIKit.h>
#define MIK_APPLICATION_CLASS UIApplication
#define MIK_WINDOW_CLASS UIWindow
#define MIK_VIEW_CLASS UIView
#import <UIKit/UIKit.h>
#define MIK_APPLICATION_CLASS UIApplication
#define MIK_WINDOW_CLASS UIWindow
#define MIK_VIEW_CLASS UIView

#else

#import <Cocoa/Cocoa.h>
#define MIK_APPLICATION_CLASS NSApplication
#define MIK_WINDOW_CLASS NSWindow
#define MIK_VIEW_CLASS NSView
#import <Cocoa/Cocoa.h>
#define MIK_APPLICATION_CLASS NSApplication
#define MIK_WINDOW_CLASS NSWindow
#define MIK_VIEW_CLASS NSView

#endif

Expand Down Expand Up @@ -64,6 +64,20 @@
*/
- (void)unregisterMIDIResponder:(id<MIKMIDIResponder>)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)
Expand All @@ -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.
*
Expand All @@ -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<MIKMIDIResponder>)MIDIResponderWithIdentifier:(NSString *)identifier;
Expand All @@ -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
113 changes: 80 additions & 33 deletions Source/NSUIApplication+MIKMIDI.m
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,22 @@ static BOOL MIKObjectRespondsToMIDICommand(id object, MIKMIDICommand *command)

@interface MIKMIDIResponderHierarchyManager : NSObject

// Public
- (void)refreshRespondersAndSubresponders;
- (id<MIKMIDIResponder>)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
Expand All @@ -70,28 +81,42 @@ - (instancetype)init
if (self) {
NSPointerFunctionsOptions options = [[self class] hashTableOptions];
_registeredMIKMIDIResponders = [[NSHashTable alloc] initWithOptions:options capacity:0];

_shouldCacheMIKMIDISubresponders = NO;
}
return self;
}

- (void)registerMIDIResponder:(id<MIKMIDIResponder>)responder;
{
[self.registeredMIKMIDIResponders addObject:responder];
[self refreshRespondersAndSubresponders];
}

- (void)unregisterMIDIResponder:(id<MIKMIDIResponder>)responder;
{
[self.registeredMIKMIDIResponders addObject:responder];
[self refreshRespondersAndSubresponders];
}

#pragma mark - Public

- (void)refreshRespondersAndSubresponders
{
self.subrespondersCache = nil;
}

- (id<MIKMIDIResponder>)MIDIResponderWithIdentifier:(NSString *)identifier
{
NSSet *registeredResponders = self.registeredMIKMIDIRespondersAndSubresponders;
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"MIDIIdentifier LIKE %@", identifier];
NSSet *results = [registeredResponders filteredSetUsingPredicate:predicate];
id<MIKMIDIResponder> result = [results anyObject];

id<MIKMIDIResponder> result = nil;
for (id<MIKMIDIResponder> responder in registeredResponders) {
if ([[responder MIDIIdentifier] isEqualToString:identifier]) {
result = responder;
break;
}
}

#if MIKMIDI_SEARCH_VIEW_HIERARCHY_FOR_RESPONDERS
if (!result) {
Expand Down Expand Up @@ -127,6 +152,10 @@ - (NSSet *)recursiveSubrespondersOfMIDIResponder:(id<MIKMIDIResponder>)responder

- (NSSet *)registeredMIKMIDIRespondersAndSubresponders
{
if (self.shouldCacheMIKMIDISubresponders && self.subrespondersCache != nil) {
return [self.subrespondersCache setRepresentation];
}

NSMutableSet *result = [NSMutableSet set];
for (id<MIKMIDIResponder> responder in self.registeredMIKMIDIResponders) {
[result unionSet:[self recursiveSubrespondersOfMIDIResponder:responder]];
Expand All @@ -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
Expand Down Expand Up @@ -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<MIKMIDIResponder>)responder; { [self.mikmidi_responderHierarchyManager registerMIDIResponder:responder]; }

- (void)registerMIDIResponder:(id<MIKMIDIResponder>)responder;
{
[[[self class] mik_MIDIResponderHierarchyManager] registerMIDIResponder:responder];
}

- (void)unregisterMIDIResponder:(id<MIKMIDIResponder>)responder;
{
[[[self class] mik_MIDIResponderHierarchyManager] unregisterMIDIResponder:responder];
}
- (void)unregisterMIDIResponder:(id<MIKMIDIResponder>)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;
Expand All @@ -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<MIKMIDIResponder> responder in registeredResponders) {
[responder handleMIDICommand:command];
Expand All @@ -240,13 +274,11 @@ - (void)handleMIDICommand:(MIKMIDICommand *)command;

- (id<MIKMIDIResponder>)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

Expand All @@ -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

0 comments on commit 0db61bb

Please sign in to comment.