From 7351beda3e4870e2cb739e11fe3e7d8785ca5bba Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Sat, 5 Sep 2026 10:32:15 -0400 Subject: [PATCH] feat(api): centralize request-summary metric calculations and formatting in `SummaryMetricComparison`, preserving history labels, order, units, rounding, percentages, trends, and panel links. --- CHANGELOG.md | 1 + README.md | 28 +- src/Comparison/SummaryMetricComparison.php | 195 +++ .../SummaryMetricComparisonTest.php | 92 ++ .../SummaryMetricComparisonProvider.php | 1056 +++++++++++++++++ 5 files changed, 1371 insertions(+), 1 deletion(-) create mode 100644 src/Comparison/SummaryMetricComparison.php create mode 100644 tests/Comparison/SummaryMetricComparisonTest.php create mode 100644 tests/Provider/SummaryMetricComparisonProvider.php diff --git a/CHANGELOG.md b/CHANGELOG.md index c3002b4..51967d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,3 +38,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - feat(ui): expose captured trace totals and style clickable Log severity counters as filter pills alongside History status shortcuts. - refactor(ui): unify Request, Server, Session, Input, routing, and tabs with collapsible filters, focus states, and refreshed assets. - feat(api): centralize typed structural payload differences and add immutable fluent toolbar item and panel construction while preserving diagnostic values, constructors, and serialized payloads. +- feat(api): centralize request-summary metric calculations and formatting in `SummaryMetricComparison`, preserving history labels, order, units, rounding, percentages, trends, and panel links. diff --git a/README.md b/README.md index 81090aa..0f7116f 100644 --- a/README.md +++ b/README.md @@ -134,4 +134,30 @@ distinct. Leaf paths escape `~` and `/`; list positions matter, while map insert The comparison fingerprints typed leaves temporarily and retains only counts in its result. It does not alter or redact the source payloads. Adapters retain responsibility for capture/failure precedence, state-only changes, panel ordering, -labels, and metric presentation. See the [architecture review](docs/architecture-review.md) for boundaries and follow-up work. +and labels. See the [architecture review](docs/architecture-review.md) for boundaries and follow-up work. + +## Request-summary metric comparison + +`PHPForge\Debug\Comparison\SummaryMetricComparison::between($baseline, $target)` accepts two `RequestSummary` +instances and returns an ordered list of immutable comparisons. Each result exposes `label`, `baseline`, `target`, +`delta`, `trend`, and nullable `panelId`. Adapters map these fields into their own public models; no framework dependency, +capture policy, payload comparison, or snapshot mutation is involved. + +The canonical order is Status, Method, AJAX, Duration, Peak memory, SQL queries, Mail messages, and Excessive DB callers. +Duration uses milliseconds and memory uses bytes divided by 1,048,576 with the existing `MB` label. Both use two decimal +places; counters use none. Decimal points, comma grouping, signs, one-decimal percentages, and related panel IDs remain +identical to the original adapters. + +Missing profiling values remain `Not captured`; one missing side produces `Not comparable` with a neutral trend. +Two missing values produce `No change`. Status zero means `Not captured`, AJAX `false` means `No`, and an empty method +remains an empty string. Captured numeric zero is never treated as missing, and zero baselines omit percentages. +Deltas subtract the scaled values before formatting, while percentages use the original values. Comparisons use exact +floating-point results, not rounded display values or an epsilon: a displayed zero delta may still have a direction. + +### Coordinated publication + +Publish the Core revision containing `SummaryMetricComparison` before either adapter revision that consumes it. +Both adapters currently require `php-forge/debug-core` at `^0.1@dev`; this constraint alone does not ensure that an +installed or locked development revision includes the new class. Update and verify consuming application locks together. +Local adapter installations linked to this workspace verify integration but do not validate older published artifacts. +No adapter constructor, property, getter, return type, template, asset, or persisted representation changes. diff --git a/src/Comparison/SummaryMetricComparison.php b/src/Comparison/SummaryMetricComparison.php new file mode 100644 index 0000000..86537f5 --- /dev/null +++ b/src/Comparison/SummaryMetricComparison.php @@ -0,0 +1,195 @@ + + */ + public static function between(RequestSummary $baseline, RequestSummary $target): array + { + return [ + self::textMetric( + 'Status', + self::status($baseline->statusCode), + self::status($target->statusCode), + ), + self::textMetric( + 'Method', + $baseline->method, + $target->method, + ), + self::textMetric( + 'AJAX', + self::yesNo($baseline->ajax), + self::yesNo($target->ajax), + ), + self::nullableFloatMetric( + 'Duration', + $baseline->processingTime, + $target->processingTime, + 1000, + 'ms', + 'profiling', + ), + self::nullableFloatMetric( + 'Peak memory', + $baseline->peakMemory, + $target->peakMemory, + 1 / 1_048_576, + 'MB', + 'profiling', + ), + self::integerMetric( + 'SQL queries', + $baseline->sqlCount, + $target->sqlCount, + 'db', + ), + self::integerMetric( + 'Mail messages', + $baseline->mailCount, + $target->mailCount, + 'mail', + ), + self::integerMetric( + 'Excessive DB callers', + $baseline->excessiveCallersCount, + $target->excessiveCallersCount, + 'db', + ), + ]; + } + + private static function formatNumber(float|int $value, string $unit, int $precision): string + { + $formatted = number_format($value, $precision, '.', ','); + + return $unit === '' ? $formatted : "{$formatted} {$unit}"; + } + + private static function integerMetric( + string $label, + int $baseline, + int $target, + string|null $panelId = null, + ): self { + return self::numericMetric( + $label, + $baseline, + $target, + 1, + '', + $panelId, + 0, + ); + } + + private static function nullableFloatMetric( + string $label, + float|int|null $baseline, + float|int|null $target, + float $scale, + string $unit, + string|null $panelId = null, + ): self { + if ($baseline === null || $target === null) { + return new self( + label: $label, + baseline: $baseline === null ? 'Not captured' : self::formatNumber($baseline * $scale, $unit, 2), + target: $target === null ? 'Not captured' : self::formatNumber($target * $scale, $unit, 2), + delta: $baseline === $target ? 'No change' : 'Not comparable', + trend: 'neutral', + panelId: $panelId, + ); + } + + return self::numericMetric( + $label, + $baseline, + $target, + $scale, + $unit, + $panelId, + 2, + ); + } + + private static function numericMetric( + string $label, + float|int $baseline, + float|int $target, + float $scale, + string $unit, + string|null $panelId, + int $precision, + ): self { + $scaledBaseline = $baseline * $scale; + $scaledTarget = $target * $scale; + $scaledDelta = $scaledTarget - $scaledBaseline; + $trend = $scaledDelta > 0 ? 'up' : ($scaledDelta < 0 ? 'down' : 'neutral'); + + $delta = 'No change'; + + if ($scaledDelta !== 0.0) { + $sign = $trend === 'up' ? '+' : ''; + $percentage = (float) $baseline !== 0.0 + ? " ({$sign}" . number_format((($target - $baseline) / $baseline) * 100, 1) . '%)' + : ''; + $delta = $sign . self::formatNumber($scaledDelta, $unit, $precision) . $percentage; + } + + return new self( + label: $label, + baseline: self::formatNumber($scaledBaseline, $unit, $precision), + target: self::formatNumber($scaledTarget, $unit, $precision), + delta: $delta, + trend: $trend, + panelId: $panelId, + ); + } + + private static function status(int $statusCode): string + { + return $statusCode === 0 ? 'Not captured' : (string) $statusCode; + } + + private static function textMetric(string $label, string $baseline, string $target): self + { + return new self( + label: $label, + baseline: $baseline, + target: $target, + delta: $baseline === $target ? 'No change' : 'Changed', + trend: 'neutral', + ); + } + + private static function yesNo(bool $value): string + { + return $value ? 'Yes' : 'No'; + } +} diff --git a/tests/Comparison/SummaryMetricComparisonTest.php b/tests/Comparison/SummaryMetricComparisonTest.php new file mode 100644 index 0000000..0d65540 --- /dev/null +++ b/tests/Comparison/SummaryMetricComparisonTest.php @@ -0,0 +1,92 @@ + $expected + */ + #[DataProviderExternal(SummaryMetricComparisonProvider::class, 'summaries')] + public function testBetweenPreservesAllMetricContracts( + RequestSummary $baseline, + RequestSummary $target, + array $expected, + ): void { + $beforeBaseline = $baseline->jsonSerialize(); + $beforeTarget = $target->jsonSerialize(); + $actual = []; + + foreach (SummaryMetricComparison::between($baseline, $target) as $metric) { + $actual[] = self::row($metric); + } + + self::assertSame( + $expected, + $actual, + 'Labels, order, values, deltas, trends, and panel IDs must remain exact.', + ); + self::assertSame( + $beforeBaseline, + $baseline->jsonSerialize(), + 'The baseline must remain unchanged.' + ); + self::assertSame( + $beforeTarget, + $target->jsonSerialize(), + 'The target must remain unchanged.' + ); + } + + /** + * @param array{string, string, string, string, string, string|null} $expected + */ + #[DataProviderExternal(SummaryMetricComparisonProvider::class, 'metrics')] + public function testBetweenPreservesMetricBoundaries( + RequestSummary $baseline, + RequestSummary $target, + int $index, + array $expected, + ): void { + $metrics = SummaryMetricComparison::between($baseline, $target); + + if (!isset($metrics[$index])) { + self::fail( + 'The metric must retain its canonical position.', + ); + } + + self::assertSame( + $expected, + self::row($metrics[$index]), + 'Metric arithmetic and formatting must remain exact.' + ); + } + + /** + * @return array{string, string, string, string, string, string|null} + */ + private static function row(SummaryMetricComparison $metric): array + { + return [ + $metric->label, + $metric->baseline, + $metric->target, + $metric->delta, + $metric->trend, + $metric->panelId, + ]; + } +} diff --git a/tests/Provider/SummaryMetricComparisonProvider.php b/tests/Provider/SummaryMetricComparisonProvider.php new file mode 100644 index 0000000..03cd6c7 --- /dev/null +++ b/tests/Provider/SummaryMetricComparisonProvider.php @@ -0,0 +1,1056 @@ + + */ + public static function metrics(): iterable + { + yield 'processingTime pair 13' => [ + self::summary(['processingTime' => null]), + self::summary(['processingTime' => 1234.567895]), + 3, + [ + 'Duration', + 'Not captured', + '1,234,567.90 ms', + 'Not comparable', + 'neutral', + 'profiling', + ], + ]; + yield 'processingTime pair 182' => [ + self::summary(['processingTime' => 1234.567895]), + self::summary(['processingTime' => null]), + 3, + [ + 'Duration', + '1,234,567.90 ms', + 'Not captured', + 'Not comparable', + 'neutral', + 'profiling', + ], + ]; + yield 'processingTime pair 18' => [ + self::summary(['processingTime' => 0.0]), + self::summary(['processingTime' => 5e-06]), + 3, + [ + 'Duration', + '0.00 ms', + '0.01 ms', + '+0.01 ms', + 'up', + 'profiling', + ], + ]; + yield 'processingTime pair 57' => [ + self::summary(['processingTime' => 5e-06]), + self::summary(['processingTime' => 0.0]), + 3, + [ + 'Duration', + '0.01 ms', + '0.00 ms', + '-0.01 ms (-100.0%)', + 'down', + 'profiling', + ], + ]; + yield 'processingTime pair 26' => [ + self::summary(['processingTime' => 0.0]), + self::summary(['processingTime' => -0.001]), + 3, + [ + 'Duration', + '0.00 ms', + '-1.00 ms', + '-1.00 ms', + 'down', + 'profiling', + ], + ]; + yield 'processingTime pair 173' => [ + self::summary(['processingTime' => -0.001]), + self::summary(['processingTime' => 0.001]), + 3, + [ + 'Duration', + '-1.00 ms', + '1.00 ms', + '+2.00 ms (+-200.0%)', + 'up', + 'profiling', + ], + ]; + yield 'processingTime pair 169' => [ + self::summary(['processingTime' => -0.001]), + self::summary(['processingTime' => 0.0]), + 3, + [ + 'Duration', + '-1.00 ms', + '0.00 ms', + '+1.00 ms (+-100.0%)', + 'up', + 'profiling', + ], + ]; + yield 'processingTime pair 135' => [ + self::summary(['processingTime' => 0.015]), + self::summary(['processingTime' => 0.015]), + 3, + [ + 'Duration', + '15.00 ms', + '15.00 ms', + 'No change', + 'neutral', + 'profiling', + ], + ]; + yield 'processingTime pair 131' => [ + self::summary(['processingTime' => 0.015]), + self::summary(['processingTime' => 0.001]), + 3, + [ + 'Duration', + '15.00 ms', + '1.00 ms', + '-14.00 ms (-93.3%)', + 'down', + 'profiling', + ], + ]; + yield 'processingTime pair 76' => [ + self::summary(['processingTime' => 0.001]), + self::summary(['processingTime' => 0.001004999999]), + 3, + [ + 'Duration', + '1.00 ms', + '1.00 ms', + '+0.00 ms (+0.5%)', + 'up', + 'profiling', + ], + ]; + yield 'processingTime pair 77' => [ + self::summary(['processingTime' => 0.001]), + self::summary(['processingTime' => 0.001005]), + 3, + [ + 'Duration', + '1.00 ms', + '1.01 ms', + '+0.01 ms (+0.5%)', + 'up', + 'profiling', + ], + ]; + yield 'processingTime pair 91' => [ + self::summary(['processingTime' => 0.001004999999]), + self::summary(['processingTime' => 0.001005]), + 3, + [ + 'Duration', + '1.00 ms', + '1.01 ms', + '+0.00 ms (+0.0%)', + 'up', + 'profiling', + ], + ]; + yield 'processingTime pair 106' => [ + self::summary(['processingTime' => 0.001005]), + self::summary(['processingTime' => 0.001005000001]), + 3, + [ + 'Duration', + '1.01 ms', + '1.01 ms', + '+0.00 ms (+0.0%)', + 'up', + 'profiling', + ], + ]; + yield 'processingTime pair 164' => [ + self::summary(['processingTime' => 0.30000000000000004]), + self::summary(['processingTime' => 0.3]), + 3, + [ + 'Duration', + '300.00 ms', + '300.00 ms', + '0.00 ms (0.0%)', + 'down', + 'profiling', + ], + ]; + yield 'processingTime pair 151' => [ + self::summary(['processingTime' => 0.3]), + self::summary(['processingTime' => 0.30000000000000004]), + 3, + [ + 'Duration', + '300.00 ms', + '300.00 ms', + '+0.00 ms (+0.0%)', + 'up', + 'profiling', + ], + ]; + yield 'peakMemory pair 7' => [ + self::summary(['peakMemory' => null]), + self::summary(['peakMemory' => 10485760000000]), + 4, + [ + 'Peak memory', + 'Not captured', + '10,000,000.00 MB', + 'Not comparable', + 'neutral', + 'profiling', + ], + ]; + yield 'peakMemory pair 56' => [ + self::summary(['peakMemory' => 10485760000000]), + self::summary(['peakMemory' => null]), + 4, + [ + 'Peak memory', + '10,000,000.00 MB', + 'Not captured', + 'Not comparable', + 'neutral', + 'profiling', + ], + ]; + yield 'peakMemory pair 10' => [ + self::summary(['peakMemory' => 0]), + self::summary(['peakMemory' => 1]), + 4, + [ + 'Peak memory', + '0.00 MB', + '0.00 MB', + '+0.00 MB', + 'up', + 'profiling', + ], + ]; + yield 'peakMemory pair 17' => [ + self::summary(['peakMemory' => 1]), + self::summary(['peakMemory' => 0]), + 4, + [ + 'Peak memory', + '0.00 MB', + '0.00 MB', + '0.00 MB (-100.0%)', + 'down', + 'profiling', + ], + ]; + yield 'peakMemory pair 37' => [ + self::summary(['peakMemory' => 5242]), + self::summary(['peakMemory' => 5243]), + 4, + [ + 'Peak memory', + '0.00 MB', + '0.01 MB', + '+0.00 MB (+0.0%)', + 'up', + 'profiling', + ], + ]; + yield 'peakMemory pair 44' => [ + self::summary(['peakMemory' => 5243]), + self::summary(['peakMemory' => 5242]), + 4, + [ + 'Peak memory', + '0.01 MB', + '0.00 MB', + '0.00 MB (0.0%)', + 'down', + 'profiling', + ], + ]; + yield 'peakMemory pair 54' => [ + self::summary(['peakMemory' => 1048576]), + self::summary(['peakMemory' => 1048576]), + 4, + [ + 'Peak memory', + '1.00 MB', + '1.00 MB', + 'No change', + 'neutral', + 'profiling', + ], + ]; + yield 'peakMemory pair 11' => [ + self::summary(['peakMemory' => 0]), + self::summary(['peakMemory' => -1]), + 4, + [ + 'Peak memory', + '0.00 MB', + '0.00 MB', + '0.00 MB', + 'down', + 'profiling', + ], + ]; + yield 'sqlCount pair 0' => [ + self::summary(['sqlCount' => 0]), + self::summary(['sqlCount' => 0]), + 5, + [ + 'SQL queries', + '0', + '0', + 'No change', + 'neutral', + 'db', + ], + ]; + yield 'sqlCount pair 4' => [ + self::summary(['sqlCount' => 0]), + self::summary(['sqlCount' => 1000]), + 5, + [ + 'SQL queries', + '0', + '1,000', + '+1,000', + 'up', + 'db', + ], + ]; + yield 'sqlCount pair 24' => [ + self::summary(['sqlCount' => 1000]), + self::summary(['sqlCount' => 0]), + 5, + [ + 'SQL queries', + '1,000', + '0', + '-1,000 (-100.0%)', + 'down', + 'db', + ], + ]; + yield 'sqlCount pair 11' => [ + self::summary(['sqlCount' => 1]), + self::summary(['sqlCount' => 1001]), + 5, + [ + 'SQL queries', + '1', + '1,001', + '+1,000 (+100,000.0%)', + 'up', + 'db', + ], + ]; + yield 'sqlCount pair 31' => [ + self::summary(['sqlCount' => 1001]), + self::summary(['sqlCount' => 1]), + 5, + [ + 'SQL queries', + '1,001', + '1', + '-1,000 (-99.9%)', + 'down', + 'db', + ], + ]; + yield 'sqlCount pair 21' => [ + self::summary(['sqlCount' => 2]), + self::summary(['sqlCount' => 2]), + 5, + [ + 'SQL queries', + '2', + '2', + 'No change', + 'neutral', + 'db', + ], + ]; + yield 'sqlCount pair 13' => [ + self::summary(['sqlCount' => -1]), + self::summary(['sqlCount' => 1]), + 5, + [ + 'SQL queries', + '-1', + '1', + '+2 (+-200.0%)', + 'up', + 'db', + ], + ]; + yield 'mailCount pair 2' => [ + self::summary(['mailCount' => 0]), + self::summary(['mailCount' => 9]), + 6, + [ + 'Mail messages', + '0', + '9', + '+9', + 'up', + 'mail', + ], + ]; + yield 'mailCount pair 7' => [ + self::summary(['mailCount' => 9]), + self::summary(['mailCount' => 1]), + 6, + [ + 'Mail messages', + '9', + '1', + '-8 (-88.9%)', + 'down', + 'mail', + ], + ]; + yield 'excessiveCallersCount pair 2' => [ + self::summary(['excessiveCallersCount' => 0]), + self::summary(['excessiveCallersCount' => 8]), + 7, + [ + 'Excessive DB callers', + '0', + '8', + '+8', + 'up', + 'db', + ], + ]; + yield 'excessiveCallersCount pair 7' => [ + self::summary(['excessiveCallersCount' => 8]), + self::summary(['excessiveCallersCount' => 1]), + 7, + [ + 'Excessive DB callers', + '8', + '1', + '-7 (-87.5%)', + 'down', + 'db', + ], + ]; + yield 'statusCode pair 1' => [ + self::summary(['statusCode' => 0]), + self::summary(['statusCode' => 200]), + 0, + [ + 'Status', + 'Not captured', + '200', + 'Changed', + 'neutral', + null, + ], + ]; + yield 'statusCode pair 6' => [ + self::summary(['statusCode' => 500]), + self::summary(['statusCode' => 0]), + 0, + [ + 'Status', + '500', + 'Not captured', + 'Changed', + 'neutral', + null, + ], + ]; + yield 'method pair 1' => [ + self::summary(['method' => '']), + self::summary(['method' => '0']), + 1, + [ + 'Method', + '', + '0', + 'Changed', + 'neutral', + null, + ], + ]; + yield 'method pair 4' => [ + self::summary(['method' => '0']), + self::summary(['method' => '']), + 1, + [ + 'Method', + '0', + '', + 'Changed', + 'neutral', + null, + ], + ]; + yield 'method pair 10' => [ + self::summary(['method' => 'GET']), + self::summary(['method' => 'GET']), + 1, + [ + 'Method', + 'GET', + 'GET', + 'No change', + 'neutral', + null, + ], + ]; + yield 'ajax pair 2' => [ + self::summary(['ajax' => true]), + self::summary(['ajax' => false]), + 2, + [ + 'AJAX', + 'Yes', + 'No', + 'Changed', + 'neutral', + null, + ], + ]; + yield 'ajax pair 0' => [ + self::summary(['ajax' => false]), + self::summary(['ajax' => false]), + 2, + [ + 'AJAX', + 'No', + 'No', + 'No change', + 'neutral', + null, + ], + ]; + } + + /** + * @return iterable}> + */ + public static function summaries(): iterable + { + yield 'equal absent metrics' => [ + self::summary([]), + self::summary([]), + [ + [ + 'Status', + 'Not captured', + 'Not captured', + 'No change', + 'neutral', + null, + ], + [ + 'Method', + '', + '', + 'No change', + 'neutral', + null, + ], + [ + 'AJAX', + 'No', + 'No', + 'No change', + 'neutral', + null, + ], + [ + 'Duration', + 'Not captured', + 'Not captured', + 'No change', + 'neutral', + 'profiling', + ], + [ + 'Peak memory', + 'Not captured', + 'Not captured', + 'No change', + 'neutral', + 'profiling', + ], + [ + 'SQL queries', + '0', + '0', + 'No change', + 'neutral', + 'db', + ], + [ + 'Mail messages', + '0', + '0', + 'No change', + 'neutral', + 'mail', + ], + [ + 'Excessive DB callers', + '0', + '0', + 'No change', + 'neutral', + 'db', + ], + ], + ]; + yield 'all metrics increase' => [ + self::summary( + [ + 'statusCode' => 200, + 'method' => 'GET', + 'ajax' => false, + 'processingTime' => 0.01, + 'peakMemory' => 1048576, + 'sqlCount' => 2, + 'mailCount' => 4, + 'excessiveCallersCount' => 6, + ], + ), + self::summary( + [ + 'statusCode' => 500, + 'method' => 'POST', + 'ajax' => true, + 'processingTime' => 0.015, + 'peakMemory' => 2097152, + 'sqlCount' => 5, + 'mailCount' => 7, + 'excessiveCallersCount' => 10, + ] + ), + [ + [ + 'Status', + '200', + '500', + 'Changed', + 'neutral', + null, + ], + [ + 'Method', + 'GET', + 'POST', + 'Changed', + 'neutral', + null, + ], + [ + 'AJAX', + 'No', + 'Yes', + 'Changed', + 'neutral', + null, + ], + [ + 'Duration', + '10.00 ms', + '15.00 ms', + '+5.00 ms (+50.0%)', + 'up', + 'profiling', + ], + [ + 'Peak memory', + '1.00 MB', + '2.00 MB', + '+1.00 MB (+100.0%)', + 'up', + 'profiling', + ], + [ + 'SQL queries', + '2', + '5', + '+3 (+150.0%)', + 'up', + 'db', + ], + [ + 'Mail messages', + '4', + '7', + '+3 (+75.0%)', + 'up', + 'mail', + ], + [ + 'Excessive DB callers', + '6', + '10', + '+4 (+66.7%)', + 'up', + 'db', + ], + ], + ]; + yield 'zero becomes captured' => [ + self::summary([]), + self::summary( + [ + 'processingTime' => 0.0, + 'peakMemory' => 0, + 'method' => '0', + ], + ), + [ + [ + 'Status', + 'Not captured', + 'Not captured', + 'No change', + 'neutral', + null, + ], + [ + 'Method', + '', + '0', + 'Changed', + 'neutral', + null, + ], + [ + 'AJAX', + 'No', + 'No', + 'No change', + 'neutral', + null, + ], + [ + 'Duration', + 'Not captured', + '0.00 ms', + 'Not comparable', + 'neutral', + 'profiling', + ], + [ + 'Peak memory', + 'Not captured', + '0.00 MB', + 'Not comparable', + 'neutral', + 'profiling', + ], + [ + 'SQL queries', + '0', + '0', + 'No change', + 'neutral', + 'db', + ], + [ + 'Mail messages', + '0', + '0', + 'No change', + 'neutral', + 'mail', + ], + [ + 'Excessive DB callers', + '0', + '0', + 'No change', + 'neutral', + 'db', + ], + ], + ]; + yield 'rounding boundary' => [ + self::summary( + [ + 'processingTime' => 0.001004999999, + 'peakMemory' => 5242, + ], + ), + self::summary( + [ + 'processingTime' => 0.001005, + 'peakMemory' => 5243, + ], + ), + [ + [ + 'Status', + 'Not captured', + 'Not captured', + 'No change', + 'neutral', + null, + ], + [ + 'Method', + '', + '', + 'No change', + 'neutral', + null, + ], + [ + 'AJAX', + 'No', + 'No', + 'No change', + 'neutral', + null, + ], + [ + 'Duration', + '1.00 ms', + '1.01 ms', + '+0.00 ms (+0.0%)', + 'up', + 'profiling', + ], + [ + 'Peak memory', + '0.00 MB', + '0.01 MB', + '+0.00 MB (+0.0%)', + 'up', + 'profiling', + ], + [ + 'SQL queries', + '0', + '0', + 'No change', + 'neutral', + 'db', + ], + [ + 'Mail messages', + '0', + '0', + 'No change', + 'neutral', + 'mail', + ], + [ + 'Excessive DB callers', + '0', + '0', + 'No change', + 'neutral', + 'db', + ], + ], + ]; + yield 'all metrics decrease' => [ + self::summary( + [ + 'statusCode' => 500, + 'method' => 'POST', + 'ajax' => true, + 'processingTime' => 0.015, + 'peakMemory' => 2097152, + 'sqlCount' => 5, + 'mailCount' => 7, + 'excessiveCallersCount' => 10, + ], + ), + self::summary( + [ + 'statusCode' => 200, + 'method' => 'GET', + 'ajax' => false, + 'processingTime' => 0.01, + 'peakMemory' => 1048576, + 'sqlCount' => 2, + 'mailCount' => 4, + 'excessiveCallersCount' => 6, + ] + ), + [ + [ + 'Status', + '500', + '200', + 'Changed', + 'neutral', + null, + ], + [ + 'Method', + 'POST', + 'GET', + 'Changed', + 'neutral', + null, + ], + [ + 'AJAX', + 'Yes', + 'No', + 'Changed', + 'neutral', + null, + ], + [ + 'Duration', + '15.00 ms', + '10.00 ms', + '-5.00 ms (-33.3%)', + 'down', + 'profiling', + ], + [ + 'Peak memory', + '2.00 MB', + '1.00 MB', + '-1.00 MB (-50.0%)', + 'down', + 'profiling', + ], + [ + 'SQL queries', + '5', + '2', + '-3 (-60.0%)', + 'down', + 'db', + ], + [ + 'Mail messages', + '7', + '4', + '-3 (-42.9%)', + 'down', + 'mail', + ], + [ + 'Excessive DB callers', + '10', + '6', + '-4 (-40.0%)', + 'down', + 'db', + ], + ], + ]; + yield 'captured zero becomes absent' => [ + self::summary( + [ + 'processingTime' => 0.0, + 'peakMemory' => 0, + 'method' => '0', + ], + ), + self::summary([]), + [ + [ + 'Status', + 'Not captured', + 'Not captured', + 'No change', + 'neutral', + null, + ], + [ + 'Method', + '0', + '', + 'Changed', + 'neutral', + null, + ], + [ + 'AJAX', + 'No', + 'No', + 'No change', + 'neutral', + null, + ], + [ + 'Duration', + '0.00 ms', + 'Not captured', + 'Not comparable', + 'neutral', + 'profiling', + ], + [ + 'Peak memory', + '0.00 MB', + 'Not captured', + 'Not comparable', + 'neutral', + 'profiling', + ], + [ + 'SQL queries', + '0', + '0', + 'No change', + 'neutral', + 'db', + ], + [ + 'Mail messages', + '0', + '0', + 'No change', + 'neutral', + 'mail', + ], + [ + 'Excessive DB callers', + '0', + '0', + 'No change', + 'neutral', + 'db', + ], + ], + ]; + } + + /** + * @param array $values + */ + private static function summary(array $values): RequestSummary + { + return RequestSummary::fromArray(array_replace(RequestSummary::create('sample')->jsonSerialize(), $values)); + } +}