Skip to content

Commit b92f4d9

Browse files
Jinming-HuCopilot
andauthored
Perf framework: align with .NET Azure.Test.Perf (#7201)
* Perf framework: align with .NET Azure.Test.Perf (Option A parity) Brings the C++ perf framework (sdk/core/perf) and Storage Blob perf tests to format / contract parity with the .NET Azure.Test.Perf reference, which the cross-language perf-automation pipeline keys off. Framework (sdk/core/perf) ------------------------- * New per-op latency collector (latency_stats.{hpp,cpp}) emitting the .NET 8-percentile distribution: 50 / 75 / 90 / 99 / 99.9 / 99.99 / 99.999 / 100 with the exact `=== Latency Distribution ===` header and `{pct,7:N3}% {ms,8:N2}ms` row format. * New CPU + memory sampler (process_stats.{hpp,cpp}); the throughput `Completed N operations ... (Y ops/s, Z s/op, P% CPU)` line now includes inline CPU like .NET while preserving the existing `(... ops/s` substring that downstream Cpp.cs regex relies on. * New result_output.{hpp,cpp}: - `--results-file` writes `[{ "Time": <ms>, "Size": <bytes> }, ...]` matching .NET OperationResult schema (PascalCase, Size = -1 when test has no SizeOptions). - `--statistics` / `--job-statistics` wraps a `BenchmarkOutput` envelope between `#StartJobStatistics` and `#EndJobStatistics` with Metadata before Measurements (key order matches .NET). - Timestamp emitted at 100-nanosecond (7-digit) resolution like .NET DateTime.ToString("O"). * New versions.{hpp,cpp} printing a `=== Versions ===` block as the last thing emitted by the run (matches .NET ordering). * New options: `--status-interval`, `--results-file`, `--sync` (all present in .NET PerfOptions). * New non-breaking CLI aliases matching .NET names: - `--job-statistics` (bare switch) alongside existing `--statistics <0|1>` - `--no-cleanup` (bare switch) alongside existing `--noclean <0|1>` * GTest coverage for latency, process_stats, circular_stream, and result_output (9 tests, all passing). Storage Blob perf tests (sdk/storage/azure-storage-blobs/test/perf) ------------------------------------------------------------------- * New blob-test flags aligning the C++ UploadBlob / DownloadBlob / ListBlob scenarios with the .NET / Go test surface: - `--upload-method` (buffer | stream | single) - `--download-method` (buffer | stream) - `--block-size`, `--concurrency`, `--num-blobs`, `--page-size` * Memory-budget guard (memory_budget.hpp) prevents OOM in buffer-mode tests at multi-GiB payloads. Verification ------------ Built MinSizeRel on Windows / VS 2026 with vcpkg x64-windows-static (curl, openssl, gtest). All 9 unit tests pass. A live perf run against the `euap` storage account using AzureCliCredential produced output that diffs byte-clean against an equivalent .NET Azure.Storage.Blobs.Perf run for every contract emitted by this change (latency distribution header / rows, throughput line shape including `% CPU`, BenchmarkOutput JSON shape and key order, timestamp precision, Versions-block ordering, results-file schema). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Perf framework: drop versions.hpp / versions.cpp The `=== Versions ===` block didn't actually mirror .NET's PerfProgram.PrintAssemblyVersions: it printed compiler / __cplusplus strings rather than runtime + Azure assembly versions, no caller ever populated the `injectedVersions` extension point, and the data the perf-automation pipeline consumes (the per-test `VCPKG_*_VERSION` lines and the throughput / latency / BenchmarkOutput contracts) is unaffected. The module produced output that no parser reads and that isn't faithful to the framework it claims parity with, so drop it. Also revert the azure-storage-blobs CHANGELOG entry from the previous commit: the perf harness is internal and isn't customer-facing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Storage Blobs perf: drop memory_budget.hpp The guard threw a friendly std::runtime_error when `--size x --parallel` would exceed 80%% of system memory, instead of letting buffer-mode allocations OOM-kill the process. Assuming perf runs target hosts with enough memory for the configured size, this is dead defensive code: drop the header and the two `CheckMemoryBudget` calls in upload_blob_test.hpp / download_blob_test.hpp. Oversized buffer-mode runs now fail with std::bad_alloc / OS OOM as they would natively. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Perf: fix stack-overflow risk and stale CPU baseline after sampler reset Addresses two Copilot review comments on PR #7201 that are real bugs: 1. download_blob_test.hpp: the streaming-download per-op loop declared `uint8_t buffer[1024*1024]` on the stack. On Windows the default thread stack is 1 MiB, so a single call already overflows; high `--parallel` makes it worse. Replace with a function-local `static thread_local std::vector<uint8_t>`: each worker thread allocates the 1 MiB drain buffer once on the heap and reuses it across operations. 2. process_stats.cpp: `ProcessStatsSampler::Reset()` updated members under the mutex but the sampler thread's `Run()` had already cached `previousCpuSeconds` / `previousTime` in locals, so the first sample after reset computed cpuDelta / wall against the pre-reset baseline and reported a wrong CPU%. Reset now stops and restarts the thread, which forces `Run()` to re-read the fresh baselines. Verified: 9/9 perf unit tests still pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Perf: fix CI validations (ASCII, clang-format, cspell) - Replace em-dashes with -- in two comments - Add 'perfstress' to cspell dictionary (used in BenchmarkDotNet-compatible metric name to match .NET Azure.Test.Perf output) - Apply clang-format to perf framework files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review feedback: drop --sync, --job-statistics alias, and CPU/memory sampler Per @jalauzon-msft review on PR #7201: - Remove --sync (parsed-and-ignored option had no behavior). PerfAutomation is updated separately to set NoSync=true for the Cpp language so it never appends --sync to test arguments. - Remove the --job-statistics bare-switch alias; keep --statistics <0|1> which is what perf-automation actually invokes. - Remove the CPU/memory sampler (ProcessStatsSampler) and the associated ' Memory(MiB)' / '% CPU' columns; perf-automation tracks the process itself, so per-run sampling in C++ added complexity without value. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * perf: accept --sync as a no-op flag for cross-language CLI compatibility PerfAutomation appends '--sync' to test runs for sync-only languages. C++ has no async variant, but the driver still passes --sync, so the perf binary must accept it. Register --sync as a bare switch that is parsed and intentionally ignored, with no corresponding Sync field on GlobalTestOptions (so it doesn't show up in the JSON options dump or anywhere else). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Revert perf-tests.yml additions to avoid confusion The new --upload-method/--download-method/--block-size/--concurrency/--page-size flags are available on the binaries but should be tuned per host, not baked into the CI matrix. Keep perf-tests.yml at the pre-PR baseline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 8b9372c commit b92f4d9

17 files changed

Lines changed: 1007 additions & 63 deletions

File tree

.vscode/cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@
226226
"PCERT",
227227
"PBYTE",
228228
"pdbs",
229+
"perfstress",
229230
"phoebusm",
230231
"Piotrowski",
231232
"pkcs",

sdk/core/perf/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ set(
1818
inc/azure/perf/argagg.hpp
1919
inc/azure/perf/base_test.hpp
2020
inc/azure/perf/dynamic_test_options.hpp
21+
inc/azure/perf/latency_stats.hpp
2122
inc/azure/perf/options.hpp
2223
inc/azure/perf/program.hpp
2324
inc/azure/perf/random_stream.hpp
25+
inc/azure/perf/result_output.hpp
2426
inc/azure/perf/test.hpp
2527
inc/azure/perf/test_metadata.hpp
2628
inc/azure/perf/test_options.hpp
@@ -30,9 +32,11 @@ set(
3032
AZURE_PERFORMANCE_SOURCE
3133
src/arg_parser.cpp
3234
src/base_test.cpp
35+
src/latency_stats.cpp
3336
src/options.cpp
3437
src/program.cpp
3538
src/random_stream.cpp
39+
src/result_output.cpp
3640
)
3741

3842
add_library(azure-perf ${AZURE_PERFORMANCE_HEADER} ${AZURE_PERFORMANCE_SOURCE})

sdk/core/perf/inc/azure/perf.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@
1212
#include "azure/perf/argagg.hpp"
1313
#include "azure/perf/base_test.hpp"
1414
#include "azure/perf/dynamic_test_options.hpp"
15+
#include "azure/perf/latency_stats.hpp"
1516
#include "azure/perf/options.hpp"
1617
#include "azure/perf/program.hpp"
18+
#include "azure/perf/random_stream.hpp"
19+
#include "azure/perf/result_output.hpp"
1720
#include "azure/perf/test.hpp"
1821
#include "azure/perf/test_metadata.hpp"
1922
#include "azure/perf/test_options.hpp"
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
/**
5+
* @file
6+
* @brief Per-operation latency collector and percentile summary.
7+
*
8+
*/
9+
10+
#pragma once
11+
12+
#include <chrono>
13+
#include <cstdint>
14+
#include <mutex>
15+
#include <string>
16+
#include <vector>
17+
18+
namespace Azure { namespace Perf {
19+
20+
/**
21+
* @brief Thread-safe collector of per-operation latency samples.
22+
*
23+
* @remark Records nanosecond-resolution durations from many worker threads and computes
24+
* percentile summaries (p50/p90/p95/p99/max) on demand. Designed to match the latency
25+
* reporting added in the Go perf framework so cross-language results are comparable.
26+
*
27+
*/
28+
class LatencyCollector {
29+
public:
30+
/**
31+
* @brief A single latency sample, optionally tagged by call type.
32+
*
33+
*/
34+
struct Sample
35+
{
36+
std::chrono::nanoseconds Duration{0};
37+
std::string CallType;
38+
};
39+
40+
/**
41+
* @brief Latency summary expressed in milliseconds, matching the .NET
42+
* `Azure.Test.Perf` percentile distribution: 50, 75, 90, 99, 99.9, 99.99, 99.999, 100.
43+
*
44+
*/
45+
struct Summary
46+
{
47+
uint64_t Count = 0;
48+
double P50Ms = 0;
49+
double P75Ms = 0;
50+
double P90Ms = 0;
51+
double P99Ms = 0;
52+
double P999Ms = 0;
53+
double P9999Ms = 0;
54+
double P99999Ms = 0;
55+
double P100Ms = 0;
56+
double MeanMs = 0;
57+
};
58+
59+
/**
60+
* @brief Record a single latency sample with no call-type tag.
61+
*
62+
* @param duration The latency to record.
63+
*/
64+
void Record(std::chrono::nanoseconds duration);
65+
66+
/**
67+
* @brief Record a single latency sample tagged with a call type.
68+
*
69+
* @param callType A short label for the operation (e.g. "Upload").
70+
* @param duration The latency to record.
71+
*/
72+
void Record(std::string const& callType, std::chrono::nanoseconds duration);
73+
74+
/**
75+
* @brief Clear all recorded samples.
76+
*
77+
*/
78+
void Reset();
79+
80+
/**
81+
* @brief Compute the summary over all recorded samples.
82+
*
83+
* @return The percentile summary.
84+
*/
85+
Summary Summarize() const;
86+
87+
/**
88+
* @brief Compute summaries grouped by call type.
89+
*
90+
* @return A vector of (callType, summary) pairs, sorted by callType.
91+
*/
92+
std::vector<std::pair<std::string, Summary>> SummarizeByCallType() const;
93+
94+
/**
95+
* @brief Snapshot all recorded samples (copy).
96+
*
97+
*/
98+
std::vector<Sample> Samples() const;
99+
100+
private:
101+
mutable std::mutex m_mutex;
102+
std::vector<Sample> m_samples;
103+
};
104+
105+
}} // namespace Azure::Perf

sdk/core/perf/inc/azure/perf/options.hpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,22 @@ namespace Azure { namespace Perf {
108108
*/
109109
std::vector<std::string> TestProxies;
110110

111+
/**
112+
* @brief Interval in seconds between live status lines printed during a run.
113+
*
114+
*/
115+
int StatusInterval = 1;
116+
117+
/**
118+
* @brief When set, write a per-operation results file (JSON) containing the per-op
119+
* latency (ms) and the per-op size (bytes) for every measured operation, matching the
120+
* .NET `OperationResult { Time, Size }` schema.
121+
*
122+
* @remark Only populated when #Latency is also enabled.
123+
*
124+
*/
125+
std::string ResultsFile;
126+
111127
/**
112128
* @brief Create an array of the performance framework options.
113129
*
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
/**
5+
* @file
6+
* @brief Run-summary helpers: `--results-file` writer and `#StartJobStatistics` printer.
7+
*
8+
*/
9+
10+
#pragma once
11+
12+
#include "azure/perf/latency_stats.hpp"
13+
14+
#include <cstdint>
15+
#include <string>
16+
#include <vector>
17+
18+
namespace Azure { namespace Perf {
19+
20+
/**
21+
* @brief A consolidated run summary used by the framework. Fields mirror the data
22+
* already printed in the .NET reference framework's results block.
23+
*
24+
*/
25+
struct RunSummary
26+
{
27+
std::string TestName;
28+
int Parallel = 1;
29+
int DurationSeconds = 0;
30+
int Warmup = 0;
31+
int Iterations = 1;
32+
uint64_t TotalOperations = 0;
33+
double WeightedAverageSeconds = 0;
34+
double OperationsPerSecond = 0;
35+
double SecondsPerOperation = 0;
36+
LatencyCollector::Summary Latency;
37+
std::vector<std::pair<std::string, LatencyCollector::Summary>> LatencyByCallType;
38+
};
39+
40+
/**
41+
* @brief A single per-operation result, matching the .NET
42+
* `Azure.Test.Perf.OperationResult { Time, Size }` schema.
43+
*
44+
* `Time` is the operation latency in milliseconds; `Size` is the operation size in
45+
* bytes (or -1 if the test does not have a meaningful size).
46+
*
47+
*/
48+
struct OperationResult
49+
{
50+
double Time = 0;
51+
int64_t Size = -1;
52+
};
53+
54+
/**
55+
* @brief Write per-operation results to `path` as a JSON array of
56+
* `OperationResult { Time, Size }` objects, matching the .NET `--results-file` output
57+
* shape.
58+
*
59+
* @param path Destination file.
60+
* @param results The per-operation samples to write.
61+
*/
62+
void WriteResultsFile(std::string const& path, std::vector<OperationResult> const& results);
63+
64+
/**
65+
* @brief Print the `#StartJobStatistics`/`#EndJobStatistics` JSON block consumed by the
66+
* perf-automation tool.
67+
*
68+
* @details The payload matches the .NET reference framework's `BenchmarkOutput`
69+
* envelope:
70+
* ```
71+
* { "Metadata": [
72+
* {"Source","Name","ShortDescription","LongDescription","Format"}
73+
* ],
74+
* "Measurements": [
75+
* {"Timestamp","Name","Value"}
76+
* ]
77+
* }
78+
* ```
79+
*
80+
* @param summary The run summary to serialize.
81+
*/
82+
void PrintJobStatistics(RunSummary const& summary);
83+
84+
}} // namespace Azure::Perf

sdk/core/perf/src/arg_parser.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ Azure::Perf::GlobalTestOptions Azure::Perf::Program::ArgParser::Parse(
7878
{
7979
options.NoCleanup = parsedArgs["NoCleanup"].as<bool>();
8080
}
81+
// .NET-compatible bare-switch alias --no-cleanup; presence implies true.
82+
if (parsedArgs["NoCleanupSwitch"])
83+
{
84+
options.NoCleanup = true;
85+
}
8186
if (parsedArgs["Parallel"])
8287
{
8388
options.Parallel = parsedArgs["Parallel"];
@@ -103,6 +108,14 @@ Azure::Perf::GlobalTestOptions Azure::Perf::Program::ArgParser::Parse(
103108
options.TestProxies.push_back(proxy);
104109
}
105110
}
111+
if (parsedArgs["StatusInterval"])
112+
{
113+
options.StatusInterval = parsedArgs["StatusInterval"];
114+
}
115+
if (parsedArgs["ResultsFile"])
116+
{
117+
options.ResultsFile = parsedArgs["ResultsFile"].as<std::string>();
118+
}
106119

107120
return options;
108121
}

0 commit comments

Comments
 (0)