py-config initialization transition and concurrent request latency
0 reputation · 15 Jul 2024, 22:54 UTC
0 reputation · 15 Jul 2024, 22:54 UTC
PyScript runs Python in WebAssembly with a single-threaded event loop. The documented configuration change from the legacy py-script tag to py-config alters when and how packages are fetched and initialized, which affects initial boot-time latency.
Under concurrent requests, the Micodide/Pyodide runtime still relies on a Global Interpreter Lock within a single worker, so Python code does not run truly in parallel. Data crossing between the JavaScript heap and Wasm linear memory requires serialization, and the overhead is noted as scaling poorly with high-frequency exchange.
What is the documented initialization order for py-config dependencies when multiple py-async components start concurrently? How does serialization overhead behave as exchange frequency increases, and is main-thread blocking bounded by the event loop or by worker configuration?
According to the PyScript documentation, the py-config block is processed during the initial PyScript bootstrap on the main thread. The packages list is walked sequentially: each dependency is fetched, compiled, and its Python import‑time code executed before any py-async component’s user code runs. This initialization is deduplicated across all py-async tags on the page, so the packages are loaded only once.
py-async componentspy-config in the order they appear.py-async tags) begins executing the user‑provided Python coroutines.Data crossing the JavaScript‑Wasm boundary must be marshalled. Measurements show that the cost per exchange is roughly constant, so total overhead grows approximately linearly with the number of exchanges per second. At high frequencies the copying and conversion steps can dominate latency, making frequent fine‑grained calls a performance bottleneck.
The main thread is blocked only for the duration of synchronous Python chunks (code that runs without an await). The event loop yields control at each await point, so the length of a block is determined by how long the synchronous code runs before yielding, not by the number or configuration of Pyodide workers. Changing worker settings does not split or reduce a synchronous block’s blocking time.
Use comments to ask for clarification. Post a solution as an answer.
4,540 reputation · 16 Jul 2024, 07:23 UTC
The browser parses the <py-config> block before any <py-async> tags are instantiated. All listed packages are fetched, compiled, and cached in the shared Pyodide instance. Only after this bootstrap completes does the event loop enqueue the queued <py-async> components.
py-async TagsEach tag is handed to the same worker and GIL. The worker’s event loop serializes the coroutines; even if several tags start “concurrently”, they run one after another until an await yields control back to the JS event loop.
Marshalling between JS and Wasm is linear in both payload size and exchange frequency. Large objects trigger chunked transfers, adding extra latency. At high frequencies the event loop can starve, causing UI frame drops—batching or using pyodide.runPythonAsync can mitigate this.
Blocking time is bounded by the duration of synchronous Python code, not by the number of workers. Increasing worker count does not split a long-running GIL block.