diff --git a/sdk/core/azure_core/Cargo.toml b/sdk/core/azure_core/Cargo.toml index 8b3fdd23e5..1749930b87 100644 --- a/sdk/core/azure_core/Cargo.toml +++ b/sdk/core/azure_core/Cargo.toml @@ -104,3 +104,8 @@ required-features = ["xml"] [[bench]] name = "http_transport_benchmarks" harness = false + +[[test]] +name = "perf" +path = "perf/perf.rs" +harness = false diff --git a/sdk/core/azure_core/perf-tests.yml b/sdk/core/azure_core/perf-tests.yml new file mode 100644 index 0000000000..b5a198c8cb --- /dev/null +++ b/sdk/core/azure_core/perf-tests.yml @@ -0,0 +1,23 @@ +Service: core + +Project: azure_core_perf + +PrimaryPackage: azure_core + +PackageVersions: +# - azure_core: 1.0.0 +- azure_core: source + +Tests: +- Test: mock_json + Class: mock_json + Arguments: + - --count 5 --parallel 64 + - --count 500 --parallel 32 + - --count 50000 --parallel 32 --warmup 60 --duration 60 +# - Test: mock_xml +# Class: mock_xml +# Arguments: +# - --count 5 --parallel 64 +# - --count 500 --parallel 32 +# - --count 50000 --parallel 32 --warmup 60 --duration 60 diff --git a/sdk/core/azure_core/perf.yml b/sdk/core/azure_core/perf.yml new file mode 100644 index 0000000000..6211838728 --- /dev/null +++ b/sdk/core/azure_core/perf.yml @@ -0,0 +1,47 @@ +trigger: none + +pr: none + +schedules: +- cron: "0 7 * * *" + displayName: Daily midnight build + branches: + include: + - main + always: true + +parameters: +- name: PackageVersions + displayName: PackageVersions (regex of package versions to run) + type: string + default: '12|source' +- name: Tests + displayName: Tests (regex of tests to run) + type: string + default: '^(mock_json|mock_xml)$' +- name: Arguments + displayName: Arguments (regex of arguments to run) + type: string + default: '(5)|(500)|(50000)' +- name: Iterations + displayName: Iterations (times to run each test) + type: number + default: '5' +- name: Profile + type: boolean + default: false +- name: AdditionalArguments + displayName: AdditionalArguments (passed to PerfAutomation) + type: string + default: '' + +extends: + template: /eng/pipelines/templates/jobs/perf.yml + parameters: + ServiceDirectory: core/azure_core + PackageVersions: ${{ parameters.PackageVersions }} + Tests: ${{ parameters.Tests }} + Arguments: ${{ parameters.Arguments }} + Iterations: ${{ parameters.Iterations }} + AdditionalArguments: ${{ parameters.AdditionalArguments }} + Profile: ${{ parameters.Profile }} diff --git a/sdk/core/azure_core/perf/mock.rs b/sdk/core/azure_core/perf/mock.rs new file mode 100644 index 0000000000..525b1a73b3 --- /dev/null +++ b/sdk/core/azure_core/perf/mock.rs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +pub mod json; +#[cfg(feature = "xml")] +pub mod xml; + +use std::sync::Arc; + +use azure_core::{ + base64::{ + self, + option::{deserialize, serialize}, + }, + http::{headers::Headers, BufResponse, ClientOptions, Pipeline, StatusCode, Transport}, + time::{self, OffsetDateTime}, + Bytes, +}; +use azure_core_test::http::MockHttpClient; +use futures::FutureExt as _; +use serde::{Deserialize, Serialize}; + +const DEFAULT_COUNT: usize = 25; + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct List { + #[serde(default, rename = "name")] + name: Option, + + #[serde(default, rename = "container")] + container: Option, + + #[serde(default, rename = "next")] + next: Option, +} + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct ListItemsContainer { + #[serde(default, rename = "items")] + items: Option>, +} + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct ListItem { + #[serde(default, rename = "name")] + name: Option, + + #[serde(default, rename = "properties")] + properties: Option, +} + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct ListItemProperties { + #[serde(default, rename = "etag")] + etag: Option, + + #[serde(default, rename = "creationTime", with = "time::rfc7231::option")] + creation_time: Option, + + #[serde(default, rename = "lastModified", with = "time::rfc7231::option")] + last_modified: Option, + + #[serde( + default, + rename = "contentMD5", + serialize_with = "serialize", + deserialize_with = "deserialize" + )] + content_md5: Option>, +} + +fn create_pipeline(count: usize, f: F) -> azure_core::Result +where + F: Fn(&List) -> azure_core::Result, +{ + let mut list = List { + name: Some("t0123456789abcdef".into()), + ..Default::default() + }; + let mut items = Vec::with_capacity(count); + let now = OffsetDateTime::now_utc(); + for i in 0..count { + let name = format!("testItem{i}"); + let hash = base64::encode(&name).into_bytes(); + items.push(ListItem { + name: Some(name), + properties: Some(ListItemProperties { + etag: Some(i.to_string().into()), + creation_time: Some(now), + last_modified: Some(now), + content_md5: Some(hash), + }), + }); + } + list.container = Some(ListItemsContainer { items: Some(items) }); + + let body = f(&list)?; + println!("Serialized {count} items in {} bytes", body.len()); + + let client = Arc::new(MockHttpClient::new(move |_| { + let body = body.clone(); + async move { + // Yield simulates an expected network call but kills performance by ~45%. + tokio::task::yield_now().await; + Ok(BufResponse::from_bytes( + StatusCode::Ok, + Headers::new(), + body, + )) + } + .boxed() + })); + let options = ClientOptions { + transport: Some(Transport::new(client)), + ..Default::default() + }; + let pipeline = Pipeline::new( + Some("perf"), + Some("0.1.0"), + options, + Vec::new(), + Vec::new(), + None, + ); + Ok(pipeline) +} diff --git a/sdk/core/azure_core/perf/mock/json.rs b/sdk/core/azure_core/perf/mock/json.rs new file mode 100644 index 0000000000..e64d2a7c3a --- /dev/null +++ b/sdk/core/azure_core/perf/mock/json.rs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use super::List; +use azure_core::{ + http::{Context, JsonFormat, Method, Pipeline, RawResponse, Request, Response}, + json, +}; +use azure_core_test::{ + perf::{CreatePerfTestReturn, PerfRunner, PerfTest, PerfTestMetadata, PerfTestOption}, + TestContext, +}; +use futures::FutureExt as _; +use std::{hint::black_box, sync::Arc}; + +pub struct MockJsonTest { + pipeline: Pipeline, +} + +impl MockJsonTest { + fn create_items(runner: PerfRunner) -> CreatePerfTestReturn { + async move { + let count = runner + .try_get_test_arg("count")? + .cloned() + .unwrap_or(super::DEFAULT_COUNT); + let pipeline = super::create_pipeline(count, json::to_json)?; + Ok(Box::new(MockJsonTest { pipeline }) as Box) + } + .boxed() + } + + pub fn test_metadata() -> PerfTestMetadata { + PerfTestMetadata { + name: "mock_json", + description: "Mock transport that returns JSON", + options: vec![PerfTestOption { + name: "count", + display_message: "Number of items per page", + mandatory: false, + short_activator: None, + long_activator: "count", + expected_args_len: 1, + ..Default::default() + }], + create_test: Self::create_items, + } + } +} + +#[async_trait::async_trait] +impl PerfTest for MockJsonTest { + async fn setup(&self, _context: Arc) -> azure_core::Result<()> { + Ok(()) + } + + async fn run(&self, _context: Arc) -> azure_core::Result<()> { + let ctx = Context::new(); + let mut request = Request::new( + "https://contoso.com/containers/t0123456789abcdef?api-version=2025-10-15".parse()?, + Method::Get, + ); + let response = self.pipeline.send(&ctx, &mut request, None).await?; + // Make sure we deserialize the response. + let (status, headers, body) = response.deconstruct(); + let response: Response = + RawResponse::from_bytes(status, headers, body).into(); + let list: List = tokio::spawn(async move { + tokio::task::yield_now().await; + response.into_body() + }) + .await + .unwrap()?; + assert_eq!(black_box(list.name), Some("t0123456789abcdef".into())); + + Ok(()) + } + + async fn cleanup(&self, _context: Arc) -> azure_core::Result<()> { + Ok(()) + } +} diff --git a/sdk/core/azure_core/perf/mock/xml.rs b/sdk/core/azure_core/perf/mock/xml.rs new file mode 100644 index 0000000000..3044208d95 --- /dev/null +++ b/sdk/core/azure_core/perf/mock/xml.rs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use super::List; +use azure_core::{ + http::{Context, Method, Pipeline, RawResponse, Request, Response, XmlFormat}, + xml, +}; +use azure_core_test::{ + perf::{CreatePerfTestReturn, PerfRunner, PerfTest, PerfTestMetadata, PerfTestOption}, + TestContext, +}; +use futures::FutureExt as _; +use std::{hint::black_box, sync::Arc}; + +pub struct MockXmlTest { + pipeline: Pipeline, +} + +impl MockXmlTest { + fn create_items(runner: PerfRunner) -> CreatePerfTestReturn { + async move { + let count = runner + .try_get_test_arg("count")? + .cloned() + .unwrap_or(super::DEFAULT_COUNT); + let pipeline = super::create_pipeline(count, xml::to_xml)?; + Ok(Box::new(MockXmlTest { pipeline }) as Box) + } + .boxed() + } + + pub fn test_metadata() -> PerfTestMetadata { + PerfTestMetadata { + name: "mock_xml", + description: "Mock transport that returns XML", + options: vec![PerfTestOption { + name: "count", + display_message: "Number of items per page", + mandatory: false, + short_activator: None, + long_activator: "count", + expected_args_len: 1, + ..Default::default() + }], + create_test: Self::create_items, + } + } +} + +#[async_trait::async_trait] +impl PerfTest for MockXmlTest { + async fn setup(&self, _context: Arc) -> azure_core::Result<()> { + Ok(()) + } + + async fn run(&self, _context: Arc) -> azure_core::Result<()> { + let ctx = Context::new(); + let mut request = Request::new( + "https://contoso.com/containers/t0123456789abcdef?api-version=2025-10-15".parse()?, + Method::Get, + ); + let response = self.pipeline.send(&ctx, &mut request, None).await?; + // Make sure we deserialize the response. + let (status, headers, body) = response.deconstruct(); + let response: Response = + RawResponse::from_bytes(status, headers, body).into(); + let list: List = tokio::spawn(async move { + tokio::task::yield_now().await; + response.into_body() + }) + .await + .unwrap()?; + assert_eq!(black_box(list.name), Some("t0123456789abcdef".into())); + + Ok(()) + } + + async fn cleanup(&self, _context: Arc) -> azure_core::Result<()> { + Ok(()) + } +} diff --git a/sdk/core/azure_core/perf/perf.rs b/sdk/core/azure_core/perf/perf.rs new file mode 100644 index 0000000000..b0caf755cc --- /dev/null +++ b/sdk/core/azure_core/perf/perf.rs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +mod mock; + +use azure_core_test::perf::PerfRunner; + +#[tokio::main] +async fn main() -> azure_core::Result<()> { + let runner = PerfRunner::new( + env!("CARGO_MANIFEST_DIR"), + file!(), + vec![ + mock::json::MockJsonTest::test_metadata(), + #[cfg(feature = "xml")] + mock::xml::MockXmlTest::test_metadata(), + ], + )?; + + runner.run().await?; + + Ok(()) +}