diff --git a/benches/main.rs b/benches/main.rs index 9d46131d1..4b8a2b106 100644 --- a/benches/main.rs +++ b/benches/main.rs @@ -458,6 +458,50 @@ fn complete_model(bench: &mut Bencher) { }) } +#[bench] +fn nested_model_using_definitions(bench: &mut Bencher) { + Python::with_gil(|py| { + let sys_path = py.import("sys").unwrap().getattr("path").unwrap(); + sys_path.call_method1("append", ("./tests/benchmarks/",)).unwrap(); + + let complete_schema = py.import("nested_schema").unwrap(); + let mut schema = complete_schema.call_method0("schema_using_defs").unwrap(); + schema = validate_core_schema(py, schema, None).unwrap().extract().unwrap(); + let validator = SchemaValidator::py_new(py, schema, None).unwrap(); + + let input = complete_schema.call_method0("input_data_valid").unwrap(); + let input = black_box(input); + + validator.validate_python(py, input, None, None, None, None).unwrap(); + + bench.iter(|| { + black_box(validator.validate_python(py, input, None, None, None, None).unwrap()); + }) + }) +} + +#[bench] +fn nested_model_inlined(bench: &mut Bencher) { + Python::with_gil(|py| { + let sys_path = py.import("sys").unwrap().getattr("path").unwrap(); + sys_path.call_method1("append", ("./tests/benchmarks/",)).unwrap(); + + let complete_schema = py.import("nested_schema").unwrap(); + let mut schema = complete_schema.call_method0("inlined_schema").unwrap(); + schema = validate_core_schema(py, schema, None).unwrap().extract().unwrap(); + let validator = SchemaValidator::py_new(py, schema, None).unwrap(); + + let input = complete_schema.call_method0("input_data_valid").unwrap(); + let input = black_box(input); + + validator.validate_python(py, input, None, None, None, None).unwrap(); + + bench.iter(|| { + black_box(validator.validate_python(py, input, None, None, None, None).unwrap()); + }) + }) +} + #[bench] fn literal_ints_few_python(bench: &mut Bencher) { Python::with_gil(|py| { diff --git a/tests/benchmarks/nested_schema.py b/tests/benchmarks/nested_schema.py new file mode 100644 index 000000000..0d91d1217 --- /dev/null +++ b/tests/benchmarks/nested_schema.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from pydantic_core import core_schema as cs + +N = 5 # arbitrary number that takes ~0.05s per run + + +class MyModel: + # __slots__ is not required, but it avoids __pydantic_fields_set__ falling into __dict__ + __slots__ = '__dict__', '__pydantic_fields_set__', '__pydantic_extra__', '__pydantic_private__' + + +def schema_using_defs() -> cs.CoreSchema: + definitions: list[cs.CoreSchema] = [ + {'type': 'int', 'ref': 'int'}, + { + 'type': 'model', + 'cls': MyModel, + 'schema': { + 'type': 'model-fields', + 'fields': { + str(c): {'type': 'model-field', 'schema': {'type': 'definition-ref', 'schema_ref': 'int'}} + for c in range(N) + }, + }, + 'ref': f'model_{N}', + }, + ] + level = N + for level in reversed(range(N)): + definitions.append( + { + 'type': 'model', + 'cls': MyModel, + 'schema': { + 'type': 'model-fields', + 'fields': { + str(c): { + 'type': 'model-field', + 'schema': {'type': 'definition-ref', 'schema_ref': f'model_{level+1}'}, + } + for c in range(N) + }, + }, + 'ref': f'model_{level}', + } + ) + return { + 'type': 'definitions', + 'definitions': definitions, + 'schema': {'type': 'definition-ref', 'schema_ref': 'model_0'}, + } + + +def inlined_schema() -> cs.CoreSchema: + level = N + schema: cs.CoreSchema = { + 'type': 'model', + 'cls': MyModel, + 'schema': { + 'type': 'model-fields', + 'fields': {str(c): {'type': 'model-field', 'schema': {'type': 'int'}} for c in range(N)}, + }, + 'ref': f'model_{N}', + } + for level in reversed(range(N)): + schema = { + 'type': 'model', + 'cls': MyModel, + 'schema': { + 'type': 'model-fields', + 'fields': {str(c): {'type': 'model-field', 'schema': schema} for c in range(N)}, + }, + 'ref': f'model_{level}', + } + return schema + + +def input_data_valid(levels: int = N) -> Any: + data = {str(c): 1 for c in range(N)} + for _ in range(levels): + data = {str(c): data for c in range(N)} + return data + + +if __name__ == '__main__': + from pydantic_core import SchemaValidator + + SchemaValidator(schema_using_defs()).validate_python(input_data_valid()) + SchemaValidator(inlined_schema()).validate_python(input_data_valid()) diff --git a/tests/benchmarks/test_nested_benchmark.py b/tests/benchmarks/test_nested_benchmark.py new file mode 100644 index 000000000..6c8d50e83 --- /dev/null +++ b/tests/benchmarks/test_nested_benchmark.py @@ -0,0 +1,23 @@ +""" +Benchmarks for nested / recursive schemas using definitions. +""" + +from typing import Callable + +from pydantic_core import SchemaValidator + +from .nested_schema import inlined_schema, input_data_valid, schema_using_defs + + +def test_nested_schema_using_defs(benchmark: Callable[..., None]) -> None: + v = SchemaValidator(schema_using_defs()) + data = input_data_valid() + v.validate_python(data) + benchmark(v.validate_python, data) + + +def test_nested_schema_inlined(benchmark: Callable[..., None]) -> None: + v = SchemaValidator(inlined_schema()) + data = input_data_valid() + v.validate_python(data) + benchmark(v.validate_python, data)