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.
Files changed (53) hide show
  1. browser_py-0.1.0/AGENTS.md +45 -0
  2. browser_py-0.1.0/MANIFEST.in +4 -0
  3. browser_py-0.1.0/PKG-INFO +53 -0
  4. browser_py-0.1.0/README.md +34 -0
  5. browser_py-0.1.0/benchmarks/legacy_outputs.pickle +0 -0
  6. browser_py-0.1.0/pyproject.toml +68 -0
  7. browser_py-0.1.0/setup.cfg +4 -0
  8. browser_py-0.1.0/source/__browser_benchmark__.py +14 -0
  9. browser_py-0.1.0/source/__browser_py__.py +5 -0
  10. browser_py-0.1.0/source/__browser_triangle__.py +82 -0
  11. browser_py-0.1.0/source/__comparison__.py +103 -0
  12. browser_py-0.1.0/source/__http_server__.py +44 -0
  13. browser_py-0.1.0/source/benchmark_build.py +81 -0
  14. browser_py-0.1.0/source/benchmark_host.py +254 -0
  15. browser_py-0.1.0/source/browser_compile.py +27 -0
  16. browser_py-0.1.0/source/browser_measurement.py +131 -0
  17. browser_py-0.1.0/source/browser_py.egg-info/PKG-INFO +53 -0
  18. browser_py-0.1.0/source/browser_py.egg-info/SOURCES.txt +51 -0
  19. browser_py-0.1.0/source/browser_py.egg-info/dependency_links.txt +1 -0
  20. browser_py-0.1.0/source/browser_py.egg-info/entry_points.txt +2 -0
  21. browser_py-0.1.0/source/browser_py.egg-info/requires.txt +14 -0
  22. browser_py-0.1.0/source/browser_py.egg-info/top_level.txt +7 -0
  23. browser_py-0.1.0/source/browser_source.py +598 -0
  24. browser_py-0.1.0/source/compiler_command.py +53 -0
  25. browser_py-0.1.0/source/compiler_modes.py +10 -0
  26. browser_py-0.1.0/source/http_process.py +49 -0
  27. browser_py-0.1.0/source/javascript_output.py +9 -0
  28. browser_py-0.1.0/source/kernels.py +62 -0
  29. browser_py-0.1.0/source/meshing.py +260 -0
  30. browser_py-0.1.0/source/meshing_vectors.py +160 -0
  31. browser_py-0.1.0/source/numeric_ir.py +315 -0
  32. browser_py-0.1.0/source/numeric_source.py +266 -0
  33. browser_py-0.1.0/source/pscript_backend.py +59 -0
  34. browser_py-0.1.0/source/style.css +10 -0
  35. browser_py-0.1.0/source/triangle_command.py +93 -0
  36. browser_py-0.1.0/source/triangle_native.py +98 -0
  37. browser_py-0.1.0/source/triangle_render.py +46 -0
  38. browser_py-0.1.0/source/webgpu_bindings.py +308 -0
  39. browser_py-0.1.0/source/workloads.py +53 -0
  40. browser_py-0.1.0/tests/shootyfield_surface_nets.bin +0 -0
  41. browser_py-0.1.0/tests/test_browser_compile.py +118 -0
  42. browser_py-0.1.0/tests/test_browser_source.py +118 -0
  43. browser_py-0.1.0/tests/test_build.py +117 -0
  44. browser_py-0.1.0/tests/test_compiler_boundary.py +50 -0
  45. browser_py-0.1.0/tests/test_geometry.py +173 -0
  46. browser_py-0.1.0/tests/test_http_process.py +98 -0
  47. browser_py-0.1.0/tests/test_numeric_source.py +223 -0
  48. browser_py-0.1.0/tests/test_packaging.py +31 -0
  49. browser_py-0.1.0/tests/test_pscript.py +46 -0
  50. browser_py-0.1.0/tests/test_reports.py +197 -0
  51. browser_py-0.1.0/tests/test_source_invariants.py +86 -0
  52. browser_py-0.1.0/tests/test_webgpu.py +241 -0
  53. 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,4 @@
1
+ include AGENTS.md README.md pyproject.toml uv.lock
2
+ recursive-include source *.py *.css
3
+ recursive-include tests *.py *.bin
4
+ recursive-include benchmarks *.pickle
@@ -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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,5 @@
1
+ # Runtime ownership: command -> source admission -> mypyc IR -> staged JavaScript and IR -> published files.
2
+ from compiler_command import main
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -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}&amp;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)