browser-py 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- browser_py-0.1.0/AGENTS.md +45 -0
- browser_py-0.1.0/MANIFEST.in +4 -0
- browser_py-0.1.0/PKG-INFO +53 -0
- browser_py-0.1.0/README.md +34 -0
- browser_py-0.1.0/benchmarks/legacy_outputs.pickle +0 -0
- browser_py-0.1.0/pyproject.toml +68 -0
- browser_py-0.1.0/setup.cfg +4 -0
- browser_py-0.1.0/source/__browser_benchmark__.py +14 -0
- browser_py-0.1.0/source/__browser_py__.py +5 -0
- browser_py-0.1.0/source/__browser_triangle__.py +82 -0
- browser_py-0.1.0/source/__comparison__.py +103 -0
- browser_py-0.1.0/source/__http_server__.py +44 -0
- browser_py-0.1.0/source/benchmark_build.py +81 -0
- browser_py-0.1.0/source/benchmark_host.py +254 -0
- browser_py-0.1.0/source/browser_compile.py +27 -0
- browser_py-0.1.0/source/browser_measurement.py +131 -0
- browser_py-0.1.0/source/browser_py.egg-info/PKG-INFO +53 -0
- browser_py-0.1.0/source/browser_py.egg-info/SOURCES.txt +51 -0
- browser_py-0.1.0/source/browser_py.egg-info/dependency_links.txt +1 -0
- browser_py-0.1.0/source/browser_py.egg-info/entry_points.txt +2 -0
- browser_py-0.1.0/source/browser_py.egg-info/requires.txt +14 -0
- browser_py-0.1.0/source/browser_py.egg-info/top_level.txt +7 -0
- browser_py-0.1.0/source/browser_source.py +598 -0
- browser_py-0.1.0/source/compiler_command.py +53 -0
- browser_py-0.1.0/source/compiler_modes.py +10 -0
- browser_py-0.1.0/source/http_process.py +49 -0
- browser_py-0.1.0/source/javascript_output.py +9 -0
- browser_py-0.1.0/source/kernels.py +62 -0
- browser_py-0.1.0/source/meshing.py +260 -0
- browser_py-0.1.0/source/meshing_vectors.py +160 -0
- browser_py-0.1.0/source/numeric_ir.py +315 -0
- browser_py-0.1.0/source/numeric_source.py +266 -0
- browser_py-0.1.0/source/pscript_backend.py +59 -0
- browser_py-0.1.0/source/style.css +10 -0
- browser_py-0.1.0/source/triangle_command.py +93 -0
- browser_py-0.1.0/source/triangle_native.py +98 -0
- browser_py-0.1.0/source/triangle_render.py +46 -0
- browser_py-0.1.0/source/webgpu_bindings.py +308 -0
- browser_py-0.1.0/source/workloads.py +53 -0
- browser_py-0.1.0/tests/shootyfield_surface_nets.bin +0 -0
- browser_py-0.1.0/tests/test_browser_compile.py +118 -0
- browser_py-0.1.0/tests/test_browser_source.py +118 -0
- browser_py-0.1.0/tests/test_build.py +117 -0
- browser_py-0.1.0/tests/test_compiler_boundary.py +50 -0
- browser_py-0.1.0/tests/test_geometry.py +173 -0
- browser_py-0.1.0/tests/test_http_process.py +98 -0
- browser_py-0.1.0/tests/test_numeric_source.py +223 -0
- browser_py-0.1.0/tests/test_packaging.py +31 -0
- browser_py-0.1.0/tests/test_pscript.py +46 -0
- browser_py-0.1.0/tests/test_reports.py +197 -0
- browser_py-0.1.0/tests/test_source_invariants.py +86 -0
- browser_py-0.1.0/tests/test_webgpu.py +241 -0
- browser_py-0.1.0/uv.lock +355 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Working rules
|
|
2
|
+
|
|
3
|
+
browser-py is an independent experimental Python-to-browser compiler competing with Transcrypt. Its numeric backend emits JavaScript from mypyc IR; its browser-source backend emits a deliberately limited native-browser subset. Do not present either as general Python support. PScript is a numeric comparison baseline, never a compilation fallback.
|
|
4
|
+
|
|
5
|
+
Keep runnable Python in `source/`, regression checks in `tests/`, and frozen benchmark outputs in `benchmarks/`. Keep these folders flat. The root compiler command is the `browser_py.py` symlink; supporting comparisons and GPU demos run through `source/__comparison__.py`. Do not import the double-underscore entry files.
|
|
6
|
+
|
|
7
|
+
Use synchronous procedural execution, three-space indentation, top-level `from` imports, explicit dispatch, and plain dataclasses without slots or methods. Do not add callbacks, global mutable application state, compatibility layers, or handwritten JavaScript. JavaScript emitted by the compiler is generated output, not application source.
|
|
8
|
+
|
|
9
|
+
Keep generated pages, compiler copies, logs, reports, bytecode, environments, and tool caches on tmpfs. Do not add ignore files. Do not change installed dependencies during builds.
|
|
10
|
+
|
|
11
|
+
## Boundaries and invariants
|
|
12
|
+
|
|
13
|
+
- The compiler command owns admission, compilation, and output publication; the comparison entry owns build, server, browser, and verification lifetimes in that order. `browser_compile.py` admits the whole local graph, builds mypyc IR once, and stages one module's emitted ES module/IR pair at a time before publication; an admission or emission failure leaves existing outputs untouched. `javascript_output.py` owns independent rjsmin compression. Benchmark output uses minified browser-py as its speedup reference.
|
|
14
|
+
- `browser_source.py` owns browser-source admission and iterative ES module emission: direct positional calls, native DOM/GPU values, required-field dataclasses, ordered loops, and native try/catch/finally. `from browser import document, window` names native browser globals at compile time; it emits no runtime import or shim. Reject unsupported imports, expressions, annotations, state, and dispatch instead of delegating to another compiler. Range emission evaluates bounds in order and keeps its private counter separate from the Python target.
|
|
15
|
+
- Benchmark and GPU builders own their source selection, staged sources, and generated pages; GPU builds must not import numeric benchmark compilers. Transcrypt compilation and its patch were removed only after the independent browser build passed every workload payload and native/browser GPU pixel verification.
|
|
16
|
+
- `http_process.py` owns check-server startup and cleanup; `__http_server__.py` owns a single-threaded listener and ordered static-file requests. Read the little-endian uint16 readiness port once with an unbuffered two-byte read, never by parsing human status output. Park silent browser preconnections in native sockets until readable or their idle deadline expires; native handlers run synchronously with bounded socket I/O. A shutdown timeout kills and reaps the process, then still fails the check; cleanup closes its stdout pipe, while the caller owns the log.
|
|
17
|
+
- The benchmark browser entry owns one blocking measurement, its binary download, and its untimed preview. Page navigation starts a fresh run; no event subscription controls application decisions. The preview owns the mesh-kind check, projects each vertex once into packed native float64 XY pairs, preserves triangle order, and never mutates the workload buffer.
|
|
18
|
+
- `triangle_render.py` owns the shared shader and rendering commands. `triangle_native.py` owns synchronous native GPU/window lifetimes and ordered frames. `__browser_triangle__.py` owns browser GPU/canvas initialization, one rendered frame, and binary pixel readback.
|
|
19
|
+
- Adapter selection rejects software/fallback GPUs before device creation; native frames block on GPU completion before presentation. Teardown scopes nest so one failing cleanup cannot skip another owner's cleanup, while a successful browser preview keeps its configured device for the page lifetime.
|
|
20
|
+
- Only the GPU browser entry may use explicit async/await for browser-required adapter/device initialization and mapped-buffer readback. Native execution, compilation, host orchestration, and benchmarks stay synchronous; do not introduce callbacks or a scheduling framework.
|
|
21
|
+
- `webgpu_bindings.py` lowers a deliberately small, typed, straight-line wgpu subset to direct browser API calls at build time, then the browser-source backend emits the app. It is not the mypyc IR backend and must reject unsupported imports, receivers, calls, descriptors, and statements instead of guessing or providing runtime shims.
|
|
22
|
+
- GPU lowering checks descriptor fields, RGBA shape, sequence elements, and positional argument types before emission; aliases retain those checked types. GPU APIs own scalar-value and resource-state validation.
|
|
23
|
+
- The GPU check validates every native pixel against the shader's independent triangle geometry, opens a native window, then compares every browser RGBA8 value with native output, allowing one quantization unit. Validate the fixed byte count and bound reads before comparing.
|
|
24
|
+
- The host spools one workload's CPython reference payloads to a scoped tmpfs binary file and keeps only record metadata and timing summaries in memory. It validates peer headers before one-record float64-array reads, reconciles each payload and the warmup/component checksum, and rewinds the reference for each compiler. The host scope closes the file; generating the next workload replaces its contents.
|
|
25
|
+
- Component buffers and procedural dataclass vectors deliberately represent different benchmark subjects. Preserve their numeric equivalence; do not scalarize away the vector allocation workload.
|
|
26
|
+
- Preserve the density model, negative halo, edge ownership, triangle winding, and area-weighted normals. Frozen outputs and oriented geometry hashes guard these properties.
|
|
27
|
+
- `numeric_source.py` owns numeric annotation admission, dataclass field order, and acyclic direct-call/import graphs for both numeric backends; each backend owns emission and rejects its unsupported operations. Function signatures, dataclass fields, and present local annotations use the same admitted numeric shapes; reject unsupported annotation expressions before collecting call edges instead of letting a backend erase them. Imports name functions defined directly by the target module, never reexports, classes, or entry files. Admission checks positional arity against actual definitions and field counts, keeps named callables unshadowed and nonescaping, and rejects global/nonlocal state before emission.
|
|
28
|
+
- The compiler consumes actual mypyc IR, emits structured control flow without recursion, and rejects unsupported operations rather than substituting another compiler.
|
|
29
|
+
- Timing is observational, not deterministic; workload inputs, execution order, generated numeric code, and output validation must be deterministic. Build-duration metadata is observational too.
|
|
30
|
+
|
|
31
|
+
## Dependency limitations
|
|
32
|
+
|
|
33
|
+
Compilation and minification require neither Transcrypt nor Java. Numeric and browser-source emission produce native ES modules; the GPU entry additionally uses browser-required async/await. Every numeric compiler configuration has separate `-minified` and `-unminified` modes, including shared browser code; report each separately.
|
|
34
|
+
|
|
35
|
+
wgpu 0.29.0 has no `py.typed` marker. Mypy follows its installed Python definitions; import GPU types from the public `wgpu.classes` module so their definitions remain visible. Do not add local stubs or invented browser types.
|
|
36
|
+
|
|
37
|
+
A GPU check has aborted during native shutdown after passing pixel verification; the cause is not isolated and repeated comparisons did not reproduce it. A native abort still fails the check: do not retry automatically, suppress its exit code, or skip teardown.
|
|
38
|
+
|
|
39
|
+
Native browser APIs remain untyped external boundaries. Keep DOM and typed-array calls at the browser boundary; do not invent Python browser types or embed stubs in application files. Playwright exposes a synchronous interface used by the host; its internal implementation is outside application ownership.
|
|
40
|
+
|
|
41
|
+
## Checks
|
|
42
|
+
|
|
43
|
+
Install the `test` extra for validation or the `tools` extra for comparison/demo dependencies; direct compilation needs neither. Run `python -B browser_py.py validate` for Ruff, mypy, and Python regressions. Run `python -B source/__comparison__.py check --browser firefox` for all ten workloads under every configured compiler mode in Firefox. Install the matching Playwright Firefox before the browser check. Preserve frozen fixtures until a replacement has demonstrated the same outputs.
|
|
44
|
+
|
|
45
|
+
Run `python -B source/__comparison__.py triangle check` for shared rendering in a native window and headed Firefox; headless Firefox does not expose WebGPU here. A working GPU/display is required, with no headless or software-rendering fallback. In the isolated X11 check environment, select the actual available display with `PYGLFW_LIBRARY_VARIANT=x11 MOZ_ENABLE_WAYLAND=0`. The browser entry reports native WebGPU failures and rethrows them so the host can retain the browser diagnostic.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: browser-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An experimental Python-to-browser compiler and Transcrypt competitor
|
|
5
|
+
Requires-Python: <3.13,>=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: rjsmin==1.2.5
|
|
8
|
+
Requires-Dist: mypy==1.18.2
|
|
9
|
+
Requires-Dist: setuptools>=68
|
|
10
|
+
Provides-Extra: tools
|
|
11
|
+
Requires-Dist: playwright<2,>=1.56; extra == "tools"
|
|
12
|
+
Requires-Dist: pscript<0.9,>=0.8; extra == "tools"
|
|
13
|
+
Requires-Dist: wgpu==0.29.0; extra == "tools"
|
|
14
|
+
Requires-Dist: glfw<3,>=2.9; extra == "tools"
|
|
15
|
+
Provides-Extra: test
|
|
16
|
+
Requires-Dist: browser-py[tools]; extra == "test"
|
|
17
|
+
Requires-Dist: pytest<10,>=9; extra == "test"
|
|
18
|
+
Requires-Dist: ruff<1,>=0.14; extra == "test"
|
|
19
|
+
|
|
20
|
+
# browser-py
|
|
21
|
+
|
|
22
|
+
An experimental Python-to-browser compiler, built as a competitor to Transcrypt.
|
|
23
|
+
Write Python and compile it for the browser.
|
|
24
|
+
|
|
25
|
+
It supports typed numeric code and a small browser-facing subset, not arbitrary
|
|
26
|
+
Python packages. Compilation, browser examples, and checks work without Transcrypt.
|
|
27
|
+
|
|
28
|
+
Install with Python 3.12:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
python -m pip install browser-py
|
|
32
|
+
browser-py compile path/to/your_module.py
|
|
33
|
+
browser-py --help
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
In a source checkout, the equivalent compiler command is:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
python -B browser_py.py compile source/kernels.py
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Compiled files are saved to temporary storage; the command prints their location.
|
|
43
|
+
Compiler comparisons and the triangle demo are supporting tools, available in the
|
|
44
|
+
source checkout or source distribution (not the compiler wheel). Install the `tools`
|
|
45
|
+
extra to use them:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
python -m pip install 'browser-py[tools]'
|
|
49
|
+
python -B source/__comparison__.py --help
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
For development, install the `test` extra and run `python -B browser_py.py validate`
|
|
53
|
+
from a source checkout.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# browser-py
|
|
2
|
+
|
|
3
|
+
An experimental Python-to-browser compiler, built as a competitor to Transcrypt.
|
|
4
|
+
Write Python and compile it for the browser.
|
|
5
|
+
|
|
6
|
+
It supports typed numeric code and a small browser-facing subset, not arbitrary
|
|
7
|
+
Python packages. Compilation, browser examples, and checks work without Transcrypt.
|
|
8
|
+
|
|
9
|
+
Install with Python 3.12:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
python -m pip install browser-py
|
|
13
|
+
browser-py compile path/to/your_module.py
|
|
14
|
+
browser-py --help
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
In a source checkout, the equivalent compiler command is:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
python -B browser_py.py compile source/kernels.py
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Compiled files are saved to temporary storage; the command prints their location.
|
|
24
|
+
Compiler comparisons and the triangle demo are supporting tools, available in the
|
|
25
|
+
source checkout or source distribution (not the compiler wheel). Install the `tools`
|
|
26
|
+
extra to use them:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
python -m pip install 'browser-py[tools]'
|
|
30
|
+
python -B source/__comparison__.py --help
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
For development, install the `test` extra and run `python -B browser_py.py validate`
|
|
34
|
+
from a source checkout.
|
|
Binary file
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "browser-py"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "An experimental Python-to-browser compiler and Transcrypt competitor"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12,<3.13"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"rjsmin==1.2.5",
|
|
13
|
+
"mypy==1.18.2",
|
|
14
|
+
"setuptools>=68",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
browser-py = "compiler_command:main"
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
tools = ["playwright>=1.56,<2", "pscript>=0.8,<0.9", "wgpu==0.29.0", "glfw>=2.9,<3"]
|
|
22
|
+
test = ["browser-py[tools]", "pytest>=9,<10", "ruff>=0.14,<1"]
|
|
23
|
+
|
|
24
|
+
[tool.setuptools]
|
|
25
|
+
packages = []
|
|
26
|
+
package-dir = {"" = "source"}
|
|
27
|
+
py-modules = [
|
|
28
|
+
"browser_compile",
|
|
29
|
+
"browser_source",
|
|
30
|
+
"compiler_command",
|
|
31
|
+
"javascript_output",
|
|
32
|
+
"numeric_ir",
|
|
33
|
+
"numeric_source",
|
|
34
|
+
"webgpu_bindings",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
pythonpath = ["source"]
|
|
39
|
+
testpaths = ["tests"]
|
|
40
|
+
cache_dir = "/tmp/python-js-pytest-cache"
|
|
41
|
+
|
|
42
|
+
[tool.mypy]
|
|
43
|
+
python_version = "3.12"
|
|
44
|
+
mypy_path = "source"
|
|
45
|
+
cache_dir = "/tmp/python-js-mypy-cache"
|
|
46
|
+
exclude = ['(^|/)(__browser_benchmark__|browser_measurement|__browser_triangle__)\.py$']
|
|
47
|
+
disallow_untyped_defs = true
|
|
48
|
+
check_untyped_defs = true
|
|
49
|
+
|
|
50
|
+
[[tool.mypy.overrides]]
|
|
51
|
+
module = ["pscript", "glfw", "rjsmin"]
|
|
52
|
+
ignore_missing_imports = true
|
|
53
|
+
|
|
54
|
+
[[tool.mypy.overrides]]
|
|
55
|
+
module = ["wgpu", "wgpu.*"]
|
|
56
|
+
follow_untyped_imports = true
|
|
57
|
+
|
|
58
|
+
[tool.ruff]
|
|
59
|
+
indent-width = 3
|
|
60
|
+
line-length = 110
|
|
61
|
+
cache-dir = "/tmp/python-js-ruff-cache"
|
|
62
|
+
target-version = "py312"
|
|
63
|
+
|
|
64
|
+
[tool.ruff.lint]
|
|
65
|
+
select = ["E4", "E7", "E9", "F", "I"]
|
|
66
|
+
|
|
67
|
+
[tool.ruff.lint.isort]
|
|
68
|
+
known-first-party = ["benchmark_build", "benchmark_host", "browser_compile", "browser_measurement", "browser_source", "compiler_command", "compiler_modes", "http_process", "javascript_output", "kernels", "meshing", "meshing_vectors", "numeric_ir", "numeric_source", "pscript_backend", "triangle_command", "triangle_native", "triangle_render", "webgpu_bindings", "workloads"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Runtime ownership: page -> synchronous measurement -> binary download and mesh preview.
|
|
2
|
+
from browser import document, window
|
|
3
|
+
|
|
4
|
+
from browser_measurement import measure_page
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
measure_page(
|
|
8
|
+
document.body.getAttribute("data-case"),
|
|
9
|
+
document.body.getAttribute("data-kind"),
|
|
10
|
+
window.location.search,
|
|
11
|
+
)
|
|
12
|
+
except Exception as error:
|
|
13
|
+
document.getElementById("status").textContent = str(error)
|
|
14
|
+
document.getElementById("status").setAttribute("data-state", "error")
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Runtime ownership: page -> awaited hardware GPU/device -> canvas preview -> mapped readback -> binary download.
|
|
2
|
+
from browser import document, window
|
|
3
|
+
|
|
4
|
+
from triangle_render import create_pipeline, draw
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def render_page():
|
|
8
|
+
try:
|
|
9
|
+
if window.navigator.gpu:
|
|
10
|
+
adapter = await window.navigator.gpu.requestAdapter({"powerPreference": "high-performance"})
|
|
11
|
+
else:
|
|
12
|
+
raise ValueError("This browser does not expose WebGPU")
|
|
13
|
+
if adapter and adapter.info.isFallbackAdapter is False:
|
|
14
|
+
device = await adapter.requestDevice()
|
|
15
|
+
else:
|
|
16
|
+
raise ValueError("WebGPU requires a hardware adapter")
|
|
17
|
+
try:
|
|
18
|
+
device.pushErrorScope("validation")
|
|
19
|
+
canvas = document.getElementById("screen")
|
|
20
|
+
context = canvas.getContext("webgpu")
|
|
21
|
+
texture_format = window.navigator.gpu.getPreferredCanvasFormat()
|
|
22
|
+
context.configure({"device": device, "format": texture_format, "alphaMode": "opaque"})
|
|
23
|
+
try:
|
|
24
|
+
pipeline = create_pipeline(device, texture_format)
|
|
25
|
+
draw(device, pipeline, context.getCurrentTexture())
|
|
26
|
+
target = device.createTexture(
|
|
27
|
+
{
|
|
28
|
+
"size": [256, 256, 1],
|
|
29
|
+
"format": "rgba8unorm",
|
|
30
|
+
"usage": window.GPUTextureUsage.RENDER_ATTACHMENT | window.GPUTextureUsage.COPY_SRC,
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
try:
|
|
34
|
+
readback = device.createBuffer(
|
|
35
|
+
{
|
|
36
|
+
"size": 256 * 256 * 4,
|
|
37
|
+
"usage": window.GPUBufferUsage.COPY_DST | window.GPUBufferUsage.MAP_READ,
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
try:
|
|
41
|
+
reference_pipeline = create_pipeline(device, "rgba8unorm")
|
|
42
|
+
draw(device, reference_pipeline, target)
|
|
43
|
+
encoder = device.createCommandEncoder()
|
|
44
|
+
encoder.copyTextureToBuffer(
|
|
45
|
+
{"texture": target},
|
|
46
|
+
{"buffer": readback, "offset": 0, "bytesPerRow": 256 * 4, "rowsPerImage": 256},
|
|
47
|
+
[256, 256, 1],
|
|
48
|
+
)
|
|
49
|
+
device.queue.submit([encoder.finish()])
|
|
50
|
+
await readback.mapAsync(window.GPUMapMode.READ)
|
|
51
|
+
try:
|
|
52
|
+
pixels = window.Uint8Array.__new__(readback.getMappedRange())
|
|
53
|
+
# Blob copies bytes before unmap; the host reconciles the downloaded pixels.
|
|
54
|
+
blob = window.Blob.__new__([pixels], {"type": "application/octet-stream"})
|
|
55
|
+
finally:
|
|
56
|
+
readback.unmap()
|
|
57
|
+
finally:
|
|
58
|
+
readback.destroy()
|
|
59
|
+
finally:
|
|
60
|
+
target.destroy()
|
|
61
|
+
error = await device.popErrorScope()
|
|
62
|
+
if error:
|
|
63
|
+
raise ValueError(error.message)
|
|
64
|
+
link = document.getElementById("download")
|
|
65
|
+
link.href = window.URL.createObjectURL(blob)
|
|
66
|
+
link.download = "triangle.rgba"
|
|
67
|
+
link.hidden = False
|
|
68
|
+
document.getElementById("status").textContent = "Triangle ready."
|
|
69
|
+
document.getElementById("status").setAttribute("data-state", "done")
|
|
70
|
+
except:
|
|
71
|
+
context.unconfigure()
|
|
72
|
+
raise
|
|
73
|
+
except:
|
|
74
|
+
device.destroy()
|
|
75
|
+
raise
|
|
76
|
+
except Exception as error:
|
|
77
|
+
document.getElementById("status").textContent = str(error)
|
|
78
|
+
document.getElementById("status").setAttribute("data-state", "error")
|
|
79
|
+
raise
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
render_page()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Runtime ownership: comparison/demo -> build -> server -> browser -> verification and reports -> cleanup.
|
|
2
|
+
from argparse import ArgumentParser
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from subprocess import run
|
|
5
|
+
from sys import executable
|
|
6
|
+
from tempfile import mkdtemp
|
|
7
|
+
|
|
8
|
+
from benchmark_build import build_all
|
|
9
|
+
from benchmark_host import check
|
|
10
|
+
from triangle_command import build_triangle, check_triangle
|
|
11
|
+
from triangle_native import show_triangle
|
|
12
|
+
from workloads import Workload, workloads
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
parser = ArgumentParser(description="Compare browser-py with other compilers or run the GPU demo")
|
|
16
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
17
|
+
commands.add_parser("build", help="Build comparison pages in temporary memory")
|
|
18
|
+
serve = commands.add_parser("serve", help="Build and serve the compiler comparisons")
|
|
19
|
+
serve.add_argument("--host", default="127.0.0.1")
|
|
20
|
+
serve.add_argument("--port", type=int, default=8000)
|
|
21
|
+
smoke = commands.add_parser("check", help="Verify every compiler and workload in a browser")
|
|
22
|
+
measure = commands.add_parser("measure", help="Record verified browser timings")
|
|
23
|
+
for command in (smoke, measure):
|
|
24
|
+
command.add_argument("--browser", choices=("firefox", "chromium", "webkit", "all"), default="firefox")
|
|
25
|
+
measure.add_argument("--suite", choices=("meshing", "scalar", "all"), default="meshing")
|
|
26
|
+
measure.add_argument(
|
|
27
|
+
"--case", dest="cases", action="append", choices=tuple(item.name for item in workloads())
|
|
28
|
+
)
|
|
29
|
+
measure.add_argument("--rounds", type=int, default=11)
|
|
30
|
+
triangle = commands.add_parser("triangle", help="Run the shared Python GPU example")
|
|
31
|
+
triangle_commands = triangle.add_subparsers(dest="triangle_command", required=True)
|
|
32
|
+
triangle_commands.add_parser("build", help="Build the browser app in temporary memory")
|
|
33
|
+
triangle_commands.add_parser("check", help="Verify the desktop window and Firefox pixels")
|
|
34
|
+
triangle_native = triangle_commands.add_parser("native", help="Open the regular Python desktop app")
|
|
35
|
+
triangle_native.add_argument(
|
|
36
|
+
"--frames", type=int, default=0, help="Exit after this many frames; zero runs until closed"
|
|
37
|
+
)
|
|
38
|
+
triangle_serve = triangle_commands.add_parser("serve", help="Build and serve the browser app")
|
|
39
|
+
triangle_serve.add_argument("--host", default="127.0.0.1")
|
|
40
|
+
triangle_serve.add_argument("--port", type=int, default=8000)
|
|
41
|
+
args = parser.parse_args()
|
|
42
|
+
if args.command == "measure" and args.rounds < 3:
|
|
43
|
+
parser.error("--rounds must be at least three")
|
|
44
|
+
if args.command == "triangle" and args.triangle_command == "native":
|
|
45
|
+
show_triangle(args.frames)
|
|
46
|
+
print(
|
|
47
|
+
"Orchestration: command -> native window -> synchronous GPU initialization -> ordered frames -> present -> cleanup"
|
|
48
|
+
)
|
|
49
|
+
raise SystemExit(0)
|
|
50
|
+
output = Path(mkdtemp(prefix="browser-py-comparisons-", dir="/tmp"))
|
|
51
|
+
if args.command == "triangle":
|
|
52
|
+
build_triangle(output)
|
|
53
|
+
else:
|
|
54
|
+
build_all(output)
|
|
55
|
+
if args.command == "serve" or (args.command == "triangle" and args.triangle_command == "serve"):
|
|
56
|
+
run(
|
|
57
|
+
[
|
|
58
|
+
executable,
|
|
59
|
+
"-B",
|
|
60
|
+
"-m",
|
|
61
|
+
"http.server",
|
|
62
|
+
str(args.port),
|
|
63
|
+
"--bind",
|
|
64
|
+
args.host,
|
|
65
|
+
"--directory",
|
|
66
|
+
str(output),
|
|
67
|
+
],
|
|
68
|
+
check=True,
|
|
69
|
+
)
|
|
70
|
+
elif args.command == "triangle" and args.triangle_command == "check":
|
|
71
|
+
check_triangle(output)
|
|
72
|
+
elif args.command == "check":
|
|
73
|
+
for browser in ("firefox", "chromium", "webkit") if args.browser == "all" else (args.browser,):
|
|
74
|
+
check(output, workloads(), 3, True, browser)
|
|
75
|
+
elif args.command == "measure":
|
|
76
|
+
selected: list[Workload] = []
|
|
77
|
+
for item in workloads():
|
|
78
|
+
if args.cases is not None:
|
|
79
|
+
if item.name in args.cases:
|
|
80
|
+
selected.append(item)
|
|
81
|
+
elif args.suite == "all" or (item.kind == "scalar") == (args.suite == "scalar"):
|
|
82
|
+
selected.append(item)
|
|
83
|
+
for browser in ("firefox", "chromium", "webkit") if args.browser == "all" else (args.browser,):
|
|
84
|
+
check(output, tuple(selected), args.rounds, False, browser)
|
|
85
|
+
if args.command == "triangle":
|
|
86
|
+
if args.triangle_command == "check":
|
|
87
|
+
print(
|
|
88
|
+
"Orchestration: command -> build -> native reference and window -> HTTP process -> Firefox -> pixel verification -> cleanup"
|
|
89
|
+
)
|
|
90
|
+
elif args.triangle_command == "serve":
|
|
91
|
+
print(
|
|
92
|
+
"Orchestration: command -> build -> HTTP process; page -> awaited GPU initialization -> shared rendering -> pixel download"
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
print("Orchestration: command -> WebGPU bindings -> compiler -> generated browser app")
|
|
96
|
+
elif args.command == "build":
|
|
97
|
+
print("Orchestration: command -> build -> generated pages")
|
|
98
|
+
elif args.command == "serve":
|
|
99
|
+
print("Orchestration: command -> build -> HTTP process")
|
|
100
|
+
else:
|
|
101
|
+
print(
|
|
102
|
+
"Orchestration: command -> build -> HTTP process -> browser -> ordered pages -> verification -> reports"
|
|
103
|
+
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Runtime ownership: host -> HTTP process -> synchronous server -> ordered static-file requests.
|
|
2
|
+
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
|
3
|
+
from select import select
|
|
4
|
+
from socket import socket
|
|
5
|
+
from struct import pack
|
|
6
|
+
from sys import stdout
|
|
7
|
+
from time import monotonic
|
|
8
|
+
|
|
9
|
+
if __name__ == "__main__":
|
|
10
|
+
with HTTPServer(("127.0.0.1", 0), SimpleHTTPRequestHandler) as server:
|
|
11
|
+
# The server publishes its bound port as one little-endian uint16; the host validates it before navigation.
|
|
12
|
+
stdout.buffer.write(pack("<H", server.server_address[1]))
|
|
13
|
+
stdout.buffer.flush()
|
|
14
|
+
# Browsers may preconnect silently; the server dispatches readable sockets and closes idle peers.
|
|
15
|
+
waiting: dict[socket, tuple[tuple[str, int], float]] = {}
|
|
16
|
+
try:
|
|
17
|
+
while True:
|
|
18
|
+
timeout: float | None = None
|
|
19
|
+
now = monotonic()
|
|
20
|
+
for pending in waiting.values():
|
|
21
|
+
remaining = pending[1] - now
|
|
22
|
+
if timeout is None or remaining < timeout:
|
|
23
|
+
timeout = remaining
|
|
24
|
+
ready = []
|
|
25
|
+
if timeout is None or timeout > 0:
|
|
26
|
+
ready = select([server.socket, *waiting], [], [], timeout)[0]
|
|
27
|
+
if server.socket in ready:
|
|
28
|
+
incoming = server.get_request()
|
|
29
|
+
incoming[0].settimeout(10)
|
|
30
|
+
waiting[incoming[0]] = (incoming[1], monotonic() + 10)
|
|
31
|
+
for connection in tuple(waiting):
|
|
32
|
+
pending = waiting[connection]
|
|
33
|
+
if connection in ready or monotonic() >= pending[1]:
|
|
34
|
+
del waiting[connection]
|
|
35
|
+
try:
|
|
36
|
+
if connection in ready:
|
|
37
|
+
SimpleHTTPRequestHandler(connection, pending[0], server)
|
|
38
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
39
|
+
pass
|
|
40
|
+
finally:
|
|
41
|
+
server.shutdown_request(connection)
|
|
42
|
+
finally:
|
|
43
|
+
for connection in waiting:
|
|
44
|
+
server.shutdown_request(connection)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from gzip import compress
|
|
2
|
+
from html import escape
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from shutil import copyfile
|
|
6
|
+
from time import perf_counter
|
|
7
|
+
|
|
8
|
+
from browser_source import compile_browser_source
|
|
9
|
+
from compiler_modes import MODES
|
|
10
|
+
from javascript_output import write_javascript
|
|
11
|
+
from numeric_ir import compile_js
|
|
12
|
+
from pscript_backend import compile_pscript
|
|
13
|
+
from workloads import Workload, workloads
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def page(config: Workload, mode: str) -> str:
|
|
17
|
+
return f'''<!doctype html>
|
|
18
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width">
|
|
19
|
+
<title>{escape(config.title)} - {mode}</title><link rel="stylesheet" href="../style.css"></head>
|
|
20
|
+
<body data-case="{config.name}" data-kind="{config.kind}"><main>
|
|
21
|
+
<nav><a href="../index.html">All benchmarks</a></nav><h1>{escape(config.title)}</h1><h2>{mode}</h2>
|
|
22
|
+
<p>Keep this tab visible. Timing covers synchronous CPU work, not loading or drawing.</p>
|
|
23
|
+
<form method="get"><label>Work per sample <input id="iterations" name="n" type="number" min="1" value="{config.iterations}" required></label>
|
|
24
|
+
<label>Samples <input id="rounds" name="rounds" type="number" min="3" value="11" required></label>
|
|
25
|
+
<button type="submit">Rerun benchmarks</button></form>
|
|
26
|
+
<p id="status" data-state="running">Running...</p><pre id="report"></pre>
|
|
27
|
+
<a id="download" hidden>Download binary results</a>
|
|
28
|
+
<canvas id="mesh-view" width="760" height="460" aria-label="Mesh preview, not timed" hidden></canvas>
|
|
29
|
+
</main><script type="module" src="__browser_benchmark__.js"></script></body></html>'''
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def build_all(output: Path) -> None:
|
|
33
|
+
source = Path(__file__).resolve().parent
|
|
34
|
+
copyfile(source / "style.css", output / "style.css")
|
|
35
|
+
lines = [
|
|
36
|
+
"<!doctype html><html lang='en'><meta charset='utf-8'><title>browser-py compiler comparisons</title>",
|
|
37
|
+
"<link rel='stylesheet' href='style.css'><main><h1>browser-py compiler comparisons</h1>",
|
|
38
|
+
"<p>Compare the same Python workloads in your browser. Keep the tab visible.</p>",
|
|
39
|
+
"<p>Compare browser-py with PScript on the same typed numeric Python.</p>",
|
|
40
|
+
"<p>browser-py uses mypyc IR; it is experimental and not an official mypy JavaScript backend.</p>",
|
|
41
|
+
"<p>Optimized flags change semantics; these workloads use their shared numeric subset.</p>",
|
|
42
|
+
"<p>Vector records measure procedural calls and allocation costs, not proof of JIT inlining.</p>",
|
|
43
|
+
]
|
|
44
|
+
for mode in MODES:
|
|
45
|
+
target = output / mode
|
|
46
|
+
target.mkdir()
|
|
47
|
+
started = perf_counter()
|
|
48
|
+
compiler = mode.rsplit("-", 1)[0]
|
|
49
|
+
minified = mode.endswith("-minified")
|
|
50
|
+
for name in ("workloads", "browser_measurement", "__browser_benchmark__"):
|
|
51
|
+
javascript = compile_browser_source(source / f"{name}.py")
|
|
52
|
+
write_javascript(javascript, target / f"{name}.js", minified)
|
|
53
|
+
if compiler == "browser-py":
|
|
54
|
+
for name in ("kernels", "meshing", "meshing_vectors"):
|
|
55
|
+
compiled = compile_js(source / f"{name}.py")
|
|
56
|
+
write_javascript(compiled[0], target / f"{name}.js", minified)
|
|
57
|
+
(target / f"{name}.ir").write_text(compiled[1])
|
|
58
|
+
if compiler in ("pscript", "pscript-opt"):
|
|
59
|
+
for name in ("kernels", "meshing", "meshing_vectors"):
|
|
60
|
+
javascript = compile_pscript(source / f"{name}.py", optimized=compiler == "pscript-opt")
|
|
61
|
+
write_javascript(javascript, target / f"{name}.js", minified)
|
|
62
|
+
elapsed = perf_counter() - started
|
|
63
|
+
size = 0
|
|
64
|
+
gzip_size = 0
|
|
65
|
+
for script in target.glob("*.js"):
|
|
66
|
+
size += script.stat().st_size
|
|
67
|
+
gzip_size += len(compress(script.read_bytes(), mtime=0))
|
|
68
|
+
lines.append(
|
|
69
|
+
f"<h2>{mode}</h2><p>{elapsed:.3f} seconds to build; {size} bytes JS; {gzip_size} bytes gzip</p><ul>"
|
|
70
|
+
)
|
|
71
|
+
for config in workloads():
|
|
72
|
+
(target / f"{config.name}.html").write_text(page(config, mode))
|
|
73
|
+
lines.append(
|
|
74
|
+
f'<li><a href="{mode}/{config.name}.html?n={config.iterations}&rounds=11">{escape(config.title)}</a></li>'
|
|
75
|
+
)
|
|
76
|
+
lines.append("</ul>")
|
|
77
|
+
lines.append(
|
|
78
|
+
f"<p>mypy {version('mypy')}; PScript {version('pscript')}; rjsmin {version('rjsmin')}</p></main></html>"
|
|
79
|
+
)
|
|
80
|
+
(output / "index.html").write_text("\n".join(lines))
|
|
81
|
+
print(f"Built {output / 'index.html'}", flush=True)
|