Skip to content

Commit 8799c23

Browse files
committed
feat: add memory usage in PHPUnit telemetry data
1 parent 95348f5 commit 8799c23

12 files changed

Lines changed: 317 additions & 13 deletions

File tree

documentation/components/bridges/phpunit-telemetry-bridge.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Add the extension to your `phpunit.xml.dist`:
3030
<parameter name="emit_metrics" value="true"/>
3131
<parameter name="emit_test_spans" value="true"/>
3232
<parameter name="emit_test_case_spans" value="true"/>
33+
<parameter name="memory_real_usage" value="false"/>
3334
</bootstrap>
3435
</extensions>
3536
```
@@ -48,6 +49,7 @@ Add the extension to your `phpunit.xml.dist`:
4849
| `emit_metrics` | `FLOW_PHPUNIT_OTEL_EMIT_METRICS` | `true` | Enable/disable metric emission |
4950
| `emit_test_spans` | `FLOW_PHPUNIT_OTEL_EMIT_TEST_SPANS` | `true` | Create individual spans for each test |
5051
| `emit_test_case_spans` | `FLOW_PHPUNIT_OTEL_EMIT_TEST_CASE_SPANS` | `true` | Create spans for test case classes |
52+
| `memory_real_usage` | `FLOW_PHPUNIT_OTEL_MEMORY_REAL_USAGE` | `false` | Report total system-allocated memory (`real_usage=true`) instead of emalloc usage for memory metrics/attributes |
5153
| `batch_size` | `FLOW_PHPUNIT_OTEL_BATCH_SIZE` | `512` | Items per batch for span/metric/log batching processors |
5254
| `shutdown_timeout_ms` | `FLOW_PHPUNIT_OTEL_SHUTDOWN_TIMEOUT_MS` | `5000` | Wall-clock budget in ms for draining pending requests at shutdown (curl/grpc) |
5355
| `error_handler` | `FLOW_PHPUNIT_OTEL_ERROR_HANDLER` | `error_log` | How telemetry errors are surfaced (see [Error Handlers](#error-handlers)) |

src/bridge/phpunit/telemetry/src/Flow/Bridge/PHPUnit/Telemetry/Configuration.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ public function __construct(
136136
public bool $emitTestCaseSpans,
137137
public int $batchSize,
138138
public ErrorLogHandlerConfig|NullErrorHandlerConfig|StreamErrorHandlerConfig|SyslogErrorHandlerConfig|UdpSyslogErrorHandlerConfig $errorHandler,
139+
public bool $memoryRealUsage = false,
139140
) {}
140141

141142
public static function fromParameters(ParameterCollection $parameters): self
@@ -165,6 +166,7 @@ public static function fromParameters(ParameterCollection $parameters): self
165166
emitTestCaseSpans: self::resolveBool($parameters, 'emit_test_case_spans', true),
166167
batchSize: self::resolveInt($parameters, 'batch_size', self::DEFAULT_BATCH_SIZE),
167168
errorHandler: self::resolveErrorHandler($parameters),
169+
memoryRealUsage: self::resolveBool($parameters, 'memory_real_usage', false),
168170
);
169171
}
170172

src/bridge/phpunit/telemetry/src/Flow/Bridge/PHPUnit/Telemetry/Subscriber/TestFinishedSubscriber.php

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Flow\Bridge\PHPUnit\Telemetry\Configuration;
88
use Flow\Bridge\PHPUnit\Telemetry\SpanStack;
9+
use Flow\Bridge\PHPUnit\Telemetry\TestMemoryRegistry;
910
use Flow\Bridge\PHPUnit\Telemetry\TestStatusRegistry;
1011
use Flow\Telemetry\PackageVersion;
1112
use Flow\Telemetry\Telemetry;
@@ -14,13 +15,17 @@
1415
use PHPUnit\Event\Test\FinishedSubscriber;
1516
use Throwable;
1617

18+
use function memory_get_peak_usage;
19+
use function memory_get_usage;
20+
1721
final readonly class TestFinishedSubscriber implements FinishedSubscriber
1822
{
1923
public function __construct(
2024
private Telemetry $telemetry,
2125
private SpanStack $spanStack,
2226
private Configuration $config,
2327
private TestStatusRegistry $statusRegistry,
28+
private TestMemoryRegistry $memoryRegistry,
2429
) {}
2530

2631
public function notify(Finished $event): void
@@ -29,11 +34,25 @@ public function notify(Finished $event): void
2934
$testId = $event->test()->id();
3035
$status = $this->statusRegistry->getStatus($testId);
3136

37+
$startBytes = $this->memoryRegistry->getStart($testId);
38+
$peakBytes = memory_get_peak_usage($this->config->memoryRealUsage);
39+
$deltaBytes = $startBytes !== null ? memory_get_usage($this->config->memoryRealUsage) - $startBytes : null;
40+
$this->memoryRegistry->clear($testId);
41+
3242
if (!$this->config->emitTestSpans) {
3343
if ($this->config->emitMetrics) {
3444
$meter = $this->telemetry->meter('phpunit', PackageVersion::get('phpunit/phpunit'));
3545

3646
$meter->createCounter('phpunit.test.count')->add(1, ['test.status' => $status]);
47+
$meter->createHistogram('phpunit.test.memory.peak', 'bytes')->record($peakBytes, [
48+
'test.status' => $status,
49+
]);
50+
51+
if ($deltaBytes !== null) {
52+
$meter->createHistogram('phpunit.test.memory.delta', 'bytes')->record($deltaBytes, [
53+
'test.status' => $status,
54+
]);
55+
}
3756
}
3857

3958
$this->statusRegistry->clear($testId);
@@ -63,18 +82,38 @@ public function notify(Finished $event): void
6382
$span->setAttribute('exception.message', $errorMessage);
6483
}
6584

85+
$span->setAttribute('test.memory.peak_bytes', $peakBytes);
86+
87+
if ($deltaBytes !== null) {
88+
$span->setAttribute('test.memory.delta_bytes', $deltaBytes);
89+
}
90+
6691
if ($status === 'passed') {
6792
$span->setStatus(SpanStatus::ok());
6893
} else {
6994
$span->setStatus(SpanStatus::error($errorMessage ?? $status));
7095
}
7196

72-
if ($this->config->emitMetrics && $duration !== null) {
97+
if ($this->config->emitMetrics) {
7398
$meter = $this->telemetry->meter('phpunit', PackageVersion::get('phpunit/phpunit'));
7499

75-
$meter->createHistogram('phpunit.test.duration', 'ms')->record($duration, ['test.status' => $status]);
100+
if ($duration !== null) {
101+
$meter->createHistogram('phpunit.test.duration', 'ms')->record($duration, [
102+
'test.status' => $status,
103+
]);
76104

77-
$meter->createCounter('phpunit.test.count')->add(1, ['test.status' => $status]);
105+
$meter->createCounter('phpunit.test.count')->add(1, ['test.status' => $status]);
106+
}
107+
108+
$meter->createHistogram('phpunit.test.memory.peak', 'bytes')->record($peakBytes, [
109+
'test.status' => $status,
110+
]);
111+
112+
if ($deltaBytes !== null) {
113+
$meter->createHistogram('phpunit.test.memory.delta', 'bytes')->record($deltaBytes, [
114+
'test.status' => $status,
115+
]);
116+
}
78117
}
79118

80119
$tracer->complete($span);

src/bridge/phpunit/telemetry/src/Flow/Bridge/PHPUnit/Telemetry/Subscriber/TestPreparationStartedSubscriber.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,33 @@
66

77
use Flow\Bridge\PHPUnit\Telemetry\Configuration;
88
use Flow\Bridge\PHPUnit\Telemetry\SpanStack;
9+
use Flow\Bridge\PHPUnit\Telemetry\TestMemoryRegistry;
910
use Flow\Telemetry\PackageVersion;
1011
use Flow\Telemetry\Telemetry;
1112
use PHPUnit\Event\Test\PreparationStarted;
1213
use PHPUnit\Event\Test\PreparationStartedSubscriber;
1314
use Throwable;
1415

16+
use function memory_get_usage;
17+
use function memory_reset_peak_usage;
18+
1519
final readonly class TestPreparationStartedSubscriber implements PreparationStartedSubscriber
1620
{
1721
public function __construct(
1822
private Telemetry $telemetry,
1923
private SpanStack $spanStack,
2024
private Configuration $config,
25+
private TestMemoryRegistry $memoryRegistry,
2126
) {}
2227

2328
public function notify(PreparationStarted $event): void
2429
{
2530
try {
31+
if ($this->config->emitTestSpans || $this->config->emitMetrics) {
32+
memory_reset_peak_usage();
33+
$this->memoryRegistry->setStart($event->test()->id(), memory_get_usage($this->config->memoryRealUsage));
34+
}
35+
2636
if (!$this->config->emitTestSpans) {
2737
return;
2838
}

src/bridge/phpunit/telemetry/src/Flow/Bridge/PHPUnit/Telemetry/TelemetryExtension.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,18 @@ public function bootstrap(
3131
$telemetry = TelemetryFactory::create($config);
3232
$spanStack = new SpanStack();
3333
$statusRegistry = new TestStatusRegistry();
34+
$memoryRegistry = new TestMemoryRegistry();
3435

3536
$facade->registerSubscribers(
3637
new TestSuiteStartedSubscriber($telemetry, $spanStack, $config),
3738
new TestSuiteFinishedSubscriber($telemetry, $spanStack, $config),
38-
new TestPreparationStartedSubscriber($telemetry, $spanStack, $config),
39+
new TestPreparationStartedSubscriber($telemetry, $spanStack, $config, $memoryRegistry),
3940
new TestPassedSubscriber($statusRegistry),
4041
new TestFailedSubscriber($statusRegistry),
4142
new TestErroredSubscriber($statusRegistry),
4243
new TestSkippedSubscriber($statusRegistry),
4344
new TestMarkedIncompleteSubscriber($statusRegistry),
44-
new TestFinishedSubscriber($telemetry, $spanStack, $config, $statusRegistry),
45+
new TestFinishedSubscriber($telemetry, $spanStack, $config, $statusRegistry, $memoryRegistry),
4546
);
4647
} catch (Throwable) {
4748
// Silent failure - telemetry must never break tests
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\Bridge\PHPUnit\Telemetry;
6+
7+
final class TestMemoryRegistry
8+
{
9+
/**
10+
* @var array<string, int>
11+
*/
12+
private array $startBytes = [];
13+
14+
public function clear(string $testId): void
15+
{
16+
unset($this->startBytes[$testId]);
17+
}
18+
19+
public function getStart(string $testId): ?int
20+
{
21+
return $this->startBytes[$testId] ?? null;
22+
}
23+
24+
public function setStart(string $testId, int $bytes): void
25+
{
26+
$this->startBytes[$testId] = $bytes;
27+
}
28+
}

src/bridge/phpunit/telemetry/tests/Flow/Bridge/PHPUnit/Telemetry/Tests/Mother/ConfigurationMother.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,19 @@ public static function withDisabledTraces(): Configuration
139139
errorHandler: self::defaultErrorHandler(),
140140
);
141141
}
142+
143+
public static function withRealMemoryUsage(): Configuration
144+
{
145+
return new Configuration(
146+
serviceName: 'phpunit',
147+
transport: self::defaultTransport(),
148+
emitTraces: true,
149+
emitMetrics: true,
150+
emitTestSpans: true,
151+
emitTestCaseSpans: true,
152+
batchSize: Configuration::DEFAULT_BATCH_SIZE,
153+
errorHandler: self::defaultErrorHandler(),
154+
memoryRealUsage: true,
155+
);
156+
}
142157
}

src/bridge/phpunit/telemetry/tests/Flow/Bridge/PHPUnit/Telemetry/Tests/Mother/TelemetryMother.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Flow\Bridge\PHPUnit\Telemetry\Tests\Mother;
66

77
use Flow\Telemetry\Provider\Clock\SystemClock;
8+
use Flow\Telemetry\Provider\Memory\MemoryMetricProcessor;
89
use Flow\Telemetry\Provider\Memory\MemorySpanProcessor;
910
use Flow\Telemetry\Telemetry;
1011
use Flow\Telemetry\Tests\Mother\ResourceMother;
@@ -20,19 +21,26 @@
2021

2122
final class TelemetryMother
2223
{
23-
public static function create(?MemorySpanProcessor $spanProcessor = null): Telemetry
24-
{
24+
public static function create(
25+
?MemorySpanProcessor $spanProcessor = null,
26+
?MemoryMetricProcessor $metricProcessor = null,
27+
): Telemetry {
2528
$clock = new SystemClock();
2629
$contextStorage = memory_context_storage();
2730

2831
return new Telemetry(
2932
ResourceMother::default(),
3033
tracer_provider($spanProcessor ?? memory_span_processor(void_exporter()), $clock, $contextStorage),
31-
meter_provider(memory_metric_processor(void_exporter()), $clock),
34+
meter_provider($metricProcessor ?? memory_metric_processor(void_exporter()), $clock),
3235
logger_provider(memory_log_processor(void_exporter()), $clock, $contextStorage),
3336
);
3437
}
3538

39+
public static function withMetricProcessor(MemoryMetricProcessor $metricProcessor): Telemetry
40+
{
41+
return self::create(metricProcessor: $metricProcessor);
42+
}
43+
3644
public static function withSpanProcessor(MemorySpanProcessor $spanProcessor): Telemetry
3745
{
3846
return self::create($spanProcessor);

src/bridge/phpunit/telemetry/tests/Flow/Bridge/PHPUnit/Telemetry/Tests/Unit/ConfigurationTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ public function test_default_configuration_uses_curl_transport_with_localhost_en
252252
static::assertTrue($config->emitMetrics);
253253
static::assertTrue($config->emitTestSpans);
254254
static::assertTrue($config->emitTestCaseSpans);
255+
static::assertFalse($config->memoryRealUsage);
255256

256257
static::assertInstanceOf(CurlTransportConfig::class, $config->transport);
257258
static::assertSame('http://localhost:4318', $config->transport->endpoint);
@@ -300,6 +301,15 @@ public function test_emit_flags_can_be_disabled(): void
300301
static::assertFalse($config->emitTestCaseSpans);
301302
}
302303

304+
public function test_memory_real_usage_can_be_enabled(): void
305+
{
306+
$config = Configuration::fromParameters(ParameterCollection::fromArray([
307+
'memory_real_usage' => 'true',
308+
]));
309+
310+
static::assertTrue($config->memoryRealUsage);
311+
}
312+
303313
public function test_empty_env_superglobal_treated_as_unset(): void
304314
{
305315
$_ENV['FLOW_PHPUNIT_OTEL_ENDPOINT'] = '';

0 commit comments

Comments
 (0)