PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.22
ووسلام – همگام سازی ووکامرس و باسلام v1.10.22
1.10.22 1.10.21 1.10.19 1.10.20 1.10.18 1.10.17 1.10.15 1.10.14 1.10.13 1.10.12 1.10.10 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.2 1.9.1 1.9.0 1.8.8 All 55 releases
sync-basalam / tests / JobsRunnerBenchmark.php

JobsRunnerBenchmark.php in ووسلام – همگام سازی ووکامرس و باسلام 1.10.22, at tests/JobsRunnerBenchmark.php

438 lines 12.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Manual benchmark for the async jobs runner.
5 *
6 * Run from a PHP/WordPress-compatible CLI image with a MariaDB service named
7 * sync-basalam-bench-db. This file intentionally is not named *Test.php so it
8 * is not picked up by PHPUnit.
9 */
10
11 namespace SyncBasalam\Jobs {
12 defined('ABSPATH') || define('ABSPATH', '/var/www/html/');
13 require dirname(__DIR__) . '/includes/Jobs/JobType.php';
14 }
15
16 namespace SyncBasalam\Admin {
17 class Settings
18 {
19 public static function getEffectiveTasksPerMinute()
20 {
21 return 1000000000;
22 }
23 }
24 }
25
26 namespace SyncBasalam\Benchmark {
27 class JobType implements \SyncBasalam\Jobs\JobType
28 {
29 public function getType(): string
30 {
31 return 'benchmark';
32 }
33
34 public function getPriority(): int
35 {
36 return 1;
37 }
38
39 public function execute(array $payload)
40 {
41 return null;
42 }
43
44 public function canRun(): bool
45 {
46 return true;
47 }
48 }
49
50 class DiscountScheduler
51 {
52 public function process(): void {}
53 }
54
55 class HttpBlockService
56 {
57 public function SyncBasalamHttpBlock()
58 {
59 return false;
60 }
61 }
62 }
63
64 namespace {
65 defined('ABSPATH') || define('ABSPATH', '/var/www/html/');
66 define('WP_DEBUG', false);
67
68 $GLOBALS['bench_current_filter'] = '';
69 $GLOBALS['bench_doing_ajax'] = false;
70 $GLOBALS['bench_fastcgi_boundary'] = null;
71
72 function apply_filters($hook, $value)
73 {
74 if ($hook === 'query') {
75 return preg_replace('/\{[a-f0-9]{64}\}/', '%', $value);
76 }
77
78 return $value;
79 }
80
81 function add_filter($hook, $callback, $priority = 10, $acceptedArgs = 1) {}
82
83 function has_filter($hook, $callback = false)
84 {
85 return false;
86 }
87
88 function is_wp_error($value)
89 {
90 return false;
91 }
92
93 function mbstring_binary_safe_encoding() {}
94
95 function reset_mbstring_encoding() {}
96
97 function add_action($hook, $callback, $priority = 10, $acceptedArgs = 1) {}
98
99 function do_action($hook, ...$args) {}
100
101 function current_filter()
102 {
103 return $GLOBALS['bench_current_filter'];
104 }
105
106 function fastcgi_finish_request()
107 {
108 $GLOBALS['bench_fastcgi_boundary'] = hrtime(true);
109
110 return true;
111 }
112
113 function wp_doing_ajax()
114 {
115 return $GLOBALS['bench_doing_ajax'];
116 }
117
118 function wp_unslash($value)
119 {
120 return $value;
121 }
122
123 function sanitize_key($key)
124 {
125 return strtolower((string) $key);
126 }
127
128 function admin_url($path = '')
129 {
130 return 'https://bench.invalid/wp-admin/' . ltrim($path, '/');
131 }
132
133 function add_query_arg($key, $value, $url)
134 {
135 return $url . '?' . $key . '=' . rawurlencode((string) $value);
136 }
137
138 function esc_url_raw($url)
139 {
140 return $url;
141 }
142
143 function wp_create_nonce($action)
144 {
145 return 'benchmark-nonce';
146 }
147
148 function wp_remote_post($url, $args = [])
149 {
150 return ['response' => ['code' => 200]];
151 }
152
153 function get_option($name, $default = false)
154 {
155 global $wpdb;
156
157 $value = $wpdb->get_var($wpdb->prepare(
158 "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1",
159 $name
160 ));
161
162 if ($value === null) {
163 return $default;
164 }
165
166 $decoded = @unserialize($value);
167
168 return $decoded === false && $value !== 'b:0;' ? $value : $decoded;
169 }
170
171 function update_option($name, $value, $autoload = null)
172 {
173 global $wpdb;
174
175 $encoded = serialize($value);
176 $exists = $wpdb->get_var($wpdb->prepare(
177 "SELECT option_id FROM {$wpdb->options} WHERE option_name = %s LIMIT 1",
178 $name
179 ));
180
181 if ($exists === null) {
182 return $wpdb->insert(
183 $wpdb->options,
184 [
185 'option_name' => $name,
186 'option_value' => $encoded,
187 'autoload' => $autoload === false ? 'no' : 'yes',
188 ]
189 ) !== false;
190 }
191
192 return $wpdb->update(
193 $wpdb->options,
194 ['option_value' => $encoded],
195 ['option_name' => $name]
196 ) !== false;
197 }
198
199 function delete_option($name)
200 {
201 global $wpdb;
202
203 return $wpdb->delete($wpdb->options, ['option_name' => $name]) !== false;
204 }
205
206 function get_transient($name)
207 {
208 $timeout = get_option('_transient_timeout_' . $name, false);
209
210 if ($timeout !== false && (int) $timeout < time()) {
211 delete_option('_transient_' . $name);
212 delete_option('_transient_timeout_' . $name);
213
214 return false;
215 }
216
217 return get_option('_transient_' . $name, false);
218 }
219
220 function set_transient($name, $value, $expiration = 0)
221 {
222 update_option('_transient_' . $name, $value, false);
223
224 if ($expiration > 0) {
225 update_option('_transient_timeout_' . $name, time() + (int) $expiration, false);
226 }
227
228 return true;
229 }
230
231 require '/var/www/html/wp-includes/class-wpdb.php';
232 require dirname(__DIR__) . '/JobManager.php';
233 require dirname(__DIR__) . '/includes/Services/Api/CircuitBreaker.php';
234 require dirname(__DIR__) . '/includes/Jobs/JobResult.php';
235 require dirname(__DIR__) . '/includes/Jobs/Exceptions/JobException.php';
236 require dirname(__DIR__) . '/includes/Jobs/Exceptions/RetryableException.php';
237 require dirname(__DIR__) . '/includes/Jobs/Exceptions/NonRetryableException.php';
238 require dirname(__DIR__) . '/includes/Jobs/LockManager.php';
239 require dirname(__DIR__) . '/includes/Jobs/JobRegistry.php';
240 require dirname(__DIR__) . '/includes/Jobs/JobExecutor.php';
241 require dirname(__DIR__) . '/JobsRunner.php';
242
243 $wpdb = new \wpdb('bench', 'bench', 'bench', 'sync-basalam-bench-db');
244 $wpdb->prefix = 'wp_';
245 $wpdb->options = 'wp_options';
246 $wpdb->query('CREATE TABLE IF NOT EXISTS wp_options (option_id bigint unsigned NOT NULL AUTO_INCREMENT, option_name varchar(191) NOT NULL, option_value longtext NOT NULL, autoload varchar(20) NOT NULL DEFAULT \'yes\', PRIMARY KEY (option_id), UNIQUE KEY option_name (option_name)) ENGINE=InnoDB');
247 $wpdb->query('CREATE TABLE IF NOT EXISTS wp_sync_basalam_job_manager (id bigint unsigned NOT NULL AUTO_INCREMENT, job_type varchar(191) NOT NULL, status varchar(30) NOT NULL, payload longtext NULL, attempts int NOT NULL DEFAULT 0, max_attempts int NOT NULL DEFAULT 3, retry_after int NULL, started_at int NULL, created_at int NOT NULL, failed_at int NULL, error_message longtext NULL, PRIMARY KEY (id), KEY job_type_status (job_type, status)) ENGINE=InnoDB');
248
249 $jobManager = new \SyncBasalam\JobManager();
250 $jobType = new \SyncBasalam\Benchmark\JobType();
251 $registry = new \SyncBasalam\Jobs\JobRegistry([$jobType]);
252 $executor = new \SyncBasalam\Jobs\JobExecutor(
253 $jobManager,
254 new \SyncBasalam\Jobs\LockManager(),
255 $registry
256 );
257 $runner = new \SyncBasalam\JobsRunner(
258 $jobManager,
259 $executor,
260 new \SyncBasalam\Benchmark\DiscountScheduler(),
261 new \SyncBasalam\Benchmark\HttpBlockService()
262 );
263 $runBatch = new \ReflectionMethod($runner, 'runAsyncBatch');
264 $runBatch->setAccessible(true);
265
266 function seed_jobs(int $count): void
267 {
268 global $wpdb;
269
270 $wpdb->query('DELETE FROM wp_sync_basalam_job_manager');
271 $wpdb->query("DELETE FROM wp_options WHERE option_name IN ('sync_basalam_jobs_runner_last_run', '_transient_sync_basalam_jobs_runner_async_dispatch_lock', '_transient_timeout_sync_basalam_jobs_runner_async_dispatch_lock')");
272
273 for ($i = 0; $i < $count; $i++) {
274 $wpdb->insert(
275 'wp_sync_basalam_job_manager',
276 [
277 'job_type' => 'benchmark',
278 'status' => 'pending',
279 'payload' => '{}',
280 'attempts' => 0,
281 'max_attempts' => 3,
282 'created_at' => time(),
283 ]
284 );
285 }
286 }
287
288 function reset_dispatch_state(): void
289 {
290 delete_option('_transient_sync_basalam_jobs_runner_async_dispatch_lock');
291 delete_option('_transient_timeout_sync_basalam_jobs_runner_async_dispatch_lock');
292 }
293
294 function leave_unbuffered_result_open(): void
295 {
296 global $wpdb;
297
298 $GLOBALS['bench_unbuffered_result'] = mysqli_query(
299 $wpdb->dbh,
300 'SELECT 1 AS value UNION ALL SELECT 2 AS value',
301 MYSQLI_USE_RESULT
302 );
303
304 if (!($GLOBALS['bench_unbuffered_result'] instanceof \mysqli_result)) {
305 throw new \RuntimeException('Could not create the unbuffered benchmark result.');
306 }
307 }
308
309 function stats(string $label, int $iterations, callable $fn): void
310 {
311 for ($i = 0; $i < min(10, $iterations); $i++) {
312 $fn();
313 }
314
315 $samples = [];
316 $boundarySamples = [];
317
318 for ($i = 0; $i < $iterations; $i++) {
319 $GLOBALS['bench_fastcgi_boundary'] = null;
320 $start = hrtime(true);
321 $fn();
322 $end = hrtime(true);
323 $samples[] = ($end - $start) / 1000.0;
324
325 if ($GLOBALS['bench_fastcgi_boundary'] !== null) {
326 $boundarySamples[] = ($GLOBALS['bench_fastcgi_boundary'] - $start) / 1000.0;
327 }
328 }
329
330 sort($samples);
331 sort($boundarySamples);
332
333 $sum = array_sum($samples);
334 $percentile = static function (array $values, float $p) {
335 if (!$values) {
336 return null;
337 }
338
339 return $values[(int) floor((count($values) - 1) * $p)];
340 };
341
342 $mean = $sum / $iterations;
343 $line = sprintf(
344 "%-34s n=%5d mean=%9.2f us p50=%9.2f p95=%9.2f p99=%9.2f min=%9.2f max=%9.2f ops/s=%9.1f",
345 $label,
346 $iterations,
347 $mean,
348 $percentile($samples, 0.50),
349 $percentile($samples, 0.95),
350 $percentile($samples, 0.99),
351 $samples[0],
352 $samples[count($samples) - 1],
353 1000000.0 / $mean
354 );
355
356 if ($boundarySamples) {
357 $boundary = $percentile($boundarySamples, 0.50);
358 $line .= sprintf(
359 ' response-boundary-p50=%9.2f us post-boundary-p50=%9.2f us',
360 $boundary,
361 $percentile($samples, 0.50) - $boundary
362 );
363 }
364
365 echo $line, PHP_EOL;
366 }
367
368 function run_batch(int $count, \ReflectionMethod $runBatch, \SyncBasalam\JobsRunner $runner): void
369 {
370 global $wpdb;
371
372 seed_jobs($count);
373 $processed = $runBatch->invoke($runner);
374 $remaining = (int) $wpdb->get_var('SELECT COUNT(*) FROM wp_sync_basalam_job_manager');
375
376 if ($processed !== $count || $remaining !== 0) {
377 throw new \RuntimeException("Batch verification failed: processed={$processed}, remaining={$remaining}");
378 }
379 }
380
381 echo "JOB EXECUTION (real JobExecutor + real wpdb/MariaDB)\n";
382
383 foreach ([[1, 1000], [5, 500], [20, 200], [100, 50], [250, 20]] as [$count, $iterations]) {
384 stats("async batch {$count} jobs", $iterations, function () use ($count, $runBatch, $runner) {
385 run_batch($count, $runBatch, $runner);
386 });
387 }
388
389 echo "DISPATCH PATHS (real wpdb/MariaDB)\n";
390
391 $GLOBALS['bench_doing_ajax'] = false;
392 $GLOBALS['bench_request'] = [];
393 $GLOBALS['bench_current_filter'] = '';
394 seed_jobs(0);
395
396 stats('frontend request / no shutdown work', 5000, function () use ($runner) {
397 reset_dispatch_state();
398 $runner->maybeDispatchAsyncRequest();
399 });
400
401 stats('admin request / no shutdown work', 5000, function () use ($runner) {
402 reset_dispatch_state();
403 $runner->maybeDispatchAsyncRequest();
404 });
405
406 $GLOBALS['bench_current_filter'] = 'shutdown';
407
408 stats('shutdown request / healthy connection', 2000, function () use ($runner) {
409 reset_dispatch_state();
410 $runner->maybeDispatchAsyncRequest();
411 });
412
413 stats('shutdown / pending unbuffered result', 500, function () use ($runner) {
414 reset_dispatch_state();
415 leave_unbuffered_result_open();
416 try {
417 $runner->maybeDispatchAsyncRequest();
418
419 global $wpdb;
420 if ($wpdb->get_var('SELECT 1') !== '1') {
421 throw new \RuntimeException('Database connection was not repaired after error 2014.');
422 }
423 } finally {
424 unset($GLOBALS['bench_unbuffered_result']);
425 }
426 });
427
428 $GLOBALS['bench_current_filter'] = '';
429 $GLOBALS['bench_doing_ajax'] = true;
430 $_REQUEST['action'] = 'sync_basalam_run_jobs_async';
431
432 stats('async worker self-dispatch guard', 10000, function () use ($runner) {
433 $runner->maybeDispatchAsyncRequest();
434 });
435
436 echo "DONE\n";
437 }
438