PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.29
ووسلام – همگام سازی ووکامرس و باسلام v1.10.29
1.10.29 1.10.28 1.1.26 1.10.27 1.10.24 1.10.25 1.10.23 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 All 62 releases
sync-basalam / tests / JobsRunnerBenchmark.php

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

439 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__) . '/includes/Services/AsyncWorkerHealth.php';
242 require dirname(__DIR__) . '/JobsRunner.php';
243
244 $wpdb = new \wpdb('bench', 'bench', 'bench', 'sync-basalam-bench-db');
245 $wpdb->prefix = 'wp_';
246 $wpdb->options = 'wp_options';
247 $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');
248 $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');
249
250 $jobManager = new \SyncBasalam\JobManager();
251 $jobType = new \SyncBasalam\Benchmark\JobType();
252 $registry = new \SyncBasalam\Jobs\JobRegistry([$jobType]);
253 $executor = new \SyncBasalam\Jobs\JobExecutor(
254 $jobManager,
255 new \SyncBasalam\Jobs\LockManager(),
256 $registry
257 );
258 $runner = new \SyncBasalam\JobsRunner(
259 $jobManager,
260 $executor,
261 new \SyncBasalam\Benchmark\DiscountScheduler(),
262 new \SyncBasalam\Benchmark\HttpBlockService()
263 );
264 $runBatch = new \ReflectionMethod($runner, 'runAsyncBatch');
265 $runBatch->setAccessible(true);
266
267 function seed_jobs(int $count): void
268 {
269 global $wpdb;
270
271 $wpdb->query('DELETE FROM wp_sync_basalam_job_manager');
272 $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')");
273
274 for ($i = 0; $i < $count; $i++) {
275 $wpdb->insert(
276 'wp_sync_basalam_job_manager',
277 [
278 'job_type' => 'benchmark',
279 'status' => 'pending',
280 'payload' => '{}',
281 'attempts' => 0,
282 'max_attempts' => 3,
283 'created_at' => time(),
284 ]
285 );
286 }
287 }
288
289 function reset_dispatch_state(): void
290 {
291 delete_option('_transient_sync_basalam_jobs_runner_async_dispatch_lock');
292 delete_option('_transient_timeout_sync_basalam_jobs_runner_async_dispatch_lock');
293 }
294
295 function leave_unbuffered_result_open(): void
296 {
297 global $wpdb;
298
299 $GLOBALS['bench_unbuffered_result'] = mysqli_query(
300 $wpdb->dbh,
301 'SELECT 1 AS value UNION ALL SELECT 2 AS value',
302 MYSQLI_USE_RESULT
303 );
304
305 if (!($GLOBALS['bench_unbuffered_result'] instanceof \mysqli_result)) {
306 throw new \RuntimeException('Could not create the unbuffered benchmark result.');
307 }
308 }
309
310 function stats(string $label, int $iterations, callable $fn): void
311 {
312 for ($i = 0; $i < min(10, $iterations); $i++) {
313 $fn();
314 }
315
316 $samples = [];
317 $boundarySamples = [];
318
319 for ($i = 0; $i < $iterations; $i++) {
320 $GLOBALS['bench_fastcgi_boundary'] = null;
321 $start = hrtime(true);
322 $fn();
323 $end = hrtime(true);
324 $samples[] = ($end - $start) / 1000.0;
325
326 if ($GLOBALS['bench_fastcgi_boundary'] !== null) {
327 $boundarySamples[] = ($GLOBALS['bench_fastcgi_boundary'] - $start) / 1000.0;
328 }
329 }
330
331 sort($samples);
332 sort($boundarySamples);
333
334 $sum = array_sum($samples);
335 $percentile = static function (array $values, float $p) {
336 if (!$values) {
337 return null;
338 }
339
340 return $values[(int) floor((count($values) - 1) * $p)];
341 };
342
343 $mean = $sum / $iterations;
344 $line = sprintf(
345 "%-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",
346 $label,
347 $iterations,
348 $mean,
349 $percentile($samples, 0.50),
350 $percentile($samples, 0.95),
351 $percentile($samples, 0.99),
352 $samples[0],
353 $samples[count($samples) - 1],
354 1000000.0 / $mean
355 );
356
357 if ($boundarySamples) {
358 $boundary = $percentile($boundarySamples, 0.50);
359 $line .= sprintf(
360 ' response-boundary-p50=%9.2f us post-boundary-p50=%9.2f us',
361 $boundary,
362 $percentile($samples, 0.50) - $boundary
363 );
364 }
365
366 echo $line, PHP_EOL;
367 }
368
369 function run_batch(int $count, \ReflectionMethod $runBatch, \SyncBasalam\JobsRunner $runner): void
370 {
371 global $wpdb;
372
373 seed_jobs($count);
374 $processed = $runBatch->invoke($runner);
375 $remaining = (int) $wpdb->get_var('SELECT COUNT(*) FROM wp_sync_basalam_job_manager');
376
377 if ($processed !== $count || $remaining !== 0) {
378 throw new \RuntimeException("Batch verification failed: processed={$processed}, remaining={$remaining}");
379 }
380 }
381
382 echo "JOB EXECUTION (real JobExecutor + real wpdb/MariaDB)\n";
383
384 foreach ([[1, 1000], [5, 500], [20, 200], [100, 50], [250, 20]] as [$count, $iterations]) {
385 stats("async batch {$count} jobs", $iterations, function () use ($count, $runBatch, $runner) {
386 run_batch($count, $runBatch, $runner);
387 });
388 }
389
390 echo "DISPATCH PATHS (real wpdb/MariaDB)\n";
391
392 $GLOBALS['bench_doing_ajax'] = false;
393 $GLOBALS['bench_request'] = [];
394 $GLOBALS['bench_current_filter'] = '';
395 seed_jobs(0);
396
397 stats('frontend request / no shutdown work', 5000, function () use ($runner) {
398 reset_dispatch_state();
399 $runner->maybeDispatchAsyncRequest();
400 });
401
402 stats('admin request / no shutdown work', 5000, function () use ($runner) {
403 reset_dispatch_state();
404 $runner->maybeDispatchAsyncRequest();
405 });
406
407 $GLOBALS['bench_current_filter'] = 'shutdown';
408
409 stats('shutdown request / healthy connection', 2000, function () use ($runner) {
410 reset_dispatch_state();
411 $runner->maybeDispatchAsyncRequest();
412 });
413
414 stats('shutdown / pending unbuffered result', 500, function () use ($runner) {
415 reset_dispatch_state();
416 leave_unbuffered_result_open();
417 try {
418 $runner->maybeDispatchAsyncRequest();
419
420 global $wpdb;
421 if ($wpdb->get_var('SELECT 1') !== '1') {
422 throw new \RuntimeException('Database connection was not repaired after error 2014.');
423 }
424 } finally {
425 unset($GLOBALS['bench_unbuffered_result']);
426 }
427 });
428
429 $GLOBALS['bench_current_filter'] = '';
430 $GLOBALS['bench_doing_ajax'] = true;
431 $_REQUEST['action'] = 'sync_basalam_run_jobs_async';
432
433 stats('async worker self-dispatch guard', 10000, function () use ($runner) {
434 $runner->maybeDispatchAsyncRequest();
435 });
436
437 echo "DONE\n";
438 }
439