diff --git a/.audit/oberstet_fix_1983.md b/.audit/oberstet_fix_1983.md new file mode 100644 index 000000000..998fb7a9c --- /dev/null +++ b/.audit/oberstet_fix_1983.md @@ -0,0 +1,8 @@ +- [ ] I did **not** use any AI-assistance tools to help create this pull request. +- [x] I **did** use AI-assistance tools to *help* create this pull request. +- [x] I have read, understood and followed the project's AI_POLICY.md when creating code, documentation etc. for this pull request. + +Submitted by: @oberstet +Date: 2026-10-13 +Related issue(s): #1983 +Branch: oberstet:fix_1983 diff --git a/docs/index.rst b/docs/index.rst index 3d0a84715..bc14d953d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -44,6 +44,7 @@ Contents overview installation wheels-inventory + memory-footprint getting-started programming-guide/index architecture/statically-typed-region-lifecycle-python.md diff --git a/docs/memory-footprint.rst b/docs/memory-footprint.rst new file mode 100644 index 000000000..53b322e68 --- /dev/null +++ b/docs/memory-footprint.rst @@ -0,0 +1,144 @@ +.. _memory-footprint: + +Memory Footprint +================ + +This page shows how much memory a Python process uses after importing autobahn, +compared with the frameworks and libraries it builds on, for CPython and PyPy. It +helps to see what a given use of autobahn costs, and it documents how that cost +changed across releases. + +Background +---------- + +Since 25.12.1, autobahn includes its own copy of the +`FlatBuffers `_ runtime, which it needs for WAMP +FlatBuffers serialization (before that, ``flatbuffers`` was an optional dependency, +the ``serialization`` extra). That runtime imports **numpy** at module level whenever +numpy is installed. Up to 26.7.1, autobahn imported the runtime eagerly, so every +process that imported autobahn also loaded numpy - including processes that use +only WebSocket, not WAMP, such as Django Channels applications served by +`Daphne`_ [#fn-daphne]_. See `issue #1983 `_. + +Since then, the goals are: + +1. The autobahn package, and the wheels we build, include the vendored FlatBuffers + runtime, including reflection. +2. For an application that depends on both autobahn and zlmdb, both of which vendor + FlatBuffers, such as Crossbar.io, the two vendored versions are exactly the same: + :func:`autobahn.check_zlmdb_flatbuffers_version_in_sync` checks that. +3. A user of autobahn who uses **WebSocket only, not WAMP**, with either Twisted or + asyncio, loads no FlatBuffers code and no numpy. + +autobahn now loads the FlatBuffers runtime only when a FlatBuffers message is first +built or parsed. + +What is measured +---------------- + +Each row is one import, run in a fresh interpreter. The value is the peak resident +set size of that process right after the import, in MB (MiB): ``VmHWM`` from +``/proc/self/status`` on Linux. It is the memory the import costs at least, not the +memory a running application uses under load. + +(``ru_maxrss`` from :func:`resource.getrusage` would be the obvious choice, but on +Linux it carries over the peak memory of the process that started the measured one, +so for a child of a Python process it never drops below that parent's size.) + +* Every venv has numpy installed, so the tables show whether an import loads it. + A value marked **†** means numpy was loaded by that import. +* Each import runs once as a warm-up (which may compile ``.pyc`` files) and is then + measured 5 times; the table shows the median. +* Each autobahn version is measured in its own fresh venv, together with Twisted, + cryptography, numpy and Daphne. The rows that do not involve autobahn are measured + in every venv, so their spread across the columns shows the measurement noise. +* Crossbar.io [#fn-crossbar]_ is measured in a separate venv per autobahn version, with + crossbar |crossbar_version| from PyPI. No crossbar release supports autobahn 25.10.2, + hence "n/a". +* Columns: **25.10.2** is the last release with FlatBuffers as an optional + dependency, measured *without* that extra (as Daphne installs it). **26.7.1** is the + latest release. The last column is the current development version. + +.. |crossbar_version| replace:: 26.7.1 + +CPython 3.14 +------------ + +.. include:: memory-footprint/cpy314.inc + +.. include:: memory-footprint/cpy314-versions.inc + +PyPy 3.11 +--------- + +PyPy is a different runtime (a tracing JIT compiler with its own garbage collector), +so compare these values with each other, not with the CPython table. On PyPy, the +venvs use numpy 2.4.x, the last numpy release with PyPy wheels. + +.. include:: memory-footprint/pypy311.inc + +.. include:: memory-footprint/pypy311-versions.inc + +Findings +-------- + +* **This change:** every row that imports autobahn drops by about 13-17 MB on + CPython, and by about 18-24 MB on PyPy, and none of them loads numpy any more. For + example, ``from daphne.server import Server`` drops from 64.6 MB to 51.6 MB on CPython + 3.14, and ``import autobahn`` alone is back to the size of the bare interpreter. +* **Compared with 25.10.2:** the WebSocket and WAMP rows remain about 1.3-1.6 MB above + 25.10.2 on CPython (about 0.4-0.8 MB on PyPy). In 25.10.2 without the ``serialization`` + extra, cbor2 was not installed and the WAMP message module skipped that import. Since + 26.x, cbor2 is a regular dependency. +* **WebSocket only:** with Twisted, most of what remains is Twisted itself; with + asyncio, the asyncio standard library. +* **Twisted and cryptography:** ``import autobahn.twisted.websocket`` still loads the + native ``cryptography`` bindings (about 8 MB on CPython), because + ``autobahn/twisted/__init__.py`` imports ``autobahn.twisted.wamp``, which imports + ``autobahn.wamp.auth``. A pure WebSocket user does not need them. ``import cryptography`` + alone costs next to nothing; the cost comes with the native bindings + (``from cryptography.hazmat.primitives import hashes``). +* **WAMP:** WAMP users still load the FlatBuffers runtime (and numpy, if installed) + as soon as they import ``autobahn.wamp.serializer``, also when they use another + serializer. +* **Crossbar.io** loads numpy itself, so its router worker loads it regardless of + this change; on CPython, its footprint is unchanged. On PyPy, the crossbar row comes + out about 16 MB lower with this change, in repeated measurements, but we have not + analysed why. Two things to keep in mind: on PyPy, the samples of a single import + can spread by about 10 MB from run to run (garbage collection and JIT timing), and + the two crossbar venvs differ in more than autobahn: on PyPy, autobahn 26.7.1 installs + cbor2 6.x, while the development version installs cbor2 5.9.0 (see the versions + below the PyPy table). + +Reproducing +----------- + +The scripts are in `scripts/memory-footprint/ +`_. +They create throwaway venvs with `uv `_, measure, and +regenerate the tables on this page: + +.. code-block:: console + + just memory-footprint cpy314 + just memory-footprint pypy311 + +``measure.py`` alone measures the venv it runs in, e.g. for your own environment: + +.. code-block:: console + + python scripts/memory-footprint/measure.py --variant mine + +.. rubric:: Footnotes + +.. [#fn-daphne] `Daphne `_ + (`repository `_), the ASGI server of Django + Channels. Its `server `_ + and `WebSocket protocol `_ + use ``autobahn.twisted.websocket``. Row: ``from daphne.server import Server``. + +.. [#fn-crossbar] `Crossbar.io `_, the WAMP + router. Row: ``from crossbar.edge.worker.router import ExtRouterController``, the + `router worker `_. + +.. _Daphne: https://github.com/django/daphne diff --git a/docs/memory-footprint/cpy314-versions.inc b/docs/memory-footprint/cpy314-versions.inc new file mode 100644 index 000000000..5a5f7168b --- /dev/null +++ b/docs/memory-footprint/cpy314-versions.inc @@ -0,0 +1,7 @@ +* **25.10.2** (venv): autobahn 25.10.2, txaio 26.6.1, twisted 26.4.0, cryptography 50.0.2, numpy 2.5.4, daphne 4.2.3 +* **26.7.1** (venv): autobahn 26.7.1, txaio 26.6.1, twisted 26.4.0, cryptography 50.0.2, numpy 2.5.4, cbor2 6.1.5, daphne 4.2.3 +* **26.7.1** (crossbar venv): autobahn 26.7.1, txaio 26.6.1, twisted 26.4.0, cryptography 50.0.2, numpy 2.5.4, cbor2 6.1.5, crossbar 26.7.1, zlmdb 26.7.1 +* **fix_1983** (venv): autobahn 26.10.1.dev1, txaio 26.6.1, twisted 26.4.0, cryptography 50.0.2, numpy 2.5.4, cbor2 6.1.5, daphne 4.2.3 +* **fix_1983** (crossbar venv): autobahn 26.10.1.dev1, txaio 26.6.1, twisted 26.4.0, cryptography 50.0.2, numpy 2.5.4, cbor2 6.1.5, crossbar 26.7.1, zlmdb 26.7.1 + +Interpreter: ``CPython 3.14.7`` (3.14.7 (main, Sep 1 2026, 14:18:09) [Clang 22.1.3 ]); Linux 6.12.95+deb12-amd64 x86_64, median of 5 runs. diff --git a/docs/memory-footprint/cpy314.inc b/docs/memory-footprint/cpy314.inc new file mode 100644 index 000000000..56dfac0a4 --- /dev/null +++ b/docs/memory-footprint/cpy314.inc @@ -0,0 +1,89 @@ +.. list-table:: + :header-rows: 1 + :widths: 12 46 14 14 14 + + * - group + - import + - 25.10.2 + - 26.7.1 + - fix_1983 + * - interpreter + - ``bare interpreter`` + - 11.9 MB + - 11.9 MB + - 12.0 MB + * - cryptography + - ``import cryptography`` + - 12.0 MB + - 12.0 MB + - 11.9 MB + * - + - ``from cryptography.hazmat.primitives import hashes`` + - 19.9 MB + - 19.9 MB + - 19.8 MB + * - numpy + - ``import numpy`` + - 28.4 MB † + - 28.4 MB † + - 28.4 MB † + * - asyncio + - ``import asyncio`` + - 18.1 MB + - 18.2 MB + - 18.2 MB + * - twisted + - ``from twisted.internet import reactor`` + - 43.3 MB + - 43.3 MB + - 43.4 MB + * - txaio + - ``import txaio`` + - 12.0 MB + - 12.1 MB + - 12.1 MB + * - + - ``import txaio; txaio.use_asyncio()`` + - 18.3 MB + - 18.3 MB + - 18.3 MB + * - + - ``import txaio; txaio.use_twisted()`` + - 26.5 MB + - 26.5 MB + - 26.5 MB + * - autobahn + - ``import autobahn`` + - 12.1 MB + - 28.8 MB † + - 12.0 MB + * - + - ``txaio.use_asyncio(); import autobahn.wamp`` + - 31.9 MB + - 47.6 MB † + - 33.2 MB + * - + - ``txaio.use_twisted(); import autobahn.wamp`` + - 38.4 MB + - 52.9 MB † + - 39.8 MB + * - + - ``import autobahn.asyncio.websocket`` + - 36.3 MB + - 51.1 MB † + - 37.8 MB + * - + - ``import autobahn.twisted.websocket`` + - 48.9 MB + - 63.2 MB † + - 50.3 MB + * - daphne + - ``from daphne.server import Server`` + - 50.3 MB + - 64.6 MB † + - 51.6 MB + * - crossbar + - ``from crossbar.edge.worker.router import ExtRouterController`` + - n/a + - 144.7 MB † + - 144.7 MB † diff --git a/docs/memory-footprint/pypy311-versions.inc b/docs/memory-footprint/pypy311-versions.inc new file mode 100644 index 000000000..d68ae3d0d --- /dev/null +++ b/docs/memory-footprint/pypy311-versions.inc @@ -0,0 +1,7 @@ +* **25.10.2** (venv): autobahn 25.10.2, txaio 26.6.1, twisted 26.4.0, cryptography 50.0.2, numpy 2.4.6, daphne 4.2.3 +* **26.7.1** (venv): autobahn 26.7.1, txaio 26.6.1, twisted 26.4.0, cryptography 50.0.2, numpy 2.4.6, cbor2 6.1.5, daphne 4.2.3 +* **26.7.1** (crossbar venv): autobahn 26.7.1, txaio 26.6.1, twisted 26.4.0, cryptography 50.0.2, numpy 2.4.6, cbor2 6.1.5, crossbar 26.7.1, zlmdb 26.7.1 +* **fix_1983** (venv): autobahn 26.10.1.dev1, txaio 26.6.1, twisted 26.4.0, cryptography 50.0.2, numpy 2.4.6, cbor2 5.9.0, daphne 4.2.3 +* **fix_1983** (crossbar venv): autobahn 26.10.1.dev1, txaio 26.6.1, twisted 26.4.0, cryptography 50.0.2, numpy 2.4.6, cbor2 5.9.0, crossbar 26.7.1, zlmdb 26.7.1 + +Interpreter: ``PyPy 3.11.15`` (3.11.15 (194f9f44b505, May 25 2026, 19:34:11) [PyPy 7.3.23 with GCC 10.2.1 20210130 (Red Hat 10.2.1-11)]); Linux 6.12.95+deb12-amd64 x86_64, median of 5 runs. diff --git a/docs/memory-footprint/pypy311.inc b/docs/memory-footprint/pypy311.inc new file mode 100644 index 000000000..8cd895aba --- /dev/null +++ b/docs/memory-footprint/pypy311.inc @@ -0,0 +1,89 @@ +.. list-table:: + :header-rows: 1 + :widths: 12 46 14 14 14 + + * - group + - import + - 25.10.2 + - 26.7.1 + - fix_1983 + * - interpreter + - ``bare interpreter`` + - 64.4 MB + - 64.4 MB + - 64.4 MB + * - cryptography + - ``import cryptography`` + - 64.3 MB + - 64.4 MB + - 64.4 MB + * - + - ``from cryptography.hazmat.primitives import hashes`` + - 72.1 MB + - 72.0 MB + - 72.0 MB + * - numpy + - ``import numpy`` + - 88.1 MB † + - 88.1 MB † + - 88.2 MB † + * - asyncio + - ``import asyncio`` + - 78.7 MB + - 78.9 MB + - 78.8 MB + * - twisted + - ``from twisted.internet import reactor`` + - 106.1 MB + - 106.1 MB + - 106.1 MB + * - txaio + - ``import txaio`` + - 64.4 MB + - 64.4 MB + - 64.2 MB + * - + - ``import txaio; txaio.use_asyncio()`` + - 79.0 MB + - 79.0 MB + - 79.1 MB + * - + - ``import txaio; txaio.use_twisted()`` + - 88.0 MB + - 87.9 MB + - 88.0 MB + * - autobahn + - ``import autobahn`` + - 64.4 MB + - 87.9 MB † + - 64.4 MB + * - + - ``txaio.use_asyncio(); import autobahn.wamp`` + - 93.0 MB + - 112.4 MB † + - 93.7 MB + * - + - ``txaio.use_twisted(); import autobahn.wamp`` + - 100.1 MB + - 118.8 MB † + - 100.5 MB + * - + - ``import autobahn.asyncio.websocket`` + - 98.4 MB + - 117.2 MB † + - 99.0 MB + * - + - ``import autobahn.twisted.websocket`` + - 113.2 MB + - 133.5 MB † + - 113.8 MB + * - daphne + - ``from daphne.server import Server`` + - 114.8 MB + - 135.8 MB † + - 115.6 MB + * - crossbar + - ``from crossbar.edge.worker.router import ExtRouterController`` + - n/a + - 239.5 MB † + - 224.1 MB † diff --git a/justfile b/justfile index 2377099bc..7882db5b3 100644 --- a/justfile +++ b/justfile @@ -1325,7 +1325,8 @@ test-asyncio venv="" use_nvx="": (install-tools venv) (install-dev venv) # Run the import smoke test - imports core autobahn modules with crypto extras # present, guarding against import-time annotation regressions (e.g. #1878) that -# only surface on CPython < 3.14. (usage: `just test-imports cpy311`) +# only surface on CPython < 3.14, and checks that WebSocket-only imports load +# neither FlatBuffers nor numpy (#1983). (usage: `just test-imports cpy311`) test-imports venv="": (install-tools venv) (install-dev venv) #!/usr/bin/env bash set -e @@ -1337,7 +1338,8 @@ test-imports venv="": (install-tools venv) (install-dev venv) fi VENV_PYTHON=$(just --quiet _get-venv-python "${VENV_NAME}") echo "==> Running import smoke test in ${VENV_NAME}..." - ${VENV_PYTHON} -m pytest -v src/autobahn/test/test_import_all.py + ${VENV_PYTHON} -m pytest -v src/autobahn/test/test_import_all.py \ + src/autobahn/test/test_import_websocket_only.py # Run the import smoke test against the LOWEST versions our direct dependencies # allow (uv --resolution lowest-direct), in a separate '-lowest' venv. This @@ -1813,6 +1815,11 @@ bump-flatbuffers: update-flatbuffers: ./scripts/update_flatbuffers.sh +# Measure import memory footprint in throwaway venvs, regenerate docs/memory-footprint.rst tables (usage: `just memory-footprint cpy314`) +memory-footprint env="cpy314" label="": + ./scripts/memory-footprint/run.sh {{ env }} {{ label }} + python3 ./scripts/memory-footprint/render.py {{ env }} + # Build wheel only (usage: `just build cpy314`) build venv="": (install-build-tools venv) #!/usr/bin/env bash diff --git a/pyproject.toml b/pyproject.toml index 6c1df64ba..c28c5de25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "autobahn" -version = "26.9.1.dev2" +version = "26.10.1.dev1" description = "WebSocket client & server library, WAMP real-time framework" readme = "README.md" requires-python = ">=3.11" diff --git a/scripts/memory-footprint/measure.py b/scripts/memory-footprint/measure.py new file mode 100755 index 000000000..5ac18ef17 --- /dev/null +++ b/scripts/memory-footprint/measure.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python +############################################################################### +# +# The MIT License (MIT) +# +# Copyright (c) typedef int GmbH +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +############################################################################### +""" +Measure the memory footprint (peak RSS, ``VmHWM`` from ``/proc/self/status``, Linux) +of importing autobahn and related packages, each in a fresh interpreter. + +Run this with the Python of the venv to measure (it uses ``sys.executable`` for the +child processes) - see ``run.sh``, which creates the venvs. The result is written as +JSON to stdout, or to the file given with ``--output``. + +Each scenario runs ``--runs`` times in a fresh process (``python -I``, without +``USE_TWISTED`` / ``USE_ASYNCIO`` in the environment), after one discarded warm-up +run (which may compile ``.pyc`` files). The median is reported, in KiB. A scenario whose imports are not installed in the venv +is reported as unavailable. +""" + +from __future__ import annotations + +import argparse +import json +import os +import platform +import statistics +import subprocess +import sys +from importlib import metadata + +# (group, label, code) - the order here is the order of the rows in the docs tables +SCENARIOS = [ + ("interpreter", "bare interpreter", "pass"), + ("cryptography", "import cryptography", "import cryptography"), + ( + "cryptography", + "from cryptography.hazmat.primitives import hashes", + "from cryptography.hazmat.primitives import hashes", + ), + ("numpy", "import numpy", "import numpy"), + ("asyncio", "import asyncio", "import asyncio"), + ( + "twisted", + "from twisted.internet import reactor", + "from twisted.internet import reactor", + ), + ("txaio", "import txaio", "import txaio"), + ("txaio", "import txaio; txaio.use_asyncio()", "import txaio; txaio.use_asyncio()"), + ("txaio", "import txaio; txaio.use_twisted()", "import txaio; txaio.use_twisted()"), + ("autobahn", "import autobahn", "import autobahn"), + ( + "autobahn", + "txaio.use_asyncio(); import autobahn.wamp", + "import txaio; txaio.use_asyncio(); import autobahn.wamp", + ), + ( + "autobahn", + "txaio.use_twisted(); import autobahn.wamp", + "import txaio; txaio.use_twisted(); import autobahn.wamp", + ), + ( + "autobahn", + "import autobahn.asyncio.websocket", + "import autobahn.asyncio.websocket", + ), + ( + "autobahn", + "import autobahn.twisted.websocket", + "import autobahn.twisted.websocket", + ), + ("daphne", "from daphne.server import Server", "from daphne.server import Server"), + ( + "crossbar", + "from crossbar.edge.worker.router import ExtRouterController", + "from crossbar.edge.worker.router import ExtRouterController", + ), +] + +# the packages whose versions are recorded with the results +PACKAGES = [ + "autobahn", + "txaio", + "twisted", + "cryptography", + "numpy", + "cbor2", + "daphne", + "crossbar", + "zlmdb", +] + +# runs in the child: execute the scenario, then report peak RSS and whether numpy +# got loaded; an import error of the scenario itself means "not installed here". +# +# Peak RSS is read as VmHWM from /proc/self/status: the high-water mark of this +# process's own address space, which starts fresh at exec(). getrusage()'s ru_maxrss +# is not usable here: on Linux it carries over the peak of the address space replaced +# by exec(), i.e. of the measuring parent process (this script). +CHILD = """ +import json, sys +try: + exec(sys.argv[1]) +except ImportError as e: + print(json.dumps({"error": f"{type(e).__name__}: {e}"})) +else: + with open("/proc/self/status") as f: + hwm = next(int(line.split()[1]) for line in f if line.startswith("VmHWM:")) + print(json.dumps({"maxrss_kib": hwm, "numpy": "numpy" in sys.modules})) +""" + + +def run_once(code: str) -> dict: + env = { + k: v for k, v in os.environ.items() if k not in ("USE_TWISTED", "USE_ASYNCIO") + } + result = subprocess.run( + [sys.executable, "-I", "-c", CHILD, code], + capture_output=True, + text=True, + env=env, + ) + if result.returncode != 0: + return { + "error": result.stderr.strip().splitlines()[-1] + if result.stderr + else "failed" + } + return json.loads(result.stdout.strip().splitlines()[-1]) + + +def measure(code: str, runs: int) -> dict: + first = run_once(code) + if "error" in first: + return {"available": False, "error": first["error"]} + samples = [run_once(code) for _ in range(runs)] + errors = [s["error"] for s in samples if "error" in s] + if errors: + return {"available": False, "error": errors[0]} + return { + "available": True, + "maxrss_kib": int(statistics.median(s["maxrss_kib"] for s in samples)), + "samples_kib": [s["maxrss_kib"] for s in samples], + "numpy": any(s["numpy"] for s in samples), + } + + +def installed_versions() -> dict: + versions = {} + for name in PACKAGES: + try: + versions[name] = metadata.version(name) + except metadata.PackageNotFoundError: + pass + return versions + + +def main() -> None: + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + "--variant", required=True, help="label of the autobahn variant, e.g. 26.7.1" + ) + parser.add_argument( + "--runs", type=int, default=5, help="measured runs per scenario (default: 5)" + ) + parser.add_argument( + "--only-group", action="append", help="measure only these groups (repeatable)" + ) + parser.add_argument( + "--skip-group", action="append", help="do not measure these groups (repeatable)" + ) + parser.add_argument("--output", help="write JSON here instead of stdout") + args = parser.parse_args() + + results = [] + for group, label, code in SCENARIOS: + if args.only_group and group not in args.only_group: + continue + if args.skip_group and group in args.skip_group: + continue + result = measure(code, args.runs) + results.append({"group": group, "label": label, "code": code, **result}) + print( + f" {label:60s} {result.get('maxrss_kib', '-')!s:>8} KiB", file=sys.stderr + ) + + report = { + "variant": args.variant, + "python": { + "implementation": platform.python_implementation(), + "version": platform.python_version(), + "build": " ".join(sys.version.split()), + }, + "platform": { + "system": platform.system(), + "release": platform.release(), + "machine": platform.machine(), + }, + "runs": args.runs, + "packages": installed_versions(), + "results": results, + } + text = json.dumps(report, indent=2) + "\n" + if args.output: + with open(args.output, "w") as f: + f.write(text) + else: + sys.stdout.write(text) + + +if __name__ == "__main__": + main() diff --git a/scripts/memory-footprint/render.py b/scripts/memory-footprint/render.py new file mode 100644 index 000000000..4a348c2bb --- /dev/null +++ b/scripts/memory-footprint/render.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +############################################################################### +# +# The MIT License (MIT) +# +# Copyright (c) typedef int GmbH +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +############################################################################### +""" +Render the results of ``run.sh`` (``results/-*.json``) as reStructuredText +fragments, included by ``docs/memory-footprint.rst``: + +- ``docs/memory-footprint/.inc``: the table, one column per autobahn variant +- ``docs/memory-footprint/-versions.inc``: interpreter, platform and package versions + +usage: render.py [ ...] +""" + +from __future__ import annotations + +import json +import pathlib +import sys + +from measure import PACKAGES, SCENARIOS + +HERE = pathlib.Path(__file__).resolve().parent +RESULTS = HERE / "results" +DOCS = HERE.parent.parent / "docs" / "memory-footprint" + +# released variants first, in this order; any other (local) variant after them +RELEASES = ["25.10.2", "26.7.1"] + + +def load(env: str) -> tuple[list[str], dict, dict]: + """ + Returns the variants (column order), the results by (variant, label), and the + reports (without results) by variant, for the main and the crossbar venv. + """ + values, reports = {}, {} + for path in sorted(RESULTS.glob(f"{env}-*.json")): + report = json.loads(path.read_text()) + variant = report["variant"] + kind = "crossbar" if path.stem.endswith("-crossbar") else "main" + reports[(variant, kind)] = report + for result in report["results"]: + # never let "not installed in this venv" replace a measured value + if result.get("available") or (variant, result["label"]) not in values: + values[(variant, result["label"])] = result + found = {variant for variant, _ in reports} + variants = [v for v in RELEASES if v in found] + sorted(found - set(RELEASES)) + return variants, values, reports + + +def cell(result: dict | None) -> str: + if not result or not result.get("available"): + return "n/a" + text = f"{result['maxrss_kib'] / 1024:.1f} MB" + return f"{text} †" if result["numpy"] else text + + +def render_table(env: str, variants: list[str], values: dict) -> str: + lines = [ + ".. list-table::", + " :header-rows: 1", + " :widths: 12 46" + " 14" * len(variants), + "", + " * - group", + " - import", + ] + lines += [f" - {v}" for v in variants] + previous_group = None + for group, label, _ in SCENARIOS: + if not any((v, label) in values for v in variants): + continue + lines.append(f" * - {group if group != previous_group else ''}".rstrip()) + lines.append(f" - ``{label}``") + lines += [f" - {cell(values.get((v, label)))}" for v in variants] + previous_group = group + return "\n".join(lines) + "\n" + + +def render_versions(variants: list[str], reports: dict) -> str: + lines = [] + for variant in variants: + for kind in ("main", "crossbar"): + report = reports.get((variant, kind)) + if not report: + continue + python, plat = report["python"], report["platform"] + packages = ", ".join( + f"{name} {report['packages'][name]}" + for name in PACKAGES + if name in report["packages"] + ) + venv = "crossbar venv" if kind == "crossbar" else "venv" + lines.append(f"* **{variant}** ({venv}): {packages}") + build = f"{python['implementation']} {python['version']}" + context = f"{plat['system']} {plat['release']} {plat['machine']}, median of {report['runs']} runs" + lines += ["", f"Interpreter: ``{build}`` ({python['build']}); {context}."] + return "\n".join(lines) + "\n" + + +def main() -> None: + envs = sys.argv[1:] + if not envs: + sys.exit(__doc__) + DOCS.mkdir(parents=True, exist_ok=True) + for env in envs: + variants, values, reports = load(env) + if not variants: + sys.exit(f"no results for {env} in {RESULTS}") + (DOCS / f"{env}.inc").write_text(render_table(env, variants, values)) + (DOCS / f"{env}-versions.inc").write_text(render_versions(variants, reports)) + print(f"wrote {DOCS / f'{env}.inc'} and {DOCS / f'{env}-versions.inc'}") + + +if __name__ == "__main__": + main() diff --git a/scripts/memory-footprint/results/cpy314-25.10.2.json b/scripts/memory-footprint/results/cpy314-25.10.2.json new file mode 100644 index 000000000..1d7ba2522 --- /dev/null +++ b/scripts/memory-footprint/results/cpy314-25.10.2.json @@ -0,0 +1,249 @@ +{ + "variant": "25.10.2", + "python": { + "implementation": "CPython", + "version": "3.14.7", + "build": "3.14.7 (main, Sep 1 2026, 14:18:09) [Clang 22.1.3 ]" + }, + "platform": { + "system": "Linux", + "release": "6.12.95+deb12-amd64", + "machine": "x86_64" + }, + "runs": 5, + "packages": { + "autobahn": "25.10.2", + "txaio": "26.6.1", + "twisted": "26.4.0", + "cryptography": "50.0.2", + "numpy": "2.5.4", + "daphne": "4.2.3" + }, + "results": [ + { + "group": "interpreter", + "label": "bare interpreter", + "code": "pass", + "available": true, + "maxrss_kib": 12216, + "samples_kib": [ + 12212, + 12208, + 12216, + 12300, + 12304 + ], + "numpy": false + }, + { + "group": "cryptography", + "label": "import cryptography", + "code": "import cryptography", + "available": true, + "maxrss_kib": 12256, + "samples_kib": [ + 12212, + 12304, + 12188, + 12256, + 12304 + ], + "numpy": false + }, + { + "group": "cryptography", + "label": "from cryptography.hazmat.primitives import hashes", + "code": "from cryptography.hazmat.primitives import hashes", + "available": true, + "maxrss_kib": 20352, + "samples_kib": [ + 20292, + 20260, + 20352, + 20380, + 20380 + ], + "numpy": false + }, + { + "group": "numpy", + "label": "import numpy", + "code": "import numpy", + "available": true, + "maxrss_kib": 29048, + "samples_kib": [ + 29092, + 29044, + 29232, + 29044, + 29048 + ], + "numpy": true + }, + { + "group": "asyncio", + "label": "import asyncio", + "code": "import asyncio", + "available": true, + "maxrss_kib": 18552, + "samples_kib": [ + 18552, + 18536, + 18604, + 18540, + 18636 + ], + "numpy": false + }, + { + "group": "twisted", + "label": "from twisted.internet import reactor", + "code": "from twisted.internet import reactor", + "available": true, + "maxrss_kib": 44380, + "samples_kib": [ + 44484, + 44380, + 44400, + 44356, + 44356 + ], + "numpy": false + }, + { + "group": "txaio", + "label": "import txaio", + "code": "import txaio", + "available": true, + "maxrss_kib": 12336, + "samples_kib": [ + 12332, + 12336, + 12296, + 12384, + 12376 + ], + "numpy": false + }, + { + "group": "txaio", + "label": "import txaio; txaio.use_asyncio()", + "code": "import txaio; txaio.use_asyncio()", + "available": true, + "maxrss_kib": 18708, + "samples_kib": [ + 18744, + 18688, + 18752, + 18708, + 18668 + ], + "numpy": false + }, + { + "group": "txaio", + "label": "import txaio; txaio.use_twisted()", + "code": "import txaio; txaio.use_twisted()", + "available": true, + "maxrss_kib": 27104, + "samples_kib": [ + 27044, + 27068, + 27104, + 27132, + 27112 + ], + "numpy": false + }, + { + "group": "autobahn", + "label": "import autobahn", + "code": "import autobahn", + "available": true, + "maxrss_kib": 12340, + "samples_kib": [ + 12392, + 12340, + 12276, + 12300, + 12384 + ], + "numpy": false + }, + { + "group": "autobahn", + "label": "txaio.use_asyncio(); import autobahn.wamp", + "code": "import txaio; txaio.use_asyncio(); import autobahn.wamp", + "available": true, + "maxrss_kib": 32652, + "samples_kib": [ + 32636, + 32684, + 32632, + 32652, + 32768 + ], + "numpy": false + }, + { + "group": "autobahn", + "label": "txaio.use_twisted(); import autobahn.wamp", + "code": "import txaio; txaio.use_twisted(); import autobahn.wamp", + "available": true, + "maxrss_kib": 39324, + "samples_kib": [ + 39424, + 39324, + 39348, + 39312, + 39280 + ], + "numpy": false + }, + { + "group": "autobahn", + "label": "import autobahn.asyncio.websocket", + "code": "import autobahn.asyncio.websocket", + "available": true, + "maxrss_kib": 37148, + "samples_kib": [ + 37084, + 37176, + 37232, + 37100, + 37148 + ], + "numpy": false + }, + { + "group": "autobahn", + "label": "import autobahn.twisted.websocket", + "code": "import autobahn.twisted.websocket", + "available": true, + "maxrss_kib": 50052, + "samples_kib": [ + 50040, + 50068, + 50052, + 50016, + 50168 + ], + "numpy": false + }, + { + "group": "daphne", + "label": "from daphne.server import Server", + "code": "from daphne.server import Server", + "available": true, + "maxrss_kib": 51472, + "samples_kib": [ + 51472, + 51468, + 51560, + 51512, + 51400 + ], + "numpy": false + } + ] +} diff --git a/scripts/memory-footprint/results/cpy314-26.7.1-crossbar.json b/scripts/memory-footprint/results/cpy314-26.7.1-crossbar.json new file mode 100644 index 000000000..da96c16fd --- /dev/null +++ b/scripts/memory-footprint/results/cpy314-26.7.1-crossbar.json @@ -0,0 +1,41 @@ +{ + "variant": "26.7.1", + "python": { + "implementation": "CPython", + "version": "3.14.7", + "build": "3.14.7 (main, Sep 1 2026, 14:18:09) [Clang 22.1.3 ]" + }, + "platform": { + "system": "Linux", + "release": "6.12.95+deb12-amd64", + "machine": "x86_64" + }, + "runs": 5, + "packages": { + "autobahn": "26.7.1", + "txaio": "26.6.1", + "twisted": "26.4.0", + "cryptography": "50.0.2", + "numpy": "2.5.4", + "cbor2": "6.1.5", + "crossbar": "26.7.1", + "zlmdb": "26.7.1" + }, + "results": [ + { + "group": "crossbar", + "label": "from crossbar.edge.worker.router import ExtRouterController", + "code": "from crossbar.edge.worker.router import ExtRouterController", + "available": true, + "maxrss_kib": 148136, + "samples_kib": [ + 148280, + 148136, + 148136, + 148132, + 148016 + ], + "numpy": true + } + ] +} diff --git a/scripts/memory-footprint/results/cpy314-26.7.1.json b/scripts/memory-footprint/results/cpy314-26.7.1.json new file mode 100644 index 000000000..ed8efc2cb --- /dev/null +++ b/scripts/memory-footprint/results/cpy314-26.7.1.json @@ -0,0 +1,250 @@ +{ + "variant": "26.7.1", + "python": { + "implementation": "CPython", + "version": "3.14.7", + "build": "3.14.7 (main, Sep 1 2026, 14:18:09) [Clang 22.1.3 ]" + }, + "platform": { + "system": "Linux", + "release": "6.12.95+deb12-amd64", + "machine": "x86_64" + }, + "runs": 5, + "packages": { + "autobahn": "26.7.1", + "txaio": "26.6.1", + "twisted": "26.4.0", + "cryptography": "50.0.2", + "numpy": "2.5.4", + "cbor2": "6.1.5", + "daphne": "4.2.3" + }, + "results": [ + { + "group": "interpreter", + "label": "bare interpreter", + "code": "pass", + "available": true, + "maxrss_kib": 12224, + "samples_kib": [ + 12192, + 12224, + 12208, + 12280, + 12268 + ], + "numpy": false + }, + { + "group": "cryptography", + "label": "import cryptography", + "code": "import cryptography", + "available": true, + "maxrss_kib": 12268, + "samples_kib": [ + 12304, + 12224, + 12260, + 12268, + 12300 + ], + "numpy": false + }, + { + "group": "cryptography", + "label": "from cryptography.hazmat.primitives import hashes", + "code": "from cryptography.hazmat.primitives import hashes", + "available": true, + "maxrss_kib": 20340, + "samples_kib": [ + 20344, + 20384, + 20340, + 20268, + 20256 + ], + "numpy": false + }, + { + "group": "numpy", + "label": "import numpy", + "code": "import numpy", + "available": true, + "maxrss_kib": 29132, + "samples_kib": [ + 29144, + 29108, + 29132, + 29156, + 29128 + ], + "numpy": true + }, + { + "group": "asyncio", + "label": "import asyncio", + "code": "import asyncio", + "available": true, + "maxrss_kib": 18604, + "samples_kib": [ + 18624, + 18536, + 18560, + 18604, + 18656 + ], + "numpy": false + }, + { + "group": "twisted", + "label": "from twisted.internet import reactor", + "code": "from twisted.internet import reactor", + "available": true, + "maxrss_kib": 44368, + "samples_kib": [ + 44404, + 44368, + 44356, + 44492, + 44360 + ], + "numpy": false + }, + { + "group": "txaio", + "label": "import txaio", + "code": "import txaio", + "available": true, + "maxrss_kib": 12356, + "samples_kib": [ + 12356, + 12380, + 12316, + 12380, + 12276 + ], + "numpy": false + }, + { + "group": "txaio", + "label": "import txaio; txaio.use_asyncio()", + "code": "import txaio; txaio.use_asyncio()", + "available": true, + "maxrss_kib": 18780, + "samples_kib": [ + 18780, + 18780, + 18784, + 18780, + 18728 + ], + "numpy": false + }, + { + "group": "txaio", + "label": "import txaio; txaio.use_twisted()", + "code": "import txaio; txaio.use_twisted()", + "available": true, + "maxrss_kib": 27132, + "samples_kib": [ + 27132, + 27152, + 27140, + 27068, + 27048 + ], + "numpy": false + }, + { + "group": "autobahn", + "label": "import autobahn", + "code": "import autobahn", + "available": true, + "maxrss_kib": 29492, + "samples_kib": [ + 29536, + 29644, + 29468, + 29492, + 29440 + ], + "numpy": true + }, + { + "group": "autobahn", + "label": "txaio.use_asyncio(); import autobahn.wamp", + "code": "import txaio; txaio.use_asyncio(); import autobahn.wamp", + "available": true, + "maxrss_kib": 48792, + "samples_kib": [ + 48792, + 48752, + 48860, + 48808, + 48652 + ], + "numpy": true + }, + { + "group": "autobahn", + "label": "txaio.use_twisted(); import autobahn.wamp", + "code": "import txaio; txaio.use_twisted(); import autobahn.wamp", + "available": true, + "maxrss_kib": 54148, + "samples_kib": [ + 54148, + 54180, + 54028, + 54200, + 54068 + ], + "numpy": true + }, + { + "group": "autobahn", + "label": "import autobahn.asyncio.websocket", + "code": "import autobahn.asyncio.websocket", + "available": true, + "maxrss_kib": 52324, + "samples_kib": [ + 52324, + 52320, + 52244, + 52328, + 52328 + ], + "numpy": true + }, + { + "group": "autobahn", + "label": "import autobahn.twisted.websocket", + "code": "import autobahn.twisted.websocket", + "available": true, + "maxrss_kib": 64672, + "samples_kib": [ + 64852, + 64592, + 64712, + 64672, + 64652 + ], + "numpy": true + }, + { + "group": "daphne", + "label": "from daphne.server import Server", + "code": "from daphne.server import Server", + "available": true, + "maxrss_kib": 66180, + "samples_kib": [ + 66188, + 66252, + 66068, + 66136, + 66180 + ], + "numpy": true + } + ] +} diff --git a/scripts/memory-footprint/results/cpy314-fix_1983-crossbar.json b/scripts/memory-footprint/results/cpy314-fix_1983-crossbar.json new file mode 100644 index 000000000..a87b17527 --- /dev/null +++ b/scripts/memory-footprint/results/cpy314-fix_1983-crossbar.json @@ -0,0 +1,41 @@ +{ + "variant": "fix_1983", + "python": { + "implementation": "CPython", + "version": "3.14.7", + "build": "3.14.7 (main, Sep 1 2026, 14:18:09) [Clang 22.1.3 ]" + }, + "platform": { + "system": "Linux", + "release": "6.12.95+deb12-amd64", + "machine": "x86_64" + }, + "runs": 5, + "packages": { + "autobahn": "26.10.1.dev1", + "txaio": "26.6.1", + "twisted": "26.4.0", + "cryptography": "50.0.2", + "numpy": "2.5.4", + "cbor2": "6.1.5", + "crossbar": "26.7.1", + "zlmdb": "26.7.1" + }, + "results": [ + { + "group": "crossbar", + "label": "from crossbar.edge.worker.router import ExtRouterController", + "code": "from crossbar.edge.worker.router import ExtRouterController", + "available": true, + "maxrss_kib": 148160, + "samples_kib": [ + 148124, + 148124, + 148160, + 148176, + 148192 + ], + "numpy": true + } + ] +} diff --git a/scripts/memory-footprint/results/cpy314-fix_1983.json b/scripts/memory-footprint/results/cpy314-fix_1983.json new file mode 100644 index 000000000..f32d871c4 --- /dev/null +++ b/scripts/memory-footprint/results/cpy314-fix_1983.json @@ -0,0 +1,250 @@ +{ + "variant": "fix_1983", + "python": { + "implementation": "CPython", + "version": "3.14.7", + "build": "3.14.7 (main, Sep 1 2026, 14:18:09) [Clang 22.1.3 ]" + }, + "platform": { + "system": "Linux", + "release": "6.12.95+deb12-amd64", + "machine": "x86_64" + }, + "runs": 5, + "packages": { + "autobahn": "26.10.1.dev1", + "txaio": "26.6.1", + "twisted": "26.4.0", + "cryptography": "50.0.2", + "numpy": "2.5.4", + "cbor2": "6.1.5", + "daphne": "4.2.3" + }, + "results": [ + { + "group": "interpreter", + "label": "bare interpreter", + "code": "pass", + "available": true, + "maxrss_kib": 12260, + "samples_kib": [ + 12284, + 12232, + 12260, + 12260, + 12212 + ], + "numpy": false + }, + { + "group": "cryptography", + "label": "import cryptography", + "code": "import cryptography", + "available": true, + "maxrss_kib": 12224, + "samples_kib": [ + 12300, + 12224, + 12304, + 12224, + 12224 + ], + "numpy": false + }, + { + "group": "cryptography", + "label": "from cryptography.hazmat.primitives import hashes", + "code": "from cryptography.hazmat.primitives import hashes", + "available": true, + "maxrss_kib": 20296, + "samples_kib": [ + 20296, + 20304, + 20280, + 20284, + 20304 + ], + "numpy": false + }, + { + "group": "numpy", + "label": "import numpy", + "code": "import numpy", + "available": true, + "maxrss_kib": 29096, + "samples_kib": [ + 29136, + 29092, + 29148, + 29096, + 29096 + ], + "numpy": true + }, + { + "group": "asyncio", + "label": "import asyncio", + "code": "import asyncio", + "available": true, + "maxrss_kib": 18620, + "samples_kib": [ + 18684, + 18632, + 18544, + 18620, + 18516 + ], + "numpy": false + }, + { + "group": "twisted", + "label": "from twisted.internet import reactor", + "code": "from twisted.internet import reactor", + "available": true, + "maxrss_kib": 44452, + "samples_kib": [ + 44408, + 44468, + 44396, + 44452, + 44496 + ], + "numpy": false + }, + { + "group": "txaio", + "label": "import txaio", + "code": "import txaio", + "available": true, + "maxrss_kib": 12344, + "samples_kib": [ + 12292, + 12344, + 12316, + 12380, + 12348 + ], + "numpy": false + }, + { + "group": "txaio", + "label": "import txaio; txaio.use_asyncio()", + "code": "import txaio; txaio.use_asyncio()", + "available": true, + "maxrss_kib": 18700, + "samples_kib": [ + 18676, + 18772, + 18688, + 18780, + 18700 + ], + "numpy": false + }, + { + "group": "txaio", + "label": "import txaio; txaio.use_twisted()", + "code": "import txaio; txaio.use_twisted()", + "available": true, + "maxrss_kib": 27108, + "samples_kib": [ + 27184, + 27100, + 27068, + 27184, + 27108 + ], + "numpy": false + }, + { + "group": "autobahn", + "label": "import autobahn", + "code": "import autobahn", + "available": true, + "maxrss_kib": 12312, + "samples_kib": [ + 12312, + 12384, + 12312, + 12392, + 12304 + ], + "numpy": false + }, + { + "group": "autobahn", + "label": "txaio.use_asyncio(); import autobahn.wamp", + "code": "import txaio; txaio.use_asyncio(); import autobahn.wamp", + "available": true, + "maxrss_kib": 33976, + "samples_kib": [ + 33976, + 33984, + 33984, + 33948, + 33948 + ], + "numpy": false + }, + { + "group": "autobahn", + "label": "txaio.use_twisted(); import autobahn.wamp", + "code": "import txaio; txaio.use_twisted(); import autobahn.wamp", + "available": true, + "maxrss_kib": 40732, + "samples_kib": [ + 40672, + 40732, + 40684, + 40788, + 40732 + ], + "numpy": false + }, + { + "group": "autobahn", + "label": "import autobahn.asyncio.websocket", + "code": "import autobahn.asyncio.websocket", + "available": true, + "maxrss_kib": 38744, + "samples_kib": [ + 38732, + 38684, + 38748, + 38744, + 38768 + ], + "numpy": false + }, + { + "group": "autobahn", + "label": "import autobahn.twisted.websocket", + "code": "import autobahn.twisted.websocket", + "available": true, + "maxrss_kib": 51484, + "samples_kib": [ + 51468, + 51576, + 51484, + 51484, + 51388 + ], + "numpy": false + }, + { + "group": "daphne", + "label": "from daphne.server import Server", + "code": "from daphne.server import Server", + "available": true, + "maxrss_kib": 52828, + "samples_kib": [ + 52872, + 52788, + 52824, + 52828, + 52988 + ], + "numpy": false + } + ] +} diff --git a/scripts/memory-footprint/results/pypy311-25.10.2.json b/scripts/memory-footprint/results/pypy311-25.10.2.json new file mode 100644 index 000000000..df953d128 --- /dev/null +++ b/scripts/memory-footprint/results/pypy311-25.10.2.json @@ -0,0 +1,249 @@ +{ + "variant": "25.10.2", + "python": { + "implementation": "PyPy", + "version": "3.11.15", + "build": "3.11.15 (194f9f44b505, May 25 2026, 19:34:11) [PyPy 7.3.23 with GCC 10.2.1 20210130 (Red Hat 10.2.1-11)]" + }, + "platform": { + "system": "Linux", + "release": "6.12.95+deb12-amd64", + "machine": "x86_64" + }, + "runs": 5, + "packages": { + "autobahn": "25.10.2", + "txaio": "26.6.1", + "twisted": "26.4.0", + "cryptography": "50.0.2", + "numpy": "2.4.6", + "daphne": "4.2.3" + }, + "results": [ + { + "group": "interpreter", + "label": "bare interpreter", + "code": "pass", + "available": true, + "maxrss_kib": 65928, + "samples_kib": [ + 66048, + 65884, + 65948, + 65928, + 65760 + ], + "numpy": false + }, + { + "group": "cryptography", + "label": "import cryptography", + "code": "import cryptography", + "available": true, + "maxrss_kib": 65888, + "samples_kib": [ + 65888, + 65996, + 65824, + 65744, + 65968 + ], + "numpy": false + }, + { + "group": "cryptography", + "label": "from cryptography.hazmat.primitives import hashes", + "code": "from cryptography.hazmat.primitives import hashes", + "available": true, + "maxrss_kib": 73796, + "samples_kib": [ + 73796, + 73752, + 73868, + 73840, + 73736 + ], + "numpy": false + }, + { + "group": "numpy", + "label": "import numpy", + "code": "import numpy", + "available": true, + "maxrss_kib": 90232, + "samples_kib": [ + 90300, + 90232, + 90356, + 90208, + 90232 + ], + "numpy": true + }, + { + "group": "asyncio", + "label": "import asyncio", + "code": "import asyncio", + "available": true, + "maxrss_kib": 80616, + "samples_kib": [ + 80548, + 80584, + 80664, + 80704, + 80616 + ], + "numpy": false + }, + { + "group": "twisted", + "label": "from twisted.internet import reactor", + "code": "from twisted.internet import reactor", + "available": true, + "maxrss_kib": 108608, + "samples_kib": [ + 108604, + 108940, + 108692, + 108580, + 108608 + ], + "numpy": false + }, + { + "group": "txaio", + "label": "import txaio", + "code": "import txaio", + "available": true, + "maxrss_kib": 65936, + "samples_kib": [ + 66000, + 65780, + 66004, + 65936, + 65832 + ], + "numpy": false + }, + { + "group": "txaio", + "label": "import txaio; txaio.use_asyncio()", + "code": "import txaio; txaio.use_asyncio()", + "available": true, + "maxrss_kib": 80860, + "samples_kib": [ + 80772, + 80948, + 81072, + 80800, + 80860 + ], + "numpy": false + }, + { + "group": "txaio", + "label": "import txaio; txaio.use_twisted()", + "code": "import txaio; txaio.use_twisted()", + "available": true, + "maxrss_kib": 90088, + "samples_kib": [ + 90024, + 90156, + 90088, + 90088, + 90180 + ], + "numpy": false + }, + { + "group": "autobahn", + "label": "import autobahn", + "code": "import autobahn", + "available": true, + "maxrss_kib": 65952, + "samples_kib": [ + 65964, + 66048, + 65952, + 65892, + 65748 + ], + "numpy": false + }, + { + "group": "autobahn", + "label": "txaio.use_asyncio(); import autobahn.wamp", + "code": "import txaio; txaio.use_asyncio(); import autobahn.wamp", + "available": true, + "maxrss_kib": 95208, + "samples_kib": [ + 95208, + 95200, + 95248, + 95196, + 95348 + ], + "numpy": false + }, + { + "group": "autobahn", + "label": "txaio.use_twisted(); import autobahn.wamp", + "code": "import txaio; txaio.use_twisted(); import autobahn.wamp", + "available": true, + "maxrss_kib": 102552, + "samples_kib": [ + 102624, + 102488, + 102516, + 102552, + 102704 + ], + "numpy": false + }, + { + "group": "autobahn", + "label": "import autobahn.asyncio.websocket", + "code": "import autobahn.asyncio.websocket", + "available": true, + "maxrss_kib": 100724, + "samples_kib": [ + 100640, + 100724, + 100648, + 100740, + 100788 + ], + "numpy": false + }, + { + "group": "autobahn", + "label": "import autobahn.twisted.websocket", + "code": "import autobahn.twisted.websocket", + "available": true, + "maxrss_kib": 115872, + "samples_kib": [ + 115872, + 115600, + 116012, + 115944, + 115840 + ], + "numpy": false + }, + { + "group": "daphne", + "label": "from daphne.server import Server", + "code": "from daphne.server import Server", + "available": true, + "maxrss_kib": 117572, + "samples_kib": [ + 117496, + 117688, + 117572, + 117620, + 117440 + ], + "numpy": false + } + ] +} diff --git a/scripts/memory-footprint/results/pypy311-26.7.1-crossbar.json b/scripts/memory-footprint/results/pypy311-26.7.1-crossbar.json new file mode 100644 index 000000000..ecf7bd551 --- /dev/null +++ b/scripts/memory-footprint/results/pypy311-26.7.1-crossbar.json @@ -0,0 +1,41 @@ +{ + "variant": "26.7.1", + "python": { + "implementation": "PyPy", + "version": "3.11.15", + "build": "3.11.15 (194f9f44b505, May 25 2026, 19:34:11) [PyPy 7.3.23 with GCC 10.2.1 20210130 (Red Hat 10.2.1-11)]" + }, + "platform": { + "system": "Linux", + "release": "6.12.95+deb12-amd64", + "machine": "x86_64" + }, + "runs": 5, + "packages": { + "autobahn": "26.7.1", + "txaio": "26.6.1", + "twisted": "26.4.0", + "cryptography": "50.0.2", + "numpy": "2.4.6", + "cbor2": "6.1.5", + "crossbar": "26.7.1", + "zlmdb": "26.7.1" + }, + "results": [ + { + "group": "crossbar", + "label": "from crossbar.edge.worker.router import ExtRouterController", + "code": "from crossbar.edge.worker.router import ExtRouterController", + "available": true, + "maxrss_kib": 245296, + "samples_kib": [ + 245760, + 245296, + 244868, + 244504, + 245824 + ], + "numpy": true + } + ] +} diff --git a/scripts/memory-footprint/results/pypy311-26.7.1.json b/scripts/memory-footprint/results/pypy311-26.7.1.json new file mode 100644 index 000000000..4f6946788 --- /dev/null +++ b/scripts/memory-footprint/results/pypy311-26.7.1.json @@ -0,0 +1,250 @@ +{ + "variant": "26.7.1", + "python": { + "implementation": "PyPy", + "version": "3.11.15", + "build": "3.11.15 (194f9f44b505, May 25 2026, 19:34:11) [PyPy 7.3.23 with GCC 10.2.1 20210130 (Red Hat 10.2.1-11)]" + }, + "platform": { + "system": "Linux", + "release": "6.12.95+deb12-amd64", + "machine": "x86_64" + }, + "runs": 5, + "packages": { + "autobahn": "26.7.1", + "txaio": "26.6.1", + "twisted": "26.4.0", + "cryptography": "50.0.2", + "numpy": "2.4.6", + "cbor2": "6.1.5", + "daphne": "4.2.3" + }, + "results": [ + { + "group": "interpreter", + "label": "bare interpreter", + "code": "pass", + "available": true, + "maxrss_kib": 65928, + "samples_kib": [ + 65992, + 65780, + 65932, + 65776, + 65928 + ], + "numpy": false + }, + { + "group": "cryptography", + "label": "import cryptography", + "code": "import cryptography", + "available": true, + "maxrss_kib": 65944, + "samples_kib": [ + 65776, + 66008, + 66052, + 65924, + 65944 + ], + "numpy": false + }, + { + "group": "cryptography", + "label": "from cryptography.hazmat.primitives import hashes", + "code": "from cryptography.hazmat.primitives import hashes", + "available": true, + "maxrss_kib": 73740, + "samples_kib": [ + 73740, + 74004, + 73656, + 73940, + 73592 + ], + "numpy": false + }, + { + "group": "numpy", + "label": "import numpy", + "code": "import numpy", + "available": true, + "maxrss_kib": 90236, + "samples_kib": [ + 90352, + 90204, + 90392, + 90236, + 90200 + ], + "numpy": true + }, + { + "group": "asyncio", + "label": "import asyncio", + "code": "import asyncio", + "available": true, + "maxrss_kib": 80760, + "samples_kib": [ + 80864, + 80760, + 80820, + 80648, + 80588 + ], + "numpy": false + }, + { + "group": "twisted", + "label": "from twisted.internet import reactor", + "code": "from twisted.internet import reactor", + "available": true, + "maxrss_kib": 108632, + "samples_kib": [ + 108632, + 108704, + 108616, + 108704, + 108552 + ], + "numpy": false + }, + { + "group": "txaio", + "label": "import txaio", + "code": "import txaio", + "available": true, + "maxrss_kib": 65936, + "samples_kib": [ + 65924, + 65936, + 66004, + 66052, + 65824 + ], + "numpy": false + }, + { + "group": "txaio", + "label": "import txaio; txaio.use_asyncio()", + "code": "import txaio; txaio.use_asyncio()", + "available": true, + "maxrss_kib": 80908, + "samples_kib": [ + 81072, + 80788, + 80968, + 80908, + 80856 + ], + "numpy": false + }, + { + "group": "txaio", + "label": "import txaio; txaio.use_twisted()", + "code": "import txaio; txaio.use_twisted()", + "available": true, + "maxrss_kib": 90060, + "samples_kib": [ + 90020, + 90400, + 90060, + 89964, + 90236 + ], + "numpy": false + }, + { + "group": "autobahn", + "label": "import autobahn", + "code": "import autobahn", + "available": true, + "maxrss_kib": 90060, + "samples_kib": [ + 90104, + 90044, + 90060, + 90016, + 90116 + ], + "numpy": true + }, + { + "group": "autobahn", + "label": "txaio.use_asyncio(); import autobahn.wamp", + "code": "import txaio; txaio.use_asyncio(); import autobahn.wamp", + "available": true, + "maxrss_kib": 115056, + "samples_kib": [ + 115056, + 115076, + 114720, + 115060, + 115056 + ], + "numpy": true + }, + { + "group": "autobahn", + "label": "txaio.use_twisted(); import autobahn.wamp", + "code": "import txaio; txaio.use_twisted(); import autobahn.wamp", + "available": true, + "maxrss_kib": 121692, + "samples_kib": [ + 121692, + 121784, + 122016, + 121672, + 121672 + ], + "numpy": true + }, + { + "group": "autobahn", + "label": "import autobahn.asyncio.websocket", + "code": "import autobahn.asyncio.websocket", + "available": true, + "maxrss_kib": 120004, + "samples_kib": [ + 119848, + 120032, + 120004, + 120084, + 119892 + ], + "numpy": true + }, + { + "group": "autobahn", + "label": "import autobahn.twisted.websocket", + "code": "import autobahn.twisted.websocket", + "available": true, + "maxrss_kib": 136688, + "samples_kib": [ + 136868, + 135628, + 136904, + 135520, + 136688 + ], + "numpy": true + }, + { + "group": "daphne", + "label": "from daphne.server import Server", + "code": "from daphne.server import Server", + "available": true, + "maxrss_kib": 139032, + "samples_kib": [ + 139084, + 139192, + 138856, + 139008, + 139032 + ], + "numpy": true + } + ] +} diff --git a/scripts/memory-footprint/results/pypy311-fix_1983-crossbar.json b/scripts/memory-footprint/results/pypy311-fix_1983-crossbar.json new file mode 100644 index 000000000..06a4e9f4d --- /dev/null +++ b/scripts/memory-footprint/results/pypy311-fix_1983-crossbar.json @@ -0,0 +1,41 @@ +{ + "variant": "fix_1983", + "python": { + "implementation": "PyPy", + "version": "3.11.15", + "build": "3.11.15 (194f9f44b505, May 25 2026, 19:34:11) [PyPy 7.3.23 with GCC 10.2.1 20210130 (Red Hat 10.2.1-11)]" + }, + "platform": { + "system": "Linux", + "release": "6.12.95+deb12-amd64", + "machine": "x86_64" + }, + "runs": 5, + "packages": { + "autobahn": "26.10.1.dev1", + "txaio": "26.6.1", + "twisted": "26.4.0", + "cryptography": "50.0.2", + "numpy": "2.4.6", + "cbor2": "5.9.0", + "crossbar": "26.7.1", + "zlmdb": "26.7.1" + }, + "results": [ + { + "group": "crossbar", + "label": "from crossbar.edge.worker.router import ExtRouterController", + "code": "from crossbar.edge.worker.router import ExtRouterController", + "available": true, + "maxrss_kib": 229484, + "samples_kib": [ + 229500, + 229840, + 218460, + 219932, + 229484 + ], + "numpy": true + } + ] +} diff --git a/scripts/memory-footprint/results/pypy311-fix_1983.json b/scripts/memory-footprint/results/pypy311-fix_1983.json new file mode 100644 index 000000000..877e6f97c --- /dev/null +++ b/scripts/memory-footprint/results/pypy311-fix_1983.json @@ -0,0 +1,250 @@ +{ + "variant": "fix_1983", + "python": { + "implementation": "PyPy", + "version": "3.11.15", + "build": "3.11.15 (194f9f44b505, May 25 2026, 19:34:11) [PyPy 7.3.23 with GCC 10.2.1 20210130 (Red Hat 10.2.1-11)]" + }, + "platform": { + "system": "Linux", + "release": "6.12.95+deb12-amd64", + "machine": "x86_64" + }, + "runs": 5, + "packages": { + "autobahn": "26.10.1.dev1", + "txaio": "26.6.1", + "twisted": "26.4.0", + "cryptography": "50.0.2", + "numpy": "2.4.6", + "cbor2": "5.9.0", + "daphne": "4.2.3" + }, + "results": [ + { + "group": "interpreter", + "label": "bare interpreter", + "code": "pass", + "available": true, + "maxrss_kib": 65920, + "samples_kib": [ + 65884, + 65956, + 65924, + 65812, + 65920 + ], + "numpy": false + }, + { + "group": "cryptography", + "label": "import cryptography", + "code": "import cryptography", + "available": true, + "maxrss_kib": 65936, + "samples_kib": [ + 65808, + 65964, + 65884, + 65936, + 66000 + ], + "numpy": false + }, + { + "group": "cryptography", + "label": "from cryptography.hazmat.primitives import hashes", + "code": "from cryptography.hazmat.primitives import hashes", + "available": true, + "maxrss_kib": 73744, + "samples_kib": [ + 73628, + 73744, + 73848, + 73588, + 73892 + ], + "numpy": false + }, + { + "group": "numpy", + "label": "import numpy", + "code": "import numpy", + "available": true, + "maxrss_kib": 90272, + "samples_kib": [ + 90360, + 90232, + 90248, + 90280, + 90272 + ], + "numpy": true + }, + { + "group": "asyncio", + "label": "import asyncio", + "code": "import asyncio", + "available": true, + "maxrss_kib": 80728, + "samples_kib": [ + 80732, + 80728, + 80796, + 80544, + 80652 + ], + "numpy": false + }, + { + "group": "twisted", + "label": "from twisted.internet import reactor", + "code": "from twisted.internet import reactor", + "available": true, + "maxrss_kib": 108596, + "samples_kib": [ + 108648, + 108532, + 108596, + 108676, + 108552 + ], + "numpy": false + }, + { + "group": "txaio", + "label": "import txaio", + "code": "import txaio", + "available": true, + "maxrss_kib": 65760, + "samples_kib": [ + 65740, + 65764, + 65760, + 65940, + 65740 + ], + "numpy": false + }, + { + "group": "txaio", + "label": "import txaio; txaio.use_asyncio()", + "code": "import txaio; txaio.use_asyncio()", + "available": true, + "maxrss_kib": 81004, + "samples_kib": [ + 81004, + 81060, + 81052, + 80920, + 80888 + ], + "numpy": false + }, + { + "group": "txaio", + "label": "import txaio; txaio.use_twisted()", + "code": "import txaio; txaio.use_twisted()", + "available": true, + "maxrss_kib": 90068, + "samples_kib": [ + 90012, + 90152, + 90176, + 90068, + 89936 + ], + "numpy": false + }, + { + "group": "autobahn", + "label": "import autobahn", + "code": "import autobahn", + "available": true, + "maxrss_kib": 65932, + "samples_kib": [ + 65996, + 65776, + 65740, + 65960, + 65932 + ], + "numpy": false + }, + { + "group": "autobahn", + "label": "txaio.use_asyncio(); import autobahn.wamp", + "code": "import txaio; txaio.use_asyncio(); import autobahn.wamp", + "available": true, + "maxrss_kib": 95900, + "samples_kib": [ + 95904, + 95768, + 95872, + 95900, + 96100 + ], + "numpy": false + }, + { + "group": "autobahn", + "label": "txaio.use_twisted(); import autobahn.wamp", + "code": "import txaio; txaio.use_twisted(); import autobahn.wamp", + "available": true, + "maxrss_kib": 102944, + "samples_kib": [ + 102936, + 103028, + 102876, + 103092, + 102944 + ], + "numpy": false + }, + { + "group": "autobahn", + "label": "import autobahn.asyncio.websocket", + "code": "import autobahn.asyncio.websocket", + "available": true, + "maxrss_kib": 101424, + "samples_kib": [ + 101424, + 101572, + 101344, + 101352, + 101608 + ], + "numpy": false + }, + { + "group": "autobahn", + "label": "import autobahn.twisted.websocket", + "code": "import autobahn.twisted.websocket", + "available": true, + "maxrss_kib": 116580, + "samples_kib": [ + 116580, + 116704, + 116456, + 116576, + 116584 + ], + "numpy": false + }, + { + "group": "daphne", + "label": "from daphne.server import Server", + "code": "from daphne.server import Server", + "available": true, + "maxrss_kib": 118356, + "samples_kib": [ + 118412, + 118384, + 118320, + 118356, + 118296 + ], + "numpy": false + } + ] +} diff --git a/scripts/memory-footprint/run.sh b/scripts/memory-footprint/run.sh new file mode 100755 index 000000000..ada71e05c --- /dev/null +++ b/scripts/memory-footprint/run.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +############################################################################### +# +# The MIT License (MIT) +# +# Copyright (c) typedef int GmbH +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +############################################################################### +# +# Measure the import memory footprint of autobahn for one Python environment, for +# each autobahn variant, in fresh throwaway venvs, and write the results as JSON to +# scripts/memory-footprint/results/-.json (see docs/memory-footprint.rst). +# +# usage: scripts/memory-footprint/run.sh [] +# +# env cpy314 | pypy311 (same interpreters as the justfile) +# local-label column label for this working tree (default: the current git branch) +# +# Variants measured: +# 25.10.2 last release with FlatBuffers as an optional extra (from PyPI) +# 26.7.1 latest release, with the vendored FlatBuffers runtime (from PyPI) +# this working tree +# +# Every venv gets numpy installed (on PyPy numpy<2.5: 2.5 ships no PyPy wheels), so +# the tables show whether autobahn loads it. crossbar is measured in a separate venv +# (crossbar 26.7.1 from PyPI), and not for 25.10.2, which no crossbar release supports. + +set -euo pipefail + +ENV_NAME="${1:?usage: run.sh []}" +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO="$(cd "${HERE}/../.." && pwd)" +LOCAL_LABEL="${2:-$(git -C "${REPO}" rev-parse --abbrev-ref HEAD)}" +RUNS="${MEMORY_FOOTPRINT_RUNS:-5}" +CROSSBAR_VERSION="26.7.1" + +case "${ENV_NAME}" in + cpy314) PYTHON_SPEC="cpython-3.14"; NUMPY="numpy" ;; + pypy311) PYTHON_SPEC="pypy-3.11.15"; NUMPY="numpy<2.5" ;; + *) echo "unknown env: ${ENV_NAME}" >&2; exit 1 ;; +esac + +WORKDIR="$(mktemp -d -t autobahn-memory-footprint-XXXXXX)" +trap 'rm -rf "${WORKDIR}"' EXIT +mkdir -p "${HERE}/results" + +measure() { + # measure [measure.py options...] + local venv="$1" variant="$2" output="$3" + shift 3 + "${venv}/bin/python" "${HERE}/measure.py" --variant "${variant}" --runs "${RUNS}" \ + "$@" --output "${output}" +} + +for variant in 25.10.2 26.7.1 "${LOCAL_LABEL}"; do + # requirement for autobahn, with the extras crossbar uses: autobahn_req + if [ "${variant}" = "${LOCAL_LABEL}" ]; then + autobahn_req() { echo "${REPO}${1:+[$1]}"; } + else + autobahn_req() { echo "autobahn${1:+[$1]}==${variant}"; } + fi + + echo "==> ${ENV_NAME} / autobahn ${variant}" + venv="${WORKDIR}/${variant}" + uv venv -q -p "${PYTHON_SPEC}" "${venv}" + uv pip install -q -p "${venv}/bin/python" "$(autobahn_req)" "twisted[tls]" cryptography "${NUMPY}" daphne + measure "${venv}" "${variant}" "${HERE}/results/${ENV_NAME}-${variant}.json" --skip-group crossbar + + if [ "${variant}" != "25.10.2" ]; then + echo "==> ${ENV_NAME} / autobahn ${variant} / crossbar ${CROSSBAR_VERSION}" + venv="${WORKDIR}/${variant}-crossbar" + uv venv -q -p "${PYTHON_SPEC}" "${venv}" + uv pip install -q -p "${venv}/bin/python" \ + "crossbar==${CROSSBAR_VERSION}" \ + "$(autobahn_req compress,encryption,scram,serialization,twisted)" "${NUMPY}" + measure "${venv}" "${variant}" "${HERE}/results/${ENV_NAME}-${variant}-crossbar.json" --only-group crossbar + fi +done + +echo "==> results written to ${HERE}/results/${ENV_NAME}-*.json" diff --git a/src/autobahn/__init__.py b/src/autobahn/__init__.py index adbb9be17..6d08fcf29 100644 --- a/src/autobahn/__init__.py +++ b/src/autobahn/__init__.py @@ -32,8 +32,6 @@ import txaio -from autobahn import flatbuffers - def check_zlmdb_flatbuffers_version_in_sync() -> str: """ @@ -58,6 +56,10 @@ def check_zlmdb_flatbuffers_version_in_sync() -> str: """ import zlmdb.flatbuffers + # imported here, not at module level: importing autobahn must not load + # the FlatBuffers runtime (and with it numpy), see #1983 + from autobahn import flatbuffers + autobahn_version = flatbuffers.__version__ zlmdb_version = zlmdb.flatbuffers.__version__ diff --git a/src/autobahn/_version.py b/src/autobahn/_version.py index dff85dbc3..9bd75af4c 100644 --- a/src/autobahn/_version.py +++ b/src/autobahn/_version.py @@ -24,6 +24,6 @@ # ############################################################################### -__version__ = "26.9.1.dev2" +__version__ = "26.10.1.dev1" __build__ = "00000000-0000000" diff --git a/src/autobahn/test/test_import_websocket_only.py b/src/autobahn/test/test_import_websocket_only.py new file mode 100644 index 000000000..a91a50166 --- /dev/null +++ b/src/autobahn/test/test_import_websocket_only.py @@ -0,0 +1,126 @@ +############################################################################### +# +# The MIT License (MIT) +# +# Copyright (c) typedef int GmbH +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +############################################################################### + +# Importing autobahn for WebSocket only - without WAMP, with either Twisted or +# asyncio - must not load the vendored FlatBuffers runtime, nor numpy, which that +# runtime imports at module level whenever numpy is installed. See issue #1983. +# +# Each import runs in a fresh interpreter, since Twisted and asyncio bindings +# cannot share one process. The child sees a fake "numpy" package first on its +# path, so the tests do not depend on whether numpy is installed: the vendored +# runtime finds numpy via importlib.machinery.PathFinder, which finds the fake. + +from __future__ import annotations + +import json +import os +import subprocess +import sys +import tempfile +import unittest + +WEBSOCKET_ONLY_IMPORTS = [ + "autobahn", + "autobahn.twisted.websocket", + "autobahn.asyncio.websocket", +] + +# imported by autobahn/flatbuffers/__init__.py only when the runtime really loads +FLATBUFFERS_RUNTIME_MODULE = "autobahn.flatbuffers.builder" + +FAKE_NUMPY_OK = "__version__ = '0.0.0'\n" +FAKE_NUMPY_BROKEN = "raise ImportError('simulating a broken numpy install')\n" + + +def _run_child(code: str, fake_numpy: str) -> subprocess.CompletedProcess: + with tempfile.TemporaryDirectory() as tmpdir: + os.mkdir(os.path.join(tmpdir, "numpy")) + with open(os.path.join(tmpdir, "numpy", "__init__.py"), "w") as f: + f.write(fake_numpy) + # the child selects its txaio backend itself, as for a user who sets + # neither USE_TWISTED nor USE_ASYNCIO + env = { + k: v + for k, v in os.environ.items() + if k not in ("USE_TWISTED", "USE_ASYNCIO") + } + env["PYTHONPATH"] = os.pathsep.join( + [tmpdir] + [p for p in [os.environ.get("PYTHONPATH")] if p] + ) + return subprocess.run( + [sys.executable, "-c", code], capture_output=True, text=True, env=env + ) + + +def _loaded_after_import(modname: str, fake_numpy: str) -> dict: + code = ( + "import json, sys\n" + f"import {modname}\n" + "print(json.dumps({'numpy': 'numpy' in sys.modules," + f" 'flatbuffers': {FLATBUFFERS_RUNTIME_MODULE!r} in sys.modules}}))\n" + ) + result = _run_child(code, fake_numpy) + if result.returncode != 0: + raise AssertionError(f"import {modname} failed:\n{result.stderr}") + return json.loads(result.stdout.strip().splitlines()[-1]) + + +class TestWebSocketOnlyImports(unittest.TestCase): + def test_no_flatbuffers_no_numpy(self): + for modname in WEBSOCKET_ONLY_IMPORTS: + with self.subTest(modname=modname): + loaded = _loaded_after_import(modname, FAKE_NUMPY_OK) + self.assertFalse( + loaded["flatbuffers"], + f"import {modname} loads the FlatBuffers runtime", + ) + self.assertFalse(loaded["numpy"], f"import {modname} loads numpy") + + def test_broken_numpy_does_not_break_import(self): + for modname in WEBSOCKET_ONLY_IMPORTS: + with self.subTest(modname=modname): + loaded = _loaded_after_import(modname, FAKE_NUMPY_BROKEN) + self.assertFalse(loaded["numpy"]) + + +class TestFlatBuffersStillAvailable(unittest.TestCase): + def test_flatbuffers_serializer_roundtrip(self): + # after a WebSocket-only import, FlatBuffers still works when used + code = ( + "import sys, txaio\n" + "import autobahn.asyncio.websocket\n" + "from autobahn import flatbuffers\n" + "from autobahn.wamp.message import Event\n" + "from autobahn.wamp.serializer import FlatBuffersSerializer\n" + "assert isinstance(flatbuffers.Builder(0), flatbuffers.Builder)\n" + "s = FlatBuffersSerializer()\n" + "data, _ = s.serialize(Event(1, 2, args=[1, 'a'], kwargs={'x': 3}))\n" + "msg = s.unserialize(data)[0]\n" + "assert (msg.subscription, msg.publication, msg.args, msg.kwargs) == (1, 2, [1, 'a'], {'x': 3}), msg\n" + f"assert {FLATBUFFERS_RUNTIME_MODULE!r} in sys.modules\n" + ) + result = _run_child(code, FAKE_NUMPY_OK) + self.assertEqual(result.returncode, 0, result.stderr) diff --git a/src/autobahn/wamp/message.py b/src/autobahn/wamp/message.py index 1e71ccd82..bb817cb2d 100644 --- a/src/autobahn/wamp/message.py +++ b/src/autobahn/wamp/message.py @@ -27,7 +27,9 @@ from __future__ import annotations import binascii +import importlib.util import re +import sys import textwrap from pprint import pformat from typing import Any, Literal, overload @@ -37,11 +39,31 @@ from autobahn.wamp.exception import InvalidUriError, ProtocolError from autobahn.wamp.role import ROLE_NAME_TO_CLASS + +def _lazy_import(name): + """ + Import a module lazily: it is registered in ``sys.modules`` right away, but only + executed on first attribute access. This is the recipe from the ``importlib`` + documentation ("Implementing lazy imports"), plus reuse of an already loaded module. + """ + if name in sys.modules: + return sys.modules[name] + spec = importlib.util.find_spec(name) + spec.loader = importlib.util.LazyLoader(spec.loader) + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + spec.loader.exec_module(module) + return module + + +# The FlatBuffers runtime (and the code generated for WAMP) is only loaded when a +# FlatBuffers message is first built or parsed: importing autobahn for WebSocket only +# must not load it, nor numpy, which the runtime imports whenever installed, see #1983. try: import cbor2 - from autobahn import flatbuffers - from autobahn.wamp import message_fbs + flatbuffers = _lazy_import("autobahn.flatbuffers") + message_fbs = _lazy_import("autobahn.wamp.message_fbs") except ImportError: _HAS_WAMP_FLATBUFFERS = False else: