diff --git a/ci/build_linux_wheels.py b/ci/build_linux_wheels.py index 4f4d6010ae..d1ba2cb5d5 100755 --- a/ci/build_linux_wheels.py +++ b/ci/build_linux_wheels.py @@ -24,6 +24,7 @@ Environment: - GITHUB_WORKSPACE (optional; defaults to cwd) """ + from __future__ import annotations import argparse import os @@ -33,7 +34,9 @@ from typing import List # Define Python version sets directly in the Python script -RELEASE_PYTHON_VERSIONS = "cp38-cp38 cp39-cp39 cp310-cp310 cp311-cp311 cp312-cp312 cp313-cp313" +RELEASE_PYTHON_VERSIONS = ( + "cp38-cp38 cp39-cp39 cp310-cp310 cp311-cp311 cp312-cp312 cp313-cp313" +) DEFAULT_PYTHON_VERSIONS = "cp38-cp38 cp313-cp313" # Path to the container build script @@ -42,7 +45,6 @@ DEFAULT_X86_IMAGES = [ "quay.io/pypa/manylinux2014_x86_64:latest", # "quay.io/pypa/manylinux_2_28_x86_64:latest", - # bazel binaries do not work with musl # "quay.io/pypa/musllinux_1_2_x86_64:latest", ] @@ -50,7 +52,6 @@ DEFAULT_AARCH64_IMAGES = [ "quay.io/pypa/manylinux2014_aarch64:latest", # "quay.io/pypa/manylinux_2_28_aarch64:latest", - # bazel binaries do not work with musl # "quay.io/pypa/musllinux_1_2_aarch64:latest", ] @@ -65,17 +66,26 @@ "AARCH64": "arm64", } + def parse_args(): p = argparse.ArgumentParser() - p.add_argument("--arch", required=True, help="Architecture (e.g. X86, X64, AARCH64)") - p.add_argument("--release", action="store_true", help="Run full test suite for release") - p.add_argument("--dry-run", action="store_true", help="Print docker commands without running") + p.add_argument( + "--arch", required=True, help="Architecture (e.g. X86, X64, AARCH64)" + ) + p.add_argument( + "--release", action="store_true", help="Run full test suite for release" + ) + p.add_argument( + "--dry-run", action="store_true", help="Print docker commands without running" + ) return p.parse_args() + def normalize_arch(raw: str) -> str: key = raw.strip().upper() return ARCH_ALIASES.get(key, raw.strip().lower()) + def collect_images_for_arch(arch_normalized: str) -> List[str]: if arch_normalized == "x86": imgs = DEFAULT_X86_IMAGES # dedupe preserving order @@ -85,6 +95,7 @@ def collect_images_for_arch(arch_normalized: str) -> List[str]: raise SystemExit(f"Unsupported arch: {arch_normalized!r}") return imgs + def build_docker_cmd(workspace: str, image: str, release: bool = False) -> List[str]: workspace = os.path.abspath(workspace) python_versions = RELEASE_PYTHON_VERSIONS if release else DEFAULT_PYTHON_VERSIONS @@ -93,11 +104,18 @@ def build_docker_cmd(workspace: str, image: str, release: bool = False) -> List[ github_ref_name = os.environ.get("GITHUB_REF_NAME", "") cmd = [ - "docker", "run", "-i", "--rm", - "-v", f"{workspace}:/work", # (v)olume - "-w", "/work", # (w)orking directory - "-e", f"PYTHON_VERSIONS={python_versions}", # (e)nvironment variables - "-e", f"RELEASE_BUILD={'1' if release else '0'}" + "docker", + "run", + "-i", + "--rm", + "-v", + f"{workspace}:/work", # (v)olume + "-w", + "/work", # (w)orking directory + "-e", + f"PYTHON_VERSIONS={python_versions}", # (e)nvironment variables + "-e", + f"RELEASE_BUILD={'1' if release else '0'}", ] # Pass GitHub reference name if available @@ -107,7 +125,10 @@ def build_docker_cmd(workspace: str, image: str, release: bool = False) -> List[ cmd.extend([image, "bash", CONTAINER_SCRIPT_PATH]) return cmd -def run_for_images(images: List[str], workspace: str, dry_run: bool, release: bool = False) -> int: + +def run_for_images( + images: List[str], workspace: str, dry_run: bool, release: bool = False +) -> int: rc_overall = 0 for image in images: docker_cmd = build_docker_cmd(workspace, image, release=release) @@ -118,7 +139,10 @@ def run_for_images(images: List[str], workspace: str, dry_run: bool, release: bo try: completed = subprocess.run(docker_cmd) if completed.returncode != 0: - print(f"Container {image} exited with {completed.returncode}", file=sys.stderr) + print( + f"Container {image} exited with {completed.returncode}", + file=sys.stderr, + ) rc_overall = completed.returncode if rc_overall == 0 else rc_overall else: print(f"Container {image} completed successfully.") @@ -130,6 +154,7 @@ def run_for_images(images: List[str], workspace: str, dry_run: bool, release: bo return 2 return rc_overall + def main() -> int: args = parse_args() arch = normalize_arch(args.arch) @@ -148,5 +173,6 @@ def main() -> int: print(f"Selected images for arch {args.arch}: {images}") return run_for_images(images, workspace, args.dry_run, release=args.release) + if __name__ == "__main__": sys.exit(main()) diff --git a/ci/run_ci.sh b/ci/run_ci.sh index 4c9c0c1420..0d1f90dda0 100755 --- a/ci/run_ci.sh +++ b/ci/run_ci.sh @@ -289,9 +289,24 @@ case $1 in set -e rustup component add clippy-preview rustup component add rustfmt + echo "Installing protoc for protobuf compilation" + if command -v apt-get >/dev/null; then + sudo apt-get update + sudo apt-get install -y protobuf-compiler + elif command -v brew >/dev/null; then + brew install protobuf + elif command -v yum >/dev/null; then + sudo yum install -y protobuf-compiler + else + echo "Package manager not found, downloading protoc binary" + curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-linux-x86_64.zip + unzip protoc-21.12-linux-x86_64.zip -d protoc + sudo mv protoc/bin/* /usr/local/bin/ + sudo mv protoc/include/* /usr/local/include/ + fi echo "Executing fory rust tests" cd "$ROOT/rust" - cargo doc --no-deps --document-private-items --all-features --open + cargo doc --no-deps --document-private-items --all-features cargo fmt --all -- --check cargo fmt --all cargo clippy --workspace --all-features --all-targets diff --git a/ci/tasks/rust.py b/ci/tasks/rust.py index 6ef968801a..ab10ca97d2 100644 --- a/ci/tasks/rust.py +++ b/ci/tasks/rust.py @@ -24,6 +24,38 @@ def run(): logging.info("Executing fory rust tests") common.cd_project_subdir("rust") + # Install protoc for protobuf compilation + try: + if common.is_windows(): + raise Exception("Not supported on Windows") + else: + # On Linux/macOS, install via package manager + logging.info("Installing protoc") + import shutil + + if shutil.which("apt-get"): + # Ubuntu/Debian + common.exec_cmd("sudo apt-get update") + common.exec_cmd("sudo apt-get install -y protobuf-compiler") + elif shutil.which("brew"): + # macOS + common.exec_cmd("brew install protobuf") + elif shutil.which("yum"): + # CentOS/RHEL + common.exec_cmd("sudo yum install -y protobuf-compiler") + else: + # Fallback: download binary + logging.info("Package manager not found, downloading protoc binary") + common.exec_cmd( + "curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-linux-x86_64.zip" + ) + common.exec_cmd("unzip protoc-21.12-linux-x86_64.zip -d protoc") + common.exec_cmd("sudo mv protoc/bin/* /usr/local/bin/") + common.exec_cmd("sudo mv protoc/include/* /usr/local/include/") + except Exception as e: + logging.warning(f"Failed to install protoc: {e}") + logging.warning("Continuing without protoc - benchmarks may fail") + # From run_ci.sh, we should also add rustup components try: common.exec_cmd("rustup component add clippy-preview") @@ -33,7 +65,7 @@ def run(): logging.warning("Continuing with existing components") cmds = ( - "cargo doc --no-deps --document-private-items --all-features --open", + "cargo doc --no-deps --document-private-items --all-features", "cargo fmt --all -- --check", "cargo fmt --all", "cargo clippy --workspace --all-features --all-targets -- -D warnings", diff --git a/rust/.gitignore b/rust/.gitignore index 06aba01b65..1f46350746 100644 --- a/rust/.gitignore +++ b/rust/.gitignore @@ -1,2 +1,3 @@ Cargo.lock /target +**/generated \ No newline at end of file diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 8d75b5bdf1..b5cf9daba3 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -20,7 +20,8 @@ members = [ "fory-core", "fory", "fory-derive", - "tests" + "tests", + "benches" ] exclude = [ diff --git a/rust/README.md b/rust/README.md index b69bf6cea6..13043cd591 100644 --- a/rust/README.md +++ b/rust/README.md @@ -210,6 +210,12 @@ Fory is designed to work across multiple programming languages, making it ideal - **Data pipelines** spanning multiple language ecosystems - **API communication** between different technology stacks +## Benchmark + +```bash +cargo bench --package fory-benchmarks +``` + ## 🛠️ Development Status Fory Rust implementation roadmap: diff --git a/rust/benches/Cargo.toml b/rust/benches/Cargo.toml new file mode 100644 index 0000000000..4134150719 --- /dev/null +++ b/rust/benches/Cargo.toml @@ -0,0 +1,41 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +[package] +name = "fory-benchmarks" +version = "0.1.0" +edition = "2021" + +[[bench]] +name = "serialization_bench" +path = "benches/serialization_bench.rs" +harness = false + +[dependencies] +fory = { path = "../fory" } +fory-core = { path = "../fory-core" } +fory-derive = { path = "../fory-derive" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +chrono = { version = "0.4", features = ["serde"] } +prost = "0.12" +prost-types = "0.12" +rand = "0.8" +criterion = "0.5" + +[build-dependencies] +prost-build = "0.12" diff --git a/rust/benches/benches/serialization_bench.rs b/rust/benches/benches/serialization_bench.rs new file mode 100644 index 0000000000..e468769350 --- /dev/null +++ b/rust/benches/benches/serialization_bench.rs @@ -0,0 +1,22 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use criterion::{criterion_group, criterion_main}; +use fory_benchmarks::run_serialization_benchmarks; + +criterion_group!(benches, run_serialization_benchmarks); +criterion_main!(benches); diff --git a/rust/benches/build.rs b/rust/benches/build.rs new file mode 100644 index 0000000000..f50f94428c --- /dev/null +++ b/rust/benches/build.rs @@ -0,0 +1,51 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::path::Path; + +fn main() { + println!("cargo:warning=Build script running"); + println!( + "cargo:warning=OUT_DIR: {}", + std::env::var("OUT_DIR").unwrap() + ); + + let proto_files = [ + "proto/simple.proto", + "proto/medium.proto", + "proto/complex.proto", + "proto/realworld.proto", + ]; + + for proto_file in &proto_files { + if Path::new(proto_file).exists() { + println!("cargo:rerun-if-changed={}", proto_file); + println!("cargo:warning=Found proto file: {}", proto_file); + } else { + println!("cargo:warning=Proto file not found: {}", proto_file); + } + } + + let mut config = prost_build::Config::new(); + // Don't set out_dir, use the default OUT_DIR + + println!("cargo:warning=About to compile protobuf files"); + config + .compile_protos(&proto_files, &["proto/"]) + .expect("Failed to compile protobuf files"); + println!("cargo:warning=Protobuf compilation completed"); +} diff --git a/rust/benches/proto/complex.proto b/rust/benches/proto/complex.proto new file mode 100644 index 0000000000..23e929c0e3 --- /dev/null +++ b/rust/benches/proto/complex.proto @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +syntax = "proto3"; + +package complex; + +import "google/protobuf/timestamp.proto"; + +message Product { + string id = 1; + string name = 2; + double price = 3; + repeated string categories = 4; + map attributes = 5; +} + +message OrderItem { + Product product = 1; + int32 quantity = 2; + double unit_price = 3; + map customizations = 4; +} + +message Customer { + string id = 1; + string name = 2; + string email = 3; + repeated string phone_numbers = 4; + map preferences = 5; + google.protobuf.Timestamp member_since = 6; +} + +message Order { + string id = 1; + Customer customer = 2; + repeated OrderItem items = 3; + double total_amount = 4; + string status = 5; + google.protobuf.Timestamp order_date = 6; + map metadata = 7; +} + +message ProtoECommerceData { + repeated Order orders = 1; + repeated Customer customers = 2; + repeated Product products = 3; + map order_lookup = 4; +} diff --git a/rust/benches/proto/medium.proto b/rust/benches/proto/medium.proto new file mode 100644 index 0000000000..dd229e6aa6 --- /dev/null +++ b/rust/benches/proto/medium.proto @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +syntax = "proto3"; + +package medium; + +import "google/protobuf/timestamp.proto"; + +message Address { + string street = 1; + string city = 2; + string country = 3; + string zip_code = 4; +} + +message ProtoPerson { + string name = 1; + int32 age = 2; + Address address = 3; + repeated string hobbies = 4; + map metadata = 5; + google.protobuf.Timestamp created_at = 6; +} + +message ProtoCompany { + string name = 1; + repeated ProtoPerson employees = 2; + map offices = 3; + bool is_public = 4; +} diff --git a/rust/benches/proto/realworld.proto b/rust/benches/proto/realworld.proto new file mode 100644 index 0000000000..fc8d6a903c --- /dev/null +++ b/rust/benches/proto/realworld.proto @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +syntax = "proto3"; + +package realworld; + +import "google/protobuf/timestamp.proto"; + +message LogLevel { + enum Level { + UNKNOWN = 0; + DEBUG = 1; + INFO = 2; + WARN = 3; + ERROR = 4; + FATAL = 5; + } + Level level = 1; +} + +message LogEntry { + string id = 1; + LogLevel.Level level = 2; + string message = 3; + string service = 4; + google.protobuf.Timestamp timestamp = 5; + map context = 6; + repeated string tags = 7; + double duration_ms = 8; +} + +message UserProfile { + string user_id = 1; + string username = 2; + string email = 3; + map preferences = 4; + repeated string permissions = 5; + google.protobuf.Timestamp last_login = 6; + bool is_active = 7; +} + +message APIMetrics { + string endpoint = 1; + int64 request_count = 2; + double avg_response_time = 3; + int64 error_count = 4; + map status_codes = 5; + google.protobuf.Timestamp measured_at = 6; +} + +message ProtoSystemData { + repeated LogEntry logs = 1; + repeated UserProfile users = 2; + repeated APIMetrics metrics = 3; + map system_info = 4; +} diff --git a/rust/benches/proto/simple.proto b/rust/benches/proto/simple.proto new file mode 100644 index 0000000000..f8b080bffb --- /dev/null +++ b/rust/benches/proto/simple.proto @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +syntax = "proto3"; + +package simple; + +message ProtoSimpleStruct { + int32 id = 1; + string name = 2; + bool active = 3; + double score = 4; +} + +message ProtoSimpleList { + repeated int32 numbers = 1; + repeated string names = 2; +} + +message ProtoSimpleMap { + map string_to_int = 1; + map int_to_string = 2; +} diff --git a/rust/benches/src/lib.rs b/rust/benches/src/lib.rs new file mode 100644 index 0000000000..4d956129f5 --- /dev/null +++ b/rust/benches/src/lib.rs @@ -0,0 +1,327 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +pub mod models; +pub mod serializers; + +// Include generated protobuf code +include!(concat!(env!("OUT_DIR"), "/simple.rs")); +include!(concat!(env!("OUT_DIR"), "/medium.rs")); +include!(concat!(env!("OUT_DIR"), "/complex.rs")); +include!(concat!(env!("OUT_DIR"), "/realworld.rs")); + +// Benchmark function implementation +use criterion::{black_box, BenchmarkId, Criterion}; +use models::{ + complex::ECommerceData, + medium::{Company, Person}, + realworld::SystemData, + simple::{SimpleList, SimpleMap, SimpleStruct}, + TestDataGenerator, +}; +use serializers::{ + fury::FurySerializer, json::JsonSerializer, protobuf::ProtobufSerializer, Serializer, +}; + +pub fn run_serialization_benchmarks(c: &mut Criterion) { + let fury_serializer = FurySerializer::new(); + let protobuf_serializer = ProtobufSerializer::new(); + let json_serializer = JsonSerializer::new(); + + // Simple struct benchmarks + run_benchmark_group::( + c, + "simple_struct", + &fury_serializer, + &protobuf_serializer, + &json_serializer, + ); + + // Simple list benchmarks + run_benchmark_group::( + c, + "simple_list", + &fury_serializer, + &protobuf_serializer, + &json_serializer, + ); + + // Simple map benchmarks + run_benchmark_group::( + c, + "simple_map", + &fury_serializer, + &protobuf_serializer, + &json_serializer, + ); + + // Person benchmarks + run_benchmark_group::( + c, + "person", + &fury_serializer, + &protobuf_serializer, + &json_serializer, + ); + + // Company benchmarks + run_benchmark_group::( + c, + "company", + &fury_serializer, + &protobuf_serializer, + &json_serializer, + ); + + // ECommerce data benchmarks + run_benchmark_group::( + c, + "ecommerce_data", + &fury_serializer, + &protobuf_serializer, + &json_serializer, + ); + + // System data benchmarks + run_benchmark_group::( + c, + "system_data", + &fury_serializer, + &protobuf_serializer, + &json_serializer, + ); +} + +fn run_benchmark_group( + c: &mut Criterion, + group_name: &str, + fury_serializer: &FurySerializer, + protobuf_serializer: &ProtobufSerializer, + json_serializer: &JsonSerializer, +) where + T: TestDataGenerator + Clone + PartialEq, + FurySerializer: Serializer, + ProtobufSerializer: Serializer, + JsonSerializer: Serializer, +{ + let mut group = c.benchmark_group(group_name); + + // Test data sizes + let small_data = T::generate_small(); + let medium_data = T::generate_medium(); + let large_data = T::generate_large(); + + // Fury serialization benchmarks + group.bench_with_input( + BenchmarkId::new("fury_serialize", "small"), + &small_data, + |b, data| { + b.iter(|| { + let _ = black_box(fury_serializer.serialize(black_box(data)).unwrap()); + }) + }, + ); + + group.bench_with_input( + BenchmarkId::new("fury_serialize", "medium"), + &medium_data, + |b, data| { + b.iter(|| { + let _ = black_box(fury_serializer.serialize(black_box(data)).unwrap()); + }) + }, + ); + + group.bench_with_input( + BenchmarkId::new("fury_serialize", "large"), + &large_data, + |b, data| { + b.iter(|| { + let _ = black_box(fury_serializer.serialize(black_box(data)).unwrap()); + }) + }, + ); + + // Fury deserialization benchmarks + let small_serialized = fury_serializer.serialize(&small_data).unwrap(); + let medium_serialized = fury_serializer.serialize(&medium_data).unwrap(); + let large_serialized = fury_serializer.serialize(&large_data).unwrap(); + + group.bench_with_input( + BenchmarkId::new("fury_deserialize", "small"), + &small_serialized, + |b, data| { + b.iter(|| { + let _ = black_box(fury_serializer.deserialize(black_box(data)).unwrap()); + }) + }, + ); + + group.bench_with_input( + BenchmarkId::new("fury_deserialize", "medium"), + &medium_serialized, + |b, data| { + b.iter(|| { + let _ = black_box(fury_serializer.deserialize(black_box(data)).unwrap()); + }) + }, + ); + + group.bench_with_input( + BenchmarkId::new("fury_deserialize", "large"), + &large_serialized, + |b, data| { + b.iter(|| { + let _ = black_box(fury_serializer.deserialize(black_box(data)).unwrap()); + }) + }, + ); + + // Protobuf serialization benchmarks + group.bench_with_input( + BenchmarkId::new("protobuf_serialize", "small"), + &small_data, + |b, data| { + b.iter(|| { + let _ = black_box(protobuf_serializer.serialize(black_box(data)).unwrap()); + }) + }, + ); + + group.bench_with_input( + BenchmarkId::new("protobuf_serialize", "medium"), + &medium_data, + |b, data| { + b.iter(|| { + let _ = black_box(protobuf_serializer.serialize(black_box(data)).unwrap()); + }) + }, + ); + + group.bench_with_input( + BenchmarkId::new("protobuf_serialize", "large"), + &large_data, + |b, data| { + b.iter(|| { + let _ = black_box(protobuf_serializer.serialize(black_box(data)).unwrap()); + }) + }, + ); + + // Protobuf deserialization benchmarks + let small_protobuf_serialized = protobuf_serializer.serialize(&small_data).unwrap(); + let medium_protobuf_serialized = protobuf_serializer.serialize(&medium_data).unwrap(); + let large_protobuf_serialized = protobuf_serializer.serialize(&large_data).unwrap(); + + group.bench_with_input( + BenchmarkId::new("protobuf_deserialize", "small"), + &small_protobuf_serialized, + |b, data| { + b.iter(|| { + let _ = black_box(protobuf_serializer.deserialize(black_box(data)).unwrap()); + }) + }, + ); + + group.bench_with_input( + BenchmarkId::new("protobuf_deserialize", "medium"), + &medium_protobuf_serialized, + |b, data| { + b.iter(|| { + let _ = black_box(protobuf_serializer.deserialize(black_box(data)).unwrap()); + }) + }, + ); + + group.bench_with_input( + BenchmarkId::new("protobuf_deserialize", "large"), + &large_protobuf_serialized, + |b, data| { + b.iter(|| { + let _ = black_box(protobuf_serializer.deserialize(black_box(data)).unwrap()); + }) + }, + ); + + // JSON serialization benchmarks + group.bench_with_input( + BenchmarkId::new("json_serialize", "small"), + &small_data, + |b, data| { + b.iter(|| { + let _ = black_box(json_serializer.serialize(black_box(data)).unwrap()); + }) + }, + ); + + group.bench_with_input( + BenchmarkId::new("json_serialize", "medium"), + &medium_data, + |b, data| { + b.iter(|| { + let _ = black_box(json_serializer.serialize(black_box(data)).unwrap()); + }) + }, + ); + + group.bench_with_input( + BenchmarkId::new("json_serialize", "large"), + &large_data, + |b, data| { + b.iter(|| { + let _ = black_box(json_serializer.serialize(black_box(data)).unwrap()); + }) + }, + ); + + // JSON deserialization benchmarks + let small_json_serialized = json_serializer.serialize(&small_data).unwrap(); + let medium_json_serialized = json_serializer.serialize(&medium_data).unwrap(); + let large_json_serialized = json_serializer.serialize(&large_data).unwrap(); + + group.bench_with_input( + BenchmarkId::new("json_deserialize", "small"), + &small_json_serialized, + |b, data| { + b.iter(|| { + let _ = black_box(json_serializer.deserialize(black_box(data)).unwrap()); + }) + }, + ); + + group.bench_with_input( + BenchmarkId::new("json_deserialize", "medium"), + &medium_json_serialized, + |b, data| { + b.iter(|| { + let _ = black_box(json_serializer.deserialize(black_box(data)).unwrap()); + }) + }, + ); + + group.bench_with_input( + BenchmarkId::new("json_deserialize", "large"), + &large_json_serialized, + |b, data| { + b.iter(|| { + let _ = black_box(json_serializer.deserialize(black_box(data)).unwrap()); + }) + }, + ); + + group.finish(); +} diff --git a/rust/benches/src/models/complex.rs b/rust/benches/src/models/complex.rs new file mode 100644 index 0000000000..0b66db9dc0 --- /dev/null +++ b/rust/benches/src/models/complex.rs @@ -0,0 +1,508 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use crate::models::{generate_random_string, generate_random_strings, TestDataGenerator}; +use chrono::{DateTime, NaiveDateTime, Utc}; +use fory_derive::Fory; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +// Fury models +#[derive(Fory, Debug, Clone, PartialEq, Default)] +pub struct FuryProduct { + pub id: String, + pub name: String, + pub price: f64, + pub categories: Vec, + pub attributes: HashMap, +} + +#[derive(Fory, Debug, Clone, PartialEq, Default)] +pub struct FuryOrderItem { + pub product: FuryProduct, + pub quantity: i32, + pub unit_price: f64, + pub customizations: HashMap, +} + +#[derive(Fory, Debug, Clone, PartialEq, Default)] +pub struct FuryCustomer { + pub id: String, + pub name: String, + pub email: String, + pub phone_numbers: Vec, + pub preferences: HashMap, + pub member_since: NaiveDateTime, +} + +#[derive(Fory, Debug, Clone, PartialEq, Default)] +pub struct FuryOrder { + pub id: String, + pub customer: FuryCustomer, + pub items: Vec, + pub total_amount: f64, + pub status: String, + pub order_date: NaiveDateTime, + pub metadata: HashMap, +} + +#[derive(Fory, Debug, Clone, PartialEq, Default)] +pub struct ECommerceData { + pub orders: Vec, + pub customers: Vec, + pub products: Vec, + pub order_lookup: HashMap, +} + +// Serde models +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct SerdeProduct { + pub id: String, + pub name: String, + pub price: f64, + pub categories: Vec, + pub attributes: HashMap, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct SerdeOrderItem { + pub product: SerdeProduct, + pub quantity: i32, + pub unit_price: f64, + pub customizations: HashMap, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct SerdeCustomer { + pub id: String, + pub name: String, + pub email: String, + pub phone_numbers: Vec, + pub preferences: HashMap, + pub member_since: DateTime, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct SerdeOrder { + pub id: String, + pub customer: SerdeCustomer, + pub items: Vec, + pub total_amount: f64, + pub status: String, + pub order_date: DateTime, + pub metadata: HashMap, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct SerdeECommerceData { + pub orders: Vec, + pub customers: Vec, + pub products: Vec, + pub order_lookup: HashMap, +} + +impl TestDataGenerator for ECommerceData { + type Data = ECommerceData; + + fn generate_small() -> Self::Data { + let product = FuryProduct { + id: "prod_1".to_string(), + name: "Laptop".to_string(), + price: 999.99, + categories: vec!["Electronics".to_string(), "Computers".to_string()], + attributes: HashMap::from([ + ("brand".to_string(), "TechCorp".to_string()), + ("model".to_string(), "X1".to_string()), + ]), + }; + + let customer = FuryCustomer { + id: "cust_1".to_string(), + name: "John Doe".to_string(), + email: "john@example.com".to_string(), + phone_numbers: vec!["+1234567890".to_string()], + preferences: HashMap::from([ + ("language".to_string(), "en".to_string()), + ("currency".to_string(), "USD".to_string()), + ]), + member_since: DateTime::from_timestamp(1640995200, 0).unwrap().naive_utc(), + }; + + let order_item = FuryOrderItem { + product: product.clone(), + quantity: 1, + unit_price: 999.99, + customizations: HashMap::from([("color".to_string(), "black".to_string())]), + }; + + let order = FuryOrder { + id: "order_1".to_string(), + customer: customer.clone(), + items: vec![order_item], + total_amount: 999.99, + status: "completed".to_string(), + order_date: DateTime::from_timestamp(1640995200, 0).unwrap().naive_utc(), + metadata: HashMap::from([("payment_method".to_string(), "credit_card".to_string())]), + }; + + let mut order_lookup = HashMap::new(); + order_lookup.insert(order.id.clone(), order.clone()); + + ECommerceData { + orders: vec![order], + customers: vec![customer], + products: vec![product], + order_lookup, + } + } + + fn generate_medium() -> Self::Data { + let mut products = Vec::new(); + let mut customers = Vec::new(); + let mut orders = Vec::new(); + let mut order_lookup = HashMap::new(); + + // Generate 20 products + for i in 1..=20 { + let product = FuryProduct { + id: format!("prod_{}", i), + name: generate_random_string(30), + price: (i as f64) * 50.0, + categories: generate_random_strings(3, 15), + attributes: { + let mut map = HashMap::new(); + for j in 1..=5 { + map.insert(format!("attr_{}", j), generate_random_string(20)); + } + map + }, + }; + products.push(product); + } + + // Generate 10 customers + for i in 1..=10 { + let customer = FuryCustomer { + id: format!("cust_{}", i), + name: generate_random_string(40), + email: format!("user{}@example.com", i), + phone_numbers: generate_random_strings(2, 12), + preferences: { + let mut map = HashMap::new(); + for j in 1..=10 { + map.insert(format!("pref_{}", j), generate_random_string(15)); + } + map + }, + member_since: DateTime::from_timestamp(1640995200, 0).unwrap().naive_utc(), + }; + customers.push(customer); + } + + // Generate 15 orders + for i in 1..=15 { + let customer = customers[i % customers.len()].clone(); + let mut items = Vec::new(); + let mut total = 0.0; + + for j in 1..=3 { + let product = products[j % products.len()].clone(); + let quantity = ((j % 5) + 1) as i32; + let unit_price = product.price; + total += unit_price * quantity as f64; + + let item = FuryOrderItem { + product, + quantity, + unit_price, + customizations: { + let mut map = HashMap::new(); + for k in 1..=3 { + map.insert(format!("custom_{}", k), generate_random_string(10)); + } + map + }, + }; + items.push(item); + } + + let order = FuryOrder { + id: format!("order_{}", i), + customer, + items, + total_amount: total, + status: if i % 3 == 0 { + "pending".to_string() + } else { + "completed".to_string() + }, + order_date: DateTime::from_timestamp(1640995200, 0).unwrap().naive_utc(), + metadata: { + let mut map = HashMap::new(); + for k in 1..=5 { + map.insert(format!("meta_{}", k), generate_random_string(20)); + } + map + }, + }; + + order_lookup.insert(order.id.clone(), order.clone()); + orders.push(order); + } + + ECommerceData { + orders, + customers, + products, + order_lookup, + } + } + + fn generate_large() -> Self::Data { + let mut products = Vec::new(); + let mut customers = Vec::new(); + let mut orders = Vec::new(); + let mut order_lookup = HashMap::new(); + + // Generate 100 products + for i in 1..=100 { + let product = FuryProduct { + id: format!("prod_{}", i), + name: generate_random_string(50), + price: (i as f64) * 25.0, + categories: generate_random_strings(5, 20), + attributes: { + let mut map = HashMap::new(); + for j in 1..=10 { + map.insert(format!("attr_{}", j), generate_random_string(30)); + } + map + }, + }; + products.push(product); + } + + // Generate 50 customers + for i in 1..=50 { + let customer = FuryCustomer { + id: format!("cust_{}", i), + name: generate_random_string(60), + email: format!("user{}@example.com", i), + phone_numbers: generate_random_strings(3, 15), + preferences: { + let mut map = HashMap::new(); + for j in 1..=20 { + map.insert(format!("pref_{}", j), generate_random_string(25)); + } + map + }, + member_since: DateTime::from_timestamp(1640995200, 0).unwrap().naive_utc(), + }; + customers.push(customer); + } + + // Generate 100 orders + for i in 1..=100 { + let customer = customers[i % customers.len()].clone(); + let mut items = Vec::new(); + let mut total = 0.0; + + for j in 1..=5 { + let product = products[j % products.len()].clone(); + let quantity = ((j % 10) + 1) as i32; + let unit_price = product.price; + total += unit_price * quantity as f64; + + let item = FuryOrderItem { + product, + quantity, + unit_price, + customizations: { + let mut map = HashMap::new(); + for k in 1..=5 { + map.insert(format!("custom_{}", k), generate_random_string(20)); + } + map + }, + }; + items.push(item); + } + + let order = FuryOrder { + id: format!("order_{}", i), + customer, + items, + total_amount: total, + status: if i % 4 == 0 { + "pending".to_string() + } else { + "completed".to_string() + }, + order_date: DateTime::from_timestamp(1640995200, 0).unwrap().naive_utc(), + metadata: { + let mut map = HashMap::new(); + for k in 1..=10 { + map.insert(format!("meta_{}", k), generate_random_string(30)); + } + map + }, + }; + + order_lookup.insert(order.id.clone(), order.clone()); + orders.push(order); + } + + ECommerceData { + orders, + customers, + products, + order_lookup, + } + } +} + +// Conversion functions for Serde +impl From for SerdeProduct { + fn from(f: FuryProduct) -> Self { + SerdeProduct { + id: f.id, + name: f.name, + price: f.price, + categories: f.categories, + attributes: f.attributes, + } + } +} + +impl From for SerdeOrderItem { + fn from(f: FuryOrderItem) -> Self { + SerdeOrderItem { + product: f.product.into(), + quantity: f.quantity, + unit_price: f.unit_price, + customizations: f.customizations, + } + } +} + +impl From for SerdeCustomer { + fn from(f: FuryCustomer) -> Self { + SerdeCustomer { + id: f.id, + name: f.name, + email: f.email, + phone_numbers: f.phone_numbers, + preferences: f.preferences, + member_since: f.member_since.and_utc(), + } + } +} + +impl From for SerdeOrder { + fn from(f: FuryOrder) -> Self { + SerdeOrder { + id: f.id, + customer: f.customer.into(), + items: f.items.into_iter().map(|i| i.into()).collect(), + total_amount: f.total_amount, + status: f.status, + order_date: f.order_date.and_utc(), + metadata: f.metadata, + } + } +} + +impl From for SerdeECommerceData { + fn from(f: ECommerceData) -> Self { + SerdeECommerceData { + orders: f.orders.into_iter().map(|o| o.into()).collect(), + customers: f.customers.into_iter().map(|c| c.into()).collect(), + products: f.products.into_iter().map(|p| p.into()).collect(), + order_lookup: f + .order_lookup + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(), + } + } +} + +// Reverse conversions from Serde to Fury +impl From for FuryProduct { + fn from(s: SerdeProduct) -> Self { + FuryProduct { + id: s.id, + name: s.name, + price: s.price, + categories: s.categories, + attributes: s.attributes, + } + } +} + +impl From for FuryOrderItem { + fn from(s: SerdeOrderItem) -> Self { + FuryOrderItem { + product: s.product.into(), + quantity: s.quantity, + unit_price: s.unit_price, + customizations: s.customizations, + } + } +} + +impl From for FuryCustomer { + fn from(s: SerdeCustomer) -> Self { + FuryCustomer { + id: s.id, + name: s.name, + email: s.email, + phone_numbers: s.phone_numbers, + preferences: s.preferences, + member_since: s.member_since.naive_utc(), + } + } +} + +impl From for FuryOrder { + fn from(s: SerdeOrder) -> Self { + FuryOrder { + id: s.id, + customer: s.customer.into(), + items: s.items.into_iter().map(|i| i.into()).collect(), + total_amount: s.total_amount, + status: s.status, + order_date: s.order_date.naive_utc(), + metadata: s.metadata, + } + } +} + +impl From for ECommerceData { + fn from(s: SerdeECommerceData) -> Self { + ECommerceData { + orders: s.orders.into_iter().map(|o| o.into()).collect(), + customers: s.customers.into_iter().map(|c| c.into()).collect(), + products: s.products.into_iter().map(|p| p.into()).collect(), + order_lookup: s + .order_lookup + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(), + } + } +} diff --git a/rust/benches/src/models/medium.rs b/rust/benches/src/models/medium.rs new file mode 100644 index 0000000000..e5a72bbe32 --- /dev/null +++ b/rust/benches/src/models/medium.rs @@ -0,0 +1,294 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use crate::models::{generate_random_string, generate_random_strings, TestDataGenerator}; +use chrono::{DateTime, NaiveDateTime, Utc}; +use fory_derive::Fory; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +// Fury models +#[derive(Fory, Debug, Clone, PartialEq, Default)] +pub struct FuryAddress { + pub street: String, + pub city: String, + pub country: String, + pub zip_code: String, +} + +#[derive(Fory, Debug, Clone, PartialEq, Default)] +pub struct Person { + pub name: String, + pub age: i32, + pub address: FuryAddress, + pub hobbies: Vec, + pub metadata: HashMap, + pub created_at: NaiveDateTime, +} + +#[derive(Fory, Debug, Clone, PartialEq, Default)] +pub struct Company { + pub name: String, + pub employees: Vec, + pub offices: HashMap, + pub is_public: bool, +} + +// Serde models +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct SerdeAddress { + pub street: String, + pub city: String, + pub country: String, + pub zip_code: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct SerdePerson { + pub name: String, + pub age: i32, + pub address: SerdeAddress, + pub hobbies: Vec, + pub metadata: HashMap, + pub created_at: DateTime, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct SerdeCompany { + pub name: String, + pub employees: Vec, + pub offices: HashMap, + pub is_public: bool, +} + +impl TestDataGenerator for Person { + type Data = Person; + + fn generate_small() -> Self::Data { + Person { + name: "John Doe".to_string(), + age: 30, + address: FuryAddress { + street: "123 Main St".to_string(), + city: "New York".to_string(), + country: "USA".to_string(), + zip_code: "10001".to_string(), + }, + hobbies: vec!["reading".to_string(), "coding".to_string()], + metadata: HashMap::from([ + ("department".to_string(), "engineering".to_string()), + ("level".to_string(), "senior".to_string()), + ]), + created_at: DateTime::from_timestamp(1640995200, 0).unwrap().naive_utc(), + } + } + + fn generate_medium() -> Self::Data { + Person { + name: generate_random_string(30), + age: 35, + address: FuryAddress { + street: generate_random_string(50), + city: generate_random_string(20), + country: generate_random_string(15), + zip_code: generate_random_string(10), + }, + hobbies: generate_random_strings(10, 15), + metadata: { + let mut map = HashMap::new(); + for i in 1..=20 { + map.insert(format!("key_{}", i), generate_random_string(20)); + } + map + }, + created_at: DateTime::from_timestamp(1640995200, 0).unwrap().naive_utc(), + } + } + + fn generate_large() -> Self::Data { + Person { + name: generate_random_string(100), + age: 40, + address: FuryAddress { + street: generate_random_string(100), + city: generate_random_string(50), + country: generate_random_string(30), + zip_code: generate_random_string(20), + }, + hobbies: generate_random_strings(50, 25), + metadata: { + let mut map = HashMap::new(); + for i in 1..=100 { + map.insert(format!("key_{}", i), generate_random_string(50)); + } + map + }, + created_at: DateTime::from_timestamp(1640995200, 0).unwrap().naive_utc(), + } + } +} + +impl TestDataGenerator for Company { + type Data = Company; + + fn generate_small() -> Self::Data { + Company { + name: "Tech Corp".to_string(), + employees: vec![Person::generate_small()], + offices: HashMap::from([( + "HQ".to_string(), + FuryAddress { + street: "456 Tech Ave".to_string(), + city: "San Francisco".to_string(), + country: "USA".to_string(), + zip_code: "94105".to_string(), + }, + )]), + is_public: true, + } + } + + fn generate_medium() -> Self::Data { + let mut employees = Vec::new(); + for i in 1..=10 { + let mut person = Person::generate_medium(); + person.name = format!("Employee_{}", i); + employees.push(person); + } + + let mut offices = HashMap::new(); + for i in 1..=5 { + offices.insert( + format!("Office_{}", i), + FuryAddress { + street: generate_random_string(50), + city: generate_random_string(20), + country: generate_random_string(15), + zip_code: generate_random_string(10), + }, + ); + } + + Company { + name: generate_random_string(50), + employees, + offices, + is_public: true, + } + } + + fn generate_large() -> Self::Data { + let mut employees = Vec::new(); + for i in 1..=100 { + let mut person = Person::generate_large(); + person.name = format!("Employee_{}", i); + employees.push(person); + } + + let mut offices = HashMap::new(); + for i in 1..=20 { + offices.insert( + format!("Office_{}", i), + FuryAddress { + street: generate_random_string(100), + city: generate_random_string(50), + country: generate_random_string(30), + zip_code: generate_random_string(20), + }, + ); + } + + Company { + name: generate_random_string(100), + employees, + offices, + is_public: false, + } + } +} + +// Conversion functions for Serde +impl From for SerdeAddress { + fn from(f: FuryAddress) -> Self { + SerdeAddress { + street: f.street, + city: f.city, + country: f.country, + zip_code: f.zip_code, + } + } +} + +impl From for SerdePerson { + fn from(f: Person) -> Self { + SerdePerson { + name: f.name, + age: f.age, + address: f.address.into(), + hobbies: f.hobbies, + metadata: f.metadata, + created_at: f.created_at.and_utc(), + } + } +} + +impl From for SerdeCompany { + fn from(f: Company) -> Self { + SerdeCompany { + name: f.name, + employees: f.employees.into_iter().map(|e| e.into()).collect(), + offices: f.offices.into_iter().map(|(k, v)| (k, v.into())).collect(), + is_public: f.is_public, + } + } +} + +// Reverse conversions from Serde to Fury +impl From for FuryAddress { + fn from(s: SerdeAddress) -> Self { + FuryAddress { + street: s.street, + city: s.city, + country: s.country, + zip_code: s.zip_code, + } + } +} + +impl From for Person { + fn from(s: SerdePerson) -> Self { + Person { + name: s.name, + age: s.age, + address: s.address.into(), + hobbies: s.hobbies, + metadata: s.metadata, + created_at: s.created_at.naive_utc(), + } + } +} + +impl From for Company { + fn from(s: SerdeCompany) -> Self { + Company { + name: s.name, + employees: s.employees.into_iter().map(|e| e.into()).collect(), + offices: s.offices.into_iter().map(|(k, v)| (k, v.into())).collect(), + is_public: s.is_public, + } + } +} diff --git a/rust/benches/src/models/mod.rs b/rust/benches/src/models/mod.rs new file mode 100644 index 0000000000..94efc49e50 --- /dev/null +++ b/rust/benches/src/models/mod.rs @@ -0,0 +1,45 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +pub mod complex; +pub mod medium; +pub mod realworld; +pub mod simple; + +use rand::Rng; + +pub trait TestDataGenerator { + type Data; + fn generate_small() -> Self::Data; + fn generate_medium() -> Self::Data; + fn generate_large() -> Self::Data; +} + +pub fn generate_random_string(len: usize) -> String { + const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let mut rng = rand::thread_rng(); + (0..len) + .map(|_| { + let idx = rng.gen_range(0..CHARSET.len()); + CHARSET[idx] as char + }) + .collect() +} + +pub fn generate_random_strings(count: usize, len: usize) -> Vec { + (0..count).map(|_| generate_random_string(len)).collect() +} diff --git a/rust/benches/src/models/realworld.rs b/rust/benches/src/models/realworld.rs new file mode 100644 index 0000000000..01f7730231 --- /dev/null +++ b/rust/benches/src/models/realworld.rs @@ -0,0 +1,440 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use crate::models::{generate_random_string, generate_random_strings, TestDataGenerator}; +use chrono::{DateTime, NaiveDateTime, Utc}; +use fory_derive::Fory; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +// Fury models +#[derive(Fory, Debug, Clone, PartialEq, Default)] +pub struct FuryLogEntry { + pub id: String, + pub level: i32, // 0=DEBUG, 1=INFO, 2=WARN, 3=ERROR, 4=FATAL + pub message: String, + pub service: String, + pub timestamp: NaiveDateTime, + pub context: HashMap, + pub tags: Vec, + pub duration_ms: f64, +} + +#[derive(Fory, Debug, Clone, PartialEq, Default)] +pub struct FuryUserProfile { + pub user_id: String, + pub username: String, + pub email: String, + pub preferences: HashMap, + pub permissions: Vec, + pub last_login: NaiveDateTime, + pub is_active: bool, +} + +#[derive(Fory, Debug, Clone, PartialEq, Default)] +pub struct FuryAPIMetrics { + pub endpoint: String, + pub request_count: i64, + pub avg_response_time: f64, + pub error_count: i64, + pub status_codes: HashMap, + pub measured_at: NaiveDateTime, +} + +#[derive(Fory, Debug, Clone, PartialEq, Default)] +pub struct SystemData { + pub logs: Vec, + pub users: Vec, + pub metrics: Vec, + pub system_info: HashMap, +} + +// Serde models +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct SerdeLogEntry { + pub id: String, + pub level: i32, + pub message: String, + pub service: String, + pub timestamp: DateTime, + pub context: HashMap, + pub tags: Vec, + pub duration_ms: f64, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct SerdeUserProfile { + pub user_id: String, + pub username: String, + pub email: String, + pub preferences: HashMap, + pub permissions: Vec, + pub last_login: DateTime, + pub is_active: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct SerdeAPIMetrics { + pub endpoint: String, + pub request_count: i64, + pub avg_response_time: f64, + pub error_count: i64, + pub status_codes: HashMap, + pub measured_at: DateTime, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct SerdeSystemData { + pub logs: Vec, + pub users: Vec, + pub metrics: Vec, + pub system_info: HashMap, +} + +impl TestDataGenerator for SystemData { + type Data = SystemData; + + fn generate_small() -> Self::Data { + let log = FuryLogEntry { + id: "log_1".to_string(), + level: 1, // INFO + message: "User login successful".to_string(), + service: "auth-service".to_string(), + timestamp: DateTime::from_timestamp(1640995200, 0).unwrap().naive_utc(), + context: HashMap::from([ + ("user_id".to_string(), "123".to_string()), + ("ip".to_string(), "192.168.1.1".to_string()), + ]), + tags: vec!["auth".to_string(), "login".to_string()], + duration_ms: 45.2, + }; + + let user = FuryUserProfile { + user_id: "user_123".to_string(), + username: "johndoe".to_string(), + email: "john@example.com".to_string(), + preferences: HashMap::from([ + ("theme".to_string(), "dark".to_string()), + ("language".to_string(), "en".to_string()), + ]), + permissions: vec!["read".to_string(), "write".to_string()], + last_login: DateTime::from_timestamp(1640995200, 0).unwrap().naive_utc(), + is_active: true, + }; + + let metric = FuryAPIMetrics { + endpoint: "/api/users".to_string(), + request_count: 1000, + avg_response_time: 150.5, + error_count: 5, + status_codes: HashMap::from([ + ("200".to_string(), 950), + ("404".to_string(), 45), + ("500".to_string(), 5), + ]), + measured_at: DateTime::from_timestamp(1640995200, 0).unwrap().naive_utc(), + }; + + SystemData { + logs: vec![log], + users: vec![user], + metrics: vec![metric], + system_info: HashMap::from([ + ("version".to_string(), "1.0.0".to_string()), + ("environment".to_string(), "production".to_string()), + ]), + } + } + + fn generate_medium() -> Self::Data { + let mut logs = Vec::new(); + let mut users = Vec::new(); + let mut metrics = Vec::new(); + + // Generate 50 log entries + for i in 1..=50 { + let log = FuryLogEntry { + id: format!("log_{}", i), + level: i % 5, + message: generate_random_string(100), + service: format!("service_{}", i % 10), + timestamp: DateTime::from_timestamp(1640995200 + i as i64, 0) + .unwrap() + .naive_utc(), + context: { + let mut map = HashMap::new(); + for j in 1..=5 { + map.insert(format!("ctx_{}", j), generate_random_string(20)); + } + map + }, + tags: generate_random_strings(3, 10), + duration_ms: (i as f64) * 10.0, + }; + logs.push(log); + } + + // Generate 20 users + for i in 1..=20 { + let user = FuryUserProfile { + user_id: format!("user_{}", i), + username: generate_random_string(20), + email: format!("user{}@example.com", i), + preferences: { + let mut map = HashMap::new(); + for j in 1..=10 { + map.insert(format!("pref_{}", j), generate_random_string(15)); + } + map + }, + permissions: generate_random_strings(5, 15), + last_login: DateTime::from_timestamp(1640995200 + i as i64, 0) + .unwrap() + .naive_utc(), + is_active: i % 3 != 0, + }; + users.push(user); + } + + // Generate 10 metrics + for i in 1..=10 { + let metric = FuryAPIMetrics { + endpoint: format!("/api/endpoint_{}", i), + request_count: (i * 1000) as i64, + avg_response_time: (i as f64) * 50.0, + error_count: (i * 10) as i64, + status_codes: { + let mut map = HashMap::new(); + map.insert("200".to_string(), (i * 800) as i64); + map.insert("404".to_string(), (i * 50) as i64); + map.insert("500".to_string(), (i * 10) as i64); + map + }, + measured_at: DateTime::from_timestamp(1640995200 + i as i64, 0) + .unwrap() + .naive_utc(), + }; + metrics.push(metric); + } + + SystemData { + logs, + users, + metrics, + system_info: { + let mut map = HashMap::new(); + for i in 1..=10 { + map.insert(format!("info_{}", i), generate_random_string(30)); + } + map + }, + } + } + + fn generate_large() -> Self::Data { + let mut logs = Vec::new(); + let mut users = Vec::new(); + let mut metrics = Vec::new(); + + // Generate 500 log entries + for i in 1..=500 { + let log = FuryLogEntry { + id: format!("log_{}", i), + level: i % 5, + message: generate_random_string(200), + service: format!("service_{}", i % 50), + timestamp: DateTime::from_timestamp(1640995200 + i as i64, 0) + .unwrap() + .naive_utc(), + context: { + let mut map = HashMap::new(); + for j in 1..=10 { + map.insert(format!("ctx_{}", j), generate_random_string(30)); + } + map + }, + tags: generate_random_strings(5, 15), + duration_ms: (i as f64) * 5.0, + }; + logs.push(log); + } + + // Generate 100 users + for i in 1..=100 { + let user = FuryUserProfile { + user_id: format!("user_{}", i), + username: generate_random_string(30), + email: format!("user{}@example.com", i), + preferences: { + let mut map = HashMap::new(); + for j in 1..=20 { + map.insert(format!("pref_{}", j), generate_random_string(25)); + } + map + }, + permissions: generate_random_strings(10, 20), + last_login: DateTime::from_timestamp(1640995200 + i as i64, 0) + .unwrap() + .naive_utc(), + is_active: i % 4 != 0, + }; + users.push(user); + } + + // Generate 50 metrics + for i in 1..=50 { + let metric = FuryAPIMetrics { + endpoint: format!("/api/endpoint_{}", i), + request_count: (i * 2000) as i64, + avg_response_time: (i as f64) * 25.0, + error_count: (i * 20) as i64, + status_codes: { + let mut map = HashMap::new(); + map.insert("200".to_string(), (i * 1600) as i64); + map.insert("404".to_string(), (i * 200) as i64); + map.insert("500".to_string(), (i * 20) as i64); + map + }, + measured_at: DateTime::from_timestamp(1640995200 + i as i64, 0) + .unwrap() + .naive_utc(), + }; + metrics.push(metric); + } + + SystemData { + logs, + users, + metrics, + system_info: { + let mut map = HashMap::new(); + for i in 1..=20 { + map.insert(format!("info_{}", i), generate_random_string(50)); + } + map + }, + } + } +} + +// Conversion functions for Serde +impl From for SerdeLogEntry { + fn from(f: FuryLogEntry) -> Self { + SerdeLogEntry { + id: f.id, + level: f.level, + message: f.message, + service: f.service, + timestamp: f.timestamp.and_utc(), + context: f.context, + tags: f.tags, + duration_ms: f.duration_ms, + } + } +} + +impl From for SerdeUserProfile { + fn from(f: FuryUserProfile) -> Self { + SerdeUserProfile { + user_id: f.user_id, + username: f.username, + email: f.email, + preferences: f.preferences, + permissions: f.permissions, + last_login: f.last_login.and_utc(), + is_active: f.is_active, + } + } +} + +impl From for SerdeAPIMetrics { + fn from(f: FuryAPIMetrics) -> Self { + SerdeAPIMetrics { + endpoint: f.endpoint, + request_count: f.request_count, + avg_response_time: f.avg_response_time, + error_count: f.error_count, + status_codes: f.status_codes, + measured_at: f.measured_at.and_utc(), + } + } +} + +impl From for SerdeSystemData { + fn from(f: SystemData) -> Self { + SerdeSystemData { + logs: f.logs.into_iter().map(|l| l.into()).collect(), + users: f.users.into_iter().map(|u| u.into()).collect(), + metrics: f.metrics.into_iter().map(|m| m.into()).collect(), + system_info: f.system_info, + } + } +} + +// Reverse conversions from Serde to Fury +impl From for FuryLogEntry { + fn from(s: SerdeLogEntry) -> Self { + FuryLogEntry { + id: s.id, + level: s.level, + message: s.message, + service: s.service, + timestamp: s.timestamp.naive_utc(), + context: s.context, + tags: s.tags, + duration_ms: s.duration_ms, + } + } +} + +impl From for FuryUserProfile { + fn from(s: SerdeUserProfile) -> Self { + FuryUserProfile { + user_id: s.user_id, + username: s.username, + email: s.email, + preferences: s.preferences, + permissions: s.permissions, + last_login: s.last_login.naive_utc(), + is_active: s.is_active, + } + } +} + +impl From for FuryAPIMetrics { + fn from(s: SerdeAPIMetrics) -> Self { + FuryAPIMetrics { + endpoint: s.endpoint, + request_count: s.request_count, + avg_response_time: s.avg_response_time, + error_count: s.error_count, + status_codes: s.status_codes, + measured_at: s.measured_at.naive_utc(), + } + } +} + +impl From for SystemData { + fn from(s: SerdeSystemData) -> Self { + SystemData { + logs: s.logs.into_iter().map(|l| l.into()).collect(), + users: s.users.into_iter().map(|u| u.into()).collect(), + metrics: s.metrics.into_iter().map(|m| m.into()).collect(), + system_info: s.system_info, + } + } +} diff --git a/rust/benches/src/models/simple.rs b/rust/benches/src/models/simple.rs new file mode 100644 index 0000000000..7323ac6171 --- /dev/null +++ b/rust/benches/src/models/simple.rs @@ -0,0 +1,230 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use crate::models::{generate_random_string, TestDataGenerator}; +use fory_derive::Fory; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +// Fury models +#[derive(Fory, Debug, Clone, PartialEq, Default)] +pub struct SimpleStruct { + pub id: i32, + pub name: String, + pub active: bool, + pub score: f64, +} + +#[derive(Fory, Debug, Clone, PartialEq, Default)] +pub struct SimpleList { + pub numbers: Vec, + pub names: Vec, +} + +#[derive(Fory, Debug, Clone, PartialEq, Default)] +pub struct SimpleMap { + pub string_to_int: HashMap, + pub int_to_string: HashMap, +} + +// Serde models +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct SerdeSimpleStruct { + pub id: i32, + pub name: String, + pub active: bool, + pub score: f64, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct SerdeSimpleList { + pub numbers: Vec, + pub names: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct SerdeSimpleMap { + pub string_to_int: HashMap, + pub int_to_string: HashMap, +} + +impl TestDataGenerator for SimpleStruct { + type Data = SimpleStruct; + + fn generate_small() -> Self::Data { + SimpleStruct { + id: 1, + name: "test".to_string(), + active: true, + score: 95.5, + } + } + + fn generate_medium() -> Self::Data { + SimpleStruct { + id: 12345, + name: generate_random_string(50), + active: true, + score: 87.123456, + } + } + + fn generate_large() -> Self::Data { + SimpleStruct { + id: 999999, + name: generate_random_string(200), + active: false, + score: 123.456789, + } + } +} + +impl TestDataGenerator for SimpleList { + type Data = SimpleList; + + fn generate_small() -> Self::Data { + SimpleList { + numbers: vec![1, 2, 3, 4, 5], + names: vec!["a".to_string(), "b".to_string(), "c".to_string()], + } + } + + fn generate_medium() -> Self::Data { + SimpleList { + numbers: (1..=100).collect(), + names: (1..=50).map(|i| format!("name_{}", i)).collect(), + } + } + + fn generate_large() -> Self::Data { + SimpleList { + numbers: (1..=1000).collect(), + names: (1..=500).map(|_i| generate_random_string(20)).collect(), + } + } +} + +impl TestDataGenerator for SimpleMap { + type Data = SimpleMap; + + fn generate_small() -> Self::Data { + let mut string_to_int = HashMap::new(); + string_to_int.insert("one".to_string(), 1); + string_to_int.insert("two".to_string(), 2); + + let mut int_to_string = HashMap::new(); + int_to_string.insert(1, "one".to_string()); + int_to_string.insert(2, "two".to_string()); + + SimpleMap { + string_to_int, + int_to_string, + } + } + + fn generate_medium() -> Self::Data { + let mut string_to_int = HashMap::new(); + let mut int_to_string = HashMap::new(); + + for i in 1..=50 { + let key = format!("key_{}", i); + string_to_int.insert(key.clone(), i); + int_to_string.insert(i, key); + } + + SimpleMap { + string_to_int, + int_to_string, + } + } + + fn generate_large() -> Self::Data { + let mut string_to_int = HashMap::new(); + let mut int_to_string = HashMap::new(); + + for i in 1..=500 { + let key = generate_random_string(15); + string_to_int.insert(key.clone(), i); + int_to_string.insert(i, key); + } + + SimpleMap { + string_to_int, + int_to_string, + } + } +} + +// Conversion functions for Serde +impl From for SerdeSimpleStruct { + fn from(f: SimpleStruct) -> Self { + SerdeSimpleStruct { + id: f.id, + name: f.name, + active: f.active, + score: f.score, + } + } +} + +impl From for SerdeSimpleList { + fn from(f: SimpleList) -> Self { + SerdeSimpleList { + numbers: f.numbers, + names: f.names, + } + } +} + +impl From for SerdeSimpleMap { + fn from(f: SimpleMap) -> Self { + SerdeSimpleMap { + string_to_int: f.string_to_int, + int_to_string: f.int_to_string, + } + } +} + +// Reverse conversions from Serde to Fury +impl From for SimpleStruct { + fn from(s: SerdeSimpleStruct) -> Self { + SimpleStruct { + id: s.id, + name: s.name, + active: s.active, + score: s.score, + } + } +} + +impl From for SimpleList { + fn from(s: SerdeSimpleList) -> Self { + SimpleList { + numbers: s.numbers, + names: s.names, + } + } +} + +impl From for SimpleMap { + fn from(s: SerdeSimpleMap) -> Self { + SimpleMap { + string_to_int: s.string_to_int, + int_to_string: s.int_to_string, + } + } +} diff --git a/rust/benches/src/serializers/fury.rs b/rust/benches/src/serializers/fury.rs new file mode 100644 index 0000000000..f8e672be77 --- /dev/null +++ b/rust/benches/src/serializers/fury.rs @@ -0,0 +1,129 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use crate::models::complex::{ECommerceData, FuryCustomer, FuryOrder, FuryOrderItem, FuryProduct}; +use crate::models::medium::{Company, FuryAddress, Person}; +use crate::models::realworld::{FuryAPIMetrics, FuryLogEntry, FuryUserProfile, SystemData}; +use crate::models::simple::{SimpleList, SimpleMap, SimpleStruct}; +use crate::serializers::Serializer; +use fory_core::fory::Fory; + +#[derive(Default)] +pub struct FurySerializer { + fory: Fory, +} + +impl FurySerializer { + pub fn new() -> Self { + let mut fory = Fory::default(); + + // Register simple types + fory.register::(100); + fory.register::(101); + fory.register::(102); + + // Register medium types + fory.register::(200); + fory.register::(201); + fory.register::(202); + + // Register complex types + fory.register::(300); + fory.register::(301); + fory.register::(302); + fory.register::(303); + fory.register::(304); + + // Register realworld types + fory.register::(400); + fory.register::(401); + fory.register::(402); + fory.register::(403); + + Self { fory } + } +} + +impl Serializer for FurySerializer { + fn serialize(&self, data: &SimpleStruct) -> Result, Box> { + Ok(self.fory.serialize(data)) + } + + fn deserialize(&self, data: &[u8]) -> Result> { + Ok(self.fory.deserialize(data)?) + } +} + +impl Serializer for FurySerializer { + fn serialize(&self, data: &SimpleList) -> Result, Box> { + Ok(self.fory.serialize(data)) + } + + fn deserialize(&self, data: &[u8]) -> Result> { + Ok(self.fory.deserialize(data)?) + } +} + +impl Serializer for FurySerializer { + fn serialize(&self, data: &SimpleMap) -> Result, Box> { + Ok(self.fory.serialize(data)) + } + + fn deserialize(&self, data: &[u8]) -> Result> { + Ok(self.fory.deserialize(data)?) + } +} + +impl Serializer for FurySerializer { + fn serialize(&self, data: &Person) -> Result, Box> { + Ok(self.fory.serialize(data)) + } + + fn deserialize(&self, data: &[u8]) -> Result> { + Ok(self.fory.deserialize(data)?) + } +} + +impl Serializer for FurySerializer { + fn serialize(&self, data: &Company) -> Result, Box> { + Ok(self.fory.serialize(data)) + } + + fn deserialize(&self, data: &[u8]) -> Result> { + Ok(self.fory.deserialize(data)?) + } +} + +impl Serializer for FurySerializer { + fn serialize(&self, data: &ECommerceData) -> Result, Box> { + Ok(self.fory.serialize(data)) + } + + fn deserialize(&self, data: &[u8]) -> Result> { + Ok(self.fory.deserialize(data)?) + } +} + +impl Serializer for FurySerializer { + fn serialize(&self, data: &SystemData) -> Result, Box> { + Ok(self.fory.serialize(data)) + } + + fn deserialize(&self, data: &[u8]) -> Result> { + Ok(self.fory.deserialize(data)?) + } +} diff --git a/rust/benches/src/serializers/json.rs b/rust/benches/src/serializers/json.rs new file mode 100644 index 0000000000..b58f6c9415 --- /dev/null +++ b/rust/benches/src/serializers/json.rs @@ -0,0 +1,120 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use crate::models::complex::{ECommerceData, SerdeECommerceData}; +use crate::models::medium::{Company, Person, SerdeCompany, SerdePerson}; +use crate::models::realworld::{SerdeSystemData, SystemData}; +use crate::models::simple::{ + SerdeSimpleList, SerdeSimpleMap, SerdeSimpleStruct, SimpleList, SimpleMap, SimpleStruct, +}; +use crate::serializers::Serializer; +use serde_json; + +#[derive(Default)] +pub struct JsonSerializer; + +impl JsonSerializer { + pub fn new() -> Self { + Self + } +} + +impl Serializer for JsonSerializer { + fn serialize(&self, data: &SimpleStruct) -> Result, Box> { + let serde_data: SerdeSimpleStruct = data.clone().into(); + Ok(serde_json::to_vec(&serde_data)?) + } + + fn deserialize(&self, data: &[u8]) -> Result> { + let serde_data: SerdeSimpleStruct = serde_json::from_slice(data)?; + Ok(serde_data.into()) + } +} + +impl Serializer for JsonSerializer { + fn serialize(&self, data: &SimpleList) -> Result, Box> { + let serde_data: SerdeSimpleList = data.clone().into(); + Ok(serde_json::to_vec(&serde_data)?) + } + + fn deserialize(&self, data: &[u8]) -> Result> { + let serde_data: SerdeSimpleList = serde_json::from_slice(data)?; + Ok(serde_data.into()) + } +} + +impl Serializer for JsonSerializer { + fn serialize(&self, data: &SimpleMap) -> Result, Box> { + let serde_data: SerdeSimpleMap = data.clone().into(); + Ok(serde_json::to_vec(&serde_data)?) + } + + fn deserialize(&self, data: &[u8]) -> Result> { + let serde_data: SerdeSimpleMap = serde_json::from_slice(data)?; + Ok(serde_data.into()) + } +} + +impl Serializer for JsonSerializer { + fn serialize(&self, data: &Person) -> Result, Box> { + let serde_data: SerdePerson = data.clone().into(); + Ok(serde_json::to_vec(&serde_data)?) + } + + fn deserialize(&self, data: &[u8]) -> Result> { + let serde_data: SerdePerson = serde_json::from_slice(data)?; + Ok(serde_data.into()) + } +} + +impl Serializer for JsonSerializer { + fn serialize(&self, data: &Company) -> Result, Box> { + let serde_data: SerdeCompany = data.clone().into(); + Ok(serde_json::to_vec(&serde_data)?) + } + + fn deserialize(&self, data: &[u8]) -> Result> { + let serde_data: SerdeCompany = serde_json::from_slice(data)?; + Ok(serde_data.into()) + } +} + +impl Serializer for JsonSerializer { + fn serialize(&self, data: &ECommerceData) -> Result, Box> { + let serde_data: SerdeECommerceData = data.clone().into(); + Ok(serde_json::to_vec(&serde_data)?) + } + + fn deserialize(&self, data: &[u8]) -> Result> { + let serde_data: SerdeECommerceData = serde_json::from_slice(data)?; + Ok(serde_data.into()) + } +} + +impl Serializer for JsonSerializer { + fn serialize(&self, data: &SystemData) -> Result, Box> { + let serde_data: SerdeSystemData = data.clone().into(); + Ok(serde_json::to_vec(&serde_data)?) + } + + fn deserialize(&self, data: &[u8]) -> Result> { + let serde_data: SerdeSystemData = serde_json::from_slice(data)?; + Ok(serde_data.into()) + } +} + +// Conversion functions from Serde to Fury models are defined in the model files diff --git a/rust/benches/src/serializers/mod.rs b/rust/benches/src/serializers/mod.rs new file mode 100644 index 0000000000..84a1e19c7b --- /dev/null +++ b/rust/benches/src/serializers/mod.rs @@ -0,0 +1,53 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +pub mod fury; +pub mod json; +pub mod protobuf; + +use chrono::{DateTime, NaiveDateTime, Utc}; +use prost_types::Timestamp; + +pub trait Serializer { + fn serialize(&self, data: &T) -> Result, Box>; + fn deserialize(&self, data: &[u8]) -> Result>; +} + +// Helper functions for protobuf conversion +pub fn naive_datetime_to_timestamp(dt: NaiveDateTime) -> Timestamp { + Timestamp { + seconds: dt.and_utc().timestamp(), + nanos: dt.and_utc().timestamp_subsec_nanos() as i32, + } +} + +pub fn timestamp_to_naive_datetime(ts: Timestamp) -> NaiveDateTime { + DateTime::from_timestamp(ts.seconds, ts.nanos as u32) + .unwrap() + .naive_utc() +} + +pub fn datetime_to_timestamp(dt: DateTime) -> Timestamp { + Timestamp { + seconds: dt.timestamp(), + nanos: dt.timestamp_subsec_nanos() as i32, + } +} + +pub fn timestamp_to_datetime(ts: Timestamp) -> DateTime { + DateTime::from_timestamp(ts.seconds, ts.nanos as u32).unwrap() +} diff --git a/rust/benches/src/serializers/protobuf.rs b/rust/benches/src/serializers/protobuf.rs new file mode 100644 index 0000000000..363519d7ea --- /dev/null +++ b/rust/benches/src/serializers/protobuf.rs @@ -0,0 +1,526 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use crate::models::complex::{ECommerceData, FuryCustomer, FuryOrder, FuryOrderItem, FuryProduct}; +use crate::models::medium::{Company, FuryAddress, Person}; +use crate::models::realworld::{FuryAPIMetrics, FuryLogEntry, FuryUserProfile, SystemData}; +use crate::models::simple::{SimpleList, SimpleMap, SimpleStruct}; +use crate::serializers::{naive_datetime_to_timestamp, timestamp_to_naive_datetime, Serializer}; +use prost::Message; + +// Import protobuf types from the generated code (included directly in lib.rs) +use crate::{ + Address, ApiMetrics, Customer, LogEntry, Order, OrderItem, Product, ProtoCompany, + ProtoECommerceData, ProtoPerson, ProtoSimpleList, ProtoSimpleMap, ProtoSimpleStruct, + ProtoSystemData, UserProfile, +}; + +#[derive(Default)] +pub struct ProtobufSerializer; + +impl ProtobufSerializer { + pub fn new() -> Self { + Self + } +} + +// Conversion functions from Fury models to Protobuf models +impl From<&SimpleStruct> for ProtoSimpleStruct { + fn from(f: &SimpleStruct) -> Self { + ProtoSimpleStruct { + id: f.id, + name: f.name.clone(), + active: f.active, + score: f.score, + } + } +} + +impl From<&SimpleList> for ProtoSimpleList { + fn from(f: &SimpleList) -> Self { + ProtoSimpleList { + numbers: f.numbers.clone(), + names: f.names.clone(), + } + } +} + +impl From<&SimpleMap> for ProtoSimpleMap { + fn from(f: &SimpleMap) -> Self { + ProtoSimpleMap { + string_to_int: f.string_to_int.clone(), + int_to_string: f.int_to_string.clone(), + } + } +} + +impl From<&FuryAddress> for Address { + fn from(f: &FuryAddress) -> Self { + Address { + street: f.street.clone(), + city: f.city.clone(), + country: f.country.clone(), + zip_code: f.zip_code.clone(), + } + } +} + +impl From<&Person> for ProtoPerson { + fn from(f: &Person) -> Self { + ProtoPerson { + name: f.name.clone(), + age: f.age, + address: Some((&f.address).into()), + hobbies: f.hobbies.clone(), + metadata: f.metadata.clone(), + created_at: Some(naive_datetime_to_timestamp(f.created_at)), + } + } +} + +impl From<&Company> for ProtoCompany { + fn from(f: &Company) -> Self { + ProtoCompany { + name: f.name.clone(), + employees: f.employees.iter().map(|e| e.into()).collect(), + offices: f + .offices + .iter() + .map(|(k, v)| (k.clone(), v.into())) + .collect(), + is_public: f.is_public, + } + } +} + +impl From<&FuryProduct> for Product { + fn from(f: &FuryProduct) -> Self { + Product { + id: f.id.clone(), + name: f.name.clone(), + price: f.price, + categories: f.categories.clone(), + attributes: f.attributes.clone(), + } + } +} + +impl From<&FuryOrderItem> for OrderItem { + fn from(f: &FuryOrderItem) -> Self { + OrderItem { + product: Some((&f.product).into()), + quantity: f.quantity, + unit_price: f.unit_price, + customizations: f.customizations.clone(), + } + } +} + +impl From<&FuryCustomer> for Customer { + fn from(f: &FuryCustomer) -> Self { + Customer { + id: f.id.clone(), + name: f.name.clone(), + email: f.email.clone(), + phone_numbers: f.phone_numbers.clone(), + preferences: f.preferences.clone(), + member_since: Some(naive_datetime_to_timestamp(f.member_since)), + } + } +} + +impl From<&FuryOrder> for Order { + fn from(f: &FuryOrder) -> Self { + Order { + id: f.id.clone(), + customer: Some((&f.customer).into()), + items: f.items.iter().map(|i| i.into()).collect(), + total_amount: f.total_amount, + status: f.status.clone(), + order_date: Some(naive_datetime_to_timestamp(f.order_date)), + metadata: f.metadata.clone(), + } + } +} + +impl From<&ECommerceData> for ProtoECommerceData { + fn from(f: &ECommerceData) -> Self { + ProtoECommerceData { + orders: f.orders.iter().map(|o| o.into()).collect(), + customers: f.customers.iter().map(|c| c.into()).collect(), + products: f.products.iter().map(|p| p.into()).collect(), + order_lookup: f + .order_lookup + .iter() + .map(|(k, v)| (k.clone(), v.into())) + .collect(), + } + } +} + +impl From<&FuryLogEntry> for LogEntry { + fn from(f: &FuryLogEntry) -> Self { + LogEntry { + id: f.id.clone(), + level: f.level, + message: f.message.clone(), + service: f.service.clone(), + timestamp: Some(naive_datetime_to_timestamp(f.timestamp)), + context: f.context.clone(), + tags: f.tags.clone(), + duration_ms: f.duration_ms, + } + } +} + +impl From<&FuryUserProfile> for UserProfile { + fn from(f: &FuryUserProfile) -> Self { + UserProfile { + user_id: f.user_id.clone(), + username: f.username.clone(), + email: f.email.clone(), + preferences: f.preferences.clone(), + permissions: f.permissions.clone(), + last_login: Some(naive_datetime_to_timestamp(f.last_login)), + is_active: f.is_active, + } + } +} + +impl From<&FuryAPIMetrics> for ApiMetrics { + fn from(f: &FuryAPIMetrics) -> Self { + ApiMetrics { + endpoint: f.endpoint.clone(), + request_count: f.request_count, + avg_response_time: f.avg_response_time, + error_count: f.error_count, + status_codes: f.status_codes.clone(), + measured_at: Some(naive_datetime_to_timestamp(f.measured_at)), + } + } +} + +impl From<&SystemData> for ProtoSystemData { + fn from(f: &SystemData) -> Self { + ProtoSystemData { + logs: f.logs.iter().map(|l| l.into()).collect(), + users: f.users.iter().map(|u| u.into()).collect(), + metrics: f.metrics.iter().map(|m| m.into()).collect(), + system_info: f.system_info.clone(), + } + } +} + +// Conversion functions from Protobuf models to Fury models +impl From for SimpleStruct { + fn from(p: ProtoSimpleStruct) -> Self { + SimpleStruct { + id: p.id, + name: p.name, + active: p.active, + score: p.score, + } + } +} + +impl From for SimpleList { + fn from(p: ProtoSimpleList) -> Self { + SimpleList { + numbers: p.numbers, + names: p.names, + } + } +} + +impl From for SimpleMap { + fn from(p: ProtoSimpleMap) -> Self { + SimpleMap { + string_to_int: p.string_to_int, + int_to_string: p.int_to_string, + } + } +} + +impl From
for FuryAddress { + fn from(p: Address) -> Self { + FuryAddress { + street: p.street, + city: p.city, + country: p.country, + zip_code: p.zip_code, + } + } +} + +impl From for Person { + fn from(p: ProtoPerson) -> Self { + Person { + name: p.name, + age: p.age, + address: p.address.map(|a| a.into()).unwrap_or_default(), + hobbies: p.hobbies, + metadata: p.metadata, + created_at: p + .created_at + .map(timestamp_to_naive_datetime) + .unwrap_or_default(), + } + } +} + +impl From for Company { + fn from(p: ProtoCompany) -> Self { + Company { + name: p.name, + employees: p.employees.into_iter().map(|e| e.into()).collect(), + offices: p.offices.into_iter().map(|(k, v)| (k, v.into())).collect(), + is_public: p.is_public, + } + } +} + +impl From for FuryProduct { + fn from(p: Product) -> Self { + FuryProduct { + id: p.id, + name: p.name, + price: p.price, + categories: p.categories, + attributes: p.attributes, + } + } +} + +impl From for FuryOrderItem { + fn from(p: OrderItem) -> Self { + FuryOrderItem { + product: p.product.map(|prod| prod.into()).unwrap_or_default(), + quantity: p.quantity, + unit_price: p.unit_price, + customizations: p.customizations, + } + } +} + +impl From for FuryCustomer { + fn from(p: Customer) -> Self { + FuryCustomer { + id: p.id, + name: p.name, + email: p.email, + phone_numbers: p.phone_numbers, + preferences: p.preferences, + member_since: p + .member_since + .map(timestamp_to_naive_datetime) + .unwrap_or_default(), + } + } +} + +impl From for FuryOrder { + fn from(p: Order) -> Self { + FuryOrder { + id: p.id, + customer: p.customer.map(|c| c.into()).unwrap_or_default(), + items: p.items.into_iter().map(|i| i.into()).collect(), + total_amount: p.total_amount, + status: p.status, + order_date: p + .order_date + .map(timestamp_to_naive_datetime) + .unwrap_or_default(), + metadata: p.metadata, + } + } +} + +impl From for ECommerceData { + fn from(p: ProtoECommerceData) -> Self { + ECommerceData { + orders: p.orders.into_iter().map(|o| o.into()).collect(), + customers: p.customers.into_iter().map(|c| c.into()).collect(), + products: p.products.into_iter().map(|prod| prod.into()).collect(), + order_lookup: p + .order_lookup + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(), + } + } +} + +impl From for FuryLogEntry { + fn from(p: LogEntry) -> Self { + FuryLogEntry { + id: p.id, + level: p.level, + message: p.message, + service: p.service, + timestamp: p + .timestamp + .map(timestamp_to_naive_datetime) + .unwrap_or_default(), + context: p.context, + tags: p.tags, + duration_ms: p.duration_ms, + } + } +} + +impl From for FuryUserProfile { + fn from(p: UserProfile) -> Self { + FuryUserProfile { + user_id: p.user_id, + username: p.username, + email: p.email, + preferences: p.preferences, + permissions: p.permissions, + last_login: p + .last_login + .map(timestamp_to_naive_datetime) + .unwrap_or_default(), + is_active: p.is_active, + } + } +} + +impl From for FuryAPIMetrics { + fn from(p: ApiMetrics) -> Self { + FuryAPIMetrics { + endpoint: p.endpoint, + request_count: p.request_count, + avg_response_time: p.avg_response_time, + error_count: p.error_count, + status_codes: p.status_codes, + measured_at: p + .measured_at + .map(timestamp_to_naive_datetime) + .unwrap_or_default(), + } + } +} + +impl From for SystemData { + fn from(p: ProtoSystemData) -> Self { + SystemData { + logs: p.logs.into_iter().map(|l| l.into()).collect(), + users: p.users.into_iter().map(|u| u.into()).collect(), + metrics: p.metrics.into_iter().map(|m| m.into()).collect(), + system_info: p.system_info, + } + } +} + +// Serializer implementations +impl Serializer for ProtobufSerializer { + fn serialize(&self, data: &SimpleStruct) -> Result, Box> { + let proto: ProtoSimpleStruct = data.into(); + let mut buf = Vec::new(); + proto.encode(&mut buf)?; + Ok(buf) + } + + fn deserialize(&self, data: &[u8]) -> Result> { + let proto = ProtoSimpleStruct::decode(data)?; + Ok(proto.into()) + } +} + +impl Serializer for ProtobufSerializer { + fn serialize(&self, data: &SimpleList) -> Result, Box> { + let proto: ProtoSimpleList = data.into(); + let mut buf = Vec::new(); + proto.encode(&mut buf)?; + Ok(buf) + } + + fn deserialize(&self, data: &[u8]) -> Result> { + let proto = ProtoSimpleList::decode(data)?; + Ok(proto.into()) + } +} + +impl Serializer for ProtobufSerializer { + fn serialize(&self, data: &SimpleMap) -> Result, Box> { + let proto: ProtoSimpleMap = data.into(); + let mut buf = Vec::new(); + proto.encode(&mut buf)?; + Ok(buf) + } + + fn deserialize(&self, data: &[u8]) -> Result> { + let proto = ProtoSimpleMap::decode(data)?; + Ok(proto.into()) + } +} + +impl Serializer for ProtobufSerializer { + fn serialize(&self, data: &Person) -> Result, Box> { + let proto: ProtoPerson = data.into(); + let mut buf = Vec::new(); + proto.encode(&mut buf)?; + Ok(buf) + } + + fn deserialize(&self, data: &[u8]) -> Result> { + let proto = ProtoPerson::decode(data)?; + Ok(proto.into()) + } +} + +impl Serializer for ProtobufSerializer { + fn serialize(&self, data: &Company) -> Result, Box> { + let proto: ProtoCompany = data.into(); + let mut buf = Vec::new(); + proto.encode(&mut buf)?; + Ok(buf) + } + + fn deserialize(&self, data: &[u8]) -> Result> { + let proto = ProtoCompany::decode(data)?; + Ok(proto.into()) + } +} + +impl Serializer for ProtobufSerializer { + fn serialize(&self, data: &ECommerceData) -> Result, Box> { + let proto: ProtoECommerceData = data.into(); + let mut buf = Vec::new(); + proto.encode(&mut buf)?; + Ok(buf) + } + + fn deserialize(&self, data: &[u8]) -> Result> { + let proto = ProtoECommerceData::decode(data)?; + Ok(proto.into()) + } +} + +impl Serializer for ProtobufSerializer { + fn serialize(&self, data: &SystemData) -> Result, Box> { + let proto: ProtoSystemData = data.into(); + let mut buf = Vec::new(); + proto.encode(&mut buf)?; + Ok(buf) + } + + fn deserialize(&self, data: &[u8]) -> Result> { + let proto = ProtoSystemData::decode(data)?; + Ok(proto.into()) + } +}