Skip to content

Commit 4f8964b

Browse files
committed
POC opcode counter
1 parent 893ffbc commit 4f8964b

8 files changed

Lines changed: 34729 additions & 22 deletions

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
3+
namespace PhpBench\Executor\Benchmark;
4+
5+
use PhpBench\Compat\SymfonyOptionsResolverCompat;
6+
use PhpBench\Executor\BenchmarkExecutorInterface;
7+
use PhpBench\Executor\Exception\ExecutionError;
8+
use PhpBench\Executor\ExecutionContext;
9+
use PhpBench\Executor\ExecutionResults;
10+
use PhpBench\Model\Result\MemoryResult;
11+
use PhpBench\Model\Result\OpcodeResult;
12+
use PhpBench\Model\Result\TimeResult;
13+
use PhpBench\Opcache\OpcodeDebugParser;
14+
use PhpBench\Registry\Config;
15+
use PhpBench\Remote\Exception\ScriptErrorException;
16+
use PhpBench\Remote\Launcher;
17+
use Symfony\Component\OptionsResolver\OptionsResolver;
18+
19+
class OpcodeExecutor implements BenchmarkExecutorInterface
20+
{
21+
public const OPTION_PHP_CONFIG = 'php_config';
22+
public const OPTION_SAFE_PARAMETERS = 'safe_parameters';
23+
24+
private const PHP_OPTION_MAX_EXECUTION_TIME = 'max_execution_time';
25+
26+
/**
27+
* @var Launcher
28+
*/
29+
private $launcher;
30+
31+
/**
32+
* @var string
33+
*/
34+
private $templatePath;
35+
36+
/**
37+
* @var OpcodeDebugParser
38+
*/
39+
private $parser;
40+
41+
public function __construct(
42+
Launcher $launcher,
43+
OpcodeDebugParser $parser
44+
)
45+
{
46+
$this->launcher = $launcher;
47+
$this->parser = $parser;
48+
}
49+
50+
public function execute(ExecutionContext $context, Config $config): ExecutionResults
51+
{
52+
$tokens = $this->createTokens($context, $config);
53+
$payload = $this->launcher->payload(
54+
__DIR__ . '/template/remote.template',
55+
$tokens,
56+
$context->getTimeout()
57+
);
58+
$payload->mergePhpConfig(array_merge(
59+
[
60+
self::PHP_OPTION_MAX_EXECUTION_TIME => 0,
61+
],
62+
[
63+
'opcache.enable_cli' => 1,
64+
'opcache.opt_debug_level' => '0x10000',
65+
],
66+
$config[self::OPTION_PHP_CONFIG] ?? [],
67+
));
68+
69+
70+
try {
71+
$result = $payload->launchResult();
72+
} catch (ScriptErrorException $error) {
73+
throw new ExecutionError($error->getMessage(), 0, $error);
74+
}
75+
76+
$data = $result->unserializeResult();
77+
file_put_contents('example', $result->stderr());
78+
79+
return ExecutionResults::fromResults(
80+
new OpcodeResult($this->parser->countOpcodes($result->stderr())),
81+
TimeResult::fromArray($data['time']),
82+
MemoryResult::fromArray($data['mem'])
83+
);
84+
}
85+
86+
/**
87+
* {@inheritdoc}
88+
*/
89+
public function configure(OptionsResolver $options): void
90+
{
91+
$options->setDefaults([
92+
self::OPTION_PHP_CONFIG => [
93+
],
94+
self::OPTION_SAFE_PARAMETERS => false,
95+
]);
96+
$options->setAllowedTypes(self::OPTION_PHP_CONFIG, ['array']);
97+
$options->setAllowedTypes(self::OPTION_SAFE_PARAMETERS, ['bool']);
98+
SymfonyOptionsResolverCompat::setInfos($options, [
99+
self::OPTION_PHP_CONFIG => 'Key value array of ini settings, e.g. ``{"max_execution_time":100}``',
100+
self::OPTION_SAFE_PARAMETERS => 'INTERNAL: Use process process-safe parameters, this option exists for backwards-compatibility and will be removed in PHPBench 2.0'
101+
]);
102+
}
103+
104+
/**
105+
* @return array<string,mixed>
106+
*/
107+
protected function createTokens(ExecutionContext $context, Config $config) : array
108+
{
109+
return [
110+
'class' => $context->getClassName(),
111+
'file' => $context->getClassPath(),
112+
'subject' => $context->getMethodName(),
113+
'revolutions' => $context->getRevolutions(),
114+
'beforeMethods' => var_export($context->getBeforeMethods(), true),
115+
'afterMethods' => var_export($context->getAfterMethods(), true),
116+
'parameters' => var_export($this->resolveParameterSet($context, $config), true),
117+
'warmup' => $context->getWarmup() ?: 0,
118+
];
119+
}
120+
121+
/**
122+
* @return array<string,mixed>
123+
*/
124+
private function resolveParameterSet(ExecutionContext $context, Config $config): array
125+
{
126+
if (isset($config[self::OPTION_SAFE_PARAMETERS]) && $config[self::OPTION_SAFE_PARAMETERS]) {
127+
return $context->getParameterSet()->toSerializedParameters();
128+
}
129+
130+
return $context->getParameterSet()->toUnserializedParameters();
131+
}
132+
}

