xref: /php-src/benchmark/benchmark.php (revision ae2abcf3)
1<?php
2
3require_once __DIR__ . '/shared.php';
4
5foreach (array("mbstring", "sockets", "mysqli", "openssl", "gmp") as $ext) {
6    if (!extension_loaded($ext)) {
7        throw new LogicException("Extension $ext is required.");
8    }
9}
10
11$storeResult = ($argv[1] ?? 'false') === 'true';
12$phpCgi = $argv[2] ?? dirname(PHP_BINARY) . '/php-cgi';
13if (!file_exists($phpCgi)) {
14    fwrite(STDERR, "php-cgi not found\n");
15    exit(1);
16}
17
18function main() {
19    global $storeResult;
20
21    $profilesDir = __DIR__ . '/profiles';
22    if (!is_dir($profilesDir)) {
23        mkdir($profilesDir, 0755, true);
24    }
25
26    $data = [];
27    if (false !== $branch = getenv('GITHUB_REF_NAME')) {
28        $data['branch'] = $branch;
29    }
30    $data['Zend/bench.php'] = runBench(false);
31    $data['Zend/bench.php JIT'] = runBench(true);
32    $data['Symfony Demo 2.2.3'] = runSymfonyDemo(false);
33    $data['Symfony Demo 2.2.3 JIT'] = runSymfonyDemo(true);
34    $data['Wordpress 6.2'] = runWordpress(false);
35    $data['Wordpress 6.2 JIT'] = runWordpress(true);
36    $result = json_encode($data, JSON_PRETTY_PRINT) . "\n";
37
38    fwrite(STDOUT, $result);
39
40    if ($storeResult) {
41        storeResult($result);
42    }
43}
44
45function storeResult(string $result) {
46    $repo = __DIR__ . '/repos/data';
47    cloneRepo($repo, 'git@github.com:php/benchmarking-data.git');
48
49    $commitHash = getPhpSrcCommitHash();
50    $dir = $repo . '/' . substr($commitHash, 0, 2) . '/' . $commitHash;
51    $summaryFile = $dir . '/summary.json';
52    if (!is_dir($dir)) {
53        mkdir($dir, 0755, true);
54    }
55    file_put_contents($summaryFile, $result);
56}
57
58function getPhpSrcCommitHash(): string {
59    $result = runCommand(['git', 'log', '--pretty=format:%H', '-n', '1'], dirname(__DIR__));
60    return $result->stdout;
61}
62
63function runBench(bool $jit): array {
64    return runValgrindPhpCgiCommand('bench', [dirname(__DIR__) . '/Zend/bench.php'], jit: $jit);
65}
66
67function runSymfonyDemo(bool $jit): array {
68    $dir = __DIR__ . '/repos/symfony-demo-2.2.3';
69    cloneRepo($dir, 'https://github.com/php/benchmarking-symfony-demo-2.2.3.git');
70    runPhpCommand([$dir . '/bin/console', 'cache:clear']);
71    runPhpCommand([$dir . '/bin/console', 'cache:warmup']);
72    return runValgrindPhpCgiCommand('symfony-demo', [$dir . '/public/index.php'], cwd: $dir, jit: $jit, warmup: 50, repeat: 50);
73}
74
75function runWordpress(bool $jit): array {
76    $dir = __DIR__ . '/repos/wordpress-6.2';
77    cloneRepo($dir, 'https://github.com/php/benchmarking-wordpress-6.2.git');
78
79    /* FIXME: It might be better to use a stable version of PHP for this command because we can't
80     * easily alter the phar file */
81    runPhpCommand([
82        '-d error_reporting=0',
83        'wp-cli.phar',
84        'core',
85        'install',
86        '--url=wordpress.local',
87        '--title="Wordpress"',
88        '--admin_user=wordpress',
89        '--admin_password=wordpress',
90        '--admin_email=benchmark@php.net',
91    ], $dir);
92
93    // Warmup
94    runPhpCommand([$dir . '/index.php'], $dir);
95    return runValgrindPhpCgiCommand('wordpress', [$dir . '/index.php'], cwd: $dir, jit: $jit, warmup: 50, repeat: 50);
96}
97
98function runPhpCommand(array $args, ?string $cwd = null): ProcessResult {
99    return runCommand([PHP_BINARY, ...$args], $cwd);
100}
101
102function runValgrindPhpCgiCommand(
103    string $name,
104    array $args,
105    ?string $cwd = null,
106    bool $jit = false,
107    int $warmup = 0,
108    int $repeat = 1,
109): array {
110    global $phpCgi;
111
112    $profileOut = __DIR__ . "/profiles/callgrind.out.$name";
113    if ($jit) {
114        $profileOut .= '.jit';
115    }
116
117    $process = runCommand([
118        'valgrind',
119        '--tool=callgrind',
120        '--dump-instr=yes',
121        "--callgrind-out-file=$profileOut",
122        '--',
123        $phpCgi,
124        '-T' . ($warmup ? $warmup . ',' : '') . $repeat,
125        '-d max_execution_time=0',
126        '-d opcache.enable=1',
127        '-d opcache.jit=' . ($jit ? 'tracing' : 'disable'),
128        '-d opcache.jit_buffer_size=128M',
129        '-d opcache.validate_timestamps=0',
130        ...$args,
131    ]);
132    $instructions = extractInstructionsFromValgrindOutput($process->stderr);
133    if ($repeat > 1) {
134        $instructions = gmp_strval(gmp_div_q($instructions, $repeat));
135    }
136    return ['instructions' => $instructions];
137}
138
139function extractInstructionsFromValgrindOutput(string $output): string {
140    preg_match("(==[0-9]+== Events    : Ir\n==[0-9]+== Collected : (?<instructions>[0-9]+))", $output, $matches);
141    return $matches['instructions'] ?? throw new \Exception('Unexpected valgrind output');
142}
143
144main();
145