From fbf6ada2e2c82956fb6d4b56c28b739c145dad3f Mon Sep 17 00:00:00 2001 From: sji Date: Sun, 5 Dec 2021 17:08:22 +0900 Subject: [PATCH 1/6] add Clocks --- src/Lib/DateTime/Clock.php | 19 ++++++++++++++ src/Lib/DateTime/FixedClock.php | 32 ++++++++++++++++++++++++ src/Lib/DateTime/OnDemandClock.php | 22 ++++++++++++++++ tests/Lib/DateTime/FixedClockTest.php | 30 ++++++++++++++++++++++ tests/Lib/DateTime/OnDemandClockTest.php | 27 ++++++++++++++++++++ 5 files changed, 130 insertions(+) create mode 100644 src/Lib/DateTime/Clock.php create mode 100644 src/Lib/DateTime/FixedClock.php create mode 100644 src/Lib/DateTime/OnDemandClock.php create mode 100644 tests/Lib/DateTime/FixedClockTest.php create mode 100644 tests/Lib/DateTime/OnDemandClockTest.php diff --git a/src/Lib/DateTime/Clock.php b/src/Lib/DateTime/Clock.php new file mode 100644 index 00000000..884ac7fe --- /dev/null +++ b/src/Lib/DateTime/Clock.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace PhpProfiler\Lib\DateTime; + +interface Clock +{ + public function now(): \DateTimeImmutable; +} diff --git a/src/Lib/DateTime/FixedClock.php b/src/Lib/DateTime/FixedClock.php new file mode 100644 index 00000000..6297a6c9 --- /dev/null +++ b/src/Lib/DateTime/FixedClock.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace PhpProfiler\Lib\DateTime; + +final class FixedClock implements Clock +{ + public function __construct( + private \DateTimeImmutable $now, + ) { + } + + public function now(): \DateTimeImmutable + { + return $this->now; + } + + public function update(\DateTimeImmutable $now) + { + $this->now = $now; + } +} diff --git a/src/Lib/DateTime/OnDemandClock.php b/src/Lib/DateTime/OnDemandClock.php new file mode 100644 index 00000000..6df5b342 --- /dev/null +++ b/src/Lib/DateTime/OnDemandClock.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace PhpProfiler\Lib\DateTime; + +final class OnDemandClock implements Clock +{ + public function now(): \DateTimeImmutable + { + return new \DateTimeImmutable(); + } +} diff --git a/tests/Lib/DateTime/FixedClockTest.php b/tests/Lib/DateTime/FixedClockTest.php new file mode 100644 index 00000000..e95c13b0 --- /dev/null +++ b/tests/Lib/DateTime/FixedClockTest.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace PhpProfiler\Lib\DateTime; + +use PHPUnit\Framework\TestCase; + +class FixedClockTest extends TestCase +{ + public function testNow() + { + $clock = new FixedClock(new \DateTimeImmutable()); + $start = $clock->now(); + $diff = $clock->now()->diff($start); + $this->assertSame(0, (int)$diff->format('%f')); + $clock->update(new \DateTimeImmutable()); + $diff = $clock->now()->diff($start); + $this->assertgreaterThan(0, (int)$diff->format('%f')); + } +} diff --git a/tests/Lib/DateTime/OnDemandClockTest.php b/tests/Lib/DateTime/OnDemandClockTest.php new file mode 100644 index 00000000..fb27bd59 --- /dev/null +++ b/tests/Lib/DateTime/OnDemandClockTest.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace PhpProfiler\Lib\DateTime; + +use PHPUnit\Framework\TestCase; + +class OnDemandClockTest extends TestCase +{ + public function testNow() + { + $clock = new OnDemandClock(); + $now = $clock->now(); + $diff = $clock->now()->diff($now); + $this->assertGreaterThan(0, (int)$diff->format('%f')); + } +} From 5b3504cfd9de339ffa13dffb48578b8925a8532f Mon Sep 17 00:00:00 2001 From: sji Date: Sun, 5 Dec 2021 17:16:26 +0900 Subject: [PATCH 2/6] add top-like mode --- src/Command/Inspector/TopLikeCommand.php | 147 +++++++++ .../Output/TopLike/FunctionEntry.php | 29 ++ src/Inspector/Output/TopLike/Outputter.php | 19 ++ src/Inspector/Output/TopLike/Stat.php | 107 +++++++ .../Output/TopLike/TopLikeFormatter.php | 42 +++ .../TopLike/TopLikeFormatterFactory.php | 43 +++ .../Output/TopLike/TopLikeOutputter.php | 106 +++++++ tests/Inspector/Output/TopLike/StatTest.php | 284 ++++++++++++++++++ .../Output/TopLike/TopLikeFormatterTest.php | 45 +++ 9 files changed, 822 insertions(+) create mode 100644 src/Command/Inspector/TopLikeCommand.php create mode 100644 src/Inspector/Output/TopLike/FunctionEntry.php create mode 100644 src/Inspector/Output/TopLike/Outputter.php create mode 100644 src/Inspector/Output/TopLike/Stat.php create mode 100644 src/Inspector/Output/TopLike/TopLikeFormatter.php create mode 100644 src/Inspector/Output/TopLike/TopLikeFormatterFactory.php create mode 100644 src/Inspector/Output/TopLike/TopLikeOutputter.php create mode 100644 tests/Inspector/Output/TopLike/StatTest.php create mode 100644 tests/Inspector/Output/TopLike/TopLikeFormatterTest.php diff --git a/src/Command/Inspector/TopLikeCommand.php b/src/Command/Inspector/TopLikeCommand.php new file mode 100644 index 00000000..99ecd536 --- /dev/null +++ b/src/Command/Inspector/TopLikeCommand.php @@ -0,0 +1,147 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace PhpProfiler\Command\Inspector; + +use Amp\Loop; +use Amp\Promise; +use PhpProfiler\Inspector\Daemon\Dispatcher\DispatchTable; +use PhpProfiler\Inspector\Daemon\Dispatcher\WorkerPool; +use PhpProfiler\Inspector\Daemon\Reader\Context\PhpReaderContextCreator; +use PhpProfiler\Inspector\Daemon\Reader\Protocol\Message\TraceMessage; +use PhpProfiler\Inspector\Daemon\Searcher\Context\PhpSearcherContextCreator; +use PhpProfiler\Inspector\Output\TopLike\TopLikeFormatter; +use PhpProfiler\Inspector\Output\TopLike\TopLikeFormatterFactory; +use PhpProfiler\Inspector\Settings\DaemonSettings\DaemonSettingsFromConsoleInput; +use PhpProfiler\Inspector\Settings\GetTraceSettings\GetTraceSettingsFromConsoleInput; +use PhpProfiler\Inspector\Settings\TargetPhpSettings\TargetPhpSettingsFromConsoleInput; +use PhpProfiler\Inspector\Settings\TraceLoopSettings\TraceLoopSettingsFromConsoleInput; +use PhpProfiler\Lib\Console\EchoBackCanceller; +use PhpProfiler\Lib\Log\Log; +use PhpProfiler\Lib\PhpProcessReader\CallTrace; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +use function Amp\call; + +final class TopLikeCommand extends Command +{ + public function __construct( + private PhpSearcherContextCreator $php_searcher_context_creator, + private PhpReaderContextCreator $php_reader_context_creator, + private DaemonSettingsFromConsoleInput $daemon_settings_from_console_input, + private GetTraceSettingsFromConsoleInput $get_trace_settings_from_console_input, + private TargetPhpSettingsFromConsoleInput $target_php_settings_from_console_input, + private TraceLoopSettingsFromConsoleInput $trace_loop_settings_from_console_input, + private TopLikeFormatterFactory $formatter_factory, + ) { + parent::__construct(); + } + + public function configure(): void + { + $this->setName('inspector:top') + ->setDescription('show an aggregated view of traces in real time in a form similar to the UNIX top command.') + ; + $this->daemon_settings_from_console_input->setOptions($this); + $this->get_trace_settings_from_console_input->setOptions($this); + $this->trace_loop_settings_from_console_input->setOptions($this); + $this->target_php_settings_from_console_input->setOptions($this); + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $get_trace_settings = $this->get_trace_settings_from_console_input->createSettings($input); + $daemon_settings = $this->daemon_settings_from_console_input->createSettings($input); + $target_php_settings = $this->target_php_settings_from_console_input->createSettings($input); + $loop_settings = $this->trace_loop_settings_from_console_input->createSettings($input); + $formatter = $this->formatter_factory->create( + $daemon_settings->target_regex, + $output + ); + + $searcher_context = $this->php_searcher_context_creator->create(); + Promise\wait($searcher_context->start()); + Promise\wait($searcher_context->sendTargetRegex($daemon_settings->target_regex)); + + $worker_pool = WorkerPool::create( + $this->php_reader_context_creator, + $daemon_settings->threads, + $target_php_settings, + $loop_settings, + $get_trace_settings + ); + + $dispatch_table = new DispatchTable( + $worker_pool, + ); + + $_echo_back_canceler = new EchoBackCanceller(); + + Loop::onReadable( + STDIN, + /** @param resource $stream */ + function (string $watcher_id, $stream) { + $key = fread($stream, 1); + if ($key === 'q') { + Loop::cancel($watcher_id); + Loop::stop(); + } + } + ); + Loop::run(function () use ($dispatch_table, $searcher_context, $worker_pool, $formatter) { + $promises = []; + $promises[] = call(function () use ($searcher_context, $dispatch_table) { + while (1) { + Log::debug('receiving pid List'); + $update_target_message = yield $searcher_context->receivePidList(); + Log::debug('update targets', [ + 'update' => $update_target_message->target_process_list->getArray(), + 'current' => $dispatch_table->worker_pool->debugDump(), + ]); + $dispatch_table->updateTargets($update_target_message->target_process_list); + Log::debug('target updated', [$dispatch_table->worker_pool->debugDump()]); + } + }); + foreach ($worker_pool->getWorkers() as $reader) { + $promises[] = call( + function () use ($reader, $dispatch_table, $formatter) { + while (1) { + $result = yield $reader->receiveTraceOrDetachWorker(); + if ($result instanceof TraceMessage) { + $this->outputTrace($formatter, $result); + } else { + Log::debug('releaseOne', [$result]); + $dispatch_table->releaseOne($result->pid); + $this->outputTrace($formatter, new TraceMessage( + new CallTrace() + )); + } + } + } + ); + } + yield $promises; + }); + + return 0; + } + + private function outputTrace( + TopLikeFormatter $formatter, + TraceMessage $message + ): void { + $formatter->format($message->trace); + } +} diff --git a/src/Inspector/Output/TopLike/FunctionEntry.php b/src/Inspector/Output/TopLike/FunctionEntry.php new file mode 100644 index 00000000..374ce90c --- /dev/null +++ b/src/Inspector/Output/TopLike/FunctionEntry.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace PhpProfiler\Inspector\Output\TopLike; + +final class FunctionEntry +{ + public function __construct( + public string $name, + public string $file, + public int $lineno, + public int $count_exclusive = 0, + public int $count_inclusive = 0, + public int $total_count_exclusive = 0, + public int $total_count_inclusive = 0, + public float $percent_exclusive = 0, + ) { + } +} diff --git a/src/Inspector/Output/TopLike/Outputter.php b/src/Inspector/Output/TopLike/Outputter.php new file mode 100644 index 00000000..c60a3887 --- /dev/null +++ b/src/Inspector/Output/TopLike/Outputter.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace PhpProfiler\Inspector\Output\TopLike; + +interface Outputter +{ + public function display(string $trace_target, Stat $stat): void; +} diff --git a/src/Inspector/Output/TopLike/Stat.php b/src/Inspector/Output/TopLike/Stat.php new file mode 100644 index 00000000..e6fddf7d --- /dev/null +++ b/src/Inspector/Output/TopLike/Stat.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace PhpProfiler\Inspector\Output\TopLike; + +use PhpProfiler\Lib\PhpProcessReader\CallFrame; +use PhpProfiler\Lib\PhpProcessReader\CallTrace; + +final class Stat +{ + /** @param array $function_entries */ + public function __construct( + public array $function_entries = [], + public int $sample_count = 0, + public int $total_count = 0, + ) { + } + + public function addTrace(CallTrace $call_trace): void + { + $this->sample_count++; + foreach ($call_trace->call_frames as $frame_number => $call_frame) { + $this->addFrame($call_frame, $frame_number === 0); + } + } + + private function addFrame(CallFrame $call_frame, bool $is_first_frame): void + { + $name = $call_frame->getFullyQualifiedFunctionName(); + if (!isset($this->function_entries[$name])) { + $this->function_entries[$name] = new FunctionEntry( + $name, + $call_frame->file_name, + $call_frame->getLineno(), + ); + } + if ($is_first_frame) { + $this->function_entries[$name]->count_exclusive++; + } + $this->function_entries[$name]->count_inclusive++; + } + + public function updateStat() + { + if (count($this->function_entries) === 0) { + return; + } + + $this->calculateEntryTotals(); + $this->sort(); + $this->updateTotalSampleCount(); + } + + + public function sort(): void + { + \uasort($this->function_entries, function (FunctionEntry $a, FunctionEntry $b) { + if ($b->count_exclusive === $a->count_exclusive) { + if ($b->count_inclusive === $a->count_inclusive) { + if ($b->total_count_exclusive === $a->total_count_exclusive) { + return $b->total_count_inclusive <=> $a->total_count_inclusive; + } + return $b->total_count_exclusive <=> $a->total_count_exclusive; + } + return $b->count_inclusive <=> $a->count_inclusive; + } + return $b->count_exclusive <=> $a->count_exclusive; + }); + } + + public function calculateEntryTotals(): void + { + foreach ($this->function_entries as $function_entry) { + $function_entry->total_count_exclusive += $function_entry->count_exclusive; + $function_entry->total_count_inclusive += $function_entry->count_inclusive; + $function_entry->percent_exclusive = + $this->sample_count < 1 + ? 0.0 + : 100.0 * $function_entry->count_exclusive / $this->sample_count + ; + } + } + + public function updateTotalSampleCount(): void + { + $this->total_count += $this->sample_count; + } + + public function clearCurrentSamples(): void + { + $this->sample_count = 0; + foreach ($this->function_entries as $function_entry) { + $function_entry->count_exclusive = 0; + $function_entry->count_inclusive = 0; + } + } +} diff --git a/src/Inspector/Output/TopLike/TopLikeFormatter.php b/src/Inspector/Output/TopLike/TopLikeFormatter.php new file mode 100644 index 00000000..b040717f --- /dev/null +++ b/src/Inspector/Output/TopLike/TopLikeFormatter.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace PhpProfiler\Inspector\Output\TopLike; + +use PhpProfiler\Lib\DateTime\Clock; +use PhpProfiler\Lib\PhpProcessReader\CallTrace; + +final class TopLikeFormatter +{ + private Stat $stat; + private \DateTimeImmutable $previous; + + public function __construct( + private string $trace_target, + private Outputter $outputter, + private Clock $clock, + ) { + $this->stat = new Stat(); + $this->previous = $this->clock->now(); + } + + public function format(CallTrace $call_trace): void + { + $this->stat->addTrace($call_trace); + $now = $this->clock->now(); + if ($now >= $this->previous->modify('+1 second')) { + $this->outputter->display($this->trace_target, $this->stat); + $this->previous = $now; + } + } +} diff --git a/src/Inspector/Output/TopLike/TopLikeFormatterFactory.php b/src/Inspector/Output/TopLike/TopLikeFormatterFactory.php new file mode 100644 index 00000000..cf09e0be --- /dev/null +++ b/src/Inspector/Output/TopLike/TopLikeFormatterFactory.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace PhpProfiler\Inspector\Output\TopLike; + +use PhpProfiler\Lib\DateTime\OnDemandClock; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Terminal; + +final class TopLikeFormatterFactory +{ + public function __construct( + private Terminal $terminal, + private OnDemandClock $clock, + ) { + } + + public function create( + string $target_regex, + OutputInterface $output + ): TopLikeFormatter { + assert($output instanceof ConsoleOutputInterface); + return new TopLikeFormatter( + $target_regex, + new TopLikeOutputter( + $output, + $this->terminal, + ), + $this->clock + ); + } +} diff --git a/src/Inspector/Output/TopLike/TopLikeOutputter.php b/src/Inspector/Output/TopLike/TopLikeOutputter.php new file mode 100644 index 00000000..466f5dd7 --- /dev/null +++ b/src/Inspector/Output/TopLike/TopLikeOutputter.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace PhpProfiler\Inspector\Output\TopLike; + +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Helper\TableCell; +use Symfony\Component\Console\Helper\TableCellStyle; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Terminal; + +final class TopLikeOutputter implements Outputter +{ + public function __construct( + private ConsoleOutputInterface $output, + private Terminal $terminal, + ) { + } + + public function display(string $trace_target, Stat $stat): void + { + $stat->updateStat(); + $this->output($trace_target, $stat); + $stat->clearCurrentSamples(); + } + + private function output(string $trace_target, Stat $stat): void + { + $this->output->write("\e[H\e[2J"); + $this->output->writeln($trace_target); + $this->output->writeln( + sprintf( + 'samp_count=%d func_count=%d total_count=%d', + $stat->sample_count, + count($stat->function_entries), + $stat->total_count + ) + ); + $this->output->writeln(''); + $count = 7; + $width = $this->terminal->getWidth(); + $height = $this->terminal->getHeight(); + $padding_name = max(0, $width - 41); + + $rows = []; + $align_right = new TableCellStyle(['align' => 'right']); + $styled = fn (int|string $content, TableCellStyle $style): TableCell => + new TableCell( + (string)$content, + ['style' => $style] + ) + ; + foreach ($stat->function_entries as $function_entry) { + $name = $function_entry->name; + $percent = number_format($function_entry->percent_exclusive, 2); + $rows[] = [ + $styled($function_entry->total_count_inclusive, $align_right), + $styled($function_entry->total_count_exclusive, $align_right), + $styled($function_entry->count_inclusive, $align_right), + $styled($function_entry->count_exclusive, $align_right), + $styled($percent, $align_right), + $name, + ]; + if (++$count > $height) { + break; + } + } + + $output = $this->output->section(); + $table = new Table($output); + $table->setColumnMaxWidth(5, max(4, $width - 41)); + $table->setHeaders([ + $styled('total_incl', $align_right), + $styled('total_excl', $align_right), + $styled('incl', $align_right), + $styled('excl', $align_right), + $styled('%', $align_right), + str_pad('name', $padding_name), + ]); + $table->setRows($rows); + $table->setStyle('compact'); + $table->getStyle()->setCellHeaderFormat('%s'); + $table->render(); + $output->overwrite( + preg_replace( + '/( *total_incl.*)/', + '$1', + preg_replace( + '/\e[[][A-Za-z0-9]*;?[0-9]*m?/', + '', + $output->getContent() + ) + ) + ); + } +} diff --git a/tests/Inspector/Output/TopLike/StatTest.php b/tests/Inspector/Output/TopLike/StatTest.php new file mode 100644 index 00000000..c79cc523 --- /dev/null +++ b/tests/Inspector/Output/TopLike/StatTest.php @@ -0,0 +1,284 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace PhpProfiler\Inspector\Output\TopLike; + +use PhpProfiler\Lib\PhpProcessReader\CallFrame; +use PhpProfiler\Lib\PhpProcessReader\CallTrace; +use PHPUnit\Framework\TestCase; + +class StatTest extends TestCase +{ + public function testAddTrace() + { + $stat = new Stat(); + $stat->addTrace( + new CallTrace( + new CallFrame( + 'ClassName1', + 'functionName1', + 'file1', + null + ) + ) + ); + $this->assertSame(1, $stat->sample_count); + $this->assertEquals( + new FunctionEntry( + name: 'ClassName1::functionName1', + file: 'file1', + lineno: -1, + count_exclusive: 1, + count_inclusive: 1, + total_count_exclusive: 0, + total_count_inclusive: 0, + percent_exclusive: 0 + ), + $stat->function_entries['ClassName1::functionName1'] + ); + $stat->addTrace( + new CallTrace( + new CallFrame( + 'ClassName2', + 'functionName2', + 'file2', + null + ), + new CallFrame( + 'ClassName1', + 'functionName1', + 'file1', + null + ) + ) + ); + $this->assertSame(2, $stat->sample_count); + $this->assertEquals( + new FunctionEntry( + name: 'ClassName1::functionName1', + file: 'file1', + lineno: -1, + count_exclusive: 1, + count_inclusive: 2, + total_count_exclusive: 0, + total_count_inclusive: 0, + percent_exclusive: 0 + ), + $stat->function_entries['ClassName1::functionName1'] + ); + $this->assertEquals( + new FunctionEntry( + name: 'ClassName2::functionName2', + file: 'file2', + lineno: -1, + count_exclusive: 1, + count_inclusive: 1, + total_count_exclusive: 0, + total_count_inclusive: 0, + percent_exclusive: 0 + ), + $stat->function_entries['ClassName2::functionName2'] + ); + $stat->addTrace( + new CallTrace( + new CallFrame( + 'ClassName1', + 'functionName1', + 'file1', + null + ), + new CallFrame( + 'ClassName2', + 'functionName2', + 'file2', + null + ), + ) + ); + $this->assertSame(3, $stat->sample_count); + $this->assertEquals( + new FunctionEntry( + name: 'ClassName1::functionName1', + file: 'file1', + lineno: -1, + count_exclusive: 2, + count_inclusive: 3, + total_count_exclusive: 0, + total_count_inclusive: 0, + percent_exclusive: 0 + ), + $stat->function_entries['ClassName1::functionName1'] + ); + $this->assertEquals( + new FunctionEntry( + name: 'ClassName2::functionName2', + file: 'file2', + lineno: -1, + count_exclusive: 1, + count_inclusive: 2, + total_count_exclusive: 0, + total_count_inclusive: 0, + percent_exclusive: 0 + ), + $stat->function_entries['ClassName2::functionName2'] + ); + } + + public function testSort() + { + $entry1 = new FunctionEntry( + name: 'ClassName1::functionName1', + file: 'file1', + lineno: -1, + count_exclusive: 1, + count_inclusive: 2, + total_count_exclusive: 0, + total_count_inclusive: 0, + percent_exclusive: 0 + ); + $entry2 = new FunctionEntry( + name: 'ClassName2::functionName2', + file: 'file2', + lineno: -1, + count_exclusive: 1, + count_inclusive: 1, + total_count_exclusive: 1, + total_count_inclusive: 1, + percent_exclusive: 0 + ); + $entry3 = new FunctionEntry( + name: 'ClassName3::functionName3', + file: 'file3', + lineno: -1, + count_exclusive: 1, + count_inclusive: 1, + total_count_exclusive: 1, + total_count_inclusive: 2, + percent_exclusive: 0 + ); + $entry4 = new FunctionEntry( + name: 'ClassName4::functionName4', + file: 'file4', + lineno: -1, + count_exclusive: 1, + count_inclusive: 1, + total_count_exclusive: 2, + total_count_inclusive: 1, + percent_exclusive: 0 + ); + $entry5 = new FunctionEntry( + name: 'ClassName5::functionName5', + file: 'file5', + lineno: -1, + count_exclusive: 2, + count_inclusive: 2, + total_count_exclusive: 2, + total_count_inclusive: 2, + percent_exclusive: 0 + ); + $stat = new Stat( + [ + 'ClassName2::functionName2' => $entry2, + 'ClassName1::functionName1' => $entry1, + 'ClassName3::functionName3' => $entry3, + 'ClassName4::functionName4' => $entry4, + 'ClassName5::functionName5' => $entry5, + ] + ); + $entries = $stat->function_entries; + $this->assertEquals( + $entry2, current($entries) + ); + $this->assertEquals( + $entry1, next($entries) + ); + $this->assertEquals( + $entry3, next($entries) + ); + $this->assertEquals( + $entry4, next($entries) + ); + $this->assertEquals( + $entry5, next($entries) + ); + $stat->sort(); + $entries = $stat->function_entries; + $this->assertEquals( + $entry5, current($entries) + ); + $this->assertEquals( + $entry1, next($entries) + ); + $this->assertEquals( + $entry4, next($entries) + ); + $this->assertEquals( + $entry3, next($entries) + ); + $this->assertEquals( + $entry2, next($entries) + ); + } + + public function testCalculateEntryTotals() + { + $stat = new Stat([ + 'ClassName1::functionName1' => new FunctionEntry( + name: 'ClassName1::functionName1', + file: 'file1', + lineno: -1, + count_exclusive: 1, + count_inclusive: 1, + total_count_exclusive: 0, + total_count_inclusive: 0, + percent_exclusive: 0 + ), + 'ClassName2::functionName2' => new FunctionEntry( + name: 'ClassName2::functionName2', + file: 'file2', + lineno: -1, + count_exclusive: 2, + count_inclusive: 3, + total_count_exclusive: 0, + total_count_inclusive: 0, + percent_exclusive: 0 + ), + ]); + $stat->sample_count = 3; + $stat->calculateEntryTotals(); + $entry1 = $stat->function_entries['ClassName1::functionName1']; + $entry2 = $stat->function_entries['ClassName2::functionName2']; + $this->assertSame(1, $entry1->total_count_exclusive); + $this->assertSame(1, $entry1->total_count_inclusive); + $this->assertSame(100 * 1 / 3, $entry1->percent_exclusive); + $this->assertSame(2, $entry2->total_count_exclusive); + $this->assertSame(3, $entry2->total_count_inclusive); + $this->assertSame(100 * 2 / 3, $entry2->percent_exclusive); + } + + public function testUpdateTotalSampleCount() + { + $stat = new Stat(); + $this->assertSame(0, $stat->total_count); + $stat->sample_count = 3; + $stat->updateTotalSampleCount(); + $this->assertSame(3, $stat->total_count); + $stat->clearCurrentSamples(); + $stat->updateTotalSampleCount(); + $this->assertSame(3, $stat->total_count); + $stat->sample_count = 3; + $stat->updateTotalSampleCount(); + $this->assertSame(6, $stat->total_count); + } + +} diff --git a/tests/Inspector/Output/TopLike/TopLikeFormatterTest.php b/tests/Inspector/Output/TopLike/TopLikeFormatterTest.php new file mode 100644 index 00000000..3a96ee5a --- /dev/null +++ b/tests/Inspector/Output/TopLike/TopLikeFormatterTest.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace PhpProfiler\Inspector\Output\TopLike; + +use PhpProfiler\Lib\DateTime\FixedClock; +use PhpProfiler\Lib\PhpProcessReader\CallTrace; +use PHPUnit\Framework\TestCase; +use function PHPUnit\Framework\assertSame; + +class TopLikeFormatterTest extends TestCase +{ + public function testFormat() + { + $now = new \DateTimeImmutable(); + $formatter = new TopLikeFormatter( + 'regex', + $outputter = new class() implements Outputter { + public int $call_count = 0; + public function display(string $trace_target, Stat $stat): void + { + assertSame('regex', $trace_target); + $this->call_count++; + } + }, + $clock = new FixedClock($now) + ); + $this->assertSame(0, $outputter->call_count); + $formatter->format(new CallTrace()); + + $clock->update($now->modify('+1 second')); + $formatter->format(new CallTrace()); + $this->assertSame(1, $outputter->call_count); + } +} From d38f1fa5c91be9a0911e66ca69a68cdcf3b846f4 Mon Sep 17 00:00:00 2001 From: sji Date: Sun, 5 Dec 2021 17:18:53 +0900 Subject: [PATCH 3/6] update README --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index 6df84512..bd859c51 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,35 @@ Options: -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug ``` +### top-like mode +```bash +./php-profiler inspector:top --help +Description: + show an aggregated view of traces in real time in a form similar to the UNIX top command. + +Usage: + inspector:top [options] + +Options: + -P, --target-regex=TARGET-REGEX regex to find target processes which have matching command-line (required) + -T, --threads[=THREADS] number of workers (default: 8) + -d, --depth[=DEPTH] max depth + -s, --sleep-ns[=SLEEP-NS] nanoseconds between traces (default: 1000 * 1000 * 10) + -r, --max-retries[=MAX-RETRIES] max retries on contiguous errors of read (default: 10) + -S, --stop-process[=STOP-PROCESS] stop the target process while reading its trace (default: off) + --php-regex[=PHP-REGEX] regex to find the php binary loaded in the target process + --libpthread-regex[=LIBPTHREAD-REGEX] regex to find the libpthread.so loaded in the target process + --php-version[=PHP-VERSION] php version of the target (default: v80) + --php-path[=PHP-PATH] path to the php binary (only needed in tracing chrooted ZTS target) + --libpthread-path[=LIBPTHREAD-PATH] path to the libpthread.so (only needed in tracing chrooted ZTS target) + -h, --help Display help for the given command. When no command is given display help for the list command + -q, --quiet Do not output any message + -V, --version Display this application version + --ansi|--no-ansi Force (or disable --no-ansi) ANSI output + -n, --no-interaction Do not ask any interactive question + -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug +``` + ### Get the address of EG ```bash ./php-profiler inspector:eg --help From b46e56ca025b9eb34a381f9500754047cad66c97 Mon Sep 17 00:00:00 2001 From: sji Date: Sun, 5 Dec 2021 17:19:19 +0900 Subject: [PATCH 4/6] fix README --- README.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index bd859c51..84e6252f 100644 --- a/README.md +++ b/README.md @@ -33,17 +33,14 @@ The php-profiler outputs more accurate line numbers, and additionally can show e ## Requirements ### Supported PHP versions #### Execution -- PHP-8.0 64bit Linux x86_64 (NTS / ZTS) +- PHP 8.0+ (NTS / ZTS) +- 64bit Linux x86_64 - FFI extension must be enabled. - If the target process is ZTS, PCNTL extension must be enabled. #### Target -- PHP-7.0 64bit Linux x86_64 (NTS / ZTS) -- PHP-7.1 64bit Linux x86_64 (NTS / ZTS) -- PHP-7.2 64bit Linux x86_64 (NTS / ZTS) -- PHP-7.3 64bit Linux x86_64 (NTS / ZTS) -- PHP-7.4 64bit Linux x86_64 (NTS / ZTS) -- PHP-8.0 64bit Linux x86_64 (NTS / ZTS) +- PHP 7.0+ (NTS / ZTS) +- 64bit Linux x86_64 On targeting ZTS, the target process must load libpthread.so, and also you must have unstripped binary of the interpreter and the libpthread.so, to find EG from the TLS. From bb23dc004cdee4b17d77129211b36ed3801f3ce5 Mon Sep 17 00:00:00 2001 From: sji Date: Sun, 5 Dec 2021 17:28:10 +0900 Subject: [PATCH 5/6] fix errors of psalm and phpcs --- src/Command/Inspector/TopLikeCommand.php | 4 +- src/Inspector/Output/TopLike/Stat.php | 2 +- src/Lib/DateTime/FixedClock.php | 2 +- tests/Inspector/Output/TopLike/StatTest.php | 41 +++++-------------- .../Output/TopLike/TopLikeFormatterTest.php | 3 +- 5 files changed, 17 insertions(+), 35 deletions(-) diff --git a/src/Command/Inspector/TopLikeCommand.php b/src/Command/Inspector/TopLikeCommand.php index 99ecd536..c736f4ac 100644 --- a/src/Command/Inspector/TopLikeCommand.php +++ b/src/Command/Inspector/TopLikeCommand.php @@ -52,7 +52,9 @@ public function __construct( public function configure(): void { $this->setName('inspector:top') - ->setDescription('show an aggregated view of traces in real time in a form similar to the UNIX top command.') + ->setDescription( + 'show an aggregated view of traces in real time in a form similar to the UNIX top command.' + ) ; $this->daemon_settings_from_console_input->setOptions($this); $this->get_trace_settings_from_console_input->setOptions($this); diff --git a/src/Inspector/Output/TopLike/Stat.php b/src/Inspector/Output/TopLike/Stat.php index e6fddf7d..fb4b1f78 100644 --- a/src/Inspector/Output/TopLike/Stat.php +++ b/src/Inspector/Output/TopLike/Stat.php @@ -50,7 +50,7 @@ private function addFrame(CallFrame $call_frame, bool $is_first_frame): void $this->function_entries[$name]->count_inclusive++; } - public function updateStat() + public function updateStat(): void { if (count($this->function_entries) === 0) { return; diff --git a/src/Lib/DateTime/FixedClock.php b/src/Lib/DateTime/FixedClock.php index 6297a6c9..994c34c7 100644 --- a/src/Lib/DateTime/FixedClock.php +++ b/src/Lib/DateTime/FixedClock.php @@ -25,7 +25,7 @@ public function now(): \DateTimeImmutable return $this->now; } - public function update(\DateTimeImmutable $now) + public function update(\DateTimeImmutable $now): void { $this->now = $now; } diff --git a/tests/Inspector/Output/TopLike/StatTest.php b/tests/Inspector/Output/TopLike/StatTest.php index c79cc523..5133a606 100644 --- a/tests/Inspector/Output/TopLike/StatTest.php +++ b/tests/Inspector/Output/TopLike/StatTest.php @@ -196,38 +196,18 @@ public function testSort() ] ); $entries = $stat->function_entries; - $this->assertEquals( - $entry2, current($entries) - ); - $this->assertEquals( - $entry1, next($entries) - ); - $this->assertEquals( - $entry3, next($entries) - ); - $this->assertEquals( - $entry4, next($entries) - ); - $this->assertEquals( - $entry5, next($entries) - ); + $this->assertEquals($entry2, current($entries)); + $this->assertEquals($entry1, next($entries)); + $this->assertEquals($entry3, next($entries)); + $this->assertEquals($entry4, next($entries)); + $this->assertEquals($entry5, next($entries)); $stat->sort(); $entries = $stat->function_entries; - $this->assertEquals( - $entry5, current($entries) - ); - $this->assertEquals( - $entry1, next($entries) - ); - $this->assertEquals( - $entry4, next($entries) - ); - $this->assertEquals( - $entry3, next($entries) - ); - $this->assertEquals( - $entry2, next($entries) - ); + $this->assertEquals($entry5, current($entries)); + $this->assertEquals($entry1, next($entries)); + $this->assertEquals($entry4, next($entries)); + $this->assertEquals($entry3, next($entries)); + $this->assertEquals($entry2, next($entries)); } public function testCalculateEntryTotals() @@ -280,5 +260,4 @@ public function testUpdateTotalSampleCount() $stat->updateTotalSampleCount(); $this->assertSame(6, $stat->total_count); } - } diff --git a/tests/Inspector/Output/TopLike/TopLikeFormatterTest.php b/tests/Inspector/Output/TopLike/TopLikeFormatterTest.php index 3a96ee5a..fe335c7e 100644 --- a/tests/Inspector/Output/TopLike/TopLikeFormatterTest.php +++ b/tests/Inspector/Output/TopLike/TopLikeFormatterTest.php @@ -16,6 +16,7 @@ use PhpProfiler\Lib\DateTime\FixedClock; use PhpProfiler\Lib\PhpProcessReader\CallTrace; use PHPUnit\Framework\TestCase; + use function PHPUnit\Framework\assertSame; class TopLikeFormatterTest extends TestCase @@ -25,7 +26,7 @@ public function testFormat() $now = new \DateTimeImmutable(); $formatter = new TopLikeFormatter( 'regex', - $outputter = new class() implements Outputter { + $outputter = new class () implements Outputter { public int $call_count = 0; public function display(string $trace_target, Stat $stat): void { From 3ad9bf44dafa71c52bc6fe9cdeeef98137367282 Mon Sep 17 00:00:00 2001 From: sji Date: Sun, 5 Dec 2021 17:46:01 +0900 Subject: [PATCH 6/6] add a test that's better than nothing. --- .../Output/TopLike/TopLikeOutputterTest.php | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 tests/Inspector/Output/TopLike/TopLikeOutputterTest.php diff --git a/tests/Inspector/Output/TopLike/TopLikeOutputterTest.php b/tests/Inspector/Output/TopLike/TopLikeOutputterTest.php new file mode 100644 index 00000000..39c3cb49 --- /dev/null +++ b/tests/Inspector/Output/TopLike/TopLikeOutputterTest.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace PhpProfiler\Inspector\Output\TopLike; + +use Mockery; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Formatter\OutputFormatterInterface; +use Symfony\Component\Console\Formatter\WrappableOutputFormatterInterface; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\ConsoleSectionOutput; +use Symfony\Component\Console\Terminal; + +class TopLikeOutputterTest extends TestCase +{ + public function testDisplay() + { + $output = Mockery::mock(ConsoleOutputInterface::class); + $terminal = Mockery::mock(Terminal::class); + $outputter = new TopLikeOutputter( + $output, + $terminal + ); + + $output->expects()->write("\e[H\e[2J"); + $output->expects()->writeln('target_regex'); + $output->expects()->writeln('samp_count=0 func_count=0 total_count=0'); + $output->expects()->writeln(''); + $terminal->expects()->getWidth()->andReturns(80); + $terminal->expects()->getHeight()->andReturns(20); + $output->expects() + ->section() + ->andReturns( + $section = Mockery::mock(ConsoleSectionOutput::class) + ) + ; + $section->expects() + ->getFormatter() + ->andReturns( + $formatter = new OutputFormatter() + ) + ; + $section + ->expects() + ->writeln( + \Mockery::on(function ($argument) { + $this->assertStringContainsString('total_incl', $argument); + return true; + }) + ) + ; + $section + ->expects() + ->getContent() + ->andReturns('content_rendered') + ; + $section + ->expects() + ->overwrite('content_rendered') + ; + + $outputter->display('target_regex', new Stat()); + } +}