diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.cpp b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.cpp index 0f26c765..70a5d9d7 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.cpp +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.cpp @@ -38,11 +38,97 @@ namespace sofapython3 using sofa::core::objectmodel::Event; using sofa::core::objectmodel::BaseObject; +Controller_Trampoline::Controller_Trampoline() = default; + +Controller_Trampoline::~Controller_Trampoline() +{ + // Clean up Python objects while holding the GIL + if (m_cacheInitialized) + { + PythonEnvironment::gil acquire {"~Controller_Trampoline"}; + m_methodCache.clear(); + m_onEventMethod = py::object(); + m_pySelf = py::object(); + } +} + +void Controller_Trampoline::initializePythonCache() +{ + if (m_cacheInitialized) + return; + + // Must be called with GIL held + m_pySelf = py::cast(this); + + // Cache the fallback "onEvent" method if it exists + if (py::hasattr(m_pySelf, "onEvent")) + { + py::object fct = m_pySelf.attr("onEvent"); + if (PyCallable_Check(fct.ptr())) + { + m_hasOnEvent = true; + m_onEventMethod = fct; + } + } + + m_cacheInitialized = true; +} + +py::object Controller_Trampoline::getCachedMethod(const std::string& methodName) +{ + // Must be called with GIL held and cache initialized + + // Check if we've already looked up this method + auto it = m_methodCache.find(methodName); + if (it != m_methodCache.end()) + { + return it->second; + } + + // First time looking up this method - check if it exists + py::object method; + if (py::hasattr(m_pySelf, methodName.c_str())) + { + py::object fct = m_pySelf.attr(methodName.c_str()); + if (PyCallable_Check(fct.ptr())) + { + method = fct; + } + } + + // Cache the result (even if empty, to avoid repeated hasattr checks) + m_methodCache[methodName] = method; + return method; +} + +bool Controller_Trampoline::callCachedMethod(const py::object& method, Event* event) +{ + // Must be called with GIL held + if (f_printLog.getValue()) + { + std::string eventStr = py::str(PythonFactory::toPython(event)); + msg_info() << "on" << event->getClassName() << " " << eventStr; + } + + py::object result = method(PythonFactory::toPython(event)); + if (result.is_none()) + return false; + + return py::cast(result); +} + std::string Controller_Trampoline::getClassName() const { PythonEnvironment::gil acquire {"getClassName"}; - // Get the actual class name from python. - return py::str(py::cast(this).get_type().attr("__name__")); + + // Use cached self if available, otherwise cast + if (m_cacheInitialized && m_pySelf) + { + return py::str(py::type::of(m_pySelf).attr("__name__")); + } + + // Fallback for when cache isn't initialized yet + return py::str(py::type::of(py::cast(this)).attr("__name__")); } void Controller_Trampoline::draw(const sofa::core::visual::VisualParams* params) @@ -55,6 +141,8 @@ void Controller_Trampoline::draw(const sofa::core::visual::VisualParams* params) void Controller_Trampoline::init() { PythonEnvironment::executePython(this, [this](){ + // Initialize the Python object cache on first init + initializePythonCache(); PYBIND11_OVERLOAD(void, Controller, init, ); }); } @@ -92,25 +180,36 @@ bool Controller_Trampoline::callScriptMethod( void Controller_Trampoline::handleEvent(Event* event) { - PythonEnvironment::executePython(this, [this,event](){ - py::object self = py::cast(this); - std::string name = std::string("on")+event->getClassName(); - /// Is there a method with this name in the class ? - if( py::hasattr(self, name.c_str()) ) + PythonEnvironment::executePython(this, [this, event](){ + // Ensure cache is initialized (in case init() wasn't called or + // handleEvent is called before init) + if (!m_cacheInitialized) + { + initializePythonCache(); + } + + // Build the event-specific method name (e.g., "onAnimateBeginEvent") + std::string methodName = std::string("on") + event->getClassName(); + + // Try to get the cached method for this specific event type + py::object method = getCachedMethod(methodName); + + if (method) { - py::object fct = self.attr(name.c_str()); - if (PyCallable_Check(fct.ptr())) { - bool isHandled = callScriptMethod(self, event, name); - if(isHandled) - event->setHandled(); - return; - } + // Found a specific handler for this event type + bool isHandled = callCachedMethod(method, event); + if (isHandled) + event->setHandled(); + return; } - /// Is the fallback method available. - bool isHandled = callScriptMethod(self, event, "onEvent"); - if(isHandled) - event->setHandled(); + // Fall back to the generic "onEvent" method if available + if (m_hasOnEvent) + { + bool isHandled = callCachedMethod(m_onEventMethod, event); + if (isHandled) + event->setHandled(); + } }); } diff --git a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.h b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.h index d1bd9166..e61add15 100644 --- a/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.h +++ b/bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.h @@ -22,6 +22,8 @@ #include #include +#include +#include namespace sofapython3 { @@ -41,6 +43,9 @@ class Controller_Trampoline : public Controller public: SOFA_CLASS(Controller_Trampoline, Controller); + Controller_Trampoline(); + ~Controller_Trampoline() override; + void init() override; void reinit() override; void draw(const sofa::core::visual::VisualParams* params) override; @@ -50,8 +55,34 @@ class Controller_Trampoline : public Controller std::string getClassName() const override; private: + /// Initializes the Python object cache (m_pySelf and method cache) + void initializePythonCache(); + + /// Returns a cached method if it exists, or an empty object if not + pybind11::object getCachedMethod(const std::string& methodName); + + /// Calls a cached Python method with the given event + bool callCachedMethod(const pybind11::object& method, sofa::core::objectmodel::Event* event); + + /// Legacy method for uncached calls (fallback) bool callScriptMethod(const pybind11::object& self, sofa::core::objectmodel::Event* event, const std::string& methodName); + + /// Cached Python self reference (avoids repeated py::cast(this)) + pybind11::object m_pySelf; + + /// Cache of Python method objects, keyed by method name + /// Stores the method object if it exists, or an empty object if checked and not found + std::unordered_map m_methodCache; + + /// Flag indicating whether the fallback "onEvent" method exists + bool m_hasOnEvent = false; + + /// Cached reference to the fallback "onEvent" method + pybind11::object m_onEventMethod; + + /// Flag indicating whether the cache has been initialized + bool m_cacheInitialized = false; }; void moduleAddController(pybind11::module &m); diff --git a/examples/benchmarks/emptyMultipleControllers.py b/examples/benchmarks/emptyMultipleControllers.py new file mode 100644 index 00000000..bcdf7a89 --- /dev/null +++ b/examples/benchmarks/emptyMultipleControllers.py @@ -0,0 +1,45 @@ +import Sofa + +g_nb_controllers = 10 +g_nb_steps = 10000 + +class EmptyController(Sofa.Core.Controller): + + def __init__(self, *args, **kwargs): + Sofa.Core.Controller.__init__(self, *args, **kwargs) + + # Default Events ********************************************* + def onAnimateBeginEvent(self, event): # called at each begin of animation step + # print(f"{self.name.value} : onAnimateBeginEvent") + pass + +def createScene(root): + root.dt = 0.01 + root.bbox = [[-1, -1, -1], [1, 1, 1]] + root.addObject('DefaultVisualManagerLoop') + root.addObject('DefaultAnimationLoop') + + + for i in range(g_nb_controllers): + root.addObject(EmptyController(name=f"MyEmptyController{i}")) + + +def main(): + root = Sofa.Core.Node("root") + createScene(root) + Sofa.Simulation.initRoot(root) + + # Import the time library + import time + start = time.time() + for iteration in range(g_nb_steps): + Sofa.Simulation.animate(root, root.dt.value) + end = time.time() + + print(f"Scene with {g_nb_controllers} controllers and {g_nb_steps} steps took {end - start} seconds.") + + print("End of simulation.") + + +if __name__ == '__main__': + main()