lib/Extension/RunnerExtension.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use PhpBench\Executor\Benchmark\DebugExecutor;
2828
use PhpBench\Executor\Benchmark\LocalExecutor;
2929
use PhpBench\Executor\Benchmark\MemoryCentricMicrotimeExecutor;
30+
use PhpBench\Executor\Benchmark\OpcodeExecutor;
3031
use PhpBench\Executor\Benchmark\RemoteExecutor;
3132
use PhpBench\Executor\CompositeExecutor;
3233
use PhpBench\Executor\Method\ErrorHandlingExecutorDecorator;
@@ -37,6 +38,7 @@
3738
use PhpBench\Expression\Printer;
3839
use PhpBench\Expression\Printer\EvaluatingPrinter;
3940
use PhpBench\Json\JsonDecoder;
41+
use PhpBench\Opcache\OpcodeDebugParser;
4042
use PhpBench\Progress\Logger\BlinkenLogger;
4143
use PhpBench\Progress\Logger\DotsLogger;
4244
use PhpBench\Progress\Logger\HistogramLogger;
@@ -365,6 +367,15 @@ private function registerBenchmark(Container $container): void
365367
self::TAG_EXECUTOR => ['name' => 'debug']
366368
]);
367369

370+
$container->register(OpcodeExecutor::class, function (Container $container) {
371+
return new OpcodeExecutor(
372+
$container->get(Launcher::class),
373+
new OpcodeDebugParser()
374+
);
375+
}, [
376+
self::TAG_EXECUTOR => ['name' => 'opcode']
377+
]);
378+
368379
$container->register(Finder::class, function (Container $container) {
369380
return new Finder();
370381
});

lib/Model/Result/OpcodeResult.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace PhpBench\Model\Result;
4+
5+
use PhpBench\Model\ResultInterface;
6+
7+
final class OpcodeResult implements ResultInterface
8+
{
9+
/**
10+
* @var int
11+
*/
12+
private $count;
13+
14+
public function __construct(int $count)
15+
{
16+
$this->count = $count;
17+
}
18+
19+
/**
20+
* {@inheritDoc}
21+
*/
22+
public static function fromArray(array $values): ResultInterface
23+
{
24+
return new self($values['count']);
25+
}
26+
27+
/**
28+
* @return parameters
29+
*/
30+
public function getMetrics(): array
31+
{
32+
return [
33+
'count' => $this->count,
34+
];
35+
}
36+
37+
/**
38+
* {@inheritDoc}
39+
*/
40+
public function getKey(): string
41+
{
42+
return 'opcode';
43+
}
44+
}

lib/Opcache/OpcodeDebugParser.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace PhpBench\Opcache;
4+
5+
class OpcodeDebugParser
6+
{
7+
public function countOpcodes(string $string): int
8+
{
9+
$lines = explode("\n", $string);
10+
$count = 0;
11+
foreach ($lines as $line) {
12+
if (preg_match('{^[0-9]{4} }', $line)) {
13+
$count++;
14+
}
15+
}
16+
return $count;
17+
}
18+
}

lib/Remote/LaunchResult.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace PhpBench\Remote;
4+
5+
use Symfony\Component\Process\Process;
6+
7+
class LaunchResult
8+
{
9+
/**
10+
* @var Process
11+
*/
12+
private $process;
13+
14+
private function __construct(Process $process)
15+
{
16+
$this->process = $process;
17+
}
18+
19+
public static function fromProcess(Process $process): self
20+
{
21+
return new self($process);
22+
}
23+
24+
public function stdout(): string
25+
{
26+
return $this->process->getOutput();
27+
}
28+
29+
public function stderr(): string
30+
{
31+
return $this->process->getErrorOutput();
32+
}
33+
34+
/**
35+
* @return array<string, mixed>
36+
*/
37+
public function unserializeResult(): array
38+
{
39+
$result = @unserialize($this->stdout());
40+
41+
if (is_array($result)) {
42+
return $result;
43+
}
44+
45+
throw new \RuntimeException(sprintf(
46+
'Script did not return an array, got: %s',
47+
$this->stdout()
48+
));
49+
}
50+
}

lib/Remote/Payload.php

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ public function disableIni(): void
127127
$this->disableIni = true;
128128
}
129129

130-
public function launch(): array
130+
public function launchResult(): LaunchResult
131131
{
132132
$script = $this->readFile();
133133
$script = $this->replaceTokens($script);
@@ -147,7 +147,15 @@ public function launch(): array
147147
));
148148
}
149149

150-
return $this->decodeResults($process);
150+
return LaunchResult::fromProcess($process);
151+
}
152+
153+
/**
154+
* @return parameters
155+
*/
156+
public function launch(): array
157+
{
158+
return $this->launchResult()->unserializeResult();
151159
}
152160

153161
private function getIniString(): string
@@ -245,24 +253,4 @@ private function removeTmpFile(string $scriptPath): void
245253

246254
unlink($scriptPath);
247255
}
248-
249-
/**
250-
* @return array<string, mixed>
251-
*/
252-
private function decodeResults(Process $process): array
253-
{
254-
$output = $process->getOutput();
255-
256-
$result = @unserialize($output);
257-
258-
if (is_array($result)) {
259-
return $result;
260-
}
261-
262-
throw new \RuntimeException(sprintf(
263-
'Script "%s" did not return an array, got: %s',
264-
$this->template,
265-
$output
266-
));
267-
}
268256
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace PhpBench\Tests\Unit\Opcache;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use PhpBench\Opcache\OpcodeDebugParser;
7+
8+
class OpcodeDebugParserTest extends TestCase
9+
{
10+
public function testCountOpcodes(): void
11+
{
12+
$count = (new OpcodeDebugParser())->countOpcodes(file_get_contents(__DIR__ . '/examples/assertBench'));
13+
self::assertEquals(25203, $count);
14+
}
15+
}

0 commit comments

Comments
 (0)