Skip to content

Commit 8500c13

Browse files
committed
Add Apple login benchmark harness
1 parent ab5fc7f commit 8500c13

10 files changed

Lines changed: 798 additions & 2 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Apple Login Performance Evidence for Issue #323
2+
3+
## Measurement Contract
4+
5+
This document records before/after evidence for reducing Apple login latency by removing repeated Apple provider round trips from the returning Apple User login path.
6+
7+
Primary benchmark environment:
8+
9+
- Backend: local Spring Boot process with `bench` profile
10+
- Database: isolated Docker MySQL database `ontime_bench`
11+
- Apple provider: local stub
12+
- Returning Apple User: `social_type=APPLE`, `social_id=bench-apple-user`
13+
- Stub delay:
14+
- `GET /auth/keys`: 80 ms
15+
- `POST /auth/token`: 300 ms
16+
- Warmup: 2 minutes
17+
- Measurement: 5 minutes
18+
- Full run count: 10 per scenario
19+
20+
The benchmark harness lives in `scripts/benchmarks/apple-login/`.
21+
22+
## Acceptance Gate
23+
24+
Primary scenario: returning Apple User, warm cache, concurrency 1.
25+
26+
- JWKS network calls/request: `before 1.0 -> after 0.0`
27+
- Apple token exchange calls/request: `before 1.0 -> after 0.0`
28+
- Error rate: `0%`
29+
- p95 latency: `after <= before * 0.70`
30+
31+
Secondary scenarios: returning Apple User, concurrency 10 and 20.
32+
33+
- Error rate: `0%`
34+
- Apple token exchange calls/request: `after 0.0`
35+
- p95 latency: `after <= before`
36+
37+
## Results
38+
39+
Populate this table from `scripts/benchmarks/apple-login/results/*/summary.csv` after running both before and after measurements.
40+
41+
| scenario | version | runs | requests | error_rate | p50_ms | p95_ms | p99_ms | jwks_calls/request | token_exchange_calls/request |
42+
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
43+
| c1 returning warm-cache | before | TBD | TBD | TBD | TBD | TBD | TBD | TBD | TBD |
44+
| c1 returning warm-cache | after | TBD | TBD | TBD | TBD | TBD | TBD | TBD | TBD |
45+
| c10 returning warm-cache | before | TBD | TBD | TBD | TBD | TBD | TBD | TBD | TBD |
46+
| c10 returning warm-cache | after | TBD | TBD | TBD | TBD | TBD | TBD | TBD | TBD |
47+
| c20 returning warm-cache | before | TBD | TBD | TBD | TBD | TBD | TBD | TBD | TBD |
48+
| c20 returning warm-cache | after | TBD | TBD | TBD | TBD | TBD | TBD | TBD | TBD |
49+
50+
## Notes
51+
52+
- Real Apple network calls are intentionally excluded from the primary benchmark because provider and network variability would obscure backend request-path changes.
53+
- The script labels results as `before` or `after` but does not change git refs. Select the checkout explicitly before running.
54+
- Quick mode is for development feedback. Full mode is the acceptance evidence.

ontime-back/src/main/java/devkor/ontime_back/global/oauth/apple/AppleLoginService.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ public class AppleLoginService {
5454
private static final String APPLE_KEYS_URL = "https://appleid.apple.com/auth/keys";
5555
private static final String APPLE_TOKEN_URL = "https://appleid.apple.com/auth/token";
5656
private String issuer = "https://appleid.apple.com";
57+
@Value("${apple.keys-url:" + APPLE_KEYS_URL + "}")
58+
private String appleKeysUrl = APPLE_KEYS_URL;
59+
@Value("${apple.token-url:" + APPLE_TOKEN_URL + "}")
60+
private String appleTokenUrl = APPLE_TOKEN_URL;
5761
@Value("${apple.client.id}")
5862
private String clientId;
5963
@Value("${apple.team.id}")
@@ -167,7 +171,7 @@ public Claims verifyIdentityToken(String identityToken) throws
167171
log.info("Verify Apple identity credential");
168172
Map<String, String> headers = jwtUtils.parseHeaders(identityToken);
169173
// apple publickey
170-
ApplePublicKeyResponse applePublicKeyResponse = restTemplate.getForObject(APPLE_KEYS_URL, ApplePublicKeyResponse.class);
174+
ApplePublicKeyResponse applePublicKeyResponse = restTemplate.getForObject(appleKeysUrl, ApplePublicKeyResponse.class);
171175
PublicKey publicKey = applePublicKeyGenerator.generatePublicKey(headers, applePublicKeyResponse);
172176
// claim
173177
Claims tokenClaims = jwtUtils.getTokenClaims(identityToken, publicKey);
@@ -201,7 +205,7 @@ public AppleTokenResponseDto getAppleAccessTokenAndRefreshToken(String authCode)
201205
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(requestBody, headers);
202206

203207
ResponseEntity<JsonNode> responseEntity = restTemplate.exchange(
204-
APPLE_TOKEN_URL, HttpMethod.POST, requestEntity, JsonNode.class);
208+
appleTokenUrl, HttpMethod.POST, requestEntity, JsonNode.class);
205209

