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

RangeError: Maximum call stack size exceeded #2840

Open
samuelcolvin opened this issue Jul 5, 2022 · 20 comments
Open

RangeError: Maximum call stack size exceeded #2840

samuelcolvin opened this issue Jul 5, 2022 · 20 comments
Labels
bug Something isn't working

Comments

@samuelcolvin
Copy link
Sponsor

🐛 Bug

Trying to run pydantic-core's tests on wasm with node locally, and I'm getting the following crash, tests are working ok on github CI. My local machine is an M1 mac.

============================= test session starts ==============================
platform emscripten -- Python 3.10.2, pytest-7.1.2, pluggy-1.0.0
rootdir: /test_dir
plugins: hypothesis-6.49.1, speed-0.3.1
collecting ... Pyodide has suffered a fatal error. Please report this to the Pyodide maintainers.
The cause of the fatal error was:
RangeError: Maximum call stack size exceeded
    at getter_call_trampoline (/Users/samuel/code/pydantic-core/tests/node_modules/pyodide/pyodide.asm.js:7647:36)
    at wasm://wasm/0244357a:wasm-function[1069]:0x137410
    at wasm://wasm/0244357a:wasm-function[2223]:0x17ff94
    at wasm://wasm/0244357a:wasm-function[1927]:0x16e527
    at wasm://wasm/0244357a:wasm-function[1929]:0x16e7d5
    at wasm://wasm/0244357a:wasm-function[703]:0x11c70f
    at wasm://wasm/0244357a:wasm-function[702]:0x11c6bb
    at wasm://wasm/0244357a:wasm-function[701]:0x11c542
    at wasm://wasm/0244357a:wasm-function[2960]:0x1e0ea6
    at wasm://wasm/0244357a:wasm-function[1851]:0x16b264 {
  pyodide_fatal_error: true
}
RangeError: Maximum call stack size exceeded
    at getter_call_trampoline (/Users/samuel/code/pydantic-core/tests/node_modules/pyodide/pyodide.asm.js:7647:36)
    at wasm://wasm/0244357a:wasm-function[1069]:0x137410
    at wasm://wasm/0244357a:wasm-function[2223]:0x17ff94
    at wasm://wasm/0244357a:wasm-function[1927]:0x16e527
    at wasm://wasm/0244357a:wasm-function[1929]:0x16e7d5
    at wasm://wasm/0244357a:wasm-function[703]:0x11c70f
    at wasm://wasm/0244357a:wasm-function[702]:0x11c6bb
    at wasm://wasm/0244357a:wasm-function[701]:0x11c542
    at wasm://wasm/0244357a:wasm-function[2960]:0x1e0ea6
    at wasm://wasm/0244357a:wasm-function[1851]:0x16b264 {
  pyodide_fatal_error: true
}

To Reproduce

See pydantic/pydantic-core#148 and in particular pydantic/pydantic-core@782b64f

I'm just running those tests locally.

Expected behavior

no crash

Environment

  • Pyodide Version: 0.21.0-alpha.2
  • Browser version: node v16.13.0
  • Any other relevant information:
@samuelcolvin samuelcolvin added the bug Something isn't working label Jul 5, 2022
@samuelcolvin
Copy link
Sponsor Author

Forgot to add

➤ emcc --version
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.14-git

installed from brew.

In case it helps.

@samuelcolvin
Copy link
Sponsor Author

I've just checked this with docker and latest emsdk, so seems to be related specifically to arm64.

@hoodmane
Copy link
Member

hoodmane commented Jul 5, 2022

Can you run the following script and post the results printed (don't need the fatal error traceback part).

const {loadPyodide} = require("./pyodide.js");  

globalThis.jsdepth = function jsdepth(){
    let d = 0;
    function jsrec(){ d++; jsrec(); }
    try { jsrec(); } catch(e) { return d; }
}

async function main() {
    const py = await loadPyodide();

    let d = py.runPython(`
        from js import jsdepth
        jsdepth()
    `);
    let pyusage = py.runPython(`
        from js import jsdepth
        from itertools import pairwise
        def differences(l):
            return [x-y for (x, y) in pairwise(l)]

        def f(n):
            if n > 0:
                return f(n-1)
            return jsdepth()
        differences([f(x) for x in range(10)])
    `).toJs();



    const x = [0]
    py.globals.set("x", x);
    const pyrec = py.runPython(`
        import sys; sys.setrecursionlimit(100000)
        def pyrec():
            x[0] += 1
            pyrec()
        pyrec
    `);
    try {
        pyrec();
    } catch(e){}
    console.log("Max JS depth:", d);
    console.log("Max Python depth:", x[0]);
    console.log("Python usage", pyusage);
    console.log("Max JS depth/Python usage:", d/pyusage[0]);
}
main();

@samuelcolvin
Copy link
Sponsor Author

samuelcolvin commented Jul 5, 2022

I had to change the first line to const {loadPyodide} = require('pyodide'); to use the version of pyiodide from node_modules

With that, output is:

Loading distutils
Loaded distutils
Python initialization complete
Pyodide has suffered a fatal error. Please report this to the Pyodide maintainers.
The cause of the fatal error was:
RangeError: Maximum call stack size exceeded
    at js2python (/Users/samuel/code/pydantic-core/node_modules/pyodide/pyodide.asm.js:5489:16)
    at wasm://wasm/0244357a:wasm-function[314]:0xea36e
    at wasm://wasm/0244357a:wasm-function[606]:0x117bbf
    at wasm://wasm/0244357a:wasm-function[3011]:0x1e4b83
    at wasm://wasm/0244357a:wasm-function[3008]:0x1e3ade
    at wasm://wasm/0244357a:wasm-function[943]:0x12fef4
    at wasm://wasm/0244357a:wasm-function[3020]:0x1eb8c0
    at wasm://wasm/0244357a:wasm-function[3011]:0x1e8aa4
    at wasm://wasm/0244357a:wasm-function[3008]:0x1e3ade
    at wasm://wasm/0244357a:wasm-function[943]:0x12fef4 {
  pyodide_fatal_error: true
}
Stack (most recent call first):
  File "<exec>", line 4 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  File "<exec>", line 5 in pyrec
  ...
Max JS depth: 10544
Max Python depth: 839
Python usage [
  12, 13, 13, 12, 13,
  12, 13, 13, 12
]
Max JS depth/Python usage: 878.6666666666666

@hoodmane
Copy link
Member

hoodmane commented Jul 5, 2022

Try sys.setrecursionlimit(800) and then see where the error occurs. Using a depth of more than 800 is probably a bug too.

For comparison I see:

Max JS depth: 13720
Max Python depth: 1213
Python usage [
  11, 11, 12, 11, 11,
  12, 11, 11, 12
]
Max JS depth/Python usage: 1247.2727272727273

and our most recent CI run showed
https://circleci.com/api/v1.1/project/github/pyodide/pyodide/51294/output/102/0?file=true&allocation-id=62c3679bbace261555ec3110-0-build%2F5C3D0EA4

browser  py_limit  py_usage  js_depth  py_depth  js_depth/py_usage  js_depth/py_depth
firefox      1000     64.10     39103      1996                610                 19
chrome       1000     11.34     13950      1225               1230                 11
node         1000     11.38     15424      1349               1355                 11

We used to automatically compute the recursion limit but we removed it because as you can see weird stuff happens in Firefox: We want to compute js_depth and py_usage and then set the recursion limit equal to js_depth/py_usage. On Chrome and Node this gives almost the same number as py_depth, but in firefox it is 3 times smaller than py_depth. So we just hard coded it to 1000.

@samuelcolvin
Copy link
Sponsor Author

Interesting, with that I get

Loading distutils
Loaded distutils
Python initialization complete
Max JS depth: 10544
Max Python depth: 800
Python usage [
  12, 13, 13, 12, 13,
  12, 13, 13, 12
]
Max JS depth/Python usage: 878.6666666666666

Even more, if i set the value as low as 300 tests actually run, although lots fail.

@hoodmane
Copy link
Member

hoodmane commented Jul 5, 2022

If you set the value to 800 you still get RangeErrors?

@samuelcolvin
Copy link
Sponsor Author

yes, happens when pytest is collecting tests, perhaps when it's importing pydantic-core.

@samuelcolvin
Copy link
Sponsor Author

I've been playing around, I can get all tests to pass in docker running ubuntu if I use sys.setrecursionlimit(200).

When trying to run tests outside docker I now get ValueError: 'pydantic_core-0.0.1-cp310-cp310-emscripten_3_1_9_wasm32.whl' is not a pure Python 3 wheel, but that might be related to a change in pydantic/pydantic-core#149.

@hoodmane
Copy link
Member

hoodmane commented Jul 6, 2022

emscripten_3_1_9

Well that looks like the wheel was built with the wrong version of Emscripten -- should be 3_1_14 I believe. I improved that error message on main. It might also be a good idea to relax the error to a warning...

@samuelcolvin
Copy link
Sponsor Author

Ok, I had managed to build two wheels, one with 3_1_9 and one 3_1_14, just coincidence that the docker terminal was using the right one.

With that they both seem to be the same, though I still have the weird situation where pytest sometimes doesn't recognise some plugins.

@rtpg
Copy link

rtpg commented Jul 18, 2023

I've hit a similar issue here, the following html file will hit the Maximum call stack size exceeded error on Chrome but not on FF or Node

<!doctype html>
<html>
  <head>
      <script src="https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.js"></script>
  </head>
  <body>
    <script type="text/javascript">
      let scriptToRun = `
def f():
     return f()
f()
      `
      async function main(){
        let pyodide = await loadPyodide();
        console.log(pyodide.runPython(scriptToRun));
      }
      main();
    </script>
  </body>
</html>

@ryanking13
Copy link
Member

ryanking13 commented Jul 18, 2023

I've hit a similar issue here, the following html file will hit the Maximum call stack size exceeded error on Chrome but not on FF or Node

@rtpg Well, that's strange. Your code runs infinite recursion, so it must hit the maximum call stack size error on any platform including native Python. When I ran it on my local Firefox it raises the maximum call stack error as expected.

@rtpg
Copy link

rtpg commented Jul 19, 2023

<!doctype html>
<html>
  <head>
      <script src="https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.js"></script>
  </head>
  <body>
    <script type="text/javascript">
      let scriptToRun = `
import sys
print(sys.getrecursionlimit())
# locally, this causes nasty crashes at 1500 in Chrome, 10000 in Firefox
sys.setrecursionlimit(10000)

class AlwaysDefault:
  def __init__(self, default):
    self.default = default

  def __getattribute__(self, name):
    return self.default

always_three = AlwaysDefault(3)
print("RESULT IS")
# this causes an infinite recursion because of __getattribute__
print([always_three.one, always_three.two, always_three.three])
      `
      async function main(){
        console.log("STARTING TO LOAD...")
        let pyodide = await loadPyodide();
        console.log("LOADED")
          console.log(pyodide.runPython(scriptToRun));
        console.log("DONE")
      }
      main();
    </script>
  </body>
</html>

Updated version.

When I set the recursion limit (via setrecursionlimit) to 1500, on Firefox the error is:

Uncaught (in promise) PythonError: Traceback (most recent call last):
  File "/lib/python311.zip/_pyodide/_base.py", line 468, in eval_code
    .run(globals, locals)
     ^^^^^^^^^^^^^^^^^^^^
  File "/lib/python311.zip/_pyodide/_base.py", line 310, in run
    coroutine = eval(self.code, globals, locals)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<exec>", line 17, in <module>
  File "<exec>", line 12, in __getattribute__
  File "<exec>", line 12, in __getattribute__
  File "<exec>", line 12, in __getattribute__
  [Previous line repeated 1493 more times]
RecursionError: maximum recursion depth exceeded
    PythonError https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.asm.js:9
    new_error https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.asm.js:9
    _pythonexc2js https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.asm.js:9
    callPyObjectKwargs https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.asm.js:9
    callPyObject https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.asm.js:9
    apply https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.asm.js:9
    apply https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.asm.js:9
    runPython https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.asm.js:9
    main http://localhost:7000/pyodide_repro.html:30
    async* http://localhost:7000/pyodide_repro.html:33
pyodide.asm.js:9:105877
    main http://localhost:7000/pyodide_repro.html:32
    InterpretGeneratorResume self-hosted:1413
    AsyncFunctionNext self-hosted:810
    (Async: async)
    <anonymous> http://localhost:7000/pyodide_repro.html:33

which is fine and great and what I expect.

But when running this on Chrome, the following error happens. Here it's not Pyodide raising a Python error, but Pyodide hitting a failure due to stacks

pyodide.asm.js:9 Pyodide has suffered a fatal error. Please report this to the Pyodide maintainers.
API.fatal_error @ pyodide.asm.js:9
Module.callPyObjectKwargs @ pyodide.asm.js:9
Module.callPyObject @ pyodide.asm.js:9
apply @ pyodide.asm.js:9
apply @ pyodide.asm.js:9
runPython @ pyodide.asm.js:9
main @ pyodide_repro.html:30
await in main (async)
(anonymous) @ pyodide_repro.html:33
pyodide.asm.js:9 The cause of the fatal error was:
API.fatal_error @ pyodide.asm.js:9
Module.callPyObjectKwargs @ pyodide.asm.js:9
Module.callPyObject @ pyodide.asm.js:9
apply @ pyodide.asm.js:9
apply @ pyodide.asm.js:9
runPython @ pyodide.asm.js:9
main @ pyodide_repro.html:30
await in main (async)
(anonymous) @ pyodide_repro.html:33
pyodide.asm.js:9 RangeError: Maximum call stack size exceeded
    at pyodide.asm.wasm:0x428271
    at pyodide.asm.wasm:0x25cffa
    at pyodide.asm.wasm:0x26a9d3
    at pyodide.asm.wasm:0x1a0038
    at pyodide.asm.wasm:0x1fce2b
    at pyodide.asm.wasm:0x1fd020
    at pyodide.asm.wasm:0x1e4475
    at pyodide.asm.wasm:0x2699af
    at pyodide.asm.wasm:0x26a9f5
    at pyodide.asm.wasm:0x1a0038
API.fatal_error @ pyodide.asm.js:9
Module.callPyObjectKwargs @ pyodide.asm.js:9
Module.callPyObject @ pyodide.asm.js:9
apply @ pyodide.asm.js:9
apply @ pyodide.asm.js:9
runPython @ pyodide.asm.js:9
main @ pyodide_repro.html:30
await in main (async)
(anonymous) @ pyodide_repro.html:33

in practice even with the default of 1000 I was hitting this error in the system I'm looking at , but am failing to nicely reproduce the error I saw there here.

I don't really know what is ideal here, since it's not like we want to be "solving for" the stack height at every Python function call (that generates a variable number of stack frames), and Pyodide is being called within JS, so I think there's a bit of stack budget used up there as well.

At the very least it might be worth catching exceptions, checking for if they look like stack overflows, and then re-raising but with an error like "you hit a recursion limit on the WASM. This could be due to an infinite loop, or simply hitting a limit. Lowering the recursion limit in python with setrecursionlimit might give you a better error with a stack trace".

@samuelcolvin
Copy link
Sponsor Author

When I was seeing this, the error is more likely to happen when the devtools panel is closed - enabling devtools in chrome obviously changes the way JS is executed to allow introspection, there stopping the error (or making it less likely).

@ryanking13
Copy link
Member

At the very least it might be worth catching exceptions, checking for if they look like stack overflows, and then re-raising but with an error like "you hit a recursion limit on the WASM. This could be due to an infinite loop, or simply hitting a limit. Lowering the recursion limit in python with setrecursionlimit might give you a better error with a stack trace".

This sounds reasonable to me.

@rtpg
Copy link

rtpg commented Jul 21, 2023

Another thing I noticed here (though I don't know if it's really solvable at all?) is that after this happens Pyodide is placed in a permanently disabled state.

Are there any plans at all to provide a way to basically wipe all of Pyodide and re-initialize it "from scratch" in these sorts of scenarios? I suppose service workers are the way to go for that? I basically want to make this at least somewhat recoverable but maybe this is too big of an ask

@hoodmane
Copy link
Member

@rtpg You can call loadPyodide again to create a new one.

@hoodmane
Copy link
Member

To be fair, in ordinary linux it would just say Segmentation fault (core dumped).

you hit a recursion limit on the WASM

Well there are a lot of other reasons it might say RangeError: Maximum call stack size exceeded, the message could say this may or may not be due to infinite recursion. But also "Maximum call stack size exceeded" isn't all that cryptic.

@rtpg
Copy link

rtpg commented Jul 24, 2023

To be clear, currently, when hitting the error, Pyodide wraps the error and says to report it upstream. Doing this sort of error filtering would make it obvious that this isn't a Pyodide bug.

I was having an issue with trying to re-init with loadPyodide, hitting the "Pyodide is broken, sorry" code that gets used for the public API after hitting a fatal error, but I might be doing something wrong on my end, will retry 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

4 participants