Skip to content

Commit e8459fb

Browse files
committed
Introduce HttpLoadRunner and MetadataDefinitionResolver classes
- Add `HttpLoadRunner` class to perform HTTP load testing with concurrency support. - Implement core methods for initializing state, handling requests, and calculating percentiles. - Introduce `MetadataDefinitionResolver` to streamline metadata extraction and resolution. - Refactor `MetadataEngine` to use new classes for handling metadata and cache entry reading. - Enhance test coverage in `RuntimeMatrixRunnerTest` for `HttpLoadRunner` integration.
1 parent b4e1266 commit e8459fb

8 files changed

Lines changed: 769 additions & 438 deletions

File tree

scripts/benchmark/run_http_profile.php

Lines changed: 27 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
declare(strict_types=1);
44

5+
use PSFS\base\benchmark\HttpLoadRunner;
6+
7+
require dirname(__DIR__, 2) . '/vendor/autoload.php';
8+
59
$opts = getopt('', [
610
'runtime::',
711
'base-url::',
@@ -28,32 +32,47 @@
2832
$maxErrorRate = max(0.0, (float)($opts['max-error-rate'] ?? 0.0));
2933

3034
$profiles = [
31-
['name' => 'L1', 'concurrency' => max(1, (int)($opts['concurrency-l1'] ?? ($quick ? 1 : 1))), 'requests' => max(1, (int)($opts['requests-l1'] ?? ($quick ? 200 : 2000)))],
32-
['name' => 'L2', 'concurrency' => max(1, (int)($opts['concurrency-l2'] ?? ($quick ? 10 : 20))), 'requests' => max(1, (int)($opts['requests-l2'] ?? ($quick ? 500 : 4000)))],
33-
['name' => 'L3', 'concurrency' => max(1, (int)($opts['concurrency-l3'] ?? ($quick ? 25 : 100))), 'requests' => max(1, (int)($opts['requests-l3'] ?? ($quick ? 800 : 6000)))],
35+
[
36+
'name' => 'L1',
37+
'concurrency' => max(1, (int)($opts['concurrency-l1'] ?? 1)),
38+
'requests' => max(1, (int)($opts['requests-l1'] ?? ($quick ? 200 : 2000))),
39+
],
40+
[
41+
'name' => 'L2',
42+
'concurrency' => max(1, (int)($opts['concurrency-l2'] ?? ($quick ? 10 : 20))),
43+
'requests' => max(1, (int)($opts['requests-l2'] ?? ($quick ? 500 : 4000))),
44+
],
45+
[
46+
'name' => 'L3',
47+
'concurrency' => max(1, (int)($opts['concurrency-l3'] ?? ($quick ? 25 : 100))),
48+
'requests' => max(1, (int)($opts['requests-l3'] ?? ($quick ? 800 : 6000))),
49+
],
3450
];
3551

3652
$pingUrl = $baseUrl . '/_bench/ping';
3753
$metadataUrl = $baseUrl . '/_bench/metadata';
54+
$loadRunner = new HttpLoadRunner($timeout);
3855

3956
waitForHealthy($pingUrl, 45);
40-
runLoad($metadataUrl, min(20, $profiles[1]['concurrency']), $warmupRequests, 'warmup', $scenario, $timeout);
57+
$loadRunner->run($metadataUrl, min(20, $profiles[1]['concurrency']), $warmupRequests, 'warmup', $scenario);
4158

4259
$results = [];
4360
foreach ($profiles as $profile) {
44-
$results[] = runLoad(
61+
$results[] = $loadRunner->run(
4562
$metadataUrl,
4663
$profile['concurrency'],
4764
$profile['requests'],
4865
$profile['name'],
49-
$scenario,
50-
$timeout
66+
$scenario
5167
);
5268
}
5369

5470
foreach ($results as $result) {
5571
if (($result['error_rate'] ?? 1.0) > $maxErrorRate) {
56-
fwrite(STDERR, sprintf('[run_http_profile] error_rate > %.6f for %s', $maxErrorRate, (string)$result['profile']) . PHP_EOL);
72+
fwrite(
73+
STDERR,
74+
sprintf('[run_http_profile] error_rate > %.6f for %s', $maxErrorRate, (string)$result['profile']) . PHP_EOL
75+
);
5776
echo json_encode([
5877
'ok' => false,
5978
'runtime' => $runtime,
@@ -91,123 +110,3 @@ function waitForHealthy(string $url, int $timeoutSeconds): void
91110
}
92111
throw new RuntimeException('Health timeout for ' . $url);
93112
}
94-
95-
/**
96-
* @return array<string, mixed>
97-
*/
98-
function runLoad(string $url, int $concurrency, int $requests, string $profile, string $scenario, int $timeout): array
99-
{
100-
$concurrency = max(1, $concurrency);
101-
$requests = max(1, $requests);
102-
103-
$multi = curl_multi_init();
104-
$active = [];
105-
$latency = [];
106-
$errors = 0;
107-
$timeouts = 0;
108-
$bytes = 0;
109-
$sent = 0;
110-
$done = 0;
111-
$startedAt = microtime(true);
112-
113-
$enqueue = static function () use (&$sent, $requests, $url, $profile, $scenario, $timeout, $multi, &$active): void {
114-
if ($sent >= $requests) {
115-
return;
116-
}
117-
$requestUrl = $url . '?scenario=' . rawurlencode($scenario) . '&profile=' . rawurlencode($profile) . '&n=' . $sent;
118-
$ch = curl_init($requestUrl);
119-
curl_setopt_array($ch, [
120-
CURLOPT_RETURNTRANSFER => true,
121-
CURLOPT_HEADER => false,
122-
CURLOPT_CONNECTTIMEOUT => 3,
123-
CURLOPT_TIMEOUT => $timeout,
124-
CURLOPT_FOLLOWLOCATION => false,
125-
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
126-
]);
127-
curl_multi_add_handle($multi, $ch);
128-
$active[(int)$ch] = ['handle' => $ch, 'start' => microtime(true)];
129-
$sent++;
130-
};
131-
132-
for ($i = 0; $i < $concurrency; $i++) {
133-
$enqueue();
134-
}
135-
136-
do {
137-
do {
138-
$status = curl_multi_exec($multi, $running);
139-
} while ($status === CURLM_CALL_MULTI_PERFORM);
140-
141-
while ($info = curl_multi_info_read($multi)) {
142-
$ch = $info['handle'];
143-
$id = (int)$ch;
144-
$meta = $active[$id] ?? null;
145-
if ($meta === null) {
146-
curl_multi_remove_handle($multi, $ch);
147-
curl_close($ch);
148-
continue;
149-
}
150-
151-
$latency[] = (microtime(true) - (float)$meta['start']) * 1000;
152-
$body = (string)curl_multi_getcontent($ch);
153-
$bytes += strlen($body);
154-
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
155-
$curlError = curl_errno($ch);
156-
157-
if ($curlError !== 0) {
158-
$errors++;
159-
if ($curlError === CURLE_OPERATION_TIMEDOUT) {
160-
$timeouts++;
161-
}
162-
} elseif ($code !== 200) {
163-
$errors++;
164-
}
165-
166-
$done++;
167-
unset($active[$id]);
168-
curl_multi_remove_handle($multi, $ch);
169-
curl_close($ch);
170-
$enqueue();
171-
}
172-
173-
if ($running > 0) {
174-
curl_multi_select($multi, 0.5);
175-
}
176-
} while ($running > 0 || !empty($active));
177-
178-
curl_multi_close($multi);
179-
180-
sort($latency, SORT_NUMERIC);
181-
$elapsed = max(0.001, microtime(true) - $startedAt);
182-
$errorRate = $done > 0 ? $errors / $done : 1.0;
183-
$timeoutRate = $done > 0 ? $timeouts / $done : 1.0;
184-
185-
return [
186-
'profile' => $profile,
187-
'concurrency' => $concurrency,
188-
'requests' => $requests,
189-
'completed' => $done,
190-
'rps' => round($done / $elapsed, 2),
191-
'p50_ms' => round(percentile($latency, 50), 3),
192-
'p95_ms' => round(percentile($latency, 95), 3),
193-
'p99_ms' => round(percentile($latency, 99), 3),
194-
'error_count' => $errors,
195-
'error_rate' => round($errorRate, 6),
196-
'timeout_count' => $timeouts,
197-
'timeout_rate' => round($timeoutRate, 6),
198-
'bytes' => $bytes,
199-
'duration_s' => round($elapsed, 3),
200-
];
201-
}
202-
203-
/**
204-
* @param array<int, float> $samples
205-
*/
206-
function percentile(array $samples, int $percent): float
207-
{
208-
if ($samples === []) {
209-
return 0.0;
210-
}
211-
$index = (int)floor((count($samples) - 1) * (max(0, min(100, $percent)) / 100));
212-
return (float)$samples[$index];
213-
}
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
<?php
2+
3+
namespace PSFS\base\benchmark;
4+
5+
class HttpLoadRunner
6+
{
7+
public function __construct(private int $timeout = 10)
8+
{
9+
$this->timeout = max(2, $this->timeout);
10+
}
11+
12+
/**
13+
* @return array<string, mixed>
14+
*/
15+
public function run(string $url, int $concurrency, int $requests, string $profile, string $scenario): array
16+
{
17+
$state = $this->initialState($concurrency, $requests, $profile, $scenario);
18+
$multi = curl_multi_init();
19+
20+
$this->primeQueue($multi, $state, $url);
21+
$this->drainQueue($multi, $state, $url);
22+
curl_multi_close($multi);
23+
24+
return $this->result($state);
25+
}
26+
27+
public function percentile(array $samples, int $percent): float
28+
{
29+
if ($samples === []) {
30+
return 0.0;
31+
}
32+
$percent = max(0, min(100, $percent));
33+
$index = (int)floor((count($samples) - 1) * ($percent / 100));
34+
return (float)$samples[$index];
35+
}
36+
37+
/**
38+
* @return array<string, mixed>
39+
*/
40+
private function initialState(int $concurrency, int $requests, string $profile, string $scenario): array
41+
{
42+
return [
43+
'profile' => $profile,
44+
'scenario' => $scenario,
45+
'concurrency' => max(1, $concurrency),
46+
'requests' => max(1, $requests),
47+
'active' => [],
48+
'samples' => [],
49+
'errors' => 0,
50+
'timeouts' => 0,
51+
'bytes' => 0,
52+
'sent' => 0,
53+
'done' => 0,
54+
'started_at' => microtime(true),
55+
];
56+
}
57+
58+
/**
59+
* @param array<string, mixed> $state
60+
*/
61+
private function primeQueue(\CurlMultiHandle $multi, array &$state, string $url): void
62+
{
63+
for ($i = 0; $i < $state['concurrency']; $i++) {
64+
$this->enqueue($multi, $state, $url);
65+
}
66+
}
67+
68+
/**
69+
* @param array<string, mixed> $state
70+
*/
71+
private function drainQueue(\CurlMultiHandle $multi, array &$state, string $url): void
72+
{
73+
do {
74+
do {
75+
$status = curl_multi_exec($multi, $running);
76+
} while ($status === CURLM_CALL_MULTI_PERFORM);
77+
78+
$this->collectCompleted($multi, $state, $url);
79+
if ($running > 0) {
80+
curl_multi_select($multi, 0.5);
81+
}
82+
} while ($running > 0 || !empty($state['active']));
83+
}
84+
85+
/**
86+
* @param array<string, mixed> $state
87+
*/
88+
private function enqueue(\CurlMultiHandle $multi, array &$state, string $url): void
89+
{
90+
if ($state['sent'] >= $state['requests']) {
91+
return;
92+
}
93+
$ch = curl_init($this->requestUrl($url, $state));
94+
curl_setopt_array($ch, [
95+
CURLOPT_RETURNTRANSFER => true,
96+
CURLOPT_HEADER => false,
97+
CURLOPT_CONNECTTIMEOUT => 3,
98+
CURLOPT_TIMEOUT => $this->timeout,
99+
CURLOPT_FOLLOWLOCATION => false,
100+
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
101+
]);
102+
curl_multi_add_handle($multi, $ch);
103+
$state['active'][(int)$ch] = [
104+
'handle' => $ch,
105+
'start' => microtime(true),
106+
];
107+
$state['sent']++;
108+
}
109+
110+
/**
111+
* @param array<string, mixed> $state
112+
*/
113+
private function requestUrl(string $url, array $state): string
114+
{
115+
return $url
116+
. '?scenario=' . rawurlencode((string)$state['scenario'])
117+
. '&profile=' . rawurlencode((string)$state['profile'])
118+
. '&n=' . $state['sent'];
119+
}
120+
121+
/**
122+
* @param array<string, mixed> $state
123+
*/
124+
private function collectCompleted(\CurlMultiHandle $multi, array &$state, string $url): void
125+
{
126+
while ($info = curl_multi_info_read($multi)) {
127+
$ch = $info['handle'];
128+
$meta = $this->activeMeta($state, $ch);
129+
if ($meta === null) {
130+
$this->closeHandle($multi, $ch);
131+
continue;
132+
}
133+
134+
$this->recordHandleResult($state, $ch, $meta);
135+
unset($state['active'][(int)$ch]);
136+
$this->closeHandle($multi, $ch);
137+
$this->enqueue($multi, $state, $url);
138+
}
139+
}
140+
141+
/**
142+
* @param array<string, mixed> $state
143+
* @return array<string, mixed>|null
144+
*/
145+
private function activeMeta(array $state, \CurlHandle $ch): ?array
146+
{
147+
$meta = $state['active'][(int)$ch] ?? null;
148+
return is_array($meta) ? $meta : null;
149+
}
150+
151+
/**
152+
* @param array<string, mixed> $state
153+
* @param array<string, mixed> $meta
154+
*/
155+
private function recordHandleResult(array &$state, \CurlHandle $ch, array $meta): void
156+
{
157+
$state['samples'][] = (microtime(true) - (float)$meta['start']) * 1000;
158+
$state['bytes'] += strlen((string)curl_multi_getcontent($ch));
159+
$curlError = curl_errno($ch);
160+
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
161+
162+
if ($curlError !== 0) {
163+
$state['errors']++;
164+
$state['timeouts'] += $curlError === CURLE_OPERATION_TIMEDOUT ? 1 : 0;
165+
} elseif ($code !== 200) {
166+
$state['errors']++;
167+
}
168+
169+
$state['done']++;
170+
}
171+
172+
private function closeHandle(\CurlMultiHandle $multi, \CurlHandle $ch): void
173+
{
174+
curl_multi_remove_handle($multi, $ch);
175+
curl_close($ch);
176+
}
177+
178+
/**
179+
* @param array<string, mixed> $state
180+
* @return array<string, mixed>
181+
*/
182+
private function result(array $state): array
183+
{
184+
$samples = $state['samples'];
185+
sort($samples, SORT_NUMERIC);
186+
$elapsed = max(0.001, microtime(true) - (float)$state['started_at']);
187+
$done = (int)$state['done'];
188+
189+
return [
190+
'profile' => $state['profile'],
191+
'requests' => $state['requests'],
192+
'completed' => $done,
193+
'concurrency' => $state['concurrency'],
194+
'rps' => round($done / $elapsed, 2),
195+
'p50_ms' => round($this->percentile($samples, 50), 3),
196+
'p95_ms' => round($this->percentile($samples, 95), 3),
197+
'p99_ms' => round($this->percentile($samples, 99), 3),
198+
'error_count' => $state['errors'],
199+
'error_rate' => round($done > 0 ? $state['errors'] / $done : 1.0, 6),
200+
'timeout_count' => $state['timeouts'],
201+
'timeout_rate' => round($done > 0 ? $state['timeouts'] / $done : 1.0, 6),
202+
'bytes' => $state['bytes'],
203+
'duration_s' => round($elapsed, 3),
204+
];
205+
}
206+
}

0 commit comments

Comments
 (0)