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

Added lazy-loading support to ListenerManager. #307

Merged
merged 3 commits into from Mar 7, 2020

Conversation

jordanbriere
Copy link
Contributor

This PR adds lazy-loading support to ListenerManager so that hooks are only registered when a callback is first registered (to fix conflict with SourceMod, see #306).

The implementation is quite simple, when the first callback is registered, ListenerManager.initialize is called and ListenerManager.finalize is called when the last one is unregistered allowing us to add/remove our hooks only when requested.

Made OnEntityOutput lazy-loaded to fix loading conflict with SourceMod (see #306).
Copy link
Member

@Ayuto Ayuto left a comment

Choose a reason for hiding this comment

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

Wouldn't it be better to implement this via overridable virtual functions in CListenerManager?

return

# Unregister the hook on fire_output
fire_output.remove_pre_hook(_pre_fire_output)
Copy link
Member

Choose a reason for hiding this comment

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

This actually doesn't remove the hook, but just the hook handler.

@jordanbriere
Copy link
Contributor Author

Wouldn't it be better to implement this via overridable virtual functions in CListenerManager?

Not really, as it would require the insertion of an extra wrapper class in the hierarchy, which would unnecessary adds an extra layer to the MRO.

Unless we implement lazy-loading for listeners on the c++ side (e.g. the player commands listeners), we don't really need them to be virtual and simple static methods that keep the back reference is sufficient enough.

Perhaps that is something we could consider, but for now, my current goal was to fix the conflict that seemed to be getting popular and that, with minimal efforts. 😄

@@ -281,6 +282,36 @@ class OnClientSettingsChanged(ListenerManagerDecorator):
manager = on_client_settings_changed_listener_manager


class OnEntityOutpuListenerManager(ListenerManager):
Copy link
Contributor

Choose a reason for hiding this comment

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

typo?

Fixed a typo (thank @CookStar).
Added missing entry to __all__ declaration.
@Ayuto
Copy link
Member

Ayuto commented Mar 4, 2020

Not really, as it would require the insertion of an extra wrapper class in the hierarchy, which would unnecessary adds an extra layer to the MRO.

I'm fine with the extra layer, because it doesn't has any downsides. Moreover, I would prefer a well-thought solution instead of a quick and dirty one.

@jordanbriere
Copy link
Contributor Author

I'm fine with the extra layer, because it doesn't has any downsides.

The main downside would be that every manager would now become wrappers which means that any attribute retrieval from them would become slightly slower since the wrapper class would need to be looked up first before reaching the base class (for instance, every notify() calls). Which in itself, isn't critical I guess, but have to be considered if we don't intend to make use of it.

To me it comes to: do we intend to use that for any listeners managed on the c++ side or not. If we do, then yes, that layer becomes a necessity. But if we don't, it becomes unnecessary computation (which can adds up, considering some of them are noisy and notified multiple times per frame).

Moreover, I would prefer a well-thought solution instead of a quick and dirty one.

That's a concept; lazy-loading lazily implemented. 😜

@Ayuto
Copy link
Member

Ayuto commented Mar 5, 2020

Just tested the performance of both implementations with one million notify() calls. Unexpectedly, your Python implementation was slightly slower (but the difference is neglegable).

# Python implementation
No initialize/finalize:       0.6140353679656982
With initialize/finalize:     0.6300358772277832

# Lovely C++ implementation :P
No initialize/finalize:       0.6060347557067871
With initialize/finalize:     0.6070348281860352

The Python implementation has been tested with the following code:

import time
from listeners import ListenerManager

ITERATIONS = 1_000_000

manager = ListenerManager()
now = time.time()

for x in range(ITERATIONS):
    manager.notify()
    
print('No initialize/finalize:      ', time.time() - now)


def init_fin(self):
    pass
    
manager.initialize = init_fin
manager.finalize = init_fin

now = time.time()

for x in range(ITERATIONS):
    manager.notify()
    
print('With initialize/finalize:    ', time.time() - now)

The C++ implementation with the following code:

import time
from listeners import ListenerManager

ITERATIONS = 1_000_000

manager = ListenerManager()
now = time.time()

for x in range(ITERATIONS):
    manager.notify()
    
print('No initialize/finalize:      ', time.time() - now)


class MyManager(ListenerManager):
    def initialize(self):
        pass
        
    def finalize(self):
        pass

manager = MyManager()
now = time.time()

for x in range(ITERATIONS):
    manager.notify()
    
print('With initialize/finalize:    ', time.time() - now)

@jordanbriere
Copy link
Contributor Author

Where is your code? The simple fact of inserting a dummy wrapper so that we can access the overrides:

class CListenerManagerWrapper: public CListenerManager, public wrapper<CListenerManager>
{
};

//-----------------------------------------------------------------------------
// Exports CListenerManager.
//-----------------------------------------------------------------------------
void export_listener_managers(scope _listeners) 
{
	class_<CListenerManagerWrapper, boost::noncopyable>("ListenerManager")

And running the following code:

import time
from listeners import ListenerManager

ITERATIONS = 1_000_000

manager = ListenerManager()
now = time.time()

for x in range(ITERATIONS):
    manager.notify()
    
print('No initialize/finalize:      ', time.time() - now)

I get the following time: 0.8257834911346436
And the following without the wrapper: 0.4916863441467285

@Ayuto
Copy link
Member

Ayuto commented Mar 6, 2020

You don't need the CListenerManagerWrapper class. I will push my code later today.

@jordanbriere
Copy link
Contributor Author

Waiting for it. Really curious to see how you manage to dispatch the calls back to the same Python object without using a static dispatcher like I did or a wrapper class. 🤔

Besides, I just noticed:

def init_fin(self):
    pass
    
manager.initialize = init_fin
manager.finalize = init_fin

There is no reason to do that. By doing so, you bind the methods to the instance, meaning its __dict__ no longer evaluates to False which consequently means it will be looked up first whenever notify is retrieved. Just use the same MyManager subclass, so that behind the scenes behaviours are consistent and therefore results are accurate.

@Ayuto
Copy link
Member

Ayuto commented Mar 6, 2020

Besides, I just noticed: [...]

Hmm, I somehow thought you were binding the fire_output initializer/finalizer like that, but I just had a second look and it's not the case. Not sure why I thought so at first.

Here is the C++ implementation: 12a7da7

@jordanbriere
Copy link
Contributor Author

Hmm, I somehow thought you were binding the fire_output initializer/finalizer like that, but I just had a second look and it's not the case. Not sure why I thought so at first.

I guess because the first changes listed are the dynamic definition of the method itself either to a Function if we have a signature for it for that game, or to NotImplemented so it can be used as a guard variable for the listener which overall, felt better to me than an hasattr/try/except. But yeah, if you thought the initializer/finalizer were dynamically bound rather than inherited, I somehow understand better where that "dirty" came from. There is still a limit to minimal efforts I'm not yet ready to cross. :trollface:

Here is the C++ implementation: 12a7da7

Right, so you ARE using a wrapper class, just not on a subclass. Which actually, is interesting. I always was under the impression that the wrapper itself was the hungry one, but seems like it is the subclass after all. In average, it seems slightly slower to resolve names, but the difference is more than acceptable.

Ran the following code:

import time
from listeners import ListenerManager

ITERATIONS = 1_000_000

manager = ListenerManager()

for i in range(100):
    now = time.time()
    for x in range(ITERATIONS):
        manager.notify()
    
    print(time.time() - now)

And here are the results:

With wrapper
[SP] Loading plugin 'testing'...
0.655667781829834
0.586766242980957
0.5150816440582275
0.5137133598327637
0.5158054828643799
0.513249397277832
0.508051872253418
0.5090897083282471
0.5140836238861084
0.5101439952850342
0.5142228603363037
0.5134866237640381
0.5129189491271973
0.5115845203399658
0.5145909786224365
0.5120711326599121
0.5079700946807861
0.5131659507751465
0.5139086246490479
0.5132999420166016
0.5121111869812012
0.512115478515625
0.513481855392456
0.5140388011932373
0.5145237445831299
0.515493631362915
0.514362096786499
0.5126674175262451
0.5139687061309814
0.5145001411437988
0.5116047859191895
0.5150952339172363
0.515038013458252
0.5080842971801758
0.5448369979858398
0.5121603012084961
0.509852409362793
0.5131275653839111
0.6609609127044678
0.5140690803527832
0.5249824523925781
0.5129411220550537
0.5124454498291016
0.5130941867828369
0.5134785175323486
0.513209342956543
0.5138945579528809
0.6222412586212158
0.5079810619354248
0.5090060234069824
0.5070717334747314
0.50978684425354
0.5133581161499023
0.5126187801361084
0.5124685764312744
0.5143506526947021
0.5148868560791016
0.5163595676422119
0.514014482498169
0.5139954090118408
0.512054443359375
0.5121722221374512
0.5168764591217041
0.5073118209838867
0.5148131847381592
0.5147175788879395
0.679283857345581
0.5142269134521484
0.512711763381958
0.5119905471801758
0.5083565711975098
0.5077273845672607
0.5104637145996094
0.5138847827911377
0.5127148628234863
0.5129518508911133
0.5130817890167236
0.5069825649261475
0.5124702453613281
0.5125384330749512
0.5090785026550293
0.5308339595794678
0.5128374099731445
0.5086808204650879
0.5125095844268799
0.5113193988800049
0.5130314826965332
0.5068566799163818
0.5081958770751953
0.5078725814819336
0.5099940299987793
0.5070147514343262
0.5100550651550293
0.5149924755096436
0.5135345458984375
0.5086348056793213
0.5098476409912109
0.512930154800415
0.5144064426422119
0.5142066478729248
[SP] Successfully loaded plugin 'testing'.
Without wrapper
[SP] Loading plugin 'testing'...
0.4976949691772461
0.5126290321350098
0.5056502819061279
0.49695324897766113
0.49720144271850586
0.5118749141693115
0.5169034004211426
0.5109636783599854
0.5123939514160156
0.513786792755127
0.5130670070648193
0.49490952491760254
0.7126262187957764
0.5037508010864258
0.5010910034179688
0.5120658874511719
0.5137152671813965
0.5130927562713623
0.5110857486724854
0.715968132019043
0.4960341453552246
0.5152170658111572
0.5136573314666748
0.5111367702484131
0.5100414752960205
0.5091025829315186
0.5051426887512207
0.5129985809326172
0.5129411220550537
0.4969351291656494
0.5113139152526855
0.502152681350708
0.5055034160614014
0.5136196613311768
0.4966452121734619
0.49593448638916016
0.5131089687347412
0.5091228485107422
0.49521827697753906
0.49752140045166016
0.5156352519989014
0.5123670101165771
0.5110161304473877
0.5130181312561035
0.5096876621246338
0.4998326301574707
0.5006306171417236
0.5104784965515137
0.506141185760498
0.5043277740478516
0.5100080966949463
0.5085394382476807
0.509760856628418
0.5116908550262451
0.49515366554260254
0.7160160541534424
0.5051171779632568
0.5091907978057861
0.5095109939575195
0.5121867656707764
0.509772539138794
0.4982798099517822
0.49507999420166016
0.5074160099029541
0.5066852569580078
0.5089287757873535
0.5677337646484375
0.5102193355560303
0.5080184936523438
0.508056640625
0.5095362663269043
0.5104806423187256
0.5114114284515381
0.5093286037445068
0.50844407081604
0.5120689868927002
0.5139079093933105
0.7072992324829102
0.49508094787597656
0.5129373073577881
0.5075933933258057
0.5081169605255127
0.5101861953735352
0.510143518447876
0.6657710075378418
0.5080690383911133
0.49898695945739746
0.51206374168396
0.5129795074462891
0.5161032676696777
0.5140421390533447
0.7108690738677979
0.4990527629852295
0.5131006240844727
0.5120201110839844
0.5110528469085693
0.5111699104309082
0.5152299404144287
0.507803201675415
0.4958488941192627
[SP] Successfully loaded plugin 'testing'.

Not much of a difference, but without seems to bounce a tiny bit lower from time to time regardless. Which anyways, makes no difference. I went ahead and replaced the static dispatchers here directly. Thanks, that was constructive!

@Ayuto
Copy link
Member

Ayuto commented Mar 7, 2020

I guess the wrapper isn't the hungry one, because it's just a subclass on the C++ side and doesn't really add stuff to the MRO on the Python side.

I did not say that you don't need a wrapper class. I just meant that CListenerManagerWrapper is not required. And that statement was related to the additional subclass :P
I should have elaborated my post a little bit more :D

@Ayuto Ayuto self-requested a review March 7, 2020 06:26
@jordanbriere
Copy link
Contributor Author

I guess the wrapper isn't the hungry one, because it's just a subclass on the C++ side and doesn't really add stuff to the MRO on the Python side.

Look at the example I posted above, there is no extra Python class involved, just an extra C++ base and there is no bases<> exported whatsoever. Even all the methods are still pointer-to-member of the original class. Yet, it increase the resolution by 60%.

On the other hand, the wrapper has to store the Python instance so that it can use it when get_override is invoked. Which is rather confusing; that a dummy subclass that does nothing affect the resolution more than one that does. 😕

@jordanbriere jordanbriere merged commit 428f49f into master Mar 7, 2020
@jordanbriere jordanbriere deleted the listeners_lazyload branch March 7, 2020 07:16
@jordanbriere
Copy link
Contributor Author

Alright, so I made some investigating because it wasn't making any sense to me and here are my results.

Here is the Python code used to time everything:

from time import time
from test import Bar

bar = Bar()

now = time()
for i in range(10000000):
    bar.foo
print('R:', time() - now)

now = time()
for i in range(10000000):
    bar.foo()
print('C:', time() - now)

Fast:

class Foo
{
public:
	void foo() {}
};

class Bar: public Foo
{
};

DECLARE_SP_MODULE(test)
{
	// Using derived class namespace to bind base method
	class_<Bar, boost::noncopyable>("Bar").def("foo", &Bar::foo);
}

// R: 0.7031171321868896
// C: 1.5408985614776611

Fast:

class Foo
{
public:
	void foo() {}
};

class Bar: public Foo
{
};

DECLARE_SP_MODULE(test)
{
	// Using base class namespace to bind base method
	class_<Bar, boost::noncopyable>("Bar").def("foo", &Foo::foo);
}

// R: 0.749993085861206
// C: 1.5877742767333984

Slow:

// Declaring base as wrapper
class Foo: public wrapper<Foo>
{
public:
	void foo() {}
};

class Bar: public Foo
{
};

DECLARE_SP_MODULE(test)
{
	class_<Bar, boost::noncopyable>("Bar").def("foo", &Bar::foo);
}

// R: 0.7160553932189941
// C: 3.84773325920105

Slow:

class Foo
{
public:
	void foo() {}
};

// Declaring base as wrapper, on derived definition
class Bar: public Foo, public wrapper<Foo>
{
};

DECLARE_SP_MODULE(test)
{
	class_<Bar, boost::noncopyable>("Bar").def("foo", &Bar::foo);
}

// R: 0.7290487289428711
// C: 3.872662305831909

Fast:

// Declaring derived as wrapper, on base definition
class Bar;
class Foo: public wrapper<Bar>
{
public:
	void foo() {}
};

class Bar: public Foo
{
};

DECLARE_SP_MODULE(test)
{
	class_<Bar, boost::noncopyable>("Bar").def("foo", &Bar::foo);
}

// R: 0.709097146987915
// C: 1.5338754653930664

Fast:

class Foo: public wrapper<Foo>
{
public:
	void foo() {}
};

class Bar: public Foo
{
public:
	// Upcasting ourselves, then forwarding the call
	void foo() { ((Foo *)this)->foo(); }
};

DECLARE_SP_MODULE(test)
{
	class_<Bar, boost::noncopyable>("Bar").def("foo", &Bar::foo);
}

// R: 0.7091226577758789
// C: 1.5947341918945312

Fast:

class Foo: public wrapper<Foo>
{
};

class Bar: public Foo
{
public:
	// Defining the method directly in the derived class of a wrapper
	void foo() {}
};

DECLARE_SP_MODULE(test)
{
	class_<Bar, boost::noncopyable>("Bar").def("foo", &Bar::foo);
}

// R: 0.6931755542755127
// C: 1.5618219375610352

Fast:

class Foo: public wrapper<Foo>
{
public:
	void foo() {}
};

class Bar: public Foo
{
public:
	void foo() {}
};

DECLARE_SP_MODULE(test)
{
	// Declaring an overload from derived using derived namespace of a wrapper class
	class_<Bar, boost::noncopyable>("Bar").def("foo", &Bar::foo);
}

// R: 0.7798867225646973
// C: 1.5857844352722168

Slow:

class Foo: public wrapper<Foo>
{
public:
	void foo() {}
};

class Bar: public Foo
{
public:
	void foo() {}
};

DECLARE_SP_MODULE(test)
{
	// Declaring an overload from derived using base wrapper class namespace
	class_<Bar, boost::noncopyable>("Bar").def("foo", &Foo::foo);
}

// R: 0.7021214962005615
// C: 3.7878899574279785

Conclusion:

It is not the resolution that is affected, but the calls themselves. At least, any call from an object of a wrapper class that have to upcast the this pointer appears to be ~60% slower. 🤔

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

3 participants