-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #132 from sj-i/top-like
add Top-like mode
- Loading branch information
Showing
16 changed files
with
1,042 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) { | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
Oops, something went wrong.