Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,54 @@ jobs:
if-no-files-found: warn


performance-tests:
name: performance tests
runs-on: ubuntu-24.04
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: a2a
POSTGRES_PASSWORD: a2a
POSTGRES_DB: a2a
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U a2a -d a2a"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
A2A_TEST_POSTGRES_DSN: postgresql://a2a:a2a@127.0.0.1:5432/a2a
steps:
- uses: actions/checkout@v6
- name: Install deps
run: ./scripts/install_build_deps.sh
- name: Run report-only performance suite
env:
A2A_PERF_TRANSPORTS: grpc,jsonrpc,http_json
A2A_PERF_STORE_BACKENDS: inmemory,postgres
A2A_PERF_REQUESTS: 2000
A2A_PERF_CONCURRENCY: 1,4
A2A_PERF_WARMUP_SECONDS: 0
A2A_PERF_REPORT_DIR: perf-artifacts
run: ./scripts/run_performance_tests.sh
- name: Validate performance reports
run: |
test -s perf-artifacts/results.json
test -s perf-artifacts/results.csv
test -s perf-artifacts/summary.md
python3 -m json.tool perf-artifacts/results.json >/dev/null
cat perf-artifacts/summary.md >> "$GITHUB_STEP_SUMMARY"
- name: Upload performance artifacts
if: always()
uses: actions/upload-artifact@v6
with:
name: perf-artifacts
path: perf-artifacts
if-no-files-found: error


benchmarks:
name: benchmarks
runs-on: ubuntu-24.04
Expand Down
10 changes: 10 additions & 0 deletions docs/compliance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,13 @@ Expected artifacts:
- `build-tck/tck-sut.log`
- `tck-artifacts/reports/*`
- `tck-artifacts/logs/tck-run.log`

## Shared SUT binary

The conformance scripts build and run the shared `tck_sut` binary. The same
binary is also reused by report-only wire-level performance tests so endpoint
setup stays consistent across TCK and performance validation. The exposed
endpoints are REST at `/a2a`, JSON-RPC at `/rpc`, and gRPC on the configured
port plus one. Store selection continues to use
`A2A_TCK_STORE_BACKEND=inmemory|postgres`, `A2A_TCK_POSTGRES_DSN`, and
`A2A_TCK_POSTGRES_SCHEMA`.
135 changes: 135 additions & 0 deletions docs/performance-testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Performance testing

The A2A C++ SDK includes a report-only performance test kit for repeatable local
and CI measurements. The Python runner still owns matrix orchestration and writes
`results.json`, `results.csv`, and `summary.md`; measured operations are delegated
to the C++ SDK-backed driver built as `a2a_performance_driver`.

## Run locally

```bash
./scripts/run_performance_tests.sh --store-backends inmemory --requests 100 --concurrency 1,4 --warmup-seconds 0
```

The runner writes:

- `perf-artifacts/results.json`
- `perf-artifacts/results.csv`
- `perf-artifacts/summary.md`

If `A2A_PERF_DRIVER` is not set, the runner configures and builds the driver in
`build/performance`. Set `A2A_PERF_BUILD_DIR` to reuse another CMake build tree.

## Configuration

You can configure the matrix and load profile with command-line flags or the
matching environment variables.

| Setting | Environment variable | Default |
| --- | --- | --- |
| Transports (`grpc`, `jsonrpc`, `http_json`, or `all`) | `A2A_PERF_TRANSPORTS` | `grpc,jsonrpc,http_json` |
| Store backends (`inmemory`, `postgres`, or `all`) | `A2A_PERF_STORE_BACKENDS` | `inmemory,postgres` |
| Operations per result row | `A2A_PERF_REQUESTS` | `2000` |
| Concurrency levels | `A2A_PERF_CONCURRENCY` | `1,4` |
| Warmup seconds | `A2A_PERF_WARMUP_SECONDS` | `1` |
| Duration seconds metadata | `A2A_PERF_DURATION_SECONDS` | `0` |
| Report directory | `A2A_PERF_REPORT_DIR` | `perf-artifacts` |
| Existing driver binary | `A2A_PERF_DRIVER` | unset |
| Auto-build directory | `A2A_PERF_BUILD_DIR` | `build/performance` |

## Scenario coverage

The SDK-backed driver executes these real service/store flows through the shared
example executor, task lifecycle service, task subscription service, in-memory
task store, in-memory push notification store, and push notification service:

- task lifecycle: `SendMessage` create, `GetTask`, `CancelTask`, list with and
without pagination, follow-up `SendMessage`, and missing-task lookup;
- streaming and subscriptions: finite `SendStreamingMessage`, existing-task
subscription first event, multi-subscriber reads, terminal stream completion,
and a disconnect-style subscriber read where other subscriptions still run;
- push notifications: create/get/list/delete config, notify many configs for one
task update, and delivery callback latency through a local recording delivery
client implementation of the SDK delivery interface.

Each row includes the required stable fields plus `driver_type` and
`transport_path`. The current driver type is `cpp_sdk_in_process`.

## Transport coverage

The current C++ driver is SDK-backed and in-process. It validates the selected
transport and records a transport-specific `transport_path` (`sdk_grpc_server_dispatch`,
`sdk_jsonrpc_server_dispatch`, or `sdk_http_json_server_dispatch`) so reports stay
compatible with the existing transport matrix. It does not yet open sockets or
run external clients; k6 is not required. Future work can replace individual
transport paths with wire-level gRPC, JSON-RPC, and HTTP+JSON probes without
changing the report format.

## Store backend coverage

`inmemory` uses the real in-memory task and push notification stores. `postgres`
uses the repository `PostgresStoreFactory` to create real PostgreSQL-backed task
and push notification stores when the driver is built with
`A2A_ENABLE_POSTGRES_STORE=ON`. The Python runner automatically adds that CMake
option when `postgres` is selected and it needs to auto-build the driver.
PostgreSQL runs must provide the same local DSN style used by the repository
store tests (`A2A_TEST_POSTGRES_DSN`); CI starts a local PostgreSQL service for
the performance job.

## CI behavior

The performance job remains report-only: it uploads `perf-artifacts`, appends
`summary.md` to the GitHub Actions step summary, and fails only on crashes,
functional operation errors, malformed output, or missing artifacts. It does not
enforce latency or throughput thresholds.

## Larger local benchmark

```bash
A2A_PERF_TRANSPORTS=grpc,jsonrpc,http_json \
A2A_PERF_STORE_BACKENDS=inmemory,postgres \
A2A_PERF_REQUESTS=2000 \
A2A_PERF_CONCURRENCY=1,4,16,64 \
A2A_PERF_WARMUP_SECONDS=5 \
A2A_PERF_REPORT_DIR=perf-artifacts \
./scripts/run_performance_tests.sh
```

## JSON shape

`results.json` contains host metadata and a `results` array. Each result includes
scenario name, transport, store backend, concurrency, operation counts, success
and error counts, throughput, latency percentiles, max latency, SDK commit SHA in
metadata, and host OS/CPU metadata.

## Shared TCK SUT wire-level driver

The `tck_sut` binary is shared infrastructure for TCK conformance and
wire-level performance coverage. It is built by the performance runner when
needed, started on a local test port, checked for HTTP and gRPC readiness, and
stopped cleanly after each wire-level matrix entry. Startup logs are captured in
the report directory as `tck_sut_<store>_<port>.log` for CI diagnosis.

Endpoint layout is the same as the TCK flow:

* REST/HTTP+JSON: `http://<host>:<port>/a2a`
* JSON-RPC: `http://<host>:<port>/rpc`
* gRPC: `<host>:<port + 1>`

The SUT supports `A2A_TCK_STORE_BACKEND=inmemory|postgres`,
`A2A_TCK_POSTGRES_DSN`, `A2A_TCK_POSTGRES_SCHEMA`, and the existing extended
agent-card mode environment variable. Run it manually with:

```bash
cmake --build build-tck --target tck_sut
./build-tck/tests/tck_sut 127.0.0.1:50061
```

Performance reports distinguish the low-overhead SDK service/store layer from
transport-level coverage. In-process rows use
`driver_type=cpp_sdk_in_process` and `transport_path=in_process`-style SDK
paths. Wire rows use `driver_type=wire_tck_sut` and one of
`wire_http_json`, `wire_jsonrpc`, or `wire_grpc`. Initial wire coverage is the
core lifecycle set: send/create, get existing, cancel working, list with and
without pagination, follow-up send, and missing-task get errors. Streaming and
push-notification rows remain in-process until dedicated wire clients are added.
25 changes: 25 additions & 0 deletions include/a2a/server/network_utils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Vladimir Pavlov <mistervvp@outlook.com> (https://github.com/MisterVVP)

#pragma once

#include <string>
#include <string_view>

#include "a2a/core/core.h"

namespace a2a::server {

struct HostPortEndpoint final {
std::string host;
int port = 0;
};

void CloseSocketCrossPlatform(int fd) noexcept;

[[nodiscard]] std::string BuildHttpUrl(std::string_view host, int port, std::string_view path);
[[nodiscard]] a2a::core::Result<HostPortEndpoint> ParseHostPortEndpoint(std::string_view endpoint,
int max_port = 65535);
[[nodiscard]] bool SetSocketNonBlocking(int fd) noexcept;

} // namespace a2a::server
10 changes: 0 additions & 10 deletions include/a2a/server/socket_utils.h

This file was deleted.

Loading
Loading