From 0a1347c973195269b896d3c8105e175cdd7238be Mon Sep 17 00:00:00 2001 From: Florian Angerer Date: Wed, 29 Jul 2026 10:14:32 +0200 Subject: [PATCH 1/3] Add macro bench for cythonizing --- .../python/macro/c-cythonize-numpy.py | 163 ++++++++++++++++++ mx.graalpython/mx_graalpython_bench_param.py | 2 + 2 files changed, 165 insertions(+) create mode 100644 graalpython/com.oracle.graal.python.benchmarks/python/macro/c-cythonize-numpy.py diff --git a/graalpython/com.oracle.graal.python.benchmarks/python/macro/c-cythonize-numpy.py b/graalpython/com.oracle.graal.python.benchmarks/python/macro/c-cythonize-numpy.py new file mode 100644 index 0000000000..1f24bda076 --- /dev/null +++ b/graalpython/com.oracle.graal.python.benchmarks/python/macro/c-cythonize-numpy.py @@ -0,0 +1,163 @@ +# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# The Universal Permissive License (UPL), Version 1.0 +# +# Subject to the condition set forth below, permission is hereby granted to any +# person obtaining a copy of this software, associated documentation and/or +# data (collectively the "Software"), free of charge and under any and all +# copyright rights in the Software, and any and all patent rights owned or +# freely licensable by each licensor hereunder covering either (i) the +# unmodified Software as contributed to or provided by such licensor, or (ii) +# the Larger Works (as defined below), to deal in both +# +# (a) the Software, and +# +# (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if +# one is included with the Software each a "Larger Work" to which the Software +# is contributed by such licensors), +# +# without restriction, including without limitation the rights to copy, create +# derivative works of, display, perform, and distribute the Software and make, +# use, sell, offer for sale, import, export, have made, and have sold the +# Software and the Larger Work(s), and to sublicense the foregoing rights on +# either these or other terms. +# +# This license is subject to the following condition: +# +# The above copyright notice and either this complete permission notice or at a +# minimum a reference to the UPL must 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. + +import os + + +_old_pip_no_binary = os.environ.get("PIP_NO_BINARY") +_old_pip_no_cache_dir = os.environ.get("PIP_NO_CACHE_DIR") +_old_cython_compile_all = os.environ.get("CYTHON_COMPILE_ALL") +os.environ["PIP_NO_BINARY"] = ",".join(filter(None, (_old_pip_no_binary, "cython"))) +os.environ["PIP_NO_CACHE_DIR"] = "true" +# Cython only enables its default set of compiled modules automatically on +# CPython. Compile all of them so that GraalPy exercises the accelerated +# compiler instead of silently installing Cython's pure Python wheel. +os.environ["CYTHON_COMPILE_ALL"] = "true" +try: + ensure_packages(cython="3.2.4", setuptools="77.0.1") +finally: + if _old_pip_no_binary is None: + del os.environ["PIP_NO_BINARY"] + else: + os.environ["PIP_NO_BINARY"] = _old_pip_no_binary + if _old_pip_no_cache_dir is None: + del os.environ["PIP_NO_CACHE_DIR"] + else: + os.environ["PIP_NO_CACHE_DIR"] = _old_pip_no_cache_dir + if _old_cython_compile_all is None: + del os.environ["CYTHON_COMPILE_ALL"] + else: + os.environ["CYTHON_COMPILE_ALL"] = _old_cython_compile_all + +import importlib.machinery +import shutil +import subprocess +import sys +import tempfile + +from Cython.Build import cythonize +from Cython.Compiler import Scanning + + +NUMPY_VERSION = "2.2.6" +NUMPY_REPO_URL = os.environ.get("NUMPY_REPO_URL") or "https://github.com/numpy/numpy.git" +RANDOM_DIR = os.path.join("numpy", "random") +CYTHON_SOURCES = [ + os.path.join(RANDOM_DIR, name) + for name in ( + "_bounded_integers.pyx", + "_common.pyx", + "_mt19937.pyx", + "_philox.pyx", + "_pcg64.pyx", + "_sfc64.pyx", + "bit_generator.pyx", + "_generator.pyx", + "mtrand.pyx", + ) +] + +WORK_DIR = None +NUMPY_SOURCE_DIR = None +CYTHON_BUILD_DIR = None + + +def _is_extension_module(module): + module_file = getattr(module, "__file__", "") + return any(module_file.endswith(suffix) for suffix in importlib.machinery.EXTENSION_SUFFIXES) + + +def _render_template(source, output): + tempita_script = os.path.join(NUMPY_SOURCE_DIR, "numpy", "_build_utils", "tempita.py") + subprocess.run([sys.executable, tempita_script, source, "-o", output], check=True) + + +def __setup__(): + global WORK_DIR, NUMPY_SOURCE_DIR, CYTHON_BUILD_DIR + + if not _is_extension_module(Scanning): + raise RuntimeError(f"Cython.Compiler.Scanning is not a native extension: {Scanning.__file__}") + + WORK_DIR = tempfile.mkdtemp(prefix="graalpy-c-cythonize-numpy-") + NUMPY_SOURCE_DIR = os.path.join(WORK_DIR, "numpy") + CYTHON_BUILD_DIR = os.path.join(WORK_DIR, "cython-build") + + subprocess.run( + [ + "git", + "clone", + "--depth", + "1", + "--branch", + f"v{NUMPY_VERSION}", + "--single-branch", + NUMPY_REPO_URL, + NUMPY_SOURCE_DIR, + ], + check=True, + ) + + for extension in (".pyx", ".pxd"): + template = os.path.join(NUMPY_SOURCE_DIR, RANDOM_DIR, f"_bounded_integers{extension}.in") + output = os.path.join(NUMPY_SOURCE_DIR, RANDOM_DIR, f"_bounded_integers{extension}") + _render_template(template, output) + + +def __benchmark__(): + old_cwd = os.getcwd() + try: + os.chdir(NUMPY_SOURCE_DIR) + return cythonize( + CYTHON_SOURCES, + build_dir=CYTHON_BUILD_DIR, + compiler_directives={ + "freethreading_compatible": True, + "language_level": 3, + }, + force=True, + nthreads=0, + quiet=True, + ) + finally: + os.chdir(old_cwd) + + +def __teardown__(): + if WORK_DIR is not None: + shutil.rmtree(WORK_DIR) diff --git a/mx.graalpython/mx_graalpython_bench_param.py b/mx.graalpython/mx_graalpython_bench_param.py index c4fbb2518a..f64afd9f77 100644 --- a/mx.graalpython/mx_graalpython_bench_param.py +++ b/mx.graalpython/mx_graalpython_bench_param.py @@ -57,6 +57,7 @@ ITER_6 = ['-i', '6'] ITER_5 = ['-i', '5'] ITER_3 = ['-i', '3'] +ITER_1 = ['-i', '1'] WARMUP_2 = ['-w', '2'] # For benchmarking with Truffle compilation @@ -306,6 +307,7 @@ def _pickling_benchmarks(module='pickle'): MACRO_BENCHMARKS = { 'gcbench': ITER_10 + ['10'], + 'c-cythonize-numpy': ITER_1 + [], 'c-pydantic-validate': ITER_10 + ['200000'], 'c-pymupdf-parse': ITER_10 + ['1'], 'c-oracledb-load': ITER_5 + ['2000000'], From 1e57be2fb6c63cf33eda4c4f924a0a28f4e73e74 Mon Sep 17 00:00:00 2001 From: Florian Angerer Date: Wed, 29 Jul 2026 11:31:06 +0200 Subject: [PATCH 2/3] Fix positional-only code object argument count --- .../src/tests/cpyext/test_codeobject.py | 2 +- .../oracle/graal/python/builtins/objects/code/CodeNodes.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_codeobject.py b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_codeobject.py index ab78524174..33ca0cf196 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_codeobject.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_codeobject.py @@ -195,7 +195,7 @@ class TestCodeobject(CPyExtTestCase): goto done; } result = PyUnstable_Code_NewWithPosOnlyArgs( - 1, 0, 2, 3, 4, 0, + 2, 1, 1, 3, 4, 0, code, consts, names, varnames, freevars, cellvars, filename, name, qualname, diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/code/CodeNodes.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/code/CodeNodes.java index 86cb1164fc..c5dce72f05 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/code/CodeNodes.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/code/CodeNodes.java @@ -145,7 +145,8 @@ private static PCode createCode(PythonLanguage language, int argCount, * Even if the code object is not executable, it will be used for introspection when * you call `inspect.signature()` on such function-like object. */ - int posArgCount = argCount + positionalOnlyArgCount; + // Like CPython's co_argcount, argCount already includes positional-only arguments. + int posArgCount = argCount; TruffleString[] parameterNames, kwOnlyNames; if (varnames != null) { parameterNames = Arrays.copyOf(varnames, posArgCount); From 9065c652d8d2a88c592c463b47f2060e622417f0 Mon Sep 17 00:00:00 2001 From: Florian Angerer Date: Wed, 29 Jul 2026 14:42:07 +0200 Subject: [PATCH 3/3] Add Pydantic macro benchmarks --- .../python/macro/c-pydantic-schema-build.py | 94 +++++++++++++++ .../python/macro/c-pydantic-serialize-json.py | 105 +++++++++++++++++ .../macro/c-pydantic-validate-callbacks.py | 102 +++++++++++++++++ .../python/macro/c-pydantic-validate-json.py | 107 ++++++++++++++++++ .../macro/c-pydantic-validation-errors.py | 81 +++++++++++++ mx.graalpython/mx_graalpython_bench_param.py | 5 + 6 files changed, 494 insertions(+) create mode 100644 graalpython/com.oracle.graal.python.benchmarks/python/macro/c-pydantic-schema-build.py create mode 100644 graalpython/com.oracle.graal.python.benchmarks/python/macro/c-pydantic-serialize-json.py create mode 100644 graalpython/com.oracle.graal.python.benchmarks/python/macro/c-pydantic-validate-callbacks.py create mode 100644 graalpython/com.oracle.graal.python.benchmarks/python/macro/c-pydantic-validate-json.py create mode 100644 graalpython/com.oracle.graal.python.benchmarks/python/macro/c-pydantic-validation-errors.py diff --git a/graalpython/com.oracle.graal.python.benchmarks/python/macro/c-pydantic-schema-build.py b/graalpython/com.oracle.graal.python.benchmarks/python/macro/c-pydantic-schema-build.py new file mode 100644 index 0000000000..8af7cf5fd5 --- /dev/null +++ b/graalpython/com.oracle.graal.python.benchmarks/python/macro/c-pydantic-schema-build.py @@ -0,0 +1,94 @@ +# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# The Universal Permissive License (UPL), Version 1.0 +# +# Subject to the condition set forth below, permission is hereby granted to any +# person obtaining a copy of this software, associated documentation and/or +# data (collectively the "Software"), free of charge and under any and all +# copyright rights in the Software, and any and all patent rights owned or +# freely licensable by each licensor hereunder covering either (i) the +# unmodified Software as contributed to or provided by such licensor, or (ii) +# the Larger Works (as defined below), to deal in both +# +# (a) the Software, and +# +# (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if +# one is included with the Software each a "Larger Work" to which the Software +# is contributed by such licensors), +# +# without restriction, including without limitation the rights to copy, create +# derivative works of, display, perform, and distribute the Software and make, +# use, sell, offer for sale, import, export, have made, and have sold the +# Software and the Larger Work(s), and to sublicense the foregoing rights on +# either these or other terms. +# +# This license is subject to the following condition: +# +# The above copyright notice and either this complete permission notice or at a +# minimum a reference to the UPL must 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. + +from typing import Annotated, Literal + +ensure_packages(pydantic="2.12.5") +from pydantic import Field, TypeAdapter, create_model + + +ShortString = Annotated[str, Field(min_length=1, max_length=80)] +PostalCode = Annotated[str, Field(pattern=r"^[0-9]{5}$")] + + +def build_schema(index): + address = create_model( + f"Address{index}", + street=(ShortString, ...), + city=(ShortString, ...), + postal_code=(PostalCode, ...), + country=(str, "US"), + ) + user = create_model( + f"User{index}", + id=(int, ...), + name=(ShortString, ...), + email=(ShortString, ...), + address=(address, ...), + groups=(list[str], Field(default_factory=list)), + ) + created = create_model( + f"CreatedEvent{index}", + kind=(Literal["created"], "created"), + sequence=(int, ...), + actor=(user, ...), + tags=(list[str], Field(default_factory=list)), + ) + updated = create_model( + f"UpdatedEvent{index}", + kind=(Literal["updated"], "updated"), + sequence=(int, ...), + actor=(user, ...), + changes=(dict[str, str], ...), + ) + event = Annotated[created | updated, Field(discriminator="kind")] + envelope = create_model( + f"Envelope{index}", + request_id=(str, ...), + events=(list[event], ...), + users_by_name=(dict[str, user], ...), + ) + return TypeAdapter(list[envelope]).json_schema() + + +def __benchmark__(iterations=100): + schema = None + for i in range(iterations): + schema = build_schema(i) + return schema diff --git a/graalpython/com.oracle.graal.python.benchmarks/python/macro/c-pydantic-serialize-json.py b/graalpython/com.oracle.graal.python.benchmarks/python/macro/c-pydantic-serialize-json.py new file mode 100644 index 0000000000..53eefcf6c0 --- /dev/null +++ b/graalpython/com.oracle.graal.python.benchmarks/python/macro/c-pydantic-serialize-json.py @@ -0,0 +1,105 @@ +# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# The Universal Permissive License (UPL), Version 1.0 +# +# Subject to the condition set forth below, permission is hereby granted to any +# person obtaining a copy of this software, associated documentation and/or +# data (collectively the "Software"), free of charge and under any and all +# copyright rights in the Software, and any and all patent rights owned or +# freely licensable by each licensor hereunder covering either (i) the +# unmodified Software as contributed to or provided by such licensor, or (ii) +# the Larger Works (as defined below), to deal in both +# +# (a) the Software, and +# +# (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if +# one is included with the Software each a "Larger Work" to which the Software +# is contributed by such licensors), +# +# without restriction, including without limitation the rights to copy, create +# derivative works of, display, perform, and distribute the Software and make, +# use, sell, offer for sale, import, export, have made, and have sold the +# Software and the Larger Work(s), and to sublicense the foregoing rights on +# either these or other terms. +# +# This license is subject to the following condition: +# +# The above copyright notice and either this complete permission notice or at a +# minimum a reference to the UPL must 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. + +from typing import Annotated, Literal + +ensure_packages(pydantic="2.12.5") +from pydantic import BaseModel, Field, TypeAdapter + + +class User(BaseModel): + id: int + name: str + email: str + groups: list[str] + + +class CreatedEvent(BaseModel): + kind: Literal["created"] + sequence: int + actor: User + tags: list[str] + metadata: dict[str, str | int] + + +class UpdatedEvent(BaseModel): + kind: Literal["updated"] + sequence: int + actor: User + changes: dict[str, str] + active: bool + + +Event = Annotated[CreatedEvent | UpdatedEvent, Field(discriminator="kind")] +EVENT_ADAPTER = TypeAdapter(list[Event]) +EVENTS = EVENT_ADAPTER.validate_python([ + { + "kind": "created", + "sequence": i, + "actor": { + "id": i, + "name": f"User {i}", + "email": f"user{i}@example.com", + "groups": ["users", f"team-{i % 8}"], + }, + "tags": ["new", f"region-{i % 4}"], + "metadata": {"source": "api", "attempt": i % 3}, + } + if i % 2 == 0 + else { + "kind": "updated", + "sequence": i, + "actor": { + "id": i, + "name": f"User {i}", + "email": f"user{i}@example.com", + "groups": ["users", f"team-{i % 8}"], + }, + "changes": {"name": f"Updated User {i}", "team": f"team-{(i + 1) % 8}"}, + "active": True, + } + for i in range(64) +]) + + +def __benchmark__(iterations=1000): + result = None + for _ in range(iterations): + result = EVENT_ADAPTER.dump_json(EVENTS) + return result diff --git a/graalpython/com.oracle.graal.python.benchmarks/python/macro/c-pydantic-validate-callbacks.py b/graalpython/com.oracle.graal.python.benchmarks/python/macro/c-pydantic-validate-callbacks.py new file mode 100644 index 0000000000..7431ae5c1a --- /dev/null +++ b/graalpython/com.oracle.graal.python.benchmarks/python/macro/c-pydantic-validate-callbacks.py @@ -0,0 +1,102 @@ +# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# The Universal Permissive License (UPL), Version 1.0 +# +# Subject to the condition set forth below, permission is hereby granted to any +# person obtaining a copy of this software, associated documentation and/or +# data (collectively the "Software"), free of charge and under any and all +# copyright rights in the Software, and any and all patent rights owned or +# freely licensable by each licensor hereunder covering either (i) the +# unmodified Software as contributed to or provided by such licensor, or (ii) +# the Larger Works (as defined below), to deal in both +# +# (a) the Software, and +# +# (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if +# one is included with the Software each a "Larger Work" to which the Software +# is contributed by such licensors), +# +# without restriction, including without limitation the rights to copy, create +# derivative works of, display, perform, and distribute the Software and make, +# use, sell, offer for sale, import, export, have made, and have sold the +# Software and the Larger Work(s), and to sublicense the foregoing rights on +# either these or other terms. +# +# This license is subject to the following condition: +# +# The above copyright notice and either this complete permission notice or at a +# minimum a reference to the UPL must 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. + +from typing import Annotated + +ensure_packages(pydantic="2.12.5") +from pydantic import BaseModel, BeforeValidator, TypeAdapter, field_validator, model_validator + + +def strip_text(value): + return value.strip() if isinstance(value, str) else value + + +StrippedString = Annotated[str, BeforeValidator(strip_text)] + + +class Wine(BaseModel): + id: int + points: int + title: StrippedString + description: str | None + price: float | None + variety: str | None + winery: str | None + country: str + designation: StrippedString | None + + @field_validator("description", "price", "variety", "winery", "designation", mode="before") + @classmethod + def convert_null(cls, value): + return None if value == "null" else value + + @field_validator("country", mode="before") + @classmethod + def fill_country(cls, value): + return "Unknown" if not value or value == "null" else value + + @model_validator(mode="after") + def check_points(self): + if not 0 <= self.points <= 100: + raise ValueError("points must be between 0 and 100") + return self + + +WINE_ADAPTER = TypeAdapter(list[Wine]) +WINE_DATA = [ + { + "id": str(i), + "points": 80 + i % 20, + "title": f" Wine {i} ", + "description": "null" if i % 4 == 0 else f"Description for wine {i}", + "price": "null" if i % 5 == 0 else str(10 + i % 40), + "variety": "Merlot" if i % 3 else "null", + "winery": f"Winery {i % 10}", + "country": "null" if i % 7 == 0 else "US", + "designation": f" Vineyard {i % 12} ", + } + for i in range(64) +] + + +def __benchmark__(iterations=1000): + result = None + for _ in range(iterations): + result = WINE_ADAPTER.validate_python(WINE_DATA) + return result diff --git a/graalpython/com.oracle.graal.python.benchmarks/python/macro/c-pydantic-validate-json.py b/graalpython/com.oracle.graal.python.benchmarks/python/macro/c-pydantic-validate-json.py new file mode 100644 index 0000000000..b77642b67d --- /dev/null +++ b/graalpython/com.oracle.graal.python.benchmarks/python/macro/c-pydantic-validate-json.py @@ -0,0 +1,107 @@ +# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# The Universal Permissive License (UPL), Version 1.0 +# +# Subject to the condition set forth below, permission is hereby granted to any +# person obtaining a copy of this software, associated documentation and/or +# data (collectively the "Software"), free of charge and under any and all +# copyright rights in the Software, and any and all patent rights owned or +# freely licensable by each licensor hereunder covering either (i) the +# unmodified Software as contributed to or provided by such licensor, or (ii) +# the Larger Works (as defined below), to deal in both +# +# (a) the Software, and +# +# (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if +# one is included with the Software each a "Larger Work" to which the Software +# is contributed by such licensors), +# +# without restriction, including without limitation the rights to copy, create +# derivative works of, display, perform, and distribute the Software and make, +# use, sell, offer for sale, import, export, have made, and have sold the +# Software and the Larger Work(s), and to sublicense the foregoing rights on +# either these or other terms. +# +# This license is subject to the following condition: +# +# The above copyright notice and either this complete permission notice or at a +# minimum a reference to the UPL must 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. + +import json +from typing import Annotated, Literal + +ensure_packages(pydantic="2.12.5") +from pydantic import BaseModel, Field, TypeAdapter + + +class User(BaseModel): + id: int + name: str + email: str + groups: list[str] + + +class CreatedEvent(BaseModel): + kind: Literal["created"] + sequence: int + actor: User + tags: list[str] + metadata: dict[str, str | int] + + +class UpdatedEvent(BaseModel): + kind: Literal["updated"] + sequence: int + actor: User + changes: dict[str, str] + active: bool + + +Event = Annotated[CreatedEvent | UpdatedEvent, Field(discriminator="kind")] +EVENT_ADAPTER = TypeAdapter(list[Event]) +EVENT_DATA = [ + { + "kind": "created", + "sequence": str(i), + "actor": { + "id": str(i), + "name": f"User {i}", + "email": f"user{i}@example.com", + "groups": ["users", f"team-{i % 8}"], + }, + "tags": ["new", f"region-{i % 4}"], + "metadata": {"source": "api", "attempt": i % 3}, + } + if i % 2 == 0 + else { + "kind": "updated", + "sequence": str(i), + "actor": { + "id": str(i), + "name": f"User {i}", + "email": f"user{i}@example.com", + "groups": ["users", f"team-{i % 8}"], + }, + "changes": {"name": f"Updated User {i}", "team": f"team-{(i + 1) % 8}"}, + "active": "true", + } + for i in range(64) +] +JSON_DATA = json.dumps(EVENT_DATA, separators=(",", ":")).encode() + + +def __benchmark__(iterations=2000): + result = None + for _ in range(iterations): + result = EVENT_ADAPTER.validate_json(JSON_DATA) + return result diff --git a/graalpython/com.oracle.graal.python.benchmarks/python/macro/c-pydantic-validation-errors.py b/graalpython/com.oracle.graal.python.benchmarks/python/macro/c-pydantic-validation-errors.py new file mode 100644 index 0000000000..14bf7f71e2 --- /dev/null +++ b/graalpython/com.oracle.graal.python.benchmarks/python/macro/c-pydantic-validation-errors.py @@ -0,0 +1,81 @@ +# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# The Universal Permissive License (UPL), Version 1.0 +# +# Subject to the condition set forth below, permission is hereby granted to any +# person obtaining a copy of this software, associated documentation and/or +# data (collectively the "Software"), free of charge and under any and all +# copyright rights in the Software, and any and all patent rights owned or +# freely licensable by each licensor hereunder covering either (i) the +# unmodified Software as contributed to or provided by such licensor, or (ii) +# the Larger Works (as defined below), to deal in both +# +# (a) the Software, and +# +# (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if +# one is included with the Software each a "Larger Work" to which the Software +# is contributed by such licensors), +# +# without restriction, including without limitation the rights to copy, create +# derivative works of, display, perform, and distribute the Software and make, +# use, sell, offer for sale, import, export, have made, and have sold the +# Software and the Larger Work(s), and to sublicense the foregoing rights on +# either these or other terms. +# +# This license is subject to the following condition: +# +# The above copyright notice and either this complete permission notice or at a +# minimum a reference to the UPL must 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. + +from typing import Literal + +ensure_packages(pydantic="2.12.5") +from pydantic import BaseModel, Field, TypeAdapter, ValidationError + + +class LineItem(BaseModel): + sku: str = Field(min_length=3) + quantity: int = Field(gt=0) + price: float = Field(gt=0) + + +class Order(BaseModel): + order_id: int + priority: Literal["low", "normal", "high"] + customer: str = Field(min_length=1) + items: list[LineItem] = Field(min_length=1) + + +ORDER_ADAPTER = TypeAdapter(list[Order]) +INVALID_ORDERS = [ + { + "order_id": f"order-{i}", + "priority": "urgent", + "customer": "", + "items": [ + {"sku": "x", "quantity": 0, "price": -1}, + {"sku": "", "quantity": "many", "price": "free"}, + ], + } + for i in range(16) +] + + +def __benchmark__(iterations=1000): + errors = None + for _ in range(iterations): + try: + ORDER_ADAPTER.validate_python(INVALID_ORDERS) + except ValidationError as exc: + errors = exc.errors(include_url=False) + return errors diff --git a/mx.graalpython/mx_graalpython_bench_param.py b/mx.graalpython/mx_graalpython_bench_param.py index f64afd9f77..4c06cac338 100644 --- a/mx.graalpython/mx_graalpython_bench_param.py +++ b/mx.graalpython/mx_graalpython_bench_param.py @@ -308,7 +308,12 @@ def _pickling_benchmarks(module='pickle'): MACRO_BENCHMARKS = { 'gcbench': ITER_10 + ['10'], 'c-cythonize-numpy': ITER_1 + [], + 'c-pydantic-schema-build': ITER_10 + ['100'], + 'c-pydantic-serialize-json': ITER_10 + ['1000'], 'c-pydantic-validate': ITER_10 + ['200000'], + 'c-pydantic-validate-callbacks': ITER_10 + ['1000'], + 'c-pydantic-validate-json': ITER_10 + ['2000'], + 'c-pydantic-validation-errors': ITER_10 + ['1000'], 'c-pymupdf-parse': ITER_10 + ['1'], 'c-oracledb-load': ITER_5 + ['2000000'], }