Skip to content

Commit

Permalink
Merge pull request #132 from sj-i/top-like
Browse files Browse the repository at this point in the history
add Top-like mode
  • Loading branch information
sj-i committed Dec 5, 2021
2 parents 7a3ef1f + 3ad9bf4 commit f1ccc26
Show file tree
Hide file tree
Showing 16 changed files with 1,042 additions and 7 deletions.
40 changes: 33 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -127,6 +124,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
Expand Down
149 changes: 149 additions & 0 deletions src/Command/Inspector/TopLikeCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php

/**
* This file is part of the sj-i/ package.
*
* (c) sji <sji@sj-i.dev>
*
* 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);
}
}
29 changes: 29 additions & 0 deletions src/Inspector/Output/TopLike/FunctionEntry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

/**
* This file is part of the sj-i/ package.
*
* (c) sji <sji@sj-i.dev>
*
* 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,
) {
}
}
19 changes: 19 additions & 0 deletions src/Inspector/Output/TopLike/Outputter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

/**
* This file is part of the sj-i/ package.
*
* (c) sji <sji@sj-i.dev>
*
* 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;
}
107 changes: 107 additions & 0 deletions src/Inspector/Output/TopLike/Stat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

/**
* This file is part of the sj-i/ package.
*
* (c) sji <sji@sj-i.dev>
*
* 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<string, FunctionEntry> $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(): void
{
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;
}
}
}
Loading

0 comments on commit f1ccc26

Please sign in to comment.