Skip to content

Commit 6919e88

Browse files
committed
Add performance logging under a --debug-performance flag
1 parent ad1920c commit 6919e88

File tree

7 files changed

+121
-22
lines changed

7 files changed

+121
-22
lines changed

src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php

+21
Original file line numberDiff line numberDiff line change
@@ -557,8 +557,29 @@ function (FunctionLikeParameter $p) {
557557
]);
558558
}
559559

560+
$time = \microtime(true);
561+
562+
$project_analyzer = $statements_analyzer->getProjectAnalyzer();
563+
560564
$statements_analyzer->analyze($function_stmts, $context, $global_context, true);
561565

566+
if (!$context->collect_initializations
567+
&& !$context->collect_mutations
568+
&& $project_analyzer->debug_performance
569+
&& $cased_method_id
570+
) {
571+
$traverser = new PhpParser\NodeTraverser;
572+
573+
$node_counter = new \Psalm\Internal\PhpVisitor\NodeCounterVisitor();
574+
$traverser->addVisitor($node_counter);
575+
$traverser->traverse($function_stmts);
576+
577+
if ($node_counter->count > 5) {
578+
$time_taken = \microtime(true) - $time;
579+
$codebase->analyzer->addFunctionTiming($cased_method_id, $time_taken / $node_counter->count);
580+
}
581+
}
582+
562583
$this->examineParamTypes($statements_analyzer, $context, $codebase);
563584

564585
foreach ($storage->params as $offset => $function_param) {

src/Psalm/Internal/Analyzer/ProjectAnalyzer.php

+5
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ class ProjectAnalyzer
135135
*/
136136
public $debug_lines = false;
137137

138+
/**
139+
* @var bool
140+
*/
141+
public $debug_performance = false;
142+
138143
/**
139144
* @var bool
140145
*/

src/Psalm/Internal/Analyzer/StatementsAnalyzer.php

+4
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ public function analyze(
151151
Context $global_context = null,
152152
$root_scope = false
153153
) {
154+
if (!$stmts) {
155+
return;
156+
}
157+
154158
// hoist functions to the top
155159
$this->hoistFunctions($stmts);
156160

src/Psalm/Internal/Codebase/Analyzer.php

+41-22
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
* file_references_to_missing_class_members: array<string, array<string,bool>>,
4747
* mixed_counts: array<string, array{0: int, 1: int}>,
4848
* mixed_member_names: array<string, array<string, bool>>,
49+
* function_timings: array<string, float>,
4950
* file_manipulations: array<string, FileManipulation[]>,
5051
* method_references_to_class_members: array<string, array<string,bool>>,
5152
* method_references_to_missing_class_members: array<string, array<string,bool>>,
@@ -108,6 +109,13 @@ class Analyzer
108109
*/
109110
private $count_mixed = true;
110111

112+
/**
113+
* Used to store debug performance data
114+
*
115+
* @var array<string, float>
116+
*/
117+
private $function_timings = [];
118+
111119
/**
112120
* We analyze more files than we necessarily report errors in
113121
*
@@ -316,7 +324,7 @@ function (string $file_path) : bool {
316324
}
317325
}
318326

319-
private function doAnalysis(ProjectAnalyzer $project_analyzer, int $pool_size, bool $rerun = false) : void
327+
private function doAnalysis(ProjectAnalyzer $project_analyzer, int $pool_size) : void
320328
{
321329
$this->progress->start(count($this->files_to_analyze));
322330

@@ -426,7 +434,7 @@ function () {
426434
},
427435
$analysis_worker,
428436
/** @return WorkerData */
429-
function () use ($rerun) {
437+
function () {
430438
$project_analyzer = ProjectAnalyzer::getInstance();
431439
$codebase = $project_analyzer->getCodebase();
432440
$analyzer = $codebase->analyzer;
@@ -438,22 +446,23 @@ function () use ($rerun) {
438446
return [
439447
'issues' => IssueBuffer::getIssuesData(),
440448
'fixable_issue_counts' => IssueBuffer::getFixableIssues(),
441-
'nonmethod_references_to_classes' => $rerun ? [] : $file_reference_provider->getAllNonMethodReferencesToClasses(),
442-
'method_references_to_classes' => $rerun ? [] : $file_reference_provider->getAllMethodReferencesToClasses(),
443-
'file_references_to_class_members' => $rerun ? [] : $file_reference_provider->getAllFileReferencesToClassMembers(),
444-
'method_references_to_class_members' => $rerun ? [] : $file_reference_provider->getAllMethodReferencesToClassMembers(),
445-
'file_references_to_missing_class_members' => $rerun ? [] : $file_reference_provider->getAllFileReferencesToMissingClassMembers(),
446-
'method_references_to_missing_class_members' => $rerun ? [] : $file_reference_provider->getAllMethodReferencesToMissingClassMembers(),
447-
'method_param_uses' => $rerun ? [] : $file_reference_provider->getAllMethodParamUses(),
448-
'mixed_member_names' => $rerun ? [] : $analyzer->getMixedMemberNames(),
449-
'file_manipulations' => $rerun ? [] : FileManipulationBuffer::getAll(),
450-
'mixed_counts' => $rerun ? [] : $analyzer->getMixedCounts(),
451-
'analyzed_methods' => $rerun ? [] : $analyzer->getAnalyzedMethods(),
452-
'file_maps' => $rerun ? [] : $analyzer->getFileMaps(),
453-
'class_locations' => $rerun ? [] : $file_reference_provider->getAllClassLocations(),
454-
'class_method_locations' => $rerun ? [] : $file_reference_provider->getAllClassMethodLocations(),
455-
'class_property_locations' => $rerun ? [] : $file_reference_provider->getAllClassPropertyLocations(),
456-
'possible_method_param_types' => $rerun ? [] : $analyzer->getPossibleMethodParamTypes(),
449+
'nonmethod_references_to_classes' => $file_reference_provider->getAllNonMethodReferencesToClasses(),
450+
'method_references_to_classes' => $file_reference_provider->getAllMethodReferencesToClasses(),
451+
'file_references_to_class_members' => $file_reference_provider->getAllFileReferencesToClassMembers(),
452+
'method_references_to_class_members' => $file_reference_provider->getAllMethodReferencesToClassMembers(),
453+
'file_references_to_missing_class_members' => $file_reference_provider->getAllFileReferencesToMissingClassMembers(),
454+
'method_references_to_missing_class_members' => $file_reference_provider->getAllMethodReferencesToMissingClassMembers(),
455+
'method_param_uses' => $file_reference_provider->getAllMethodParamUses(),
456+
'mixed_member_names' => $analyzer->getMixedMemberNames(),
457+
'file_manipulations' => FileManipulationBuffer::getAll(),
458+
'mixed_counts' => $analyzer->getMixedCounts(),
459+
'function_timings' => $analyzer->getFunctionTimings(),
460+
'analyzed_methods' => $analyzer->getAnalyzedMethods(),
461+
'file_maps' => $analyzer->getFileMaps(),
462+
'class_locations' => $file_reference_provider->getAllClassLocations(),
463+
'class_method_locations' => $file_reference_provider->getAllClassMethodLocations(),
464+
'class_property_locations' => $file_reference_provider->getAllClassPropertyLocations(),
465+
'possible_method_param_types' => $analyzer->getPossibleMethodParamTypes(),
457466
'taint_data' => $codebase->taint,
458467
'unused_suppressions' => $codebase->track_unused_suppressions ? IssueBuffer::getUnusedSuppressions() : [],
459468
'used_suppressions' => $codebase->track_unused_suppressions ? IssueBuffer::getUsedSuppressions() : [],
@@ -486,10 +495,6 @@ function () use ($rerun) {
486495
$codebase->taint->addThreadData($pool_data['taint_data']);
487496
}
488497

489-
if ($rerun) {
490-
continue;
491-
}
492-
493498
$codebase->file_reference_provider->addNonMethodReferencesToClasses(
494499
$pool_data['nonmethod_references_to_classes']
495500
);
@@ -514,6 +519,7 @@ function () use ($rerun) {
514519
$this->addMixedMemberNames(
515520
$pool_data['mixed_member_names']
516521
);
522+
$this->function_timings += $pool_data['function_timings'];
517523
$codebase->file_reference_provider->addClassLocations(
518524
$pool_data['class_locations']
519525
);
@@ -1145,6 +1151,19 @@ public function getMixedCounts()
11451151
return array_intersect_key($this->mixed_counts, $all_deep_scanned_files);
11461152
}
11471153

1154+
/**
1155+
* @return array<string, float>
1156+
*/
1157+
public function getFunctionTimings()
1158+
{
1159+
return $this->function_timings;
1160+
}
1161+
1162+
public function addFunctionTiming(string $function_id, float $time_per_node) : void
1163+
{
1164+
$this->function_timings[$function_id] = $time_per_node;
1165+
}
1166+
11481167
/**
11491168
* @return void
11501169
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
namespace Psalm\Internal\PhpVisitor;
3+
4+
use PhpParser;
5+
6+
/**
7+
* @internal
8+
*/
9+
class NodeCounterVisitor extends PhpParser\NodeVisitorAbstract implements PhpParser\NodeVisitor
10+
{
11+
/** @var int */
12+
public $count = 0;
13+
14+
/**
15+
* @param PhpParser\Node $node
16+
*
17+
* @return null|int
18+
*/
19+
public function enterNode(PhpParser\Node $node)
20+
{
21+
$this->count++;
22+
}
23+
}

src/Psalm/IssueBuffer.php

+22
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,28 @@ function (IssueData $d1, IssueData $d2) : int {
633633
echo $codebase->analyzer->getNonMixedStats();
634634
echo "\n";
635635
}
636+
637+
if ($project_analyzer->debug_performance) {
638+
echo '-----------------' . "\n";
639+
echo 'Slow-to-analyze functions' . "\n";
640+
echo '-----------------' . "\n\n";
641+
642+
$function_timings = $codebase->analyzer->getFunctionTimings();
643+
644+
\arsort($function_timings);
645+
646+
$i = 0;
647+
648+
foreach ($function_timings as $function_id => $time) {
649+
if (++$i > 10) {
650+
break;
651+
}
652+
653+
echo $function_id . ': ' . \round(1000 * $time, 2) . 'ms per node' . "\n";
654+
}
655+
656+
echo "\n";
657+
}
636658
}
637659
}
638660

src/psalm.php

+5
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
'config:',
8181
'debug',
8282
'debug-by-line',
83+
'debug-performance',
8384
'debug-emitted-issues',
8485
'diff',
8586
'diff-methods',
@@ -624,6 +625,10 @@ function ($arg) {
624625
$project_analyzer->debug_lines = true;
625626
}
626627

628+
if (array_key_exists('debug-performance', $options)) {
629+
$project_analyzer->debug_performance = true;
630+
}
631+
627632
if ($config->find_unused_code) {
628633
$find_unused_code = 'auto';
629634
}

0 commit comments

Comments
 (0)