Caching helpers.expects_arguments to cut signature introspection on element-heavy pages #6098
Replies: 2 comments
-
|
Thanks for the careful write-up, @maybites! The diagnosis is spot on. The path you describe is exactly right: every A couple of things to consider before we settle on a fix, though.
In a short script that's invisible, but NiceGUI is a long-running server with clients connecting/disconnecting and pages rebuilding, so this grows without bound. The codebase already works hard to avoid exactly this (the weakref in There's already a precedent for the better approach. The So my preference order would be:
If you're up for a PR, option 1 would be very welcome — happy to review. Thanks again for profiling this and raising it! |
Beta Was this translation helpful? Give feedback.
-
|
The PR has landed: #6108 - looking forward to your feedback |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Upon a claude session to improve the render speed for my node graph based on nicegui it proposed the following:
Observation. When building a page with many elements, a large share of Python-side render time is spent in
inspect.signature(), called viahelpers.expects_arguments(). The path is: every.props()/.classes()/.style()mutation writes to the element's observable dict, which fires its change handler; that handler goes throughevents.handle_event(), which callshelpers.expects_arguments(handler)to decide whether to invoke the handler with or without arguments.expects_argumentsrunsinspect.signature(handler)on every call. Since each element typically carries several prop/style/class mutations during construction, this fires a very large number of times on a dense page — and the handlers themselves are a small, fixed set of functions whose signatures never change, so the same booleans are recomputed thousands of times.inspect.signature()is comparatively expensive (it walks__code__, builds Parameter objects, resolves kinds/defaults), so this dominates a profile of element construction even though the answer is effectively constant.Fix.
expects_arguments(func)is a pure function offunc, and handler identities are stable for the process lifetime, so memoizing on the function object is safe. We wrap it withfunctools.lru_cache(maxsize=None)at startup: the first call per distinct handler runs the realinspect.signature(), every subsequent call is a dict lookup. The cache fills near-instantly (the handler set is tiny) and turns nearly all the introspection calls into hash lookups. In a render-heavy scenario this gave a measurable end-to-end speedup with no behavioural change.What would make this easier upstream. Adding
@functools.cachedirectly tohelpers.expects_argumentswould make this a one-line, framework-wide win — it's a pure, hot, low-cardinality lookup, exactly the shapefunctools.cacheis for. More broadly,handle_eventcould resolve the "expects arguments" decision once per handler rather than per call (e.g. cached at the point a handler is registered), since it cannot change between invocations. Either approach removes the per-mutationinspect.signature()cost without callers needing a startup monkeypatch.If this sounds like a reasonable feature, I am happy to (let claude) do a PR
Beta Was this translation helpful? Give feedback.
All reactions