webassembly: reuse JavaScript proxy references if possible, to improve identity/equality relationships#17758
Conversation
|
@WebReflection FYI. This improved object equality as we discussed. See the tests for what improvements to expect. Note that |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #17758 +/- ##
=======================================
Coverage 98.38% 98.38%
=======================================
Files 171 171
Lines 22257 22257
=======================================
Hits 21898 21898
Misses 359 359 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
| // js_obj cannot be undefined | ||
| function proxy_js_add_obj(js_obj) { | ||
| // See if there is an existing JsProxy reference, and use that if there is. | ||
| if (proxy_js_ref_map.has(js_obj)) { |
There was a problem hiding this comment.
I know it's an ugly workaround but AFAIK there are no optimizations for has immediately followed by get in the JS Map world (runtimes) so that when performance is meant to be "best" and/or on the critical path, JS devs tend to abuse undefined returned when no entry is found.
That is:
const known = proxy_js_ref_map.get(js_obj);
if (known) return known;We're still talking O(1) with JS Map lookup for the key but while O(1) + O(1) is still O(1) in the theoretical side of BigO notation, you can skip that "O(2)" by reaching directly the value, if available, via .get(key).
Not a blocker, of course, but years of perf tuning makes me always use that ugly fast-path whenever it's crucial.
edit for correctness and history sake ... the suggested improvements fails when:
- a
keyin a Map is meant to be falsy or evenundefined... for whatever reason; IIRC this would never be the case in here - if you prefer strict
known !== undefinedin thatifstatement, that's OK, the mentioned perf boost won't be affected
There was a problem hiding this comment.
Thanks for the review.
I was wondering if there was a more efficient way to do this, and I've now changed it to what you suggested.
Note that proxy_js_ref_map contains integers and could potentially return 0, so we do need the known !== undefined check.
There was a problem hiding this comment.
yes, that's the falsy value I was referring to ... if 0 could be returned, you definitively need that !== undefined check 👍
06ef5d4 to
d6351ee
Compare
|
Code size report: |
This option is needed for ports such as webassembly where objects are proxied and can be identical without being the same C pointer. Signed-off-by: Damien George <damien@micropython.org>
Signed-off-by: Damien George <damien@micropython.org>
This reduces memory use by reusing objects, and improves identity/equality relationships of JavaScript objects on the Python side. In 77bd8fe PyProxy's were reused when the same Python object was proxied across to JavaScript. This commit does the same thing but for JsProxy's going from JS to Python. If an existing JsProxy reference exists for the JS object about to be proxied across, then it's reused. This helps reduce the number of alive objects (memory use), and, more importantly, improves equality relationships of JavaScript objects on the Python side. Eg we now get, on the Python side: import js print(js.Object == js.Object) that prints True. Previously it was False. Note that this change does not make identity work with `is`, for example `js.Object is js.Object` is actually False. With more work that could be made True but for now we leave that as-is. The behaviour with this commit matches Pyodide semantics. Signed-off-by: Damien George <damien@micropython.org>
d6351ee to
ffa98cb
Compare
Commit ffa98cb improved equality for `JsProxy` objects so that, eg, `js.Object == js.Object` is true. As mentioned in micropython#17758, a further optimisation is to make identity work in that case, eg `js.Object is js.Object` should be true (on the Python side). This commit implements that, by keeping track of all `JsProxy` Python objects and reusing them where possible: where the underlying JS ref is equal, ie they point to the same JS object. That reduces memory churn and gives better identity behaviour of JS objects proxied over to Python. As part of this, a bug is fixed where JS objects can be freed while there's still a `JsProxy` referring to that JS object. A test is added for that exact scenario, and the test now passes. Signed-off-by: Damien George <damien@micropython.org>
Commit ffa98cb improved equality for `JsProxy` objects so that, eg, `js.Object == js.Object` is true. As mentioned in micropython#17758, a further optimisation is to make identity work in that case, eg `js.Object is js.Object` should be true (on the Python side). This commit implements that, by keeping track of all `JsProxy` Python objects and reusing them where possible: where the underlying JS ref is equal, ie they point to the same JS object. That reduces memory churn and gives better identity behaviour of JS objects proxied over to Python. As part of this, a bug is fixed where JS objects can be freed while there's still a `JsProxy` referring to that JS object. A test is added for that exact scenario, and the test now passes. Signed-off-by: Damien George <damien@micropython.org>
Summary
In 77bd8fe PyProxy's were reused when the same Python object was proxied across to JavaScript.
This PR does the same thing but for JsProxy's going from JS to Python. If an existing JsProxy reference exists for the JS object about to be proxied across, then it's reused. This helps keep down the number of alive objects, and, more importantly, improves equality relationships. Eg we now get, on the Python side:
that prints
True. Previously it wasFalse.Testing
Tests have been added to CI. They pass locally.
Trade-offs and Alternatives
This does not make identity work with
is, egjs.Object is js.Objectis actuallyFalse. With more work that could be madeTruebut for now we leave that as-is. This matches Pyodide semantics (only == is True, while is is False).This needs to maintain a separate
Mapon the JS side, of all the proxied JS objects. But that's (a) necessary and (b) worth it because of the reduced churn of JS objects when proxying them across to Python.