Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for having multiple interface state delegates. #979

Merged
merged 6 commits into from
Jun 27, 2018

Conversation

garrettmoon
Copy link
Member

Hopefully in a performant way.

* The defualt state, ASInterfaceStateNone, means that the element is not predicted to be onscreen soon and
* preloading should not be performed. Swift: use [] for the default behavior.
*/
typedef NS_OPTIONS(NSUInteger, ASInterfaceState)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just moved, no changes.

ASInterfaceStateInHierarchy = ASInterfaceStateMeasureLayout | ASInterfaceStatePreload | ASInterfaceStateDisplay | ASInterfaceStateVisible,
};

@protocol ASInterfaceStateDelegate <NSObject>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only change here is that these are now all optional.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also added hierarchyDisplayDidFinish

@ghost
Copy link

ghost commented Jun 21, 2018

🚫 CI failed with log

_interfaceDidEnterPreloadDelegates = [NSHashTable weakObjectsHashTable];
_interfaceDidExitPreloadDelegates = [NSHashTable weakObjectsHashTable];
_interfaceNodeDidLayoutDelegates = [NSHashTable weakObjectsHashTable];
_interfaceNodeDidLoadDelegates = [NSHashTable weakObjectsHashTable];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Rather than separate tables, I think it's fine to use class_respondsToSelector before each call. That call actually runs through the same message-send machinery as a normal call, plus it warms the impcache. It's plenty fast.
  • Let's use -[NSHashTable initWithOptions:NSHashTableWeakMemory | NSHashTableObjectPointerPersonality capacity:0] (0 means default.)

That'll let you remove this intermediary object and it won't be slower by any amount that matters.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought, using -respondsToSelector: is only a tiny bit slower and allows for proxying, so that's probably the way to go. Still fast.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should have to create all these up front...since we would only have to use the respondsToSelector: once per class (not even once per delegate), we should get a significant win by relying on just one object tracking the delegates, and we could use a simple bitfield / NS_OPTIONS to cache the ASInterfaceStateDelegateMethodsImplemented (similar to the flags in ASCollection that cache what the delegate / datasource implement, but just an NS_OPTIONS).

Copy link
Member

@Adlai-Holler Adlai-Holler Jun 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In recent versions of Objective-C, respondsToSelector: is truly very fast. It amortizes to 1 additional objc_msgSend, 3 static function invocations, and one inline-assembly routine. It also has the added benefit of checking-from and storing-to the ObjC impcache, so the subsequent message-send is optimized.

Writing our own bitfield-by-class would amount to us reimplementing that portion of the impcache, without the benefit of direct access to the class method table. And even accessing our table – say it were in a singleton registry object – would require retaining-releasing it unless we coded around that, which would easily subsume the entire rest of the lookup. The added boilerplate is also nontrivial.

So there is a win available here, but I believe it's small. Let's avoid these calls only if we can do it in a simple and tremendously fast & scalable way. If we can't, then we shouldn't worry about it – I believe we can absolutely achieve far superior optimizations by using resources for other parts of the framework (such as transfer-collections or more judicious applications of __unsafe_unretained).

Implementations:

@@ -543,8 +543,8 @@ - (void)_didLoad
for (ASDisplayNodeDidLoadBlock block in onDidLoadBlocks) {
block(self);
}

[_interfaceStateDelegate nodeDidLoad];

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: No trailing white spaces.

@@ -47,6 +47,11 @@ - (instancetype)init
return self;
}

- (void)dealloc
{
[_weakNode removeInterfaceStateDelegate:self];
Copy link
Member

@nguyenhuy nguyenhuy Jun 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-removeInterfaceStateDelegate: needs to be called on main. and objects are de-allocated off main by default (code here) But since node controller is an associated object of display node, if the node is de-allocated off main, the node controller will be as well. And since NSHashTable is not thread-safe, we have to dispatch to main here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@garrettmoon I still think that we should dispatch to main here, no?

// Not a fan of lazy loading, but this method won't get called very often and avoiding
// the overhead of creating this is probably worth it.
if (_interfaceStateDelegateManager == nil) {
[[ASDisplayNodeInterfaceDelegateManager alloc] init];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing an assign to _interfaceStateDelegateManager?

@@ -292,6 +262,24 @@ AS_EXTERN NSInteger const ASDefaultDrawingPriority;
*/
@property (readonly) ASInterfaceState interfaceState;

/**
* @abstract Adds a delegate to recieve notifications on interfaceState changes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Typo: "recieve"

- (void)addInterfaceStateDelegate:(id <ASInterfaceStateDelegate>)interfaceStateDelegate;

/**
* @abstract Removes a delegate from recieving notifications on interfaceState changes.
Copy link
Contributor

@maicki maicki Jun 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Typo: "recieving"

@appleguy
Copy link
Member

This could be pretty useful! Could you share some background on the use case? I've found ASNodeController quite valuable in the cases I've come across, but maybe it doesn't work for the case you have.

I think we can design an ideal solution if we have the same / general use case in mind to know where we need to scale. It seems maybe more generalized than needed to track an arbitrary number of delegates.

@garrettmoon
Copy link
Member Author

@appleguy We have two use cases, both related to tracking performance. It'd be ideal if there were separate managers for each of these which could monitor the states.

@ghost
Copy link

ghost commented Jun 27, 2018

🚫 CI failed with log

@@ -433,6 +433,9 @@ - (void)dealloc

// TODO: Remove this? If supernode isn't already nil, this method isn't dealloc-safe anyway.
[self _setSupernode:nil];

ASPerformMainThreadDeallocation(&_interfaceStateDelegates);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is necessary. If the node is gone, _interfaceStateDelegates can be safely deallocated as well, especially because it only stores weak references.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, sorry, I thought the comment about the dealloc was here, now I get what you mean, thank you :)

@@ -47,6 +47,11 @@ - (instancetype)init
return self;
}

- (void)dealloc
{
[_weakNode removeInterfaceStateDelegate:self];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@garrettmoon I still think that we should dispatch to main here, no?

@TextureGroup TextureGroup deleted a comment Jun 27, 2018
@garrettmoon garrettmoon merged commit a4f78ad into master Jun 27, 2018
Copy link
Contributor

@maicki maicki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me.

mikezucc pushed a commit to mikezucc/Texture that referenced this pull request Oct 2, 2018
…roup#979)

* Adds support for having multiple interface state delegates.

Hopefully in a performant way.

* Switch to respondsToSelector for int del instead of separate object

* Add CHANGELOG

* Make ASDisplayNode+InterfaceState.h public

* Huy's comments

* Don't even bother removing since it's a weak hash table.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants