Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Modules\Profiler\Application\Handlers;

use Modules\Profiler\Application\EventHandlerInterface;
use Modules\Profiler\Application\MetricsHelper;

// TODO: fix diff calculation
final class CalculateDiffsBetweenEdges implements EventHandlerInterface
Expand All @@ -18,16 +19,21 @@ public function handle(array $event): array
[$parent, $func] = $this->splitName($name);

if ($parent) {
$parentValues = $parents[$parent] ?? ['cpu' => 0, 'wt' => 0, 'mu' => 0, 'pmu' => 0];
$parentValues = $parents[$parent] ?? MetricsHelper::getAllMetrics([]);

// Use MetricsHelper to safely access metrics with defaults
$currentMetrics = MetricsHelper::getAllMetrics($values);

$event['profile'][$name] = \array_merge([
'd_cpu' => $parentValues['cpu'] - $values['cpu'],
'd_wt' => $parentValues['wt'] - $values['wt'],
'd_mu' => $parentValues['mu'] - $values['mu'],
'd_pmu' => $parentValues['pmu'] - $values['pmu'],
'd_cpu' => $parentValues['cpu'] - $currentMetrics['cpu'],
'd_wt' => $parentValues['wt'] - $currentMetrics['wt'],
'd_mu' => $parentValues['mu'] - $currentMetrics['mu'],
'd_pmu' => $parentValues['pmu'] - $currentMetrics['pmu'],
], $values);
}

$parents[$func] = $values;
// Store normalized metrics for parent lookup
$parents[$func] = MetricsHelper::getAllMetrics($values);
}

return $event;
Expand Down
10 changes: 9 additions & 1 deletion app/modules/Profiler/Application/Handlers/PrepareEdges.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Modules\Profiler\Application\Handlers;

use Modules\Profiler\Application\EventHandlerInterface;
use Modules\Profiler\Application\MetricsHelper;

final class PrepareEdges implements EventHandlerInterface
{
Expand All @@ -23,9 +24,16 @@ public function handle(array $event): array

$parentId = $parents[$parent] ?? $prev;

// Normalize metrics to ensure all required fields are present
$normalizedValues = MetricsHelper::getAllMetrics($values);

// Calculate percentages with safe metric access
foreach (['cpu', 'mu', 'pmu', 'wt'] as $key) {
$peakValue = MetricsHelper::getMetric($event['peaks'], $key);
$values['p_' . $key] = \round(
$values[$key] > 0 ? ($values[$key]) / $event['peaks'][$key] * 100 : 0,
$normalizedValues[$key] > 0 && $peakValue > 0
? ($normalizedValues[$key]) / $peakValue * 100
: 0,
3,
);
}
Expand Down
13 changes: 6 additions & 7 deletions app/modules/Profiler/Application/Handlers/PreparePeaks.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,19 @@
namespace Modules\Profiler\Application\Handlers;

use Modules\Profiler\Application\EventHandlerInterface;
use Modules\Profiler\Application\MetricsHelper;

final class PreparePeaks implements EventHandlerInterface
{
public function handle(array $event): array
{
// TODO: fix peaks calculation
// @see \Modules\Profiler\Interfaces\Queries\FindTopFunctionsByUuidHandler:$overallTotals
$event['peaks'] = $event['profile']['main()'] ?? [
'wt' => 0,
'ct' => 0,
'mu' => 0,
'pmu' => 0,
'cpu' => 0,
];

// Get main() metrics or use defaults if not available
$mainMetrics = $event['profile']['main()'] ?? [];

$event['peaks'] = MetricsHelper::getAllMetrics($mainMetrics);

return $event;
}
Expand Down
60 changes: 60 additions & 0 deletions app/modules/Profiler/Application/MetricsHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace Modules\Profiler\Application;

/**
* Helper class for safely handling profiler metrics with potential missing values
*/
final class MetricsHelper
{
/**
* Default metric values when not available in profile data
*/
private const DEFAULT_METRICS = [
'cpu' => 0,
'wt' => 0,
'mu' => 0,
'pmu' => 0,
'ct' => 0,
];

/**
* Get metric value with fallback to default if missing
*/
public static function getMetric(array $data, string $metric): int|float
{
return $data[$metric] ?? self::DEFAULT_METRICS[$metric] ?? 0;
}

/**
* Normalize metrics array by ensuring all required metrics are present
*/
public static function normalizeMetrics(array $metrics): array
{
return \array_merge(self::DEFAULT_METRICS, $metrics);
}

/**
* Get all available metrics from data, with defaults for missing ones
*/
public static function getAllMetrics(array $data): array
{
return [
'cpu' => self::getMetric($data, 'cpu'),
'wt' => self::getMetric($data, 'wt'),
'mu' => self::getMetric($data, 'mu'),
'pmu' => self::getMetric($data, 'pmu'),
'ct' => self::getMetric($data, 'ct'),
];
}

/**
* Check if any CPU-related metrics are available
*/
public static function hasCpuMetrics(array $data): bool
{
return isset($data['cpu']) && $data['cpu'] > 0;
}
}
3 changes: 2 additions & 1 deletion app/modules/Profiler/Application/ProfilerBootloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Modules\Profiler\Application\Handlers\PrepareEdges;
use Modules\Profiler\Application\Handlers\PreparePeaks;
use Modules\Profiler\Application\Handlers\StoreProfile;
use Modules\Profiler\Application\Service\FunctionMetricsCalculator;
use Modules\Profiler\Domain\EdgeFactoryInterface;
use Modules\Profiler\Domain\ProfileFactoryInterface;
use Modules\Profiler\Integration\CycleOrm\EdgeFactory;
Expand All @@ -30,6 +31,7 @@ public function defineSingletons(): array
return [
ProfileFactoryInterface::class => ProfileFactory::class,
EdgeFactoryInterface::class => EdgeFactory::class,

EventHandlerInterface::class => static fn(
ContainerInterface $container,
): EventHandlerInterface => new EventHandler($container, [
Expand All @@ -40,7 +42,6 @@ public function defineSingletons(): array
StoreProfile::class,
]),


StoreProfile::class => static fn(
FactoryInterface $factory,
QueueConnectionProviderInterface $provider,
Expand Down
129 changes: 129 additions & 0 deletions app/modules/Profiler/Application/Service/FunctionMetricsCalculator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

declare(strict_types=1);

namespace Modules\Profiler\Application\Service;

use Modules\Profiler\Domain\Edge;
use Modules\Profiler\Domain\Edge\Cost;
use Modules\Profiler\Domain\FunctionMetrics;

/**
* Service for aggregating and calculating function performance metrics
*/
final class FunctionMetricsCalculator
{
/**
* Calculate aggregated function metrics from edges
*
* @param Edge[] $edges
* @return FunctionMetrics[]
*/
public function calculateMetrics(array $edges): array
{
$functions = [];
$overallTotals = $this->initializeOverallTotals();

// First pass: aggregate inclusive metrics per function
foreach ($edges as $edge) {
$functionName = $edge->getCallee();

if (!isset($functions[$functionName])) {
$functions[$functionName] = FunctionMetrics::fromEdge($edge);
} else {
$functions[$functionName] = $functions[$functionName]->addEdge($edge);
}
}

// Calculate overall totals from main() function or first function
$overallTotals = $this->calculateOverallTotals($functions);

// Second pass: calculate exclusive metrics by subtracting child costs
$functions = $this->calculateExclusiveMetrics($functions, $edges);

return [$functions, $overallTotals];
}

/**
* Sort functions by specified metric
*
* @param FunctionMetrics[] $functions
*/
public function sortFunctions(array $functions, string $sortMetric): array
{
usort(
$functions,
static fn(FunctionMetrics $a, FunctionMetrics $b) => $b->getMetricForSort(
$sortMetric,
) <=> $a->getMetricForSort($sortMetric),
);

return $functions;
}

/**
* Convert function metrics to array format for API response
*
* @param FunctionMetrics[] $functions
*/
public function toArrayFormat(array $functions, Cost $overallTotals): array
{
return array_map(
fn(FunctionMetrics $metrics) => $metrics->toArray($overallTotals),
$functions,
);
}

private function initializeOverallTotals(): Cost
{
return new Cost(cpu: 0, wt: 0, ct: 0, mu: 0, pmu: 0);
}

/**
* @param FunctionMetrics[] $functions
*/
private function calculateOverallTotals(array $functions): Cost
{
// Try to get totals from main() function first
if (isset($functions['main()'])) {
return $functions['main()']->inclusive;
}

// If no main(), calculate from all functions (less accurate but workable)
$totals = $this->initializeOverallTotals();

foreach ($functions as $function) {
// Only add call counts, other metrics should not be summed across functions
$totals = new Cost(
cpu: max($totals->cpu, $function->inclusive->cpu),
wt: max($totals->wt, $function->inclusive->wt),
ct: $totals->ct + $function->inclusive->ct,
mu: max($totals->mu, $function->inclusive->mu),
pmu: max($totals->pmu, $function->inclusive->pmu),
);
}

return $totals;
}

/**
* Calculate exclusive metrics by subtracting child function costs
*
* @param FunctionMetrics[] $functions
* @param Edge[] $edges
* @return FunctionMetrics[]
*/
private function calculateExclusiveMetrics(array $functions, array $edges): array
{
// Build parent-child relationships and subtract child costs
foreach ($edges as $edge) {
$caller = $edge->getCaller();

if ($caller && isset($functions[$caller])) {
$functions[$caller] = $functions[$caller]->subtractChild($edge->getCost());
}
}

return $functions;
}
}
73 changes: 73 additions & 0 deletions app/modules/Profiler/Domain/Edge/Cost.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,77 @@ public function __construct(
#[Column(type: 'integer')]
public int $pmu,
) {}

/**
* Get metric value by name, with safe fallback to 0
*/
public function getMetric(string $metric): int
{
return match ($metric) {
'cpu' => $this->cpu,
'wt' => $this->wt,
'ct' => $this->ct,
'mu' => $this->mu,
'pmu' => $this->pmu,
default => 0,
};
}

/**
* Get all metrics as associative array
*/
public function toArray(): array
{
return [
'cpu' => $this->cpu,
'wt' => $this->wt,
'ct' => $this->ct,
'mu' => $this->mu,
'pmu' => $this->pmu,
];
}

/**
* Add another Cost to this one (for aggregation)
*/
public function add(Cost $other): Cost
{
return new self(
cpu: $this->cpu + $other->cpu,
wt: $this->wt + $other->wt,
ct: $this->ct + $other->ct,
mu: $this->mu + $other->mu,
pmu: $this->pmu + $other->pmu,
);
}

/**
* Subtract another Cost from this one
*/
public function subtract(Cost $other): Cost
{
return new self(
cpu: max(0, $this->cpu - $other->cpu),
wt: max(0, $this->wt - $other->wt),
ct: max(0, $this->ct - $other->ct),
mu: max(0, $this->mu - $other->mu),
pmu: max(0, $this->pmu - $other->pmu),
);
}

/**
* Check if this cost has any CPU metrics
*/
public function hasCpuMetrics(): bool
{
return $this->cpu > 0;
}

/**
* Create a new Cost with only exclusive metrics (all metrics minus given cost)
*/
public function getExclusive(Cost $inclusive): Cost
{
return $this->subtract($inclusive);
}
}
Loading
Loading