diff --git a/docs-taxonomy.yml b/docs-taxonomy.yml index 0f3ce8e7..9d09b57d 100644 --- a/docs-taxonomy.yml +++ b/docs-taxonomy.yml @@ -27,6 +27,7 @@ collections: services: application-development: Application Development azure-application-gateway: Azure Application Gateway + azure-application-gateway-for-containers: Azure Application Gateway for Containers azure-ai-search: Azure AI Search azure-api-management: Azure API Management azure-app-service: Azure App Service @@ -36,6 +37,7 @@ services: azure-cosmos-db: Azure Cosmos DB azure-database-for-mysql: Azure Database for MySQL azure-functions: Azure Functions + azure-front-door: Azure Front Door azure-hdinsight: Azure HDInsight azure-kubernetes-service: Azure Kubernetes Service azure-load-testing: Azure Load Testing @@ -126,3 +128,4 @@ official_source_hosts: - modelcontextprotocol.io - py.sdk.modelcontextprotocol.io - prices.azure.com + - www.rfc-editor.org diff --git a/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/index.md b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/index.md new file mode 100644 index 00000000..38fcca3f --- /dev/null +++ b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/index.md @@ -0,0 +1,158 @@ +--- +title: HTTP/3 개요와 Azure L7 지원 현황 +description: HTTP/3의 특징, Azure 주요 L7 서비스의 공개 지원 정보, 실제 AKS·AGFC 프로토콜 점검 결과를 구분해 정리합니다. +document_type: research +services: + - azure-application-gateway-for-containers + - azure-application-gateway + - azure-front-door + - azure-api-management + - azure-kubernetes-service +technologies: + - kubernetes + - docker + - go +tags: + - evaluate + - optimize + - networking +status: current +verification_status: needs-review +sources_checked_at: 2026-09-15 +official_sources: + - title: "RFC 9114: HTTP/3" + url: https://www.rfc-editor.org/rfc/rfc9114 + - title: HTTP/3 support in Azure Application Gateway - Preview + url: https://learn.microsoft.com/azure/application-gateway/http3-quic-support + - title: What is Application Gateway for Containers? + url: https://learn.microsoft.com/azure/application-gateway/for-containers/overview + - title: Application Gateway for Containers components + url: https://learn.microsoft.com/azure/application-gateway/for-containers/application-gateway-for-containers-components + - title: What is Application Gateway Ingress Controller? + url: https://learn.microsoft.com/azure/application-gateway/ingress-controller-overview + - title: Azure Front Door Frequently Asked Questions (FAQ) + url: https://learn.microsoft.com/azure/frontdoor/front-door-faq + - title: API Gateway Overview + url: https://learn.microsoft.com/azure/api-management/api-management-gateways-overview +published_at: 2026-09-15 +--- + +# HTTP/3 개요와 Azure L7 지원 현황 + +**기준일: 2026-09-15.** Azure 주요 L7 서비스의 공개 문서와 이날 수행한 +점검 결과를 정리한다. **Application Gateway의 HTTP/3는 Preview로 문서화되어 +있고, 실제 구성한 AGFC에서는 HTTP/2는 성공했지만 HTTP/3는 연결되지 않았다.** 두 제품의 지원 +상태를 동일하게 취급하면 안 된다. + +## HTTP/3란 + +HTTP/3는 HTTP의 요청·응답 의미를 유지하면서 전송 계층으로 **TCP 대신 +UDP 기반 QUIC**를 사용하는 프로토콜이다. QUIC에 TLS 1.3이 통합되어 있으며, +UDP를 사용하더라도 스트림별 신뢰성 있는 순서 보장 전송을 제공한다. + +| 특징 | 의미 | +|---|---| +| 연결 성립 지연 감소 | TCP 연결과 별도의 TLS 협상에 필요한 왕복을 줄여, RTT가 큰 환경의 초기 응답에 유리할 수 있음 | +| 독립적인 스트림 | HTTP/2의 TCP 연결에서 생기는 전송 계층 head-of-line blocking을 피함. 다만 연결 전체의 혼잡 제어는 공유하므로 손실의 모든 영향이 사라지는 것은 아님 | +| 연결 이동 기능 | QUIC 연결 ID를 이용해 IP·포트가 바뀌어도 연결을 유지할 수 있음. 실제 망 전환 동작은 클라이언트·서버의 지원과 설정에 달림 | + +**HTTP/3가 항상 더 빠른 것은 아니다.** 구현, RTT, 손실, 연결 재사용과 +워크로드에 따라 결과가 달라진다. 아래 로컬 실험에서도 개선과 악화를 모두 +관측했다. 망 전환·배터리 절약 효과는 이번에 측정하지 않았다. + +## Azure 주요 L7 서비스의 HTTP/3 지원 + +비교 기준은 **클라이언트 → L7 프런트엔드** 구간이다. 앞단에서 HTTP/3를 +종단하더라도 백엔드까지 HTTP/3일 필요는 없다. 예를 들어 Application Gateway의 +HTTP/3 Preview는 클라이언트 구간에만 적용되며 백엔드는 HTTP/1.1을 사용한다. + +| 서비스 | 확인한 공개 지원 정보 | 이번 직접 점검 | +|---|---|---| +| **Azure Application Gateway** | **HTTP/3 Preview 지원.** Basic listener와 TLS 1.3을 지원하는 2022 사전 정의 TLS 정책 필요. WAF gateway·multi-site listener·IPv6 listener·상호 인증은 해당 Preview에서 지원하지 않음 | 미수행 — 문서 확인만 수행 | +| **Application Gateway for Containers(AGFC)** | 프런트엔드 HTTP/1.1·HTTP/2가 명시됨. 확인한 문서에는 HTTP/3 지원·활성화 방법이 없음 | **정상 HTTPS 구성에서 HTTP/2 성공, HTTP/3 연결 불성립** | +| **Azure Front Door** | FAQ에 HTTP·HTTPS·HTTP/2 지원이 명시됨. HTTP/3 지원은 명시되어 있지 않음 | 미수행 | +| **Azure API Management** | 게이트웨이 기능표에 종류·계층별 HTTP/2 지원이 명시됨. HTTP/3 지원은 명시되어 있지 않음 | 미수행 | + +API Management는 API 게이트웨이로 함께 비교했다. **문서에 HTTP/3가 없다는 +사실과 제품 전체의 미지원 확정은 구분한다.** 이 표는 모든 L7 제품의 목록이나 +미공개 Preview의 판정표가 아니며, 실제 Azure 경로를 점검한 제품은 AGFC뿐이다. +Application Gateway의 HTTP/3 Preview는 SLA가 제공되지 않고 프로덕션 사용을 권장하지 +않는다는 공식 주의사항도 있다. + +### AGIC와 AGFC는 다르다 + +**AGIC**는 기존 Application Gateway를 설정하는 Kubernetes 컨트롤러다. +반면 **AGFC**는 별도 제어·데이터 플레인을 가진 제품이며 ALB Controller로 +관리한다. 따라서 기존 Application Gateway에 추가된 HTTP/3 기능이 AGFC에도 +자동 적용된다고 볼 수 없다. AGIC 자체가 HTTP/3 Preview 설정을 관리하는지도 +이번에 검증하지 않았다. + +## 실제 AKS + AGFC에서 확인한 사실 + +Korea Central에 임시 **AKS 1.35 + AGFC(ALB Controller 1.10.30)**를 구성했다. +경로는 `클라이언트 → AGFC HTTPS/443 → AKS 백엔드`이며 별도 Application +Gateway는 없었다. HTTPS 리스너·라우트와 백엔드는 정상이고, 인증서 검증을 +켠 상태에서 같은 주소를 대상으로 비교했다. + +| 접속 위치·클라이언트 | HTTP/2 강제 연결 | HTTP/3 강제 연결 | +|---|---|---| +| 로컬, curl/ngtcp2 | 3/3 성공, HTTP 200 | 0/3 성공, 모두 8초 타임아웃 | +| Azure AKS Pod, quic-go | 3/3 성공, HTTP 200 | 0/3 성공, 모두 8초 타임아웃 | +| 로컬, quic-go | 1/1 성공, HTTP 200 | 0/1 성공, 8초 타임아웃 | + +- 추가 **15초 제한**의 HTTP/3 연결도 타임아웃이었다. 이후 HTTP/2 응답은 + 여전히 정상이었다. +- 같은 로컬·Azure 환경의 외부 HTTP/3 대조군은 정상 연결됐다. + AGFC 서브넷에는 사용자 NSG·라우팅 테이블이 없었다. +- **자동 fallback을 허용한 요청은 성공했지만 실제 프로토콜은 HTTP/2였다.** + 요청 성공만으로 HTTP/3 연결 성공이라고 판단할 수 없다. +- 임시 AKS·AGFC와 관련 리소스 그룹은 삭제 완료를 확인했다. + +**판정: 점검한 AGFC HTTPS 구성에서는 HTTP/3를 사용할 수 없었다.** +다만 관측은 명시적인 프로토콜 거절 응답이 아닌 타임아웃이다. AGFC 측 경로의 +UDP 처리 제한과 HTTP/3 리스너 부재를 이 결과만으로 구별하지는 못하며, +모든 지역·미공개 Preview·향후 버전으로 결론을 확대하지 않는다. + +## 별도 모바일망 모의실험에서 관측한 성능 + +다음은 **AGFC가 아닌 로컬 HTTP/2·HTTP/3 서버**의 응답 완료 시간이다. +추가 RTT 80 ms, 방향별 10 Mbit/s, 방향별 손실 0%·1% 조건에서 각 조합을 +30회 측정했다. 연결 재사용 항목의 응답 16개는 각각 16 KiB다. +p50·p95는 표본에서 계산한 50·95번째 백분위수다. + +| 조건·작업 | HTTP/2 | HTTP/3 | 관측 결과 | +|---|---:|---:|---| +| 무손실, 새 연결·1 KiB 응답 p50 | 257.0 ms | 174.2 ms | 32.2% 단축 | +| 무손실, 연결 재사용·16개 응답 p50 | 481.3 ms | 364.0 ms | 24.4% 단축 | +| 손실 1%, 연결 재사용·16개 응답 p95 | 1611.9 ms | 884.8 ms | 45.1% 단축 | +| 손실 1%, 새 연결·1 KiB 응답 p95 | 367.5 ms | 421.8 ms | **14.8% 악화** | + +추가 지연·손실이 없는 로컬 조건에서는 HTTP/3가 더 느렸다. 위 결과는 +조건별 30개 표본의 탐색적 관측이며 **실제 스마트폰·통신사망·Azure L7의 +성능 개선율이 아니다.** AGFC 경로의 HTTP/3 성능 개선율은 연결이 성립하지 +않았으므로 **미측정**이다. + +## 핵심 정리 + +- HTTP/3는 높은 RTT의 연결 성립과 손실 환경의 다중 스트림 전송에 이점이 + 있을 수 있지만, 모든 응답 시간·꼬리 지연이 개선되는 것은 아니다. +- HTTP/3 적용 여부는 **클라이언트가 연결하는 L7 종단의 제품·기능·제약**으로 + 판단한다. TLS 1.3 지원이나 Application Gateway라는 이름만으로 추정하지 않는다. +- **공식 문서의 지원 정보, 실제 프로토콜 연결 결과, 별도 환경의 성능 수치는 + 서로 다른 근거**다. + +상세 절차 대신 비식별화한 AGFC 연결 결과와 로컬 성능 원시 데이터를 +각각 이 topic의 결과 아티팩트와 기존 sample에 보존한다. + +## 공식 출처 + +- [RFC 9114: HTTP/3](https://www.rfc-editor.org/rfc/rfc9114): HTTP 의미, QUIC 전송과 독립 스트림. +- [Application Gateway HTTP/3 Preview](https://learn.microsoft.com/azure/application-gateway/http3-quic-support): 클라이언트 구간 지원과 Preview 제약. +- [AGFC overview](https://learn.microsoft.com/azure/application-gateway/for-containers/overview) · [components](https://learn.microsoft.com/azure/application-gateway/for-containers/application-gateway-for-containers-components): 별도 데이터 플레인과 지원 프로토콜. +- [AGIC overview](https://learn.microsoft.com/azure/application-gateway/ingress-controller-overview): 기존 Application Gateway를 관리하는 컨트롤러. +- [Azure Front Door FAQ](https://learn.microsoft.com/azure/frontdoor/front-door-faq): 지원 프로토콜. +- [API Management gateway overview](https://learn.microsoft.com/azure/api-management/api-management-gateways-overview): 게이트웨이 종류·계층별 기능. + +공식 원문은 직접 대조했으나 Microsoft Learn MCP를 통한 재검증은 수행하지 +못해 문서 검증 상태는 `needs-review`로 남겼다. 위 AGFC 실측은 별도로 +확인한 관측 기록이다. diff --git a/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/agfc-protocol-check/README.md b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/agfc-protocol-check/README.md new file mode 100644 index 00000000..610fbd18 --- /dev/null +++ b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/agfc-protocol-check/README.md @@ -0,0 +1,36 @@ +# AGFC protocol acceptance results + +This is a **result artifact**, not a deployment project or a performance +benchmark. The [research page](../../index.md) summarizes the product facts +and observations without the detailed test procedure. + +## Recorded facts + +[result.json](result.json) preserves the sanitized record from 2026-09-15: + +- A disposable AKS 1.35 cluster and Application Gateway for Containers in + Korea Central, using ALB Controller 1.10.30. +- Seven successful strict HTTP/2 requests and seven failed strict HTTP/3 + attempts across local and Azure sources. The HTTP/3 attempts timed out. +- An additional 15-second HTTP/3 timeout, successful external HTTP/3 + controls, an HTTP/2 fallback result, and final HTTP/2 liveness. +- Healthy HTTPS listener and route conditions, enabled certificate + validation, and no customer NSG or route table on the AGFC subnet. +- Confirmed deletion of the temporary main and node resource groups. + +A timeout is not an explicit protocol rejection. These observations do not +establish unsupported status for every region or preview, and they do not +measure mobile-device performance. A nonzero curl TLS verification field +on a connection timeout is not evidence of a certificate rejection. + +## Use, validation, and cleanup + +Read the JSON as evidence for the specific configuration and date. Its +14 main trial records were checked against the recorded counts, actual +protocols, errors, and backend responses before archiving. The additional +timeout, controls, and fallback are outside those main trial totals. + +The artifact contains no cloud resource identifiers, actual AGFC addresses, +private keys, or kubeconfig. It creates no resources and requires no cleanup. +Do not combine it with the separate local performance dataset as if both +were measurements of the same deployment. diff --git a/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/agfc-protocol-check/result.json b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/agfc-protocol-check/result.json new file mode 100644 index 00000000..29832642 --- /dev/null +++ b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/agfc-protocol-check/result.json @@ -0,0 +1,262 @@ +{ + "checked_at_utc": "2026-09-15T13:50:21.305516+00:00", + "scope": "Actual disposable Azure AKS + Application Gateway for Containers, not the earlier local mobile-network simulation", + "conclusion": "HTTP/2 accepted; strict HTTP/3 produced no HTTP response on this configured AGFC frontend from either tested network", + "region": "koreacentral", + "aks_version": "1.35", + "alb_controller_version": "1.10.30", + "topology": "Client -> AGFC HTTPS/443 -> AKS HTTP echo service; no separate Application Gateway", + "sni": "probe.example.test", + "same_destination_ip_for_all_agfc_protocol_probes": true, + "certificate_validation_enabled": true, + "gateway_and_route_programmed": true, + "subnet_filtering": { + "delegations": [ + "Microsoft.ServiceNetworking/trafficControllers" + ], + "nsgAttached": false, + "routeTableAttached": false + }, + "trial_totals": { + "strict_h2_successes": 7, + "strict_h2_attempts": 7, + "strict_h3_successes": 0, + "strict_h3_attempts": 7 + }, + "trials": [ + { + "source": "local-macos", + "client": "curl-8.22.0-ngtcp2", + "attempt": 1, + "requested_protocol": "h2", + "http_version": "2", + "status": "200", + "exit": 0, + "seconds": 0.603332, + "tls_verify": 0 + }, + { + "source": "azure-aks-pod", + "client": "quic-go-0.62.0", + "attempt": 1, + "requested_protocol": "h2", + "http_version": "HTTP/2.0", + "http_status": 200, + "alpn": "h2", + "tls": "TLS 1.3", + "alt_svc": "", + "body": "agfc-http3-probe\n", + "seconds": 0.13002377 + }, + { + "source": "local-macos", + "client": "curl-8.22.0-ngtcp2", + "attempt": 1, + "requested_protocol": "h3", + "http_version": "0", + "status": "000", + "exit": 28, + "seconds": 8.00145, + "tls_verify": 1 + }, + { + "source": "azure-aks-pod", + "client": "quic-go-0.62.0", + "attempt": 1, + "requested_protocol": "h3", + "http_version": "", + "http_status": 0, + "alpn": "", + "tls": "", + "alt_svc": "", + "body": "", + "seconds": 8.000638948, + "error": "Get \"https://probe.example.test/\": context deadline exceeded (Client.Timeout exceeded while awaiting headers)" + }, + { + "source": "local-macos", + "client": "curl-8.22.0-ngtcp2", + "attempt": 2, + "requested_protocol": "h2", + "http_version": "2", + "status": "200", + "exit": 0, + "seconds": 0.646205, + "tls_verify": 0 + }, + { + "source": "azure-aks-pod", + "client": "quic-go-0.62.0", + "attempt": 2, + "requested_protocol": "h2", + "http_version": "HTTP/2.0", + "http_status": 200, + "alpn": "h2", + "tls": "TLS 1.3", + "alt_svc": "", + "body": "agfc-http3-probe\n", + "seconds": 0.126402094 + }, + { + "source": "local-macos", + "client": "curl-8.22.0-ngtcp2", + "attempt": 2, + "requested_protocol": "h3", + "http_version": "0", + "status": "000", + "exit": 28, + "seconds": 8.001046, + "tls_verify": 1 + }, + { + "source": "azure-aks-pod", + "client": "quic-go-0.62.0", + "attempt": 2, + "requested_protocol": "h3", + "http_version": "", + "http_status": 0, + "alpn": "", + "tls": "", + "alt_svc": "", + "body": "", + "seconds": 8.000913953, + "error": "Get \"https://probe.example.test/\": context deadline exceeded (Client.Timeout exceeded while awaiting headers)" + }, + { + "source": "local-macos", + "client": "curl-8.22.0-ngtcp2", + "attempt": 3, + "requested_protocol": "h2", + "http_version": "2", + "status": "200", + "exit": 0, + "seconds": 0.62395, + "tls_verify": 0 + }, + { + "source": "azure-aks-pod", + "client": "quic-go-0.62.0", + "attempt": 3, + "requested_protocol": "h2", + "http_version": "HTTP/2.0", + "http_status": 200, + "alpn": "h2", + "tls": "TLS 1.3", + "alt_svc": "", + "body": "agfc-http3-probe\n", + "seconds": 0.129878205 + }, + { + "source": "local-macos", + "client": "curl-8.22.0-ngtcp2", + "attempt": 3, + "requested_protocol": "h3", + "http_version": "0", + "status": "000", + "exit": 28, + "seconds": 8.001606, + "tls_verify": 1 + }, + { + "source": "azure-aks-pod", + "client": "quic-go-0.62.0", + "attempt": 3, + "requested_protocol": "h3", + "http_version": "", + "http_status": 0, + "alpn": "", + "tls": "", + "alt_svc": "", + "body": "", + "seconds": 8.000398991, + "error": "Get \"https://probe.example.test/\": context deadline exceeded (Client.Timeout exceeded while awaiting headers)" + }, + { + "source": "local-macos", + "client": "quic-go-0.62.0", + "attempt": 1, + "requested_protocol": "h2", + "http_version": "HTTP/2.0", + "http_status": 200, + "alpn": "h2", + "tls": "TLS 1.3", + "alt_svc": "", + "body": "agfc-http3-probe\n", + "seconds": 0.765737416 + }, + { + "source": "local-macos", + "client": "quic-go-0.62.0", + "attempt": 1, + "requested_protocol": "h3", + "http_version": "", + "http_status": 0, + "alpn": "", + "tls": "", + "alt_svc": "", + "body": "", + "seconds": 8.001475375, + "error": "Get \"https://probe.example.test/\": context deadline exceeded (Client.Timeout exceeded while awaiting headers)" + } + ], + "additional_15_second_strict_h3_probe": { + "http_version": "0", + "status": "000", + "exit": 28, + "seconds": 15.002221, + "tls_verify": 1 + }, + "external_http3_controls_after_probes": [ + { + "source": "local", + "target": "https://www.google.com/generate_204", + "requested_protocol": "h3", + "http_version": "HTTP/3.0", + "http_status": 204, + "alpn": "h3", + "tls": "TLS 1.3", + "alt_svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "body": "", + "seconds": 0.139722334 + }, + { + "source": "azure", + "target": "https://www.google.com/generate_204", + "requested_protocol": "h3", + "http_version": "HTTP/3.0", + "http_status": 204, + "alpn": "h3", + "tls": "TLS 1.3", + "alt_svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "body": "", + "seconds": 0.140216604 + } + ], + "curl_http3_fallback_allowed": { + "http_version": "2", + "status": "200", + "exit": 0, + "seconds": 1.686332, + "tls_verify": 0 + }, + "final_http2_liveness": { + "http_version": "2", + "status": "200", + "exit": 0, + "tls_verify": 0 + }, + "limitations": [ + "A timeout is not an explicit server-side protocol rejection packet.", + "This verifies observed behavior of this region, listener configuration, and date, not all private previews or future versions.", + "The same-source successful HTTP/3 controls and second Azure source reduce the likelihood of a general local UDP/client issue; an AGFC-specific managed datapath restriction cannot be distinguished from no HTTP/3 listener using client timeouts alone.", + "No mobile-device performance measurement was performed in this check.", + "Official-source MCP was unavailable during this check; setup articles were read directly, but this is runtime evidence, not a newly MCP-verified product support statement.", + "A nonzero curl TLS verification field on a connection timeout is not evidence of a certificate rejection; the HTTP/3 connection did not produce a completed verified HTTP response." + ], + "cleanup": { + "temporary_cloud_resources_deleted": true, + "main_and_node_resource_groups_absent": true, + "verified_at_utc": "2026-09-15T14:04:03.982261+00:00", + "existing_aks_and_agfc_not_modified": true + } +} diff --git a/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/agfc-protocol-check/sample.yml b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/agfc-protocol-check/sample.yml new file mode 100644 index 00000000..fded4f5f --- /dev/null +++ b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/agfc-protocol-check/sample.yml @@ -0,0 +1,5 @@ +title: AGFC protocol acceptance results +description: Actual Azure HTTP/2 and strict HTTP/3 observations, separate from the local performance control. +kind: artifact +used_by: + - index diff --git a/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/.dockerignore b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/.dockerignore new file mode 100644 index 00000000..0f8cfcf6 --- /dev/null +++ b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/.dockerignore @@ -0,0 +1,5 @@ +* +!Dockerfile +!go.mod +!go.sum +!*.go diff --git a/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/Dockerfile b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/Dockerfile new file mode 100644 index 00000000..73a13c26 --- /dev/null +++ b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/Dockerfile @@ -0,0 +1,11 @@ +FROM golang:1.26.5-alpine3.23@sha256:622e56dbc11a8cfe87cafa2331e9a201877271cbff918af53d3be315f3da88cc AS build +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY *.go ./ +RUN go test ./... && go build -trimpath -o /out/bench . + +FROM alpine:3.23@sha256:fd791d74b68913cbb027c6546007b3f0d3bc45125f797758156952bc2d6daf40 +RUN apk add --no-cache iproute2 iproute2-tc ethtool iputils +COPY --from=build /out/bench /bench +ENTRYPOINT ["/bench"] diff --git a/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/README.md b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/README.md new file mode 100644 index 00000000..3d7a8ace --- /dev/null +++ b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/README.md @@ -0,0 +1,183 @@ +# HTTP/2 and HTTP/3 mobile-network control + +This sample compares strict HTTP/2 and HTTP/3 against the same local server. +It is **not** an AKS, Application Gateway for Containers (AGFC), mobile-device, +browser, or mobile-carrier benchmark. It does not deploy any Azure resources. + +The [owning research page](../../index.md) explains why an AGFC HTTP/3 result +cannot be inferred from this control experiment. + +## Requirements + +- Docker with Linux containers and permission to add `NET_ADMIN` to all three + isolated containers: client, server, and forwarding router. +- Permission to set `net.ipv4.ping_group_range=0 0` in the client's network + namespace. The image runs the client with GID 0; this permits its ICMP + datagram sockets without granting `NET_RAW`. +- Go 1.26 for running the native sample tests (the container builder uses 1.26.5). +- Bash. + +## Run + +```bash +go test -race ./... +bash run.sh +``` + +Use `DOCKER_CONTEXT` to select a dedicated runtime without changing the global +Docker context. For example, with an already-running dedicated context: + +```bash +DOCKER_CONTEXT=my-test-context SAMPLES=1 bash run.sh +DOCKER_CONTEXT=my-test-context SAMPLES=30 bash run.sh +``` + +`SAMPLES` is the number of pairs **per workload and network profile**, not the +number of HTTP requests. The default produces 360 measured trials and 3,060 +measured requests. An additional 180 requests prewarm connections, and the +initial protocol preflight is excluded from the dataset. + +The script bounds `SAMPLES` to 1-100. The client has a 20-second per-request +timeout. No other workload is run concurrently by the script. A failed trial +is preserved in the output, remaining trials are collected, and the command +exits 1. Setup, malformed evidence, and incomplete output are fatal. Do not +treat a nonzero exit as a successful benchmark. + +## Topology and network model + +```text +strict H2/H3 client -- isolated network -- Linux forwarding router + | + isolated network + | + same H2/H3 TLS endpoint and handler +``` + +There are three containers and two internal Docker networks, with no +published host ports. Both protocols use the same hostname, destination port +8443, route, certificate, TLS 1.3 policy, and response bytes. Static host +resolution avoids external DNS traffic. + +| Profile | Router delay per direction | Configured rate cap per direction | Random packet loss per direction | +|---|---:|---:|---:| +| `unshaped` | none added | no configured limit | none added | +| `mobile-clean` | 40 ms | 10 Mbit/s | 0% | +| `mobile-loss` | 40 ms | 10 Mbit/s | 1% | + +All profiles retain a `netem` qdisc with a **10,000-packet queue limit per +router egress interface**. `unshaped` means no added delay, rate cap, or +random loss; it is not a no-qdisc or unlimited-queue control. Keeping the +same qdisc and queue limit avoids changing the queue implementation between +profiles. The archived unshaped run had zero additional qdisc drops, but +this does not establish that queueing or scheduling had no effect. + +The two shaped profiles target **80 ms additional round-trip delay**, not +40 ms RTT. The loss setting is 1% independently in **each direction**, not +a 1% HTTP failure rate or a 1% round-trip loss setting. Loss seeds are +20260915 and 20260916 on the router's two interfaces. + +Impairment is applied on the forwarding router rather than the application +sender's qdisc. TSO, GSO, GRO, and UDP segmentation offload are disabled on +the test interfaces. The script records applied qdiscs, cumulative counters +before/after each profile, offload settings, and ICMP RTT checks. Counter +differences, not the absolute cumulative values, describe a profile. + +Linux scheduling and timer granularity still affect the realized delay and +rate. This is a simple packet-level model, not a radio-access-network model: +there is no cellular scheduler, radio power state, signal attenuation, +carrier congestion, burst-loss model, deliberate jitter, or network handover. + +## What is measured + +- `cold`: a new transport and a new TLS connection for one 1 KiB response. +- `warm`: a new transport, one **unmeasured 1 KiB warmup**, then sixteen + concurrent 16 KiB responses on that same connection (256 KiB in total). + This is a pre-established connection, not a long-running steady-state + congestion-window benchmark. +- HTTP/2-first and HTTP/3-first order alternates between pairs. Each trial + has its own transport; connection state is not shared across trials. +- Session tickets, TLS session resumption, HTTP compression, and server-side + 0-RTT acceptance are disabled. The forced HTTP/3 client does not pay an + Alt-Svc discovery request, so `cold` does not model a browser's first visit. +- Every successful response must have the requested wire HTTP version, + matching ALPN, TLS 1.3, HTTP 200, exact content, and one server connection + identifier. Warm trials must reuse the warmup's identifier. + +`elapsed_ms` runs from releasing the request batch to completion of all body +reads and verification. `headers_ms` is recorded only for the single-request +workload: it measures response-header completion, **not exact first-byte +arrival**. Neither measurement includes rendering, JavaScript execution, +mobile CPU cost, battery usage, or application database time. + +The connection identifier comes from this directly connected TLS endpoint. +It cannot prove frontend connection reuse through a reverse proxy. Do not +repurpose the identifier as AGFC frontend-connection evidence. + +## Evidence and summaries + +Runtime output goes to a unique ignored `evidence//` directory: + +- `all.jsonl` and per-profile JSONL: every trial, including failures, payload + bytes, negotiated protocols, connection identifiers, and elapsed time. +- `summary.json`: success/failure counts and nearest-rank p50/p95 over + **successful trials only**. Failed-only groups have `null`, not zero, + latency. `incomplete_requests` includes requests not sent after a failed + warmup; `warmup_failures` identifies these trials. +- qdisc, ping, offload, environment, and package-version files: verify the + configured model and the actual runtime. A nonempty qdisc backlog can + include connection-close packets after response measurement. +- `preflight.jsonl` and `server.log`: diagnostics, not performance samples. + +To recompute a summary without Docker: + +```bash +go run . summarize < evidence//all.jsonl +``` + +The reader rejects truncated JSON and success records without matching +protocol, payload, connection, and timing evidence. The runner also checks +the expected number of records for each profile. A summary alone is not +proof that an arbitrary manually supplied file contains a complete run. + +The container base images are digest-pinned; Go dependencies are locked by +the module files. Alpine package repositories can change, so the run also +records installed package versions. Identical seeds do not guarantee +identical wall-clock measurements or identical packet schedules. + +### Recorded run + +The reviewed [2026-09-15 dataset](results/2026-09-15/) contains: + +- `trials.jsonl`: 360 measured trials, copied without altering their values. +- `summary.json`: the exact runner output, independently recomputed. +- `environment.json`: scope, versions, method, counts, and SHA-256 checksums. +- `network.json`: original qdisc snapshots plus address-free RTT aggregates + and the relevant observed offload settings. + +```bash +go run . summarize < results/2026-09-15/trials.jsonl +shasum -a 256 results/2026-09-15/trials.jsonl results/2026-09-15/summary.json +``` + +The dataset includes the unshaped results where HTTP/3 was slower, and the +lossy cold-connection p95 regression. It is not an AGFC benchmark and does +not establish a production p95 or a universal HTTP/3 improvement percentage. + +## Cleanup and safety + +The script removes only its uniquely named containers, networks, certificate +volume, and run image, including on normal failure, SIGINT, or SIGTERM. +Downloaded base images and build cache can remain. SIGKILL or host failure +cannot execute shell cleanup; inspect and remove only that run's named +resources in that case. Do not use a global Docker prune on a shared host. + +Certificates are generated solely for the experiment, trusted explicitly by +the client, and deleted with the temporary volume. Containers receive only +`NET_ADMIN`, not `NET_RAW`, host networking, or privileged mode. The ping +socket setting is scoped to the client container, not the host. No Azure/Kubernetes +credentials, cloud deployment, or changes to the host's interfaces and +default Docker/Kubernetes contexts are required. + +Review any runtime logs before publishing. The recorded public dataset must +exclude certificates, actual target addresses, resource identifiers, and +machine-specific paths. Keep unreviewed runtime evidence under `evidence/`. diff --git a/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/bench_test.go b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/bench_test.go new file mode 100644 index 00000000..37c03f5c --- /dev/null +++ b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/bench_test.go @@ -0,0 +1,118 @@ +package main + +import ( + "bytes" + "crypto/tls" + "net/http" + "net/http/httptest" + "testing" +) + +func TestPayloadHandler(t *testing.T) { + for _, tc := range []struct { + name string + method string + path string + status int + size int + }{ + {"payload", "GET", "/bytes?size=1024", 200, 1024}, + {"missing size", "GET", "/bytes", 400, 0}, + {"negative size", "GET", "/bytes?size=-1", 400, 0}, + {"zero size", "GET", "/bytes?size=0", 400, 0}, + {"oversized", "GET", "/bytes?size=1048577", 400, 0}, + {"invalid size", "GET", "/bytes?size=bad", 400, 0}, + {"wrong path", "GET", "/missing?size=1024", 404, 0}, + {"wrong method", "POST", "/bytes?size=1024", 405, 0}, + } { + t.Run(tc.name, func(t *testing.T) { + recorder := httptest.NewRecorder() + payloadHandler(recorder, httptest.NewRequest(tc.method, tc.path, nil)) + if recorder.Code != tc.status { + t.Fatalf("status = %d, want %d", recorder.Code, tc.status) + } + if tc.status == http.StatusOK { + if !bytes.Equal(recorder.Body.Bytes(), payload(tc.size)) { + t.Fatal("response payload differs from requested bytes") + } + if recorder.Header().Get("Cache-Control") != "no-store" { + t.Fatal("benchmark response must disable caching") + } + } + }) + } +} + +func TestVerifyResponseRejectsInvalidMeasurements(t *testing.T) { + for _, tc := range []struct { + name string + protocol string + actualMajor int + status int + body []byte + encoding string + wantError bool + }{ + {"h2", "h2", 2, 200, payload(16), "", false}, + {"h3", "h3", 3, 200, payload(16), "", false}, + {"h3 fallback to h2", "h3", 2, 200, payload(16), "", true}, + {"h2 fallback to h1", "h2", 1, 200, payload(16), "", true}, + {"unknown protocol", "h1", 1, 200, payload(16), "", true}, + {"redirect", "h3", 3, 302, payload(16), "", true}, + {"backend error", "h2", 2, 503, payload(16), "", true}, + {"short payload", "h2", 2, 200, payload(15), "", true}, + {"wrong content", "h3", 3, 200, bytes.Repeat([]byte{255}, 16), "", true}, + {"compressed payload", "h3", 3, 200, payload(16), "gzip", true}, + } { + t.Run(tc.name, func(t *testing.T) { + response := &http.Response{ + StatusCode: tc.status, + ProtoMajor: tc.actualMajor, + Header: make(http.Header), + TLS: &tls.ConnectionState{Version: tls.VersionTLS13, NegotiatedProtocol: tc.protocol}, + } + response.Header.Set("Content-Encoding", tc.encoding) + err := verifyResponse(response, tc.body, tc.protocol, 16) + if (err != nil) != tc.wantError { + t.Fatalf("error = %v, wantError = %v", err, tc.wantError) + } + }) + } +} + +func TestVerifyResponseRejectsInvalidTLS(t *testing.T) { + for _, state := range []*tls.ConnectionState{ + nil, + {Version: tls.VersionTLS12, NegotiatedProtocol: "h2"}, + {Version: tls.VersionTLS13, NegotiatedProtocol: "http/1.1"}, + } { + response := &http.Response{StatusCode: 200, ProtoMajor: 2, Header: make(http.Header), TLS: state} + if err := verifyResponse(response, payload(16), "h2", 16); err == nil { + t.Fatalf("accepted invalid TLS state: %+v", state) + } + } +} + +func TestSummariesUseSuccessfulTrialsAndExposeFailures(t *testing.T) { + results := []result{ + {Profile: "mobile", Workload: "cold", Protocol: "h2", Requests: 1, Succeeded: 1, ElapsedMS: 30}, + {Profile: "mobile", Workload: "cold", Protocol: "h2", Requests: 1, Succeeded: 1, ElapsedMS: 10}, + {Profile: "mobile", Workload: "cold", Protocol: "h2", Requests: 1, Succeeded: 1, ElapsedMS: 20}, + {Profile: "mobile", Workload: "cold", Protocol: "h2", Requests: 1, ElapsedMS: 9000, Errors: []string{"timeout"}}, + {Profile: "mobile", Workload: "cold", Protocol: "h3", Requests: 1, Errors: []string{"QUIC unavailable"}}, + } + summaries := summarize(results) + if len(summaries) != 2 { + t.Fatalf("groups = %d, want 2", len(summaries)) + } + h2, h3 := summaries[0], summaries[1] + if h2.Trials != 4 || h2.Successes != 3 || h2.Failures != 1 || h2.FailedRequests != 1 { + t.Fatalf("failure accounting is wrong: %+v", h2) + } + if h2.P50MS == nil || *h2.P50MS != 20 || h2.P95MS == nil || *h2.P95MS != 30 { + t.Fatalf("nearest-rank percentiles are wrong: %+v", h2) + } + if h3.P50MS != nil || h3.P95MS != nil || h3.Failures != 1 { + t.Fatalf("failed-only group must not produce a zero-latency success: %+v", h3) + } +} diff --git a/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/client.go b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/client.go new file mode 100644 index 00000000..533cd6e9 --- /dev/null +++ b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/client.go @@ -0,0 +1,130 @@ +package main + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "io" + "net/http" + "os" + "sort" + "sync" + "time" + + "github.com/quic-go/quic-go" + "github.com/quic-go/quic-go/http3" + "golang.org/x/net/http2" +) + +func newClient(protocol, caFile string, timeout time.Duration) (*http.Client, func() error, error) { + if protocol != "h2" && protocol != "h3" { + return nil, nil, fmt.Errorf("unsupported protocol %q", protocol) + } + if timeout <= 0 { + return nil, nil, fmt.Errorf("timeout must be positive") + } + ca, err := os.ReadFile(caFile) + if err != nil { + return nil, nil, err + } + roots := x509.NewCertPool() + if !roots.AppendCertsFromPEM(ca) { + return nil, nil, fmt.Errorf("CA file contains no valid certificates") + } + tlsConfig := &tls.Config{RootCAs: roots, MinVersion: tls.VersionTLS13} + var transport http.RoundTripper + var closeClient func() error + if protocol == "h2" { + h2 := &http2.Transport{TLSClientConfig: tlsConfig, DisableCompression: true} + transport = h2 + closeClient = func() error { h2.CloseIdleConnections(); return nil } + } else { + h3 := &http3.Transport{ + TLSClientConfig: tlsConfig, DisableCompression: true, + QUICConfig: &quic.Config{HandshakeIdleTimeout: timeout, MaxIdleTimeout: 60 * time.Second}, + } + transport, closeClient = h3, h3.Close + } + return &http.Client{ + Transport: transport, Timeout: timeout, + CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }, + }, closeClient, nil +} + +func measureBatch(client *http.Client, protocol, origin string, count, size int, expectedConnection string) result { + r := result{Protocol: protocol, Requests: count} + if count < 1 || count > 16 || size < 1 || size > 1<<20 { + r.Errors = []string{"invalid batch dimensions"} + return r + } + type observation struct { + bytes int64 + headersMS float64 + connection string + http string + alpn string + err error + } + observations := make([]observation, count) + var ready, finished sync.WaitGroup + ready.Add(count) + finished.Add(count) + start := make(chan struct{}) + for i := range count { + go func() { + defer finished.Done() + ready.Done() + <-start + begin := time.Now() + response, err := client.Get(fmt.Sprintf("%s/bytes?size=%d&stream=%d", origin, size, i)) + o := &observations[i] + o.headersMS = float64(time.Since(begin)) / float64(time.Millisecond) + if err != nil { + o.err = err + return + } + body, readErr := io.ReadAll(io.LimitReader(response.Body, int64(size)+1)) + closeErr := response.Body.Close() + o.bytes = int64(len(body)) + o.err = errors.Join(readErr, closeErr, verifyResponse(response, body, protocol, size)) + o.connection, o.http = response.Header.Get("X-Bench-Connection"), response.Proto + if response.TLS != nil { + o.alpn = response.TLS.NegotiatedProtocol + } + if o.connection == "" || (expectedConnection != "" && o.connection != expectedConnection) { + o.err = errors.Join(o.err, fmt.Errorf("missing or changed connection identifier")) + } + }() + } + ready.Wait() + begin := time.Now() + close(start) + finished.Wait() + r.ElapsedMS = float64(time.Since(begin)) / float64(time.Millisecond) + connections := make(map[string]bool) + for i, o := range observations { + r.Bytes += o.bytes + if count == 1 { + r.HeadersMS = o.headersMS + } + if o.connection != "" { + connections[o.connection] = true + } + if o.err != nil { + r.Errors = append(r.Errors, fmt.Sprintf("stream %d: %v", i, o.err)) + continue + } + r.Succeeded++ + r.NegotiatedHTTP, r.NegotiatedALPN, r.TLSVersion = o.http, o.alpn, "TLS 1.3" + } + for id := range connections { + r.ConnectionIDs = append(r.ConnectionIDs, id) + } + sort.Strings(r.ConnectionIDs) + r.Connections = len(connections) + if r.Succeeded > 0 && r.Connections != 1 { + r.Errors = append(r.Errors, fmt.Sprintf("batch used %d connections, expected exactly one", r.Connections)) + } + return r +} diff --git a/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/go.mod b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/go.mod new file mode 100644 index 00000000..d9839de6 --- /dev/null +++ b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/go.mod @@ -0,0 +1,15 @@ +module example.com/mobile-network-bench + +go 1.26.0 + +require ( + github.com/quic-go/quic-go v0.62.0 + golang.org/x/net v0.56.0 +) + +require ( + github.com/quic-go/qpack v0.6.0 // indirect + golang.org/x/crypto v0.54.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.40.0 // indirect +) diff --git a/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/go.sum b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/go.sum new file mode 100644 index 00000000..dc2ac1f4 --- /dev/null +++ b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/go.sum @@ -0,0 +1,20 @@ +github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0= +github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.62.0 h1:ZHDjCk5OacATwGvs8PWE97CTvX7AqZiVoW7++ZOXTf8= +github.com/quic-go/quic-go v0.62.0/go.mod h1:RAro2j2yN9a9EiPACLHT9IB2NXCvGQmmo/alT0yYI0w= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= diff --git a/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/main.go b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/main.go new file mode 100644 index 00000000..7a4522b1 --- /dev/null +++ b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/main.go @@ -0,0 +1,194 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "net/url" + "os" + "os/signal" + "path/filepath" + "strings" + "syscall" + "time" +) + +var errFailedTrials = errors.New("one or more trials failed; inspect the recorded errors") + +type benchOptions struct { + origin string + ca string + profile string + samples int + timeout time.Duration +} + +func parseBenchOptions(args []string) (benchOptions, error) { + var o benchOptions + flags := flag.NewFlagSet("bench", flag.ContinueOnError) + flags.SetOutput(io.Discard) + flags.StringVar(&o.origin, "url", "https://server:8443", "benchmark origin") + flags.StringVar(&o.ca, "ca", "/certs/server.crt", "trusted local certificate") + flags.StringVar(&o.profile, "profile", "unshaped", "network profile label") + flags.IntVar(&o.samples, "samples", 30, "paired trials, 1..100") + flags.DurationVar(&o.timeout, "timeout", 20*time.Second, "per-request timeout, at most 60s") + if err := flags.Parse(args); err != nil { + return o, err + } + u, err := url.Parse(o.origin) + if err != nil { + return o, err + } + if flags.NArg() != 0 || o.samples < 1 || o.samples > 100 || o.timeout <= 0 || o.timeout > 60*time.Second || strings.TrimSpace(o.profile) == "" { + return o, fmt.Errorf("invalid arguments: require 1..100 samples, timeout in (0s,60s], and a profile label") + } + if u.Scheme != "https" || u.Hostname() == "" || u.User != nil || + u.ForceQuery || u.RawQuery != "" || strings.Contains(o.origin, "#") || + (u.Path != "" && u.Path != "/") { + return o, fmt.Errorf("url must be an HTTPS origin without credentials, path, query, or fragment") + } + o.origin = (&url.URL{Scheme: u.Scheme, Host: u.Host}).String() + return o, nil +} + +func runBench(o benchOptions, output io.Writer) error { + encoder := json.NewEncoder(output) + failed := false + for pair := 1; pair <= o.samples; pair++ { + protocols := []string{"h2", "h3"} + if pair%2 == 0 { + protocols[0], protocols[1] = protocols[1], protocols[0] + } + for _, workload := range []string{"cold", "warm"} { + for _, protocol := range protocols { + client, closeClient, err := newClient(protocol, o.ca, o.timeout) + if err != nil { + return err + } + var r result + if workload == "cold" { + r = measureBatch(client, protocol, o.origin, 1, 1024, "") + } else { + warmup := measureBatch(client, protocol, o.origin, 1, 1024, "") + if len(warmup.Errors) != 0 || warmup.Succeeded != 1 { + r = result{Requests: 16, WarmupFailed: true, Errors: append([]string{"unmeasured warmup failed"}, warmup.Errors...)} + } else { + r = measureBatch(client, protocol, o.origin, 16, 16384, warmup.ConnectionIDs[0]) + } + } + if err := closeClient(); err != nil { + r.Errors = append(r.Errors, fmt.Sprintf("close client: %v", err)) + } + r.Profile, r.Workload, r.Protocol, r.Pair = o.profile, workload, protocol, pair + if len(r.Errors) != 0 || r.Succeeded != r.Requests { + failed = true + } + if err := encoder.Encode(r); err != nil { + return err + } + } + } + } + if failed { + return errFailedTrials + } + return nil +} + +func readResults(input io.Reader) ([]result, error) { + decoder := json.NewDecoder(input) + var results []result + for { + var r result + if err := decoder.Decode(&r); err != nil { + if errors.Is(err, io.EOF) { + break + } + return nil, fmt.Errorf("invalid measurement JSON: %w", err) + } + if r.Profile == "" || (r.Protocol != "h2" && r.Protocol != "h3") || + (r.Workload != "cold" && r.Workload != "warm") || r.Requests < 1 || + r.Succeeded < 0 || r.Succeeded > r.Requests || r.ElapsedMS < 0 || r.HeadersMS < 0 || + r.Pair < 1 || r.Pair > 100 || r.Bytes < 0 || + (r.Succeeded != r.Requests && len(r.Errors) == 0) { + return nil, fmt.Errorf("invalid measurement record") + } + count, size := 1, 1024 + if r.Workload == "warm" { + count, size = 16, 16384 + } + if r.Requests != count { + return nil, fmt.Errorf("request count does not match workload") + } + if len(r.Errors) == 0 { + httpVersion := map[string]string{"h2": "HTTP/2.0", "h3": "HTTP/3.0"}[r.Protocol] + if r.WarmupFailed || r.ElapsedMS <= 0 || r.HeadersMS > r.ElapsedMS || + r.Bytes != int64(count*size) || r.Connections != 1 || + len(r.ConnectionIDs) != 1 || r.ConnectionIDs[0] == "" || + r.NegotiatedHTTP != httpVersion || r.NegotiatedALPN != r.Protocol || r.TLSVersion != "TLS 1.3" { + return nil, fmt.Errorf("successful measurement lacks matching wire-protocol, connection, timing, or payload evidence") + } + } + results = append(results, r) + } + if len(results) == 0 { + return nil, fmt.Errorf("measurement input is empty") + } + return results, nil +} + +func run(args []string) error { + if len(args) == 0 { + return fmt.Errorf("usage: bench cert DIRECTORY | serve DIRECTORY | bench [OPTIONS] | summarize < JSONL") + } + switch args[0] { + case "cert": + if len(args) != 2 { + return fmt.Errorf("cert requires a destination directory") + } + return makeCertificate(args[1]) + case "serve": + if len(args) != 2 { + return fmt.Errorf("serve requires a certificate directory") + } + s, err := newServers(":8443", filepath.Join(args[1], "server.crt"), filepath.Join(args[1], "server.key")) + if err != nil { + return err + } + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer cancel() + return s.serve(ctx) + case "bench": + o, err := parseBenchOptions(args[1:]) + if err != nil { + return err + } + return runBench(o, os.Stdout) + case "summarize": + if len(args) != 1 { + return fmt.Errorf("summarize reads JSONL from standard input and accepts no arguments") + } + results, err := readResults(os.Stdin) + if err != nil { + return err + } + encoder := json.NewEncoder(os.Stdout) + encoder.SetIndent("", " ") + return encoder.Encode(summarize(results)) + default: + return fmt.Errorf("unknown command %q", args[0]) + } +} + +func main() { + if err := run(os.Args[1:]); err != nil { + fmt.Fprintln(os.Stderr, err) + if errors.Is(err, errFailedTrials) { + os.Exit(1) + } + os.Exit(2) + } +} diff --git a/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/payload.go b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/payload.go new file mode 100644 index 00000000..ffc61cb9 --- /dev/null +++ b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/payload.go @@ -0,0 +1,66 @@ +package main + +import ( + "bytes" + "crypto/tls" + "fmt" + "log" + "net/http" + "strconv" +) + +type connectionKey struct{} + +func payload(size int) []byte { + body := make([]byte, size) + for i := range body { + body[i] = byte(i % 251) + } + return body +} + +func payloadHandler(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/bytes" { + http.NotFound(w, r) + return + } + if r.Method != http.MethodGet { + w.Header().Set("Allow", http.MethodGet) + http.Error(w, "GET required", http.StatusMethodNotAllowed) + return + } + size, err := strconv.Atoi(r.URL.Query().Get("size")) + if err != nil || size < 1 || size > 1<<20 { + http.Error(w, "size must be between 1 and 1048576 bytes", http.StatusBadRequest) + return + } + if id, ok := r.Context().Value(connectionKey{}).(uint64); ok { + w.Header().Set("X-Bench-Connection", strconv.FormatUint(id, 10)) + } + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("Content-Length", strconv.Itoa(size)) + if _, err := w.Write(payload(size)); err != nil { + log.Printf("payload write failed: %v", err) + } +} + +func verifyResponse(response *http.Response, body []byte, protocol string, size int) error { + major := map[string]int{"h2": 2, "h3": 3}[protocol] + if major == 0 || response.ProtoMajor != major { + return fmt.Errorf("protocol mismatch: requested %s, received HTTP/%d", protocol, response.ProtoMajor) + } + if response.TLS == nil || response.TLS.Version != tls.VersionTLS13 || response.TLS.NegotiatedProtocol != protocol { + return fmt.Errorf("expected TLS 1.3 with ALPN %s", protocol) + } + if response.StatusCode != http.StatusOK { + return fmt.Errorf("HTTP status %d", response.StatusCode) + } + if response.Header.Get("Content-Encoding") != "" { + return fmt.Errorf("unexpected response compression") + } + if !bytes.Equal(body, payload(size)) { + return fmt.Errorf("payload mismatch: received %d bytes, expected %d identical bytes", len(body), size) + } + return nil +} diff --git a/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/results/2026-09-15/environment.json b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/results/2026-09-15/environment.json new file mode 100644 index 00000000..a7d1d1c8 --- /dev/null +++ b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/results/2026-09-15/environment.json @@ -0,0 +1,85 @@ +{ + "schema_version": 1, + "run_started_at_utc": "2026-09-15T11:11:56Z", + "scope": "local Linux netem control, not AKS, AGFC, a mobile device, or a carrier network", + "requested_aks_agfc_http3_benchmark_performed": false, + "new_azure_resources_created": false, + "runtime": { + "virtualization": "dedicated Colima VZ VM on macOS", + "architecture": "aarch64", + "configured_vcpus": 2, + "configured_memory_gib": 2, + "docker_reported_memory_bytes": 2053578752, + "linux_kernel": "6.8.0-117-generic", + "docker": "29.5.2", + "go_builder": "1.26.5", + "quic_go": "0.62.0", + "golang_x_net": "0.56.0", + "observed_client_tcp_congestion_control": "cubic", + "iproute2": "6.17.0", + "ethtool": "6.15", + "image": "sha256:b9c5a0e399185fec112a9ee05c114e403a456f07ac78618f6d542e72ac906eba" + }, + "method": { + "pairs_per_profile_and_workload": 30, + "protocols": ["h2", "h3"], + "profiles": ["unshaped", "mobile-clean", "mobile-loss"], + "http2_first_on_odd_pairs": true, + "per_request_timeout_seconds": 20, + "tls": "TLS 1.3", + "certificate_validation": true, + "http_compression": false, + "tls_session_resumption": false, + "server_accepts_0rtt": false, + "external_dns_or_alt_svc_discovery_included": false, + "percentile_method": "nearest-rank, ceil(p*n), successful trials only", + "cold": {"requests": 1, "bytes_per_response": 1024, "new_connection": true}, + "warm": { + "requests": 16, + "bytes_per_response": 16384, + "concurrent": true, + "unmeasured_warmup_requests": 1, + "unmeasured_warmup_bytes": 1024, + "new_connection_before_warmup": true, + "same_connection_as_warmup": true + } + }, + "verified_counts": { + "measured_trials": 360, + "successful_trials": 360, + "failed_trials": 0, + "measured_requests": 3060, + "successful_measured_requests": 3060, + "unmeasured_workload_warmup_requests": 180, + "distinct_measured_connection_ids": 360 + }, + "network": { + "topology": "client - internal network - forwarding router - internal network - same server", + "containers": 3, + "host_ports_published": false, + "impairment_location": "both router egress interfaces, not the application sender", + "shaped_profile_delay_ms_per_direction": 40, + "shaped_profile_rate_mbit_per_second_per_direction": 10, + "mobile_loss_probability_per_direction": 0.01, + "mobile_loss_seed_to_client": 20260915, + "mobile_loss_seed_to_server": 20260916, + "mobile_loss_observed_drops_to_client": 129, + "mobile_loss_observed_drops_to_server": 108, + "counter_scope": "profile's HTTP/2 and HTTP/3 traffic, ICMP checks, and connection-close packets", + "segmentation_offloads_disabled": true, + "raw_configuration_and_checks": "network.json" + }, + "sha256": { + "trials.jsonl": "169684f477458a9ea9c6df61fe2fcf035b0d0222fc4bb6b82f440bd26d1dd3eb", + "summary.json": "eb6064cac6c2cc7ed891dc9a34d6489c280d908034bb3e8fe91e2d2847b23caa" + }, + "not_measured": [ + "AKS or AGFC latency", + "physical mobile devices or carrier networks", + "browser first visits and Alt-Svc discovery", + "individual stream head-of-line blocking", + "connection migration or Wi-Fi/cellular handover", + "TLS resumption or 0-RTT", + "battery, CPU, rendering, database time, or large-file throughput" + ] +} diff --git a/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/results/2026-09-15/network.json b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/results/2026-09-15/network.json new file mode 100644 index 00000000..86336326 --- /dev/null +++ b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/results/2026-09-15/network.json @@ -0,0 +1,500 @@ +{ + "profiles": { + "unshaped": { + "before": [ + { + "kind": "noqueue", + "handle": "0:", + "dev": "lo", + "root": true, + "refcnt": 2, + "options": {}, + "bytes": 0, + "packets": 0, + "drops": 0, + "overlimits": 0, + "requeues": 0, + "backlog": 0, + "qlen": 0 + }, + { + "kind": "netem", + "handle": "8003:", + "dev": "eth0", + "root": true, + "refcnt": 3, + "options": { + "limit": 10000, + "seed": 8636189665405066271, + "ecn": false, + "gap": 0 + }, + "bytes": 0, + "packets": 0, + "drops": 0, + "overlimits": 0, + "requeues": 0, + "backlog": 0, + "qlen": 0 + }, + { + "kind": "netem", + "handle": "8004:", + "dev": "eth1", + "root": true, + "refcnt": 3, + "options": { + "limit": 10000, + "seed": 16372058035622535339, + "ecn": false, + "gap": 0 + }, + "bytes": 0, + "packets": 0, + "drops": 0, + "overlimits": 0, + "requeues": 0, + "backlog": 0, + "qlen": 0 + } + ], + "after": [ + { + "kind": "noqueue", + "handle": "0:", + "dev": "lo", + "root": true, + "refcnt": 2, + "options": {}, + "bytes": 0, + "packets": 0, + "drops": 0, + "overlimits": 0, + "requeues": 0, + "backlog": 0, + "qlen": 0 + }, + { + "kind": "netem", + "handle": "8003:", + "dev": "eth0", + "root": true, + "refcnt": 3, + "options": { + "limit": 10000, + "seed": 8636189665405066271, + "ecn": false, + "gap": 0 + }, + "bytes": 17213024, + "packets": 14238, + "drops": 0, + "overlimits": 0, + "requeues": 0, + "backlog": 0, + "qlen": 0 + }, + { + "kind": "netem", + "handle": "8004:", + "dev": "eth1", + "root": true, + "refcnt": 3, + "options": { + "limit": 10000, + "seed": 16372058035622535339, + "ecn": false, + "gap": 0 + }, + "bytes": 873265, + "packets": 5216, + "drops": 0, + "overlimits": 0, + "requeues": 0, + "backlog": 0, + "qlen": 0 + } + ], + "icmp_rtt_ms": { + "min": 0.041, + "avg": 0.105, + "max": 0.194, + "mdev": 0.048 + } + }, + "mobile-clean": { + "before": [ + { + "kind": "noqueue", + "handle": "0:", + "dev": "lo", + "root": true, + "refcnt": 2, + "options": {}, + "bytes": 0, + "packets": 0, + "drops": 0, + "overlimits": 0, + "requeues": 0, + "backlog": 0, + "qlen": 0 + }, + { + "kind": "netem", + "handle": "8003:", + "dev": "eth0", + "root": true, + "refcnt": 3, + "options": { + "limit": 10000, + "delay": { + "delay": 0.04, + "jitter": 0, + "correlation": 0 + }, + "rate": { + "rate": 1250000, + "packetoverhead": 0, + "cellsize": 0, + "celloverhead": 0 + }, + "seed": 2808204822899426765, + "ecn": false, + "gap": 0 + }, + "bytes": 17213024, + "packets": 14238, + "drops": 0, + "overlimits": 0, + "requeues": 0, + "backlog": 0, + "qlen": 0 + }, + { + "kind": "netem", + "handle": "8004:", + "dev": "eth1", + "root": true, + "refcnt": 3, + "options": { + "limit": 10000, + "delay": { + "delay": 0.04, + "jitter": 0, + "correlation": 0 + }, + "rate": { + "rate": 1250000, + "packetoverhead": 0, + "cellsize": 0, + "celloverhead": 0 + }, + "seed": 12677222430787119595, + "ecn": false, + "gap": 0 + }, + "bytes": 873265, + "packets": 5216, + "drops": 0, + "overlimits": 0, + "requeues": 0, + "backlog": 0, + "qlen": 0 + } + ], + "after": [ + { + "kind": "noqueue", + "handle": "0:", + "dev": "lo", + "root": true, + "refcnt": 2, + "options": {}, + "bytes": 0, + "packets": 0, + "drops": 0, + "overlimits": 0, + "requeues": 0, + "backlog": 0, + "qlen": 0 + }, + { + "kind": "netem", + "handle": "8003:", + "dev": "eth0", + "root": true, + "refcnt": 3, + "options": { + "limit": 10000, + "delay": { + "delay": 0.04, + "jitter": 0, + "correlation": 0 + }, + "rate": { + "rate": 1250000, + "packetoverhead": 0, + "cellsize": 0, + "celloverhead": 0 + }, + "seed": 2808204822899426765, + "ecn": false, + "gap": 0 + }, + "bytes": 34361220, + "packets": 28111, + "drops": 0, + "overlimits": 0, + "requeues": 0, + "backlog": 354, + "qlen": 5 + }, + { + "kind": "netem", + "handle": "8004:", + "dev": "eth1", + "root": true, + "refcnt": 3, + "options": { + "limit": 10000, + "delay": { + "delay": 0.04, + "jitter": 0, + "correlation": 0 + }, + "rate": { + "rate": 1250000, + "packetoverhead": 0, + "cellsize": 0, + "celloverhead": 0 + }, + "seed": 12677222430787119595, + "ecn": false, + "gap": 0 + }, + "bytes": 1965947, + "packets": 14327, + "drops": 0, + "overlimits": 0, + "requeues": 0, + "backlog": 0, + "qlen": 0 + } + ], + "icmp_rtt_ms": { + "min": 80.241, + "avg": 85.043, + "max": 90.839, + "mdev": 3.224 + } + }, + "mobile-loss": { + "before": [ + { + "kind": "noqueue", + "handle": "0:", + "dev": "lo", + "root": true, + "refcnt": 2, + "options": {}, + "bytes": 0, + "packets": 0, + "drops": 0, + "overlimits": 0, + "requeues": 0, + "backlog": 0, + "qlen": 0 + }, + { + "kind": "netem", + "handle": "8003:", + "dev": "eth0", + "root": true, + "refcnt": 3, + "options": { + "limit": 10000, + "delay": { + "delay": 0.04, + "jitter": 0, + "correlation": 0 + }, + "loss-random": { + "loss": 0.01, + "correlation": 0 + }, + "rate": { + "rate": 1250000, + "packetoverhead": 0, + "cellsize": 0, + "celloverhead": 0 + }, + "seed": 20260915, + "ecn": false, + "gap": 0 + }, + "bytes": 34361574, + "packets": 28116, + "drops": 0, + "overlimits": 0, + "requeues": 0, + "backlog": 0, + "qlen": 0 + }, + { + "kind": "netem", + "handle": "8004:", + "dev": "eth1", + "root": true, + "refcnt": 3, + "options": { + "limit": 10000, + "delay": { + "delay": 0.04, + "jitter": 0, + "correlation": 0 + }, + "loss-random": { + "loss": 0.01, + "correlation": 0 + }, + "rate": { + "rate": 1250000, + "packetoverhead": 0, + "cellsize": 0, + "celloverhead": 0 + }, + "seed": 20260916, + "ecn": false, + "gap": 0 + }, + "bytes": 1966109, + "packets": 14330, + "drops": 0, + "overlimits": 0, + "requeues": 0, + "backlog": 0, + "qlen": 0 + } + ], + "after": [ + { + "kind": "noqueue", + "handle": "0:", + "dev": "lo", + "root": true, + "refcnt": 2, + "options": {}, + "bytes": 0, + "packets": 0, + "drops": 0, + "overlimits": 0, + "requeues": 0, + "backlog": 0, + "qlen": 0 + }, + { + "kind": "netem", + "handle": "8003:", + "dev": "eth0", + "root": true, + "refcnt": 3, + "options": { + "limit": 10000, + "delay": { + "delay": 0.04, + "jitter": 0, + "correlation": 0 + }, + "loss-random": { + "loss": 0.01, + "correlation": 0 + }, + "rate": { + "rate": 1250000, + "packetoverhead": 0, + "cellsize": 0, + "celloverhead": 0 + }, + "seed": 20260915, + "ecn": false, + "gap": 0 + }, + "bytes": 51544488, + "packets": 41957, + "drops": 129, + "overlimits": 0, + "requeues": 0, + "backlog": 222, + "qlen": 3 + }, + { + "kind": "netem", + "handle": "8004:", + "dev": "eth1", + "root": true, + "refcnt": 3, + "options": { + "limit": 10000, + "delay": { + "delay": 0.04, + "jitter": 0, + "correlation": 0 + }, + "loss-random": { + "loss": 0.01, + "correlation": 0 + }, + "rate": { + "rate": 1250000, + "packetoverhead": 0, + "cellsize": 0, + "celloverhead": 0 + }, + "seed": 20260916, + "ecn": false, + "gap": 0 + }, + "bytes": 3151046, + "packets": 24050, + "drops": 108, + "overlimits": 0, + "requeues": 0, + "backlog": 0, + "qlen": 0 + } + ], + "icmp_rtt_ms": { + "min": 81.051, + "avg": 85.892, + "max": 91.78, + "mdev": 3.411 + } + } + }, + "offloads": { + "client": { + "tcp-segmentation-offload": "off", + "generic-segmentation-offload": "off", + "generic-receive-offload": "off", + "tx-udp-segmentation": "off" + }, + "server": { + "tcp-segmentation-offload": "off", + "generic-segmentation-offload": "off", + "generic-receive-offload": "off", + "tx-udp-segmentation": "off" + }, + "router-eth0": { + "tcp-segmentation-offload": "off", + "generic-segmentation-offload": "off", + "generic-receive-offload": "off", + "tx-udp-segmentation": "off" + }, + "router-eth1": { + "tcp-segmentation-offload": "off", + "generic-segmentation-offload": "off", + "generic-receive-offload": "off", + "tx-udp-segmentation": "off" + } + } +} diff --git a/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/results/2026-09-15/summary.json b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/results/2026-09-15/summary.json new file mode 100644 index 00000000..7e019148 --- /dev/null +++ b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/results/2026-09-15/summary.json @@ -0,0 +1,158 @@ +[ + { + "profile": "mobile-clean", + "workload": "cold", + "protocol": "h2", + "trials": 30, + "successes": 30, + "failures": 0, + "incomplete_requests": 0, + "warmup_failures": 0, + "p50_ms": 256.986001, + "p95_ms": 264.981334, + "headers_p50_ms": 256.530876 + }, + { + "profile": "mobile-clean", + "workload": "cold", + "protocol": "h3", + "trials": 30, + "successes": 30, + "failures": 0, + "incomplete_requests": 0, + "warmup_failures": 0, + "p50_ms": 174.247501, + "p95_ms": 182.347625, + "headers_p50_ms": 174.229751 + }, + { + "profile": "mobile-clean", + "workload": "warm", + "protocol": "h2", + "trials": 30, + "successes": 30, + "failures": 0, + "incomplete_requests": 0, + "warmup_failures": 0, + "p50_ms": 481.326502, + "p95_ms": 493.763712, + "headers_p50_ms": null + }, + { + "profile": "mobile-clean", + "workload": "warm", + "protocol": "h3", + "trials": 30, + "successes": 30, + "failures": 0, + "incomplete_requests": 0, + "warmup_failures": 0, + "p50_ms": 363.952251, + "p95_ms": 366.514879, + "headers_p50_ms": null + }, + { + "profile": "mobile-loss", + "workload": "cold", + "protocol": "h2", + "trials": 30, + "successes": 30, + "failures": 0, + "incomplete_requests": 0, + "warmup_failures": 0, + "p50_ms": 258.166501, + "p95_ms": 367.470292, + "headers_p50_ms": 258.025126 + }, + { + "profile": "mobile-loss", + "workload": "cold", + "protocol": "h3", + "trials": 30, + "successes": 30, + "failures": 0, + "incomplete_requests": 0, + "warmup_failures": 0, + "p50_ms": 173.821167, + "p95_ms": 421.793166, + "headers_p50_ms": 173.802458 + }, + { + "profile": "mobile-loss", + "workload": "warm", + "protocol": "h2", + "trials": 30, + "successes": 30, + "failures": 0, + "incomplete_requests": 0, + "warmup_failures": 0, + "p50_ms": 540.523082, + "p95_ms": 1611.879335, + "headers_p50_ms": null + }, + { + "profile": "mobile-loss", + "workload": "warm", + "protocol": "h3", + "trials": 30, + "successes": 30, + "failures": 0, + "incomplete_requests": 0, + "warmup_failures": 0, + "p50_ms": 432.230916, + "p95_ms": 884.817876, + "headers_p50_ms": null + }, + { + "profile": "unshaped", + "workload": "cold", + "protocol": "h2", + "trials": 30, + "successes": 30, + "failures": 0, + "incomplete_requests": 0, + "warmup_failures": 0, + "p50_ms": 0.745958, + "p95_ms": 2.901834, + "headers_p50_ms": 0.723 + }, + { + "profile": "unshaped", + "workload": "cold", + "protocol": "h3", + "trials": 30, + "successes": 30, + "failures": 0, + "incomplete_requests": 0, + "warmup_failures": 0, + "p50_ms": 1.47325, + "p95_ms": 3.037916, + "headers_p50_ms": 1.460083 + }, + { + "profile": "unshaped", + "workload": "warm", + "protocol": "h2", + "trials": 30, + "successes": 30, + "failures": 0, + "incomplete_requests": 0, + "warmup_failures": 0, + "p50_ms": 1.841208, + "p95_ms": 3.288459, + "headers_p50_ms": null + }, + { + "profile": "unshaped", + "workload": "warm", + "protocol": "h3", + "trials": 30, + "successes": 30, + "failures": 0, + "incomplete_requests": 0, + "warmup_failures": 0, + "p50_ms": 2.273667, + "p95_ms": 4.430709, + "headers_p50_ms": null + } +] diff --git a/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/results/2026-09-15/trials.jsonl b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/results/2026-09-15/trials.jsonl new file mode 100644 index 00000000..aafaf296 --- /dev/null +++ b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/results/2026-09-15/trials.jsonl @@ -0,0 +1,360 @@ +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":1,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["5"],"elapsed_ms":2.901834,"headers_ms":2.888792,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":1,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["6"],"elapsed_ms":3.504417,"headers_ms":3.48725,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":1,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["7"],"elapsed_ms":2.17025,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":1,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["8"],"elapsed_ms":3.094792,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":2,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["9"],"elapsed_ms":2.08375,"headers_ms":2.077292,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":2,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["10"],"elapsed_ms":0.766917,"headers_ms":0.746125,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":2,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["11"],"elapsed_ms":2.541333,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":2,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["12"],"elapsed_ms":1.841208,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":3,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["13"],"elapsed_ms":1.105583,"headers_ms":1.103084,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":3,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["14"],"elapsed_ms":1.466125,"headers_ms":1.460083,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":3,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["15"],"elapsed_ms":2.442292,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":3,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["16"],"elapsed_ms":1.68375,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":4,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["17"],"elapsed_ms":1.329333,"headers_ms":1.323708,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":4,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["18"],"elapsed_ms":0.969834,"headers_ms":0.947791,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":4,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["19"],"elapsed_ms":2.016041,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":4,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["20"],"elapsed_ms":2.193666,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":5,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["21"],"elapsed_ms":0.708875,"headers_ms":0.705,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":5,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["22"],"elapsed_ms":1.720292,"headers_ms":1.714916,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":5,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["23"],"elapsed_ms":1.909375,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":5,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["24"],"elapsed_ms":2.916917,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":6,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["25"],"elapsed_ms":1.878458,"headers_ms":1.871791,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":6,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["26"],"elapsed_ms":0.757917,"headers_ms":0.7545,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":6,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["27"],"elapsed_ms":4.430709,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":6,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["28"],"elapsed_ms":2.453625,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":7,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["29"],"elapsed_ms":1.234792,"headers_ms":1.230083,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":7,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["30"],"elapsed_ms":1.805792,"headers_ms":1.799,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":7,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["31"],"elapsed_ms":1.553042,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":7,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["32"],"elapsed_ms":2.289958,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":8,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["33"],"elapsed_ms":1.604,"headers_ms":1.598084,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":8,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["34"],"elapsed_ms":0.690917,"headers_ms":0.664125,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":8,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["35"],"elapsed_ms":2.273667,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":8,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["36"],"elapsed_ms":3.288459,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":9,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["37"],"elapsed_ms":0.731916,"headers_ms":0.71175,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":9,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["38"],"elapsed_ms":1.443708,"headers_ms":1.43775,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":9,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["39"],"elapsed_ms":2.112792,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":9,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["40"],"elapsed_ms":2.060625,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":10,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["41"],"elapsed_ms":1.376458,"headers_ms":1.366375,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":10,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["42"],"elapsed_ms":0.702333,"headers_ms":0.700416,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":10,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["43"],"elapsed_ms":2.578875,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":10,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["44"],"elapsed_ms":1.777333,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":11,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["45"],"elapsed_ms":1.743,"headers_ms":1.740292,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":11,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["46"],"elapsed_ms":1.279792,"headers_ms":1.270583,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":11,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["47"],"elapsed_ms":1.826708,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":11,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["48"],"elapsed_ms":1.746125,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":12,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["49"],"elapsed_ms":1.206875,"headers_ms":1.202666,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":12,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["50"],"elapsed_ms":0.647833,"headers_ms":0.645333,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":12,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["51"],"elapsed_ms":2.374625,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":12,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["52"],"elapsed_ms":1.700458,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":13,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["53"],"elapsed_ms":0.565417,"headers_ms":0.538583,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":13,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["54"],"elapsed_ms":1.196959,"headers_ms":1.193333,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":13,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["55"],"elapsed_ms":1.628,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":13,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["56"],"elapsed_ms":1.838292,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":14,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["57"],"elapsed_ms":1.436709,"headers_ms":1.432416,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":14,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["58"],"elapsed_ms":0.634334,"headers_ms":0.631792,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":14,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["59"],"elapsed_ms":2.227708,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":14,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["60"],"elapsed_ms":1.417292,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":15,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["61"],"elapsed_ms":1.987292,"headers_ms":1.985125,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":15,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["62"],"elapsed_ms":1.47325,"headers_ms":1.458334,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":15,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["63"],"elapsed_ms":1.477458,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":15,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["64"],"elapsed_ms":2.780083,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":16,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["65"],"elapsed_ms":2.483125,"headers_ms":2.477208,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":16,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["66"],"elapsed_ms":0.629,"headers_ms":0.626375,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":16,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["67"],"elapsed_ms":2.586917,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":16,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["68"],"elapsed_ms":1.888959,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":17,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["69"],"elapsed_ms":0.83525,"headers_ms":0.8105,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":17,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["70"],"elapsed_ms":1.313416,"headers_ms":1.308959,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":17,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["71"],"elapsed_ms":3.347167,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":17,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["72"],"elapsed_ms":2.035125,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":18,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["73"],"elapsed_ms":2.928125,"headers_ms":2.924166,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":18,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["74"],"elapsed_ms":0.720833,"headers_ms":0.718,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":18,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["75"],"elapsed_ms":2.285583,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":18,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["76"],"elapsed_ms":1.832,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":19,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["77"],"elapsed_ms":0.586083,"headers_ms":0.583708,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":19,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["78"],"elapsed_ms":1.695459,"headers_ms":1.691376,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":19,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["79"],"elapsed_ms":2.613833,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":19,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["80"],"elapsed_ms":1.70025,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":20,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["81"],"elapsed_ms":1.680875,"headers_ms":1.67375,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":20,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["82"],"elapsed_ms":4.711792,"headers_ms":4.708334,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":20,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["83"],"elapsed_ms":1.67725,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":20,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["84"],"elapsed_ms":2.665459,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":21,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["85"],"elapsed_ms":0.809209,"headers_ms":0.806792,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":21,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["86"],"elapsed_ms":1.542208,"headers_ms":1.536208,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":21,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["87"],"elapsed_ms":2.09425,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":21,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["88"],"elapsed_ms":1.576708,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":22,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["89"],"elapsed_ms":1.132084,"headers_ms":1.128209,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":22,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["90"],"elapsed_ms":0.941958,"headers_ms":0.938375,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":22,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["91"],"elapsed_ms":2.767083,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":22,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["92"],"elapsed_ms":1.81475,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":23,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["93"],"elapsed_ms":0.822,"headers_ms":0.818042,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":23,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["94"],"elapsed_ms":1.903625,"headers_ms":1.897458,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":23,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["95"],"elapsed_ms":1.630125,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":23,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["96"],"elapsed_ms":2.396625,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":24,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["97"],"elapsed_ms":3.037916,"headers_ms":3.033458,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":24,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["98"],"elapsed_ms":2.855542,"headers_ms":2.838333,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":24,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["99"],"elapsed_ms":2.255625,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":24,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["100"],"elapsed_ms":1.986417,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":25,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["101"],"elapsed_ms":0.57725,"headers_ms":0.555709,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":25,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["102"],"elapsed_ms":1.2835,"headers_ms":1.275125,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":25,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["103"],"elapsed_ms":1.66825,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":25,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["104"],"elapsed_ms":1.982959,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":26,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["105"],"elapsed_ms":1.998541,"headers_ms":1.994375,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":26,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["106"],"elapsed_ms":0.745958,"headers_ms":0.723,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":26,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["107"],"elapsed_ms":2.340667,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":26,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["108"],"elapsed_ms":1.714375,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":27,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["109"],"elapsed_ms":0.858417,"headers_ms":0.826917,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":27,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["110"],"elapsed_ms":1.9775,"headers_ms":1.970542,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":27,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["111"],"elapsed_ms":1.918708,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":27,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["112"],"elapsed_ms":5.034583,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":28,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["113"],"elapsed_ms":1.434083,"headers_ms":1.429167,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":28,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["114"],"elapsed_ms":0.648833,"headers_ms":0.642291,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":28,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["115"],"elapsed_ms":1.618917,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":28,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["116"],"elapsed_ms":1.481917,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":29,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["117"],"elapsed_ms":0.648208,"headers_ms":0.645583,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":29,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["118"],"elapsed_ms":1.248542,"headers_ms":1.244542,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":29,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["119"],"elapsed_ms":2.32925,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":29,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["120"],"elapsed_ms":1.582209,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h3","pair":30,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["121"],"elapsed_ms":1.1775,"headers_ms":1.173792,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"cold","protocol":"h2","pair":30,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["122"],"elapsed_ms":0.608084,"headers_ms":0.605542,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h3","pair":30,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["123"],"elapsed_ms":2.371541,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"unshaped","workload":"warm","protocol":"h2","pair":30,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["124"],"elapsed_ms":1.750541,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":1,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["125"],"elapsed_ms":253.301086,"headers_ms":253.287336,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":1,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["126"],"elapsed_ms":179.98646,"headers_ms":179.959752,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":1,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["127"],"elapsed_ms":478.348547,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":1,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["128"],"elapsed_ms":366.514879,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":2,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["129"],"elapsed_ms":171.139001,"headers_ms":171.121877,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":2,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["130"],"elapsed_ms":262.481961,"headers_ms":262.46146,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":2,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["131"],"elapsed_ms":365.315212,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":2,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["132"],"elapsed_ms":489.225921,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":3,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["133"],"elapsed_ms":259.958752,"headers_ms":259.950502,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":3,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["134"],"elapsed_ms":167.970793,"headers_ms":167.950627,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":3,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["135"],"elapsed_ms":479.884046,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":3,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["136"],"elapsed_ms":362.801336,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":4,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["137"],"elapsed_ms":171.361418,"headers_ms":171.327209,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":4,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["138"],"elapsed_ms":259.065169,"headers_ms":259.054502,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":4,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["139"],"elapsed_ms":363.477837,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":4,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["140"],"elapsed_ms":482.472421,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":5,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["141"],"elapsed_ms":258.733627,"headers_ms":258.725002,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":5,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["142"],"elapsed_ms":167.427668,"headers_ms":167.408668,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":5,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["143"],"elapsed_ms":472.577837,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":5,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["144"],"elapsed_ms":363.101461,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":6,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["145"],"elapsed_ms":169.534877,"headers_ms":169.512043,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":6,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["146"],"elapsed_ms":250.884044,"headers_ms":249.882377,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":6,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["147"],"elapsed_ms":364.904044,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":6,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["148"],"elapsed_ms":514.165296,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":7,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["149"],"elapsed_ms":256.574919,"headers_ms":255.301252,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":7,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["150"],"elapsed_ms":170.333584,"headers_ms":170.316085,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":7,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["151"],"elapsed_ms":493.535962,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":7,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["152"],"elapsed_ms":363.547253,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":8,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["153"],"elapsed_ms":180.876751,"headers_ms":180.86096,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":8,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["154"],"elapsed_ms":252.971294,"headers_ms":251.918169,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":8,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["155"],"elapsed_ms":363.710169,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":8,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["156"],"elapsed_ms":493.763712,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":9,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["157"],"elapsed_ms":258.141585,"headers_ms":258.13246,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":9,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["158"],"elapsed_ms":177.287251,"headers_ms":177.270335,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":9,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["159"],"elapsed_ms":475.400669,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":9,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["160"],"elapsed_ms":364.675836,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":10,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["161"],"elapsed_ms":174.227835,"headers_ms":174.214918,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":10,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["162"],"elapsed_ms":260.347793,"headers_ms":259.154252,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":10,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["163"],"elapsed_ms":364.163669,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":10,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["164"],"elapsed_ms":489.792586,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":11,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["165"],"elapsed_ms":256.033835,"headers_ms":256.023377,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":11,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["166"],"elapsed_ms":174.247501,"headers_ms":174.229751,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":11,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["167"],"elapsed_ms":476.897086,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":11,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["168"],"elapsed_ms":364.225169,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":12,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["169"],"elapsed_ms":177.419459,"headers_ms":177.397543,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":12,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["170"],"elapsed_ms":259.260751,"headers_ms":259.02946,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":12,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["171"],"elapsed_ms":363.26846,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":12,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["172"],"elapsed_ms":473.921503,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":13,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["173"],"elapsed_ms":246.832334,"headers_ms":246.823209,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":13,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["174"],"elapsed_ms":183.259126,"headers_ms":183.239126,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":13,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["175"],"elapsed_ms":481.066253,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":13,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["176"],"elapsed_ms":363.931502,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":14,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["177"],"elapsed_ms":169.749917,"headers_ms":169.715626,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":14,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["178"],"elapsed_ms":262.828293,"headers_ms":262.810543,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":14,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["179"],"elapsed_ms":363.36496,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":14,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["180"],"elapsed_ms":483.395295,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":15,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["181"],"elapsed_ms":257.205459,"headers_ms":257.193209,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":15,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["182"],"elapsed_ms":175.083417,"headers_ms":175.056001,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":15,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["183"],"elapsed_ms":474.749627,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":15,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["184"],"elapsed_ms":364.219543,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":16,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["185"],"elapsed_ms":167.132959,"headers_ms":167.104751,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":16,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["186"],"elapsed_ms":266.715626,"headers_ms":266.700501,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":16,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["187"],"elapsed_ms":363.952251,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":16,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["188"],"elapsed_ms":476.650419,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":17,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["189"],"elapsed_ms":246.150876,"headers_ms":246.14146,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":17,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["190"],"elapsed_ms":172.933209,"headers_ms":172.91646,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":17,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["191"],"elapsed_ms":492.673044,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":17,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["192"],"elapsed_ms":363.784085,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":18,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["193"],"elapsed_ms":177.994251,"headers_ms":177.977751,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":18,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["194"],"elapsed_ms":261.50571,"headers_ms":261.486917,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":18,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["195"],"elapsed_ms":364.021709,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":18,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["196"],"elapsed_ms":482.525169,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":19,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["197"],"elapsed_ms":256.986001,"headers_ms":256.019376,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":19,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["198"],"elapsed_ms":173.039459,"headers_ms":173.020417,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":19,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["199"],"elapsed_ms":476.043252,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":19,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["200"],"elapsed_ms":364.048002,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":20,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["201"],"elapsed_ms":177.026626,"headers_ms":177.001168,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":20,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["202"],"elapsed_ms":259.19046,"headers_ms":259.178459,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":20,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["203"],"elapsed_ms":363.892335,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":20,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["204"],"elapsed_ms":481.326502,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":21,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["205"],"elapsed_ms":258.222292,"headers_ms":258.211209,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":21,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["206"],"elapsed_ms":174.619668,"headers_ms":174.602376,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":21,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["207"],"elapsed_ms":479.794002,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":21,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["208"],"elapsed_ms":363.645834,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":22,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["209"],"elapsed_ms":176.294417,"headers_ms":176.279042,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":22,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["210"],"elapsed_ms":256.541001,"headers_ms":256.530876,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":22,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["211"],"elapsed_ms":363.435335,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":22,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["212"],"elapsed_ms":464.475251,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":23,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["213"],"elapsed_ms":246.550668,"headers_ms":246.316084,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":23,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["214"],"elapsed_ms":175.608251,"headers_ms":175.59025,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":23,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["215"],"elapsed_ms":480.898585,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":23,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["216"],"elapsed_ms":365.910751,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":24,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["217"],"elapsed_ms":169.664834,"headers_ms":169.636167,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":24,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["218"],"elapsed_ms":258.912125,"headers_ms":257.775459,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":24,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["219"],"elapsed_ms":364.378918,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":24,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["220"],"elapsed_ms":486.683585,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":25,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["221"],"elapsed_ms":254.806251,"headers_ms":254.764043,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":25,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["222"],"elapsed_ms":179.581834,"headers_ms":179.565334,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":25,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["223"],"elapsed_ms":477.224501,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":25,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["224"],"elapsed_ms":367.255626,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":26,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["225"],"elapsed_ms":179.002209,"headers_ms":178.987375,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":26,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["226"],"elapsed_ms":264.981334,"headers_ms":264.972293,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":26,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["227"],"elapsed_ms":364.300543,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":26,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["228"],"elapsed_ms":487.229043,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":27,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["229"],"elapsed_ms":252.371667,"headers_ms":252.363083,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":27,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["230"],"elapsed_ms":178.029667,"headers_ms":178.013918,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":27,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["231"],"elapsed_ms":489.00546,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":27,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["232"],"elapsed_ms":362.541251,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":28,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["233"],"elapsed_ms":170.334793,"headers_ms":170.306834,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":28,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["234"],"elapsed_ms":255.818167,"headers_ms":254.660708,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":28,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["235"],"elapsed_ms":363.177918,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":28,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["236"],"elapsed_ms":483.636042,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":29,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["237"],"elapsed_ms":251.760084,"headers_ms":251.749209,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":29,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["238"],"elapsed_ms":182.347625,"headers_ms":182.332042,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":29,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["239"],"elapsed_ms":490.848085,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":29,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["240"],"elapsed_ms":364.101084,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h3","pair":30,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["241"],"elapsed_ms":172.716375,"headers_ms":172.699917,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"cold","protocol":"h2","pair":30,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["242"],"elapsed_ms":248.439875,"headers_ms":247.593292,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h3","pair":30,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["243"],"elapsed_ms":365.693959,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-clean","workload":"warm","protocol":"h2","pair":30,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["244"],"elapsed_ms":488.064084,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":1,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["245"],"elapsed_ms":258.166501,"headers_ms":258.025126,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":1,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["246"],"elapsed_ms":174.258375,"headers_ms":174.226833,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":1,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["247"],"elapsed_ms":714.396959,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":1,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["248"],"elapsed_ms":365.774751,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":2,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["249"],"elapsed_ms":173.921667,"headers_ms":173.904708,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":2,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["250"],"elapsed_ms":252.448333,"headers_ms":252.435083,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":2,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["251"],"elapsed_ms":884.817876,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":2,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["252"],"elapsed_ms":546.050834,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":3,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["253"],"elapsed_ms":252.725792,"headers_ms":252.647125,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":3,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["254"],"elapsed_ms":170.953459,"headers_ms":170.9335,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":3,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["255"],"elapsed_ms":477.704042,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":3,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["256"],"elapsed_ms":370.21425,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":4,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["257"],"elapsed_ms":173.136417,"headers_ms":173.118875,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":4,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["258"],"elapsed_ms":261.390376,"headers_ms":260.599458,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":4,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["259"],"elapsed_ms":376.190792,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":4,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["260"],"elapsed_ms":487.234,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":5,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["261"],"elapsed_ms":255.664167,"headers_ms":255.655792,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":5,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["262"],"elapsed_ms":399.065667,"headers_ms":399.050041,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":5,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["263"],"elapsed_ms":1611.879335,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":5,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["264"],"elapsed_ms":445.107917,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":6,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["265"],"elapsed_ms":179.763583,"headers_ms":179.747458,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":6,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["266"],"elapsed_ms":261.41775,"headers_ms":261.407667,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":6,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["267"],"elapsed_ms":489.883292,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":6,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["268"],"elapsed_ms":961.213001,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":7,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["269"],"elapsed_ms":256.389166,"headers_ms":256.381417,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":7,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["270"],"elapsed_ms":179.81975,"headers_ms":179.805333,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":7,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["271"],"elapsed_ms":677.966541,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":7,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["272"],"elapsed_ms":365.276666,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":8,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["273"],"elapsed_ms":172.246792,"headers_ms":172.226833,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":8,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["274"],"elapsed_ms":252.092376,"headers_ms":252.079333,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":8,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["275"],"elapsed_ms":426.85575,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":8,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["276"],"elapsed_ms":563.705542,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":9,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["277"],"elapsed_ms":367.470292,"headers_ms":367.460458,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":9,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["278"],"elapsed_ms":168.489917,"headers_ms":168.46375,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":9,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["279"],"elapsed_ms":1960.600958,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":9,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["280"],"elapsed_ms":395.873333,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":10,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["281"],"elapsed_ms":170.4965,"headers_ms":170.474583,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":10,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["282"],"elapsed_ms":257.191958,"headers_ms":257.18275,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":10,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["283"],"elapsed_ms":401.358833,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":10,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["284"],"elapsed_ms":482.46725,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":11,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["285"],"elapsed_ms":255.25125,"headers_ms":254.048125,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":11,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["286"],"elapsed_ms":173.74325,"headers_ms":173.71625,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":11,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["287"],"elapsed_ms":525.595458,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":11,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["288"],"elapsed_ms":365.918583,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":12,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["289"],"elapsed_ms":175.464458,"headers_ms":175.403041,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":12,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["290"],"elapsed_ms":336.240875,"headers_ms":336.231792,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":12,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["291"],"elapsed_ms":590.133083,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":12,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["292"],"elapsed_ms":975.116874,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":13,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["293"],"elapsed_ms":253.24575,"headers_ms":253.232208,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":13,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["294"],"elapsed_ms":175.546083,"headers_ms":175.529791,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":13,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["295"],"elapsed_ms":603.125625,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":13,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["296"],"elapsed_ms":377.216083,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":14,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["297"],"elapsed_ms":170.09975,"headers_ms":170.085208,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":14,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["298"],"elapsed_ms":269.462208,"headers_ms":269.451499,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":14,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["299"],"elapsed_ms":432.230916,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":14,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["300"],"elapsed_ms":490.639333,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":15,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["301"],"elapsed_ms":359.332291,"headers_ms":359.324333,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":15,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["302"],"elapsed_ms":178.25125,"headers_ms":178.233375,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":15,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["303"],"elapsed_ms":536.734375,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":15,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["304"],"elapsed_ms":469.404333,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":16,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["305"],"elapsed_ms":180.430458,"headers_ms":180.412375,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":16,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["306"],"elapsed_ms":249.579124,"headers_ms":249.548791,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":16,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["307"],"elapsed_ms":457.333583,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":16,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["308"],"elapsed_ms":934.202874,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":17,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["309"],"elapsed_ms":257.462916,"headers_ms":257.454041,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":17,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["310"],"elapsed_ms":178.921708,"headers_ms":178.899874,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":17,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["311"],"elapsed_ms":472.607458,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":17,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["312"],"elapsed_ms":368.606833,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":18,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["313"],"elapsed_ms":171.627125,"headers_ms":171.61275,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":18,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["314"],"elapsed_ms":264.01375,"headers_ms":264.003458,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":18,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["315"],"elapsed_ms":530.344582,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":18,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["316"],"elapsed_ms":491.915,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":19,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["317"],"elapsed_ms":366.030416,"headers_ms":366.02275,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":19,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["318"],"elapsed_ms":173.78075,"headers_ms":173.762917,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":19,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["319"],"elapsed_ms":594.659499,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":19,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["320"],"elapsed_ms":954.265707,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":20,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["321"],"elapsed_ms":168.415958,"headers_ms":168.400125,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":20,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["322"],"elapsed_ms":261.787041,"headers_ms":261.777749,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":20,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["323"],"elapsed_ms":430.082541,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":20,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["324"],"elapsed_ms":881.012332,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":21,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["325"],"elapsed_ms":251.4355,"headers_ms":251.426875,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":21,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["326"],"elapsed_ms":421.793166,"headers_ms":421.774624,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":21,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["327"],"elapsed_ms":488.219458,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":21,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["328"],"elapsed_ms":599.643624,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":22,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["329"],"elapsed_ms":170.094958,"headers_ms":170.080709,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":22,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["330"],"elapsed_ms":263.044041,"headers_ms":263.036,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":22,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["331"],"elapsed_ms":464.801708,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":22,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["332"],"elapsed_ms":493.173957,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":23,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["333"],"elapsed_ms":255.6835,"headers_ms":255.674166,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":23,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["334"],"elapsed_ms":179.780416,"headers_ms":179.751958,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":23,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["335"],"elapsed_ms":553.154582,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":23,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["336"],"elapsed_ms":466.215999,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":24,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["337"],"elapsed_ms":441.773666,"headers_ms":441.758208,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":24,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["338"],"elapsed_ms":260.949207,"headers_ms":260.940041,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":24,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["339"],"elapsed_ms":587.473166,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":24,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["340"],"elapsed_ms":479.626457,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":25,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["341"],"elapsed_ms":256.258874,"headers_ms":256.245124,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":25,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["342"],"elapsed_ms":178.392583,"headers_ms":178.375625,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":25,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["343"],"elapsed_ms":488.790166,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":25,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["344"],"elapsed_ms":596.281124,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":26,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["345"],"elapsed_ms":169.974,"headers_ms":169.960083,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":26,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["346"],"elapsed_ms":258.352832,"headers_ms":258.344207,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":26,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["347"],"elapsed_ms":376.692291,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":26,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["348"],"elapsed_ms":540.523082,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":27,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["349"],"elapsed_ms":265.448166,"headers_ms":265.439833,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":27,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["350"],"elapsed_ms":173.097208,"headers_ms":173.045541,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":27,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["351"],"elapsed_ms":488.154332,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":27,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["352"],"elapsed_ms":481.328499,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":28,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["353"],"elapsed_ms":179.403208,"headers_ms":179.38525,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":28,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["354"],"elapsed_ms":364.897999,"headers_ms":364.889041,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":28,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["355"],"elapsed_ms":597.102124,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":28,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["356"],"elapsed_ms":1053.803164,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":29,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["357"],"elapsed_ms":368.619624,"headers_ms":367.499749,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":29,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["358"],"elapsed_ms":173.821167,"headers_ms":173.802458,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":29,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["359"],"elapsed_ms":477.048833,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":29,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["360"],"elapsed_ms":383.422166,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h3","pair":30,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["361"],"elapsed_ms":168.7715,"headers_ms":168.755333,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"cold","protocol":"h2","pair":30,"requests":1,"succeeded":1,"bytes":1024,"connections":1,"connection_ids":["362"],"elapsed_ms":255.270957,"headers_ms":254.145291,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h3","pair":30,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["363"],"elapsed_ms":364.292832,"headers_ms":0,"negotiated_alpn":"h3","negotiated_http":"HTTP/3.0","tls_version":"TLS 1.3","warmup_failed":false} +{"profile":"mobile-loss","workload":"warm","protocol":"h2","pair":30,"requests":16,"succeeded":16,"bytes":262144,"connections":1,"connection_ids":["364"],"elapsed_ms":1008.626372,"headers_ms":0,"negotiated_alpn":"h2","negotiated_http":"HTTP/2.0","tls_version":"TLS 1.3","warmup_failed":false} diff --git a/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/run.sh b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/run.sh new file mode 100644 index 00000000..01b769a8 --- /dev/null +++ b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/run.sh @@ -0,0 +1,184 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd -- "$(dirname -- "${BASH_SOURCE[0]}")" +samples="${SAMPLES:-30}" +if ! [[ "$samples" =~ ^[0-9]+$ ]] || ((10#$samples < 1 || 10#$samples > 100)); then + printf 'SAMPLES must be an integer between 1 and 100.\n' >&2 + exit 2 +fi +samples=$((10#$samples)) +run_id="$(date -u +%Y%m%d-%H%M%S)-$$" +prefix="http3-control-$run_id" +out="evidence/$run_id" +mkdir -p "$out" + +image="$prefix:local" +client="$prefix-client" +server="$prefix-server" +router="$prefix-router" +client_net="$prefix-client-net" +server_net="$prefix-server-net" +certs="$prefix-certs" +containers=() +networks=() +volumes=() +image_created=false + +cleanup() { + status=$? + trap - EXIT + # Bash 3 treats an empty array as unset when nounset is enabled. + if ((${#containers[@]})); then + for name in "${containers[@]}"; do + if ! docker rm --force "$name" >/dev/null; then + printf 'Cleanup failed for container %s\n' "$name" >&2 + status=2 + fi + done + fi + if ((${#networks[@]})); then + for name in "${networks[@]}"; do + if ! docker network rm "$name" >/dev/null; then + printf 'Cleanup failed for network %s\n' "$name" >&2 + status=2 + fi + done + fi + if ((${#volumes[@]})); then + for name in "${volumes[@]}"; do + if ! docker volume rm "$name" >/dev/null; then + printf 'Cleanup failed for certificate volume %s\n' "$name" >&2 + status=2 + fi + done + fi + if [ "$image_created" = true ]; then + if ! docker image rm "$image" >/dev/null; then + printf 'Cleanup failed for image %s\n' "$image" >&2 + status=2 + fi + fi + exit "$status" +} +trap cleanup EXIT +trap 'exit 130' INT +trap 'exit 143' TERM + +docker info --format 'Docker={{.ServerVersion}} Kernel={{.KernelVersion}} Arch={{.Architecture}} CPUs={{.NCPU}} Memory={{.MemTotal}}' >"$out/environment.txt" +docker build --tag "$image" . >&2 +image_created=true +docker image inspect "$image" --format 'Image={{.Id}}' >>"$out/environment.txt" +docker network create --internal "$client_net" >/dev/null +networks+=("$client_net") +docker network create --internal "$server_net" >/dev/null +networks+=("$server_net") +docker volume create "$certs" >/dev/null +volumes+=("$certs") +docker run --rm --network none --cap-drop ALL \ + --volume "$certs:/certs" "$image" cert /certs + +docker run --detach --name "$router" --network "$client_net" \ + --cap-drop ALL --cap-add NET_ADMIN --read-only \ + --security-opt no-new-privileges --sysctl net.ipv4.ip_forward=1 \ + --entrypoint sleep "$image" 86400 >/dev/null +containers+=("$router") +docker network connect "$server_net" "$router" +docker run --detach --name "$server" --network "$server_net" \ + --cap-drop ALL --cap-add NET_ADMIN --read-only \ + --security-opt no-new-privileges \ + --volume "$certs:/certs:ro" "$image" serve /certs >/dev/null +containers+=("$server") + +container_ip() { + docker inspect --format "{{(index .NetworkSettings.Networks \"$2\").IPAddress}}" "$1" +} +server_ip="$(container_ip "$server" "$server_net")" +router_client_ip="$(container_ip "$router" "$client_net")" +router_server_ip="$(container_ip "$router" "$server_net")" +docker run --detach --name "$client" --network "$client_net" \ + --cap-drop ALL --cap-add NET_ADMIN --read-only \ + --sysctl 'net.ipv4.ping_group_range=0 0' \ + --security-opt no-new-privileges --add-host "server:$server_ip" \ + --volume "$certs:/certs:ro" --entrypoint sleep "$image" 86400 >/dev/null +containers+=("$client") +client_ip="$(container_ip "$client" "$client_net")" +docker exec "$client" ip route add "$server_ip/32" via "$router_client_ip" +docker exec "$server" ip route add "$client_ip/32" via "$router_server_ip" + +for name in "$client" "$server" "$router"; do + docker exec "$name" ethtool -K eth0 tso off gso off gro off tx-udp-segmentation off +done +docker exec "$router" ethtool -K eth1 tso off gso off gro off tx-udp-segmentation off +docker exec "$client" sysctl net.ipv4.tcp_congestion_control >>"$out/environment.txt" +docker exec "$client" sysctl net.ipv4.ping_group_range >>"$out/environment.txt" +docker exec "$router" tc -V >>"$out/environment.txt" +docker exec "$router" ethtool --version >>"$out/environment.txt" +docker exec "$client" apk list --installed >"$out/packages.txt" +docker exec "$client" ethtool -k eth0 >"$out/client-offloads.txt" +docker exec "$server" ethtool -k eth0 >"$out/server-offloads.txt" +for device in eth0 eth1; do + docker exec "$router" ethtool -k "$device" >"$out/router-$device-offloads.txt" +done + +ready=false +for attempt in {1..15}; do + if docker exec "$client" /bench bench --samples 1 --timeout 1s --profile preflight >"$out/preflight.jsonl"; then + ready=true + break + else + status=$? + if [ "$status" -ne 1 ]; then + printf 'Protocol preflight failed before measurements (exit %s).\n' "$status" >&2 + exit "$status" + fi + fi + sleep 0.2 +done +if [ "$ready" != true ]; then + docker logs "$server" >&2 + printf 'Strict HTTP/2 and HTTP/3 preflight did not succeed.\n' >&2 + exit 1 +fi + +failed=0 +for profile in unshaped mobile-clean mobile-loss; do + for device in eth0 eth1; do + case "$profile" in + unshaped) + docker exec "$router" tc qdisc replace dev "$device" root netem limit 10000 + ;; + mobile-clean) + docker exec "$router" tc qdisc replace dev "$device" root netem limit 10000 delay 40ms rate 10mbit + ;; + mobile-loss) + seed=20260915 + if [ "$device" = eth1 ]; then seed=20260916; fi + docker exec "$router" tc qdisc replace dev "$device" root netem limit 10000 delay 40ms loss random 1% rate 10mbit seed "$seed" + ;; + esac + done + docker exec "$router" tc -j -s qdisc show >"$out/$profile-qdisc-before.json" + docker exec "$client" ping -n -c 10 -i 0.2 -W 2 server >"$out/$profile-ping.txt" + printf 'Measuring %s: %s paired trials per workload.\n' "$profile" "$samples" >&2 + if docker exec "$client" /bench bench --samples "$samples" --profile "$profile" | + tee "$out/$profile.jsonl" >>"$out/all.jsonl"; then + : + else + status=$? + if [ "$status" -ne 1 ]; then + printf 'Benchmark execution failed (exit %s).\n' "$status" >&2 + exit "$status" + fi + failed=1 + fi + if [ "$(wc -l <"$out/$profile.jsonl")" -ne "$((samples * 4))" ]; then + printf 'Incomplete measurement file for %s.\n' "$profile" >&2 + exit 2 + fi + docker exec "$router" tc -j -s qdisc show >"$out/$profile-qdisc-after.json" +done +docker exec -i "$client" /bench summarize <"$out/all.jsonl" >"$out/summary.json" +docker logs "$server" >"$out/server.log" 2>&1 +printf 'Local control evidence: %s\n' "$out" +exit "$failed" diff --git a/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/sample.yml b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/sample.yml new file mode 100644 index 00000000..e2a964fb --- /dev/null +++ b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/sample.yml @@ -0,0 +1,5 @@ +title: HTTP/2 and HTTP/3 mobile-network control +description: Isolated transport measurements with Linux netem; not an AKS or AGFC benchmark. +kind: runnable +used_by: + - index diff --git a/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/server.go b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/server.go new file mode 100644 index 00000000..2c4f0eae --- /dev/null +++ b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/server.go @@ -0,0 +1,125 @@ +package main + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" + "math/big" + "net" + "net/http" + "os" + "path/filepath" + "sync/atomic" + "time" + + "github.com/quic-go/quic-go" + "github.com/quic-go/quic-go/http3" + "golang.org/x/net/http2" +) + +func makeCertificate(dir string) error { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return err + } + serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return err + } + template := &x509.Certificate{ + SerialNumber: serial, Subject: pkix.Name{CommonName: "local-benchmark-only"}, + NotBefore: time.Now().Add(-time.Minute), NotAfter: time.Now().Add(24 * time.Hour), + DNSNames: []string{"server", "localhost"}, IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + IsCA: true, BasicConstraintsValid: true, + } + certificate, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + if err != nil { + return err + } + privateKey, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + return err + } + if err := os.MkdirAll(dir, 0700); err != nil { + return err + } + if err := os.WriteFile(filepath.Join(dir, "server.key"), pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateKey}), 0600); err != nil { + return err + } + return os.WriteFile(filepath.Join(dir, "server.crt"), pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certificate}), 0644) +} + +type servers struct { + tcp net.Listener + udp net.PacketConn + h2 *http.Server + h3 *http3.Server +} + +func newServers(address, certFile, keyFile string) (*servers, error) { + certificate, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return nil, err + } + tlsConfig := &tls.Config{ + MinVersion: tls.VersionTLS13, Certificates: []tls.Certificate{certificate}, + SessionTicketsDisabled: true, + } + var nextConnection atomic.Uint64 + connectionContext := func(ctx context.Context) context.Context { + return context.WithValue(ctx, connectionKey{}, nextConnection.Add(1)) + } + h2 := &http.Server{ + Handler: http.HandlerFunc(payloadHandler), TLSConfig: tlsConfig.Clone(), + ReadHeaderTimeout: 10 * time.Second, IdleTimeout: 60 * time.Second, + ConnContext: func(ctx context.Context, _ net.Conn) context.Context { return connectionContext(ctx) }, + } + if err := http2.ConfigureServer(h2, &http2.Server{}); err != nil { + return nil, err + } + tcp, err := net.Listen("tcp", address) + if err != nil { + return nil, err + } + udp, err := net.ListenPacket("udp", tcp.Addr().String()) + if err != nil { + return nil, errors.Join(err, tcp.Close()) + } + h3 := &http3.Server{ + Handler: http.HandlerFunc(payloadHandler), TLSConfig: tlsConfig.Clone(), + QUICConfig: &quic.Config{Allow0RTT: false, MaxIdleTimeout: 60 * time.Second}, + ConnContext: func(ctx context.Context, _ *quic.Conn) context.Context { return connectionContext(ctx) }, + } + return &servers{tcp: tcp, udp: udp, h2: h2, h3: h3}, nil +} + +func (s *servers) serve(ctx context.Context) error { + done := make(chan error, 2) + go func() { done <- s.h2.ServeTLS(s.tcp, "", "") }() + go func() { done <- s.h3.Serve(s.udp) }() + var failure error + remaining := 2 + select { + case <-ctx.Done(): + case failure = <-done: + remaining-- + } + closeErrors := []error{s.h2.Close(), s.h3.Close(), s.udp.Close()} + for range remaining { + closeErrors = append(closeErrors, <-done) + } + for _, err := range closeErrors { + if err != nil && !errors.Is(err, http.ErrServerClosed) && !errors.Is(err, net.ErrClosed) { + failure = errors.Join(failure, err) + } + } + return failure +} diff --git a/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/summary.go b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/summary.go new file mode 100644 index 00000000..ae466aa0 --- /dev/null +++ b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/summary.go @@ -0,0 +1,87 @@ +package main + +import ( + "math" + "sort" +) + +type result struct { + Profile string `json:"profile"` + Workload string `json:"workload"` + Protocol string `json:"protocol"` + Pair int `json:"pair"` + Requests int `json:"requests"` + Succeeded int `json:"succeeded"` + Bytes int64 `json:"bytes"` + Connections int `json:"connections"` + ConnectionIDs []string `json:"connection_ids,omitempty"` + ElapsedMS float64 `json:"elapsed_ms"` + HeadersMS float64 `json:"headers_ms"` + NegotiatedALPN string `json:"negotiated_alpn"` + NegotiatedHTTP string `json:"negotiated_http"` + TLSVersion string `json:"tls_version"` + WarmupFailed bool `json:"warmup_failed"` + Errors []string `json:"errors,omitempty"` +} + +type summary struct { + Profile string `json:"profile"` + Workload string `json:"workload"` + Protocol string `json:"protocol"` + Trials int `json:"trials"` + Successes int `json:"successes"` + Failures int `json:"failures"` + FailedRequests int `json:"incomplete_requests"` + WarmupFailures int `json:"warmup_failures"` + P50MS *float64 `json:"p50_ms"` + P95MS *float64 `json:"p95_ms"` + HeadersP50MS *float64 `json:"headers_p50_ms"` +} + +func summarize(results []result) []summary { + groups := make(map[string][]result) + var keys []string + for _, r := range results { + key := r.Profile + "\x00" + r.Workload + "\x00" + r.Protocol + if _, exists := groups[key]; !exists { + keys = append(keys, key) + } + groups[key] = append(groups[key], r) + } + sort.Strings(keys) + summaries := make([]summary, 0, len(keys)) + for _, key := range keys { + group := groups[key] + s := summary{Profile: group[0].Profile, Workload: group[0].Workload, Protocol: group[0].Protocol} + var elapsed, headers []float64 + for _, r := range group { + s.Trials++ + s.FailedRequests += r.Requests - r.Succeeded + if r.WarmupFailed { + s.WarmupFailures++ + } + if len(r.Errors) != 0 || r.Succeeded != r.Requests || r.WarmupFailed { + s.Failures++ + continue + } + s.Successes++ + elapsed = append(elapsed, r.ElapsedMS) + if r.Requests == 1 { + headers = append(headers, r.HeadersMS) + } + } + s.P50MS, s.P95MS = percentile(elapsed, 0.5), percentile(elapsed, 0.95) + s.HeadersP50MS = percentile(headers, 0.5) + summaries = append(summaries, s) + } + return summaries +} + +func percentile(values []float64, fraction float64) *float64 { + if len(values) == 0 { + return nil + } + sort.Float64s(values) + value := values[int(math.Ceil(float64(len(values))*fraction))-1] + return &value +} diff --git a/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/transport_test.go b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/transport_test.go new file mode 100644 index 00000000..5cae7364 --- /dev/null +++ b/docs/services/azure-application-gateway-for-containers/http3-mobile-performance/samples/mobile-network-bench/transport_test.go @@ -0,0 +1,257 @@ +package main + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/json" + "io" + "net" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +func TestStrictTransportsAndSingleConnectionFanout(t *testing.T) { + dir := t.TempDir() + if err := makeCertificate(dir); err != nil { + t.Fatal(err) + } + servers, err := newServers("127.0.0.1:0", filepath.Join(dir, "server.crt"), filepath.Join(dir, "server.key")) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan error, 1) + go func() { done <- servers.serve(ctx) }() + t.Cleanup(func() { + cancel() + select { + case err := <-done: + if err != nil { + t.Error(err) + } + case <-time.After(5 * time.Second): + t.Error("server shutdown timed out") + } + }) + for _, protocol := range []string{"h2", "h3"} { + t.Run(protocol, func(t *testing.T) { + client, closeClient, err := newClient(protocol, filepath.Join(dir, "server.crt"), 3*time.Second) + if err != nil { + t.Fatal(err) + } + defer func() { + if err := closeClient(); err != nil { + t.Error(err) + } + }() + origin := "https://" + servers.tcp.Addr().String() + warmup := measureBatch(client, protocol, origin, 1, 1024, "") + if warmup.Succeeded != 1 || warmup.Connections != 1 || len(warmup.Errors) != 0 { + t.Fatalf("warmup failed: %+v", warmup) + } + result := measureBatch(client, protocol, origin, 16, 16384, warmup.ConnectionIDs[0]) + if result.Succeeded != 16 || result.Bytes != 16*16384 || result.Connections != 1 || len(result.Errors) != 0 { + t.Fatalf("fanout is not a valid single-connection measurement: %+v", result) + } + if result.NegotiatedALPN != protocol || result.TLSVersion != "TLS 1.3" { + t.Fatalf("wire protocol was not recorded: %+v", result) + } + mismatch := measureBatch(client, protocol, origin, 1, 16, "wrong-connection") + if mismatch.Succeeded != 0 || len(mismatch.Errors) == 0 { + t.Fatal("connection reuse mismatch was accepted") + } + }) + } + var output bytes.Buffer + options := benchOptions{ + origin: "https://" + servers.tcp.Addr().String(), ca: filepath.Join(dir, "server.crt"), + profile: "integration", samples: 2, timeout: 3 * time.Second, + } + if err := runBench(options, &output); err != nil { + t.Fatal(err) + } + records, err := readResults(&output) + if err != nil || len(records) != 8 { + t.Fatalf("paired runner did not emit eight valid records: %v", err) + } + for i, expected := range []string{"h2", "h3", "h2", "h3", "h3", "h2", "h3", "h2"} { + if records[i].Protocol != expected || records[i].Pair != i/4+1 { + t.Fatalf("pair order was not alternated: %+v", records[i]) + } + } +} + +func TestHTTP3DoesNotFallBackToTCP(t *testing.T) { + dir := t.TempDir() + if err := makeCertificate(dir); err != nil { + t.Fatal(err) + } + certificate, err := tls.LoadX509KeyPair(filepath.Join(dir, "server.crt"), filepath.Join(dir, "server.key")) + if err != nil { + t.Fatal(err) + } + server := httptest.NewUnstartedServer(http.HandlerFunc(payloadHandler)) + server.EnableHTTP2 = true + server.TLS = &tls.Config{MinVersion: tls.VersionTLS13, Certificates: []tls.Certificate{certificate}} + server.Config.ConnContext = func(ctx context.Context, _ net.Conn) context.Context { + return context.WithValue(ctx, connectionKey{}, uint64(1)) + } + server.StartTLS() + defer server.Close() + tcpClient, closeTCP, err := newClient("h2", filepath.Join(dir, "server.crt"), time.Second) + if err != nil { + t.Fatal(err) + } + control := measureBatch(tcpClient, "h2", server.URL, 1, 1024, "") + if err := closeTCP(); err != nil { + t.Fatal(err) + } + if control.Succeeded != 1 || len(control.Errors) != 0 { + t.Fatalf("TCP positive control is not reachable and trusted: %+v", control) + } + client, closeClient, err := newClient("h3", filepath.Join(dir, "server.crt"), 150*time.Millisecond) + if err != nil { + t.Fatal(err) + } + defer func() { + if err := closeClient(); err != nil { + t.Error(err) + } + }() + result := measureBatch(client, "h3", server.URL, 1, 1024, "") + if result.Succeeded != 0 || len(result.Errors) == 0 { + t.Fatalf("HTTP/3 unexpectedly succeeded without a UDP server: %+v", result) + } +} + +func TestInvalidBenchmarkOptions(t *testing.T) { + for _, args := range [][]string{ + {"--samples", "0"}, + {"--samples", "101"}, + {"--timeout", "0s"}, + {"--timeout", "61s"}, + {"--url", "http://server:8443"}, + {"--url", "https://user:password@server:8443"}, + {"--url", "https://server:8443/?token=test"}, + {"--url", "https://server:8443?"}, + {"--url", "https://server:8443/?"}, + {"--url", "https://server:8443#"}, + {"--url", "https://server:8443/#"}, + {"--url", "https://server:8443/%2F"}, + {"--url", "https://server:8443/not-the-benchmark"}, + {"--profile", ""}, + {"unexpected-positional-argument"}, + } { + if _, err := parseBenchOptions(args); err == nil { + t.Errorf("accepted invalid options: %q", args) + } + } + if _, err := parseBenchOptions([]string{"--samples", "1", "--profile", "smoke"}); err != nil { + t.Fatalf("valid options failed: %v", err) + } +} + +func TestBenchmarkOriginNormalization(t *testing.T) { + for _, origin := range []string{ + "https://server", + "https://server:8443", + "https://[::1]:8443", + } { + for _, input := range []string{origin, origin + "/"} { + t.Run(input, func(t *testing.T) { + options, err := parseBenchOptions([]string{"--url", input}) + if err != nil { + t.Fatal(err) + } + if options.origin != origin { + t.Fatalf("origin = %q, want %q", options.origin, origin) + } + }) + } + } +} + +func TestSummaryInputRejectsTruncationAndInvalidRecords(t *testing.T) { + for _, input := range []string{ + "", + `{"profile":`, + `{"profile":"mobile","protocol":"h3","workload":"cold","requests":1,"succeeded":2}`, + `{"profile":"mobile","protocol":"h3","workload":"cold","requests":1,"succeeded":1,"elapsed_ms":-1}`, + `{"profile":"mobile","protocol":"h3","workload":"cold","requests":1,"succeeded":0}`, + } { + if _, err := readResults(strings.NewReader(input)); err == nil { + t.Errorf("accepted invalid measurement file: %s", input) + } + } + record := result{ + Profile: "mobile", Protocol: "h3", Workload: "cold", Pair: 1, Requests: 1, Succeeded: 0, + Errors: []string{"timeout"}, + } + data, err := json.Marshal(record) + if err != nil { + t.Fatal(err) + } + results, err := readResults(strings.NewReader(string(data))) + if err != nil || len(results) != 1 { + t.Fatalf("valid failure record rejected: %v", err) + } +} + +func TestSummaryInputRequiresSuccessfulWireEvidence(t *testing.T) { + valid := result{ + Profile: "mobile", Protocol: "h3", Workload: "cold", Pair: 1, Requests: 1, Succeeded: 1, + Bytes: 1024, Connections: 1, ConnectionIDs: []string{"1"}, ElapsedMS: 2, HeadersMS: 1, + NegotiatedALPN: "h3", NegotiatedHTTP: "HTTP/3.0", TLSVersion: "TLS 1.3", + } + for name, invalidate := range map[string]func(*result){ + "fallback": func(r *result) { r.NegotiatedHTTP = "HTTP/2.0" }, + "wrong ALPN": func(r *result) { r.NegotiatedALPN = "h2" }, + "wrong TLS": func(r *result) { r.TLSVersion = "TLS 1.2" }, + "missing bytes": func(r *result) { r.Bytes = 0 }, + "zero latency": func(r *result) { r.ElapsedMS = 0 }, + "extra connections": func(r *result) { r.Connections = 2 }, + "missing connection": func(r *result) { + r.ConnectionIDs = nil + }, + "invalid pair": func(r *result) { r.Pair = 0 }, + } { + t.Run(name, func(t *testing.T) { + record := valid + invalidate(&record) + data, err := json.Marshal(record) + if err != nil { + t.Fatal(err) + } + if _, err := readResults(bytes.NewReader(data)); err == nil { + t.Fatalf("accepted an unverified success: %s", data) + } + }) + } +} + +func TestNewClientRejectsInvalidInput(t *testing.T) { + dir := t.TempDir() + ca := filepath.Join(dir, "invalid.crt") + if err := os.WriteFile(ca, []byte("not a certificate"), 0600); err != nil { + t.Fatal(err) + } + for _, protocol := range []string{"h1", "h2", "h3"} { + if _, closeClient, err := newClient(protocol, ca, time.Second); err == nil { + if closeClient != nil { + if err := closeClient(); err != nil { + t.Error(err) + } + } + t.Errorf("accepted invalid client configuration for %s", protocol) + } + } + if _, err := readResults(io.LimitReader(strings.NewReader("{}"), 0)); err == nil { + t.Fatal("accepted empty results") + } +} diff --git a/tests/docs/test_faceted_discovery.py b/tests/docs/test_faceted_discovery.py index df1565e1..937050a6 100644 --- a/tests/docs/test_faceted_discovery.py +++ b/tests/docs/test_faceted_discovery.py @@ -160,11 +160,11 @@ def test_explore_has_exact_member_data_and_one_card_per_topic(taxonomy, catalog) assert PurePosixPath("explore/index.md") in pages page = pages[PurePosixPath("explore/index.md")] assert yaml.safe_load(page.split("---", 2)[1])["title"] == "글 찾기" - assert "41개 주제 · 66개 문서" in page + assert f"{len(catalog.topics)}개 주제 · {len(catalog.documents)}개 문서" in page parser = Elements(page) - assert len(parser.select("data-explore-topic")) == len(catalog.topics) == 41 + assert len(parser.select("data-explore-topic")) == len(catalog.topics) members = parser.select("data-explore-member") - assert len(members) == len(catalog.documents) == 66 + assert len(members) == len(catalog.documents) by_path = {row["data-explore-member"]: row for row in members} for doc in catalog.documents: row = by_path[doc.relative_path.as_posix()] @@ -230,7 +230,7 @@ def test_home_navigation_and_article_chips_use_explore(taxonomy, catalog): nav = yaml.safe_load((ROOT / "docs/.nav.yml").read_text())["nav"] assert [next(iter(item)) for item in nav if "glob" not in item] == ["홈", "서비스별 보기", "글 찾기", "기여하기"] home = build_home_page((ROOT / "docs/index.md").read_text(), catalog.documents, taxonomy, catalog=catalog) - assert "41개 주제 · 66개 문서" in home + assert f"{len(catalog.topics)}개 주제 · {len(catalog.documents)}개 문서" in home assert "전체 글" not in home and "태그별 보기" not in home assert "(explore/index.md)" in home doc = catalog.documents[0] @@ -253,7 +253,7 @@ def test_explore_assets_are_configured_and_accessible(): def test_service_discovery_reports_topics_and_documents(taxonomy, catalog): pages = build_index_pages(catalog.documents, taxonomy, catalog=catalog) - assert "41개 주제 · 66개 문서" in pages[PurePosixPath("services/index.md")] + assert f"{len(catalog.topics)}개 주제 · {len(catalog.documents)}개 문서" in pages[PurePosixPath("services/index.md")] documents = [doc for doc in catalog.documents if "azure-monitor" in doc.metadata["services"]] topics = {catalog.by_document[doc.relative_path].entry.relative_path for doc in documents} assert f"{len(topics)}개 주제 · {len(documents)}개 문서" in pages[PurePosixPath("services/azure-monitor/index.md")] diff --git a/tests/docs/test_pre_pages.py b/tests/docs/test_pre_pages.py index 887ad5ad..b8f7090b 100644 --- a/tests/docs/test_pre_pages.py +++ b/tests/docs/test_pre_pages.py @@ -714,6 +714,20 @@ def test_resolution_rejects_a_missing_baseline_current_document( lineage_api("resolve_current_documents")(repo, inventory) +def test_resolution_rejects_baseline_loss_despite_an_unrelated_replacement( + document_history: tuple[Path, PrePagesInventory], +) -> None: + repo, inventory = document_history + (repo / "docs/services/service/topic-00/index.md").unlink() + extra = repo / "docs/services/service/unrelated-topic/index.md" + extra.parent.mkdir(parents=True) + extra.write_text("---\ntitle: Unrelated topic\n---\n# Unrelated topic\n") + catalog = build_topic_catalog(repo / "docs", load_taxonomy(repo / "docs-taxonomy.yml")) + assert len(catalog.documents) == len(inventory.documents) == 62 + with pytest.raises(AuditFormatError, match="guides/service/topic-00/index.md"): + lineage_api("resolve_current_documents")(repo, inventory) + + @pytest.mark.parametrize("extra_count", [1, 4, 7]) def test_resolution_allows_extra_current_documents_without_expanding_the_baseline( document_history: tuple[Path, PrePagesInventory], extra_count: int, diff --git a/tests/docs/test_topics.py b/tests/docs/test_topics.py index 45e71e1f..a12c7c5b 100644 --- a/tests/docs/test_topics.py +++ b/tests/docs/test_topics.py @@ -1065,8 +1065,14 @@ def test_repository_uses_only_canonical_topic_packages() -> None: ROOT / "docs", load_taxonomy(ROOT / "docs-taxonomy.yml") ) - assert len(catalog.documents) == 66 - assert len(catalog.topics) == 41 + canonical_paths = { + PurePosixPath(path.relative_to(ROOT / "docs").as_posix()) + for path in (ROOT / "docs" / "services").rglob("index.md") + if "samples" not in path.relative_to(ROOT / "docs").parts + } + assert {document.relative_path for document in catalog.documents} == canonical_paths + assert len(catalog.documents) == len(canonical_paths) + assert set(catalog.topics) == {path.parts[1:3] for path in canonical_paths} assert not any( (ROOT / "docs" / name).exists() for name in ("cases", "guides", "labs", "research")