206210
JsonNode response = responseEntity.getBody();
207211

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Database Configuration
2+
spring.datasource.url=${SPRING_DATASOURCE_URL}
3+
spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
4+
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
5+
spring.datasource.driver-class-name=${SPRING_DATASOURCE_DRIVER_CLASS_NAME:com.mysql.cj.jdbc.Driver}
6+
7+
# JPA / Hibernate
8+
spring.jpa.database=mysql
9+
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
10+
spring.jpa.hibernate.ddl-auto=validate
11+
spring.jpa.show-sql=false
12+
spring.jpa.properties.hibernate.format_sql=false
13+
14+
# Flyway
15+
spring.flyway.enabled=true
16+
spring.flyway.baseline-on-migrate=false
17+
18+
# JWT Configuration
19+
jwt.secret.key=${JWT_SECRET_KEY}
20+
jwt.access.expiration=${JWT_ACCESS_EXPIRATION:3600000}
21+
jwt.refresh.expiration=${JWT_REFRESH_EXPIRATION:1209600000}
22+
jwt.access.header=${JWT_ACCESS_HEADER:Authorization}
23+
jwt.refresh.header=${JWT_REFRESH_HEADER:Authorization-refresh}
24+
25+
# OAuth
26+
google.web.client-id=${GOOGLE_WEB_CLIENT_ID:bench-google-web-client-id}
27+
google.app.client-id=${GOOGLE_APP_CLIENT_ID:bench-google-app-client-id}
28+
apple.client.id=${APPLE_CLIENT_ID}
29+
apple.team.id=${APPLE_TEAM_ID}
30+
apple.login.key=${APPLE_LOGIN_KEY}
31+
apple.client.secret=${APPLE_CLIENT_SECRET:}
32+
apple.private-key.base64=${APPLE_PRIVATE_KEY_BASE64}
33+
apple.keys-url=${APPLE_KEYS_URL:https://appleid.apple.com/auth/keys}
34+
apple.token-url=${APPLE_TOKEN_URL:https://appleid.apple.com/auth/token}
35+
36+
# Firebase
37+
firebase.credentials.base64=${FIREBASE_CREDENTIALS_BASE64:}
38+
39+
# Logging
40+
logging.level.root=WARN
41+
logging.level.devkor.ontime_back=INFO
42+
43+
# Feature flags
44+
feature.apple-login.enabled=true
45+
analytics.preference.default-enabled=${ANALYTICS_PREFERENCE_DEFAULT_ENABLED:false}
46+
47+
# Actuator
48+
management.endpoint.health.probes.enabled=true
49+
management.endpoints.web.exposure.include=health
50+
management.health.readinessstate.enabled=true
51+
management.health.livenessstate.enabled=true
52+
server.shutdown=graceful
53+
spring.lifecycle.timeout-per-shutdown-phase=30s
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Apple Login Benchmark
2+
3+
This benchmark measures issue #323: repeated Apple provider round trips on the returning Apple User login path.
4+
5+
## Scenario
6+
7+
- Endpoint: `POST /oauth2/apple/login`
8+
- Primary path: returning Apple User, warm Apple key cache
9+
- Seeded Apple subject: `bench-apple-user`
10+
- Apple provider: local stub, not the real Apple network
11+
- Stub delay:
12+
- `GET /auth/keys`: 80 ms
13+
- `POST /auth/token`: 300 ms
14+
15+
## Why the Apple provider is stubbed
16+
17+
The primary comparison should isolate backend request-path behavior. Real Apple calls include DNS, TLS, internet routing, and provider-side variability, so they are useful as smoke checks but not as the main before/after evidence.
18+
19+
## Run
20+
21+
Install `k6` first if it is not already available:
22+
23+
```bash
24+
brew install k6
25+
```
26+
27+
Run from the repository root:
28+
29+
```bash
30+
scripts/benchmarks/apple-login/run.sh before quick
31+
scripts/benchmarks/apple-login/run.sh after quick
32+
```
33+
34+
Full evidence runs:
35+
36+
```bash
37+
scripts/benchmarks/apple-login/run.sh before full
38+
scripts/benchmarks/apple-login/run.sh after full
39+
```
40+
41+
The script starts an isolated MySQL container, starts the Apple stub, starts the backend with the `bench` Spring profile, seeds the returning Apple User, runs k6, and writes results.
42+
43+
The script does not run `git checkout`. Measure the checkout you have selected, and use `before` or `after` only as the result label.
44+
45+
## Defaults
46+
47+
- Database container: `ontime-bench-mysql`
48+
- Database: `ontime_bench`
49+
- MySQL port: `127.0.0.1:3307`
50+
- Backend port: `127.0.0.1:18081`
51+
- Apple stub port: `127.0.0.1:18080`
52+
- Quick mode: 1 run per concurrency
53+
- Full mode: 10 runs per concurrency
54+
- Warmup: 2 minutes
55+
- Measurement: 5 minutes
56+
- Concurrency: 1, 10, 20
57+
58+
Override example:
59+
60+
```bash
61+
WARMUP_DURATION=10s MEASUREMENT_DURATION=30s SCENARIOS="1" RUNS=1 \
62+
scripts/benchmarks/apple-login/run.sh before quick
63+
```
64+
65+
## Results
66+
67+
Results are written under:
68+
69+
```text
70+
scripts/benchmarks/apple-login/results/<timestamp>-<label>-<mode>/
71+
```
72+
73+
Each result directory contains:
74+
75+
- `k6-*.json`: k6 measurement summary
76+
- `stub-*.json`: Apple provider hit count during measurement
77+
- `summary.csv`: joined p50/p95/p99, error rate, and provider calls/request
78+
- `backend.log`: backend logs for the run
79+
- `apple-stub.log`: Apple stub logs for the run
80+
81+
Use `docs/performance/apple-login-323.md` for the human-readable comparison table.
82+
83+
## Acceptance Gate
84+
85+
Primary scenario: returning Apple User, warm cache, concurrency 1.
86+
87+
- JWKS network calls/request: `before 1.0 -> after 0.0`
88+
- Apple token exchange calls/request: `before 1.0 -> after 0.0`
89+
- Error rate: `0%`
90+
- p95 latency: `after <= before * 0.70`
91+
92+
Secondary scenarios: returning Apple User, concurrency 10 and 20.
93+
94+
- Error rate: `0%`
95+
- Apple token exchange calls/request: `after 0.0`
96+
- p95 latency: `after <= before`
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import http from "k6/http";
2+
import { check, sleep } from "k6";
3+
4+
const backendUrl = __ENV.BACKEND_URL || "http://127.0.0.1:18081";
5+
const stubUrl = __ENV.APPLE_STUB_URL || "http://127.0.0.1:18080";
6+
const vus = Number(__ENV.K6_VUS || 1);
7+
const duration = __ENV.K6_DURATION || "5m";
8+
const resultJson = __ENV.K6_RESULT_JSON || "k6-summary.json";
9+
10+
export const options = {
11+
vus,
12+
duration,
13+
thresholds: {
14+
http_req_failed: ["rate==0"],
15+
},
16+
};
17+
18+
export function setup() {
19+
const response = http.get(`${stubUrl}/fixture/apple-login-payload`);
20+
if (response.status !== 200) {
21+
throw new Error(`fixture request failed with status ${response.status}`);
22+
}
23+
return response.json();
24+
}
25+
26+
export default function (payload) {
27+
const response = http.post(
28+
`${backendUrl}/oauth2/apple/login`,
29+
JSON.stringify(payload),
30+
{
31+
headers: {
32+
"content-type": "application/json",
33+
},
34+
tags: {
35+
endpoint: "apple-login",
36+
},
37+
},
38+
);
39+
40+
check(response, {
41+
"status is 200": (res) => res.status === 200,
42+
});
43+
44+
sleep(0.1);
45+
}
46+
47+
export function handleSummary(data) {
48+
return {
49+
[resultJson]: JSON.stringify(data, null, 2),
50+
stdout: JSON.stringify(
51+
{
52+
requests: data.metrics.http_reqs?.count || 0,
53+
failedRate: data.metrics.http_req_failed?.rate || 0,
54+
p50: data.metrics.http_req_duration?.percentiles?.["p(50)"] || null,
55+
p95: data.metrics.http_req_duration?.percentiles?.["p(95)"] || null,
56+
p99: data.metrics.http_req_duration?.percentiles?.["p(99)"] || null,
57+
},
58+
null,
59+
2,
60+
) + "\n",
61+
};
62+
}

0 commit comments

Comments
 (0)