Skip to content

webassembly: reuse JavaScript proxy references if possible, to improve identity/equality relationships#17758

Merged
dpgeorge merged 3 commits into
micropython:masterfrom
dpgeorge:webassembly-improve-identity-v2
Jul 31, 2025
Merged

webassembly: reuse JavaScript proxy references if possible, to improve identity/equality relationships#17758
dpgeorge merged 3 commits into
micropython:masterfrom
dpgeorge:webassembly-improve-identity-v2

Conversation

@dpgeorge

Copy link
Copy Markdown
Member

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:

import js

print(js.Object == js.Object)

that prints True. Previously it was False.

Testing

Tests have been added to CI. They pass locally.

Trade-offs and Alternatives

This does not make identity work with is, eg js.Object is js.Object is actually False. With more work that could be made True but for now we leave that as-is. This matches Pyodide semantics (only == is True, while is is False).

This needs to maintain a separate Map on 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.

@dpgeorge

Copy link
Copy Markdown
Member Author

@WebReflection FYI. This improved object equality as we discussed. See the tests for what improvements to expect. Note that is (on the Python side) is still False, per Pyodide.

@codecov

codecov Bot commented Jul 24, 2025

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.38%. Comparing base (fdbd232) to head (ffa98cb).
⚠️ Report is 3 commits behind head on master.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread ports/webassembly/proxy_js.js Outdated
// 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)) {

@WebReflection WebReflection Jul 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 key in a Map is meant to be falsy or even undefined ... for whatever reason; IIRC this would never be the case in here
  • if you prefer strict known !== undefined in that if statement, that's OK, the mentioned perf boost won't be affected

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

yes, that's the falsy value I was referring to ... if 0 could be returned, you definitively need that !== undefined check 👍

@dpgeorge dpgeorge force-pushed the webassembly-improve-identity-v2 branch from 06ef5d4 to d6351ee Compare July 25, 2025 01:08
@github-actions

github-actions Bot commented Jul 25, 2025

Copy link
Copy Markdown

Code size report:

   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:    +0 +0.000% standard
      stm32:    +0 +0.000% PYBV10
     mimxrt:    +0 +0.000% TEENSY40
        rp2:    +0 +0.000% RPI_PICO_W
       samd:    +0 +0.000% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:    +0 +0.000% VIRT_RV32

dpgeorge added 3 commits July 31, 2025 11:38
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>
@dpgeorge dpgeorge force-pushed the webassembly-improve-identity-v2 branch from d6351ee to ffa98cb Compare July 31, 2025 01:44
@dpgeorge dpgeorge merged commit ffa98cb into micropython:master Jul 31, 2025
88 of 90 checks passed
@dpgeorge dpgeorge deleted the webassembly-improve-identity-v2 branch July 31, 2025 03:30
dpgeorge added a commit to dpgeorge/micropython that referenced this pull request Sep 26, 2025
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>
michael-membrowse pushed a commit to michael-membrowse/micropython that referenced this pull request Oct 27, 2025
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants