Wednesday, April 20, 2022

Python on Webassembly Part II: Rolling your own

Getting to compile Python for WASM/WASI, funny enough the first step is installing the Python-based tools that make "wasienv":

$ pip install wasienv
Processing /home/user/.cache/pip/wheels/15/72/ed/35789dff12f6ca3d9cd98454519aff343d102da520b98326dd/wasienv-0.5.4-py3-none-any.whl
Requirement already satisfied: requests in /usr/lib/python3/dist-packages (from wasienv) (2.22.0)
Installing collected packages: wasienv
Successfully installed wasienv-0.5.4

Now following instructions from https://github.com/wapm-packages/python/blob/master/README.rst, and tinkering a bit with python cmake options...

$ git clone --filter=blob:none https://github.com/wapm-packages/python.git
$ mkdir -p python/wasi/install && cd python/wasi
$ wasimake cmake -DCMAKE_INSTALL_PREFIX:PATH="$(realpath install)" -DUSE_SYSTEM_LIBRARIES=OFF ..
-- The ASM compiler identification is unknown -- Found assembler: /home/agustin/bin/wasicc -- Warning: Did not find file Compiler/-ASM ... -- Configuring done -- Generating done -- Build files have been written to: /home/agustin/wasm/python/python-wasi

Ready to make it are we?

$ make -j7
[  0%] Built target extension_testcapi
[  5%] Built target pgen
[  6%] Building C object CMakeBuild/libpython/CMakeFiles/_freeze_importlib.dir/__/__/Python-3.6.7/Objects/listobject.c.obj
[  6%] Building C object CMakeBuild/libpython/CMakeFiles/_freeze_importlib.dir/__/__/Python-3.6.7/Objects/longobject.c.obj
[  7%] Building C object CMakeBuild/libpython/CMakeFiles/_freeze_importlib.dir/__/__/Python-3.6.7/Objects/memoryobject.c.obj
[  7%] Building C object CMakeBuild/libpython/CMakeFiles/_freeze_importlib.dir/__/__/Python-3.6.7/Objects/moduleobject.c.obj
[  7%] Building C object CMakeBuild/libpython/CMakeFiles/_freeze_importlib.dir/__/__/Python-3.6.7/Objects/methodobject.c.obj
[  7%] Building C object CMakeBuild/libpython/CMakeFiles/_freeze_importlib.dir/__/__/Python-3.6.7/Objects/object.c.obj
[  7%] Building C object CMakeBuild/libpython/CMakeFiles/_freeze_importlib.dir/__/__/Python-3.6.7/Objects/obmalloc.c.obj
In file included from :1:
/home/agustin/.local/lib/python3.8/site-packages/wasienv/stubs/preamble.h:30:9: warning: 'ESHUTDOWN' macro redefined [-Wmacro-redefined]
#define ESHUTDOWN 0

...

6 warnings generated.
[ 42%] Linking C executable _freeze_importlib
[ 52%] Built target _freeze_importlib
[ 52%] Generating ../../../Python-3.6.7/Python/importlib_external.h, ../../../Python-3.6.7/Python/importlib.h
cannot open '/home/user/wasm/python/Python-3.6.7/Lib/importlib/_bootstrap_external.py' for reading
make[2]: *** [CMakeBuild/libpython/CMakeFiles/libpython-static.dir/build.make:64: ../Python-3.6.7/Python/importlib_external.h] Error 1
make[1]: *** [CMakeFiles/Makefile2:1179: CMakeBuild/libpython/CMakeFiles/libpython-static.dir/all] Error 2
make: *** [Makefile:141: all] Error 2

But the file is there, what magic is forbidding freeze_importlib from opening it? Let's check with strace...

$ strace ./_freeze_importlib /home/user/wasm/python/Python-3.6.7/Lib/importlib/_bootstrap_external.py /home/user/wasm/python/Python-3.6.7/Python/importlib_external.h
execve("./_freeze_importlib", ["./_freeze_importlib", "/home/user/wasm/python/Python"..., "/home/user/wasm/python/Python"...], 0x7ffd0490d690 /* 58 vars */) = 0
...
access("/home/user/bin/wasirun", R_OK) = 0
rt_sigprocmask(SIG_BLOCK, [INT CHLD], [], 8) = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fed289eaa10) = 69585
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigaction(SIGINT, {sa_handler=0x55d072a77480, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fed28a300c0}, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fed28a300c0}, 8) = 0
wait4(-1, cannot open '/home/user/wasm/python/Python-3.6.7/Lib/importlib/_bootstrap_external.py' for reading (errno=44)
fopen: No such file or directory

Looks like the binary is wrapped by "wasirun", I guess to run it inside a virtualroot sandbox, and some required paths are not visible there.

Time to debug...

"DEBUG" can be enabled by tweaking wasirun/tools.py:

Now the same call to make dumps what wasienv is doing:

[ 52%] Generating ../../../Python-3.6.7/Python/importlib_external.h, ../../../Python-3.6.7/Python/importlib.h
wasienv run process: wasmer run --dir=. --enable-all /home/user/wasm/python/python-wasi/CMakeBuild/libpython/_freeze_importlib.wasm -- /home/user/wasm/python/Python-3.6.7/Lib/importlib/_bootstrap_external.py /home/user/wasm/python/Python-3.6.7/Python/importlib_external.h
cannot open '/home/agustin/wasm/python/Python-3.6.7/Lib/importlib/_bootstrap_external.py' for reading (errno=44)
fopen: No such file or directory

The problem is that only current dir "." is exposed in the WASM context. Let's call wasmer run directly with dir=/

$ wasmer run --dir / --enable-all /home/user/wasm/python/python-wasi/CMakeBuild/libpython/_freeze_importlib.wasm -- /home/user/wasm/python/Python-3.6.7/Lib/importlib/_bootstrap_external.py /home/user/wasm/python/Python-3.6.7/Python/importlib_external.h
error: failed to run `/home/user/wasm/python/python-wasi/CMakeBuild/libpython/_freeze_importlib.wasm`
│   1: RuntimeError: indirect call type mismatch
           at _PyCFunction_FastCallDict (_freeze_importlib.wasm[1839]:0x15c040)
           at _PyObject_FastCallDict (_freeze_importlib.wasm[658]:0x86973)
           at callmethod (_freeze_importlib.wasm[737]:0x905ee)
           at _PyObject_CallMethodId (_freeze_importlib.wasm[736]:0x90451)
           at flush_std_files (_freeze_importlib.wasm[2975]:0x279fee)
           at Py_FinalizeEx (_freeze_importlib.wasm[2980]:0x27afa9)
           at Py_Finalize (_freeze_importlib.wasm[2979]:0x27aeed)
           at main (_freeze_importlib.wasm[42]:0x4a08)
           at __original_main (_freeze_importlib.wasm[7190]:0x69da7e)
           at _start (_freeze_importlib.wasm[30]:0x3ca8)
╰─▶ 2: bad_sig

Now the files are reachable, and feeze_importlib is crashing.

We need better debug information...

Meet "The Pain of Debugging WebAssembly".

Monday, April 18, 2022

Python on WebAssembly: How does it perform? (Part I)

Intrigued by Christian Heimes's "web browser journey" of Python 3.11 and Lin Clark's talks about WASI, I am curious about the feasibility of running performance-aware Python apps inside WebAssembly standalone (non-web) runtime(s).

So how does Python perform inside WebAssembly?

Here is a roadmap to produce my own answers:

0. Placing code and recipes generated during this research at github.com/gatopeich/python-wasm-bench, so results can be reproduced easily and expanded...

1. Compare CPython performance inside Wasm with the native version

  • Preliminary result: Wasmer/WAPM Python 3.6 package is 3~4 times slower than Docker equivalent. My measures here match the expectations set by Chris Heimes. WAPM is the easiest way to have a standalone wasm Python interpreter, however the package itself is a bit outdated.

2. Learn about performance profiling in WASM/WASI: will I be able to find any obvious bottleneck?

  • In particular: how is the core Python "dict" implementation working there? I am a bit skeptical sure how highly optimized state of the art code will suffer in the translation to four-type WASM code.

3. Could it be helped with some native implementations via WASI? Will it be worth it or required at all? Will there be just another way to optimize things for the WASM runtime and JIT?


(Note to self: move to a blog engine more suitable for coding matters)