From 68160078c4b755454b4659ac1246a81eca9ec008 Mon Sep 17 00:00:00 2001 From: Chad Sikorra Date: Sat, 18 Jul 2026 15:04:29 -0400 Subject: [PATCH 1/4] Cap the query at the look-through. --- .../ServerSearchHandler.php | 9 +++--- .../Backend/Storage/Adapter/PdoStorage.php | 4 +-- tests/performance/Workload/Worker.php | 2 +- .../ServerSearchHandlerTest.php | 28 ++++++++++++++++ .../Storage/Adapter/PdoStorageTest.php | 32 +++++++++++++++++++ 5 files changed, 68 insertions(+), 7 deletions(-) diff --git a/src/FreeDSx/Ldap/Protocol/ServerProtocolHandler/ServerSearchHandler.php b/src/FreeDSx/Ldap/Protocol/ServerProtocolHandler/ServerSearchHandler.php index fe7b2c4c..f53671f5 100644 --- a/src/FreeDSx/Ldap/Protocol/ServerProtocolHandler/ServerSearchHandler.php +++ b/src/FreeDSx/Ldap/Protocol/ServerProtocolHandler/ServerSearchHandler.php @@ -142,15 +142,16 @@ private function filteredEntryStream( continue; } - yield $projection->project($filtered); - $emitted++; - $state->entriesReturned++; - + // A further match past the cap proves overflow: signal without emitting it. if ($sizeLimit > 0 && $emitted >= $sizeLimit) { $state->resultCode = ResultCode::SIZE_LIMIT_EXCEEDED; return; } + + yield $projection->project($filtered); + $emitted++; + $state->entriesReturned++; } } } diff --git a/src/FreeDSx/Ldap/Server/Backend/Storage/Adapter/PdoStorage.php b/src/FreeDSx/Ldap/Server/Backend/Storage/Adapter/PdoStorage.php index 6b4cf7d1..67c9ad12 100644 --- a/src/FreeDSx/Ldap/Server/Backend/Storage/Adapter/PdoStorage.php +++ b/src/FreeDSx/Ldap/Server/Backend/Storage/Adapter/PdoStorage.php @@ -189,10 +189,10 @@ public function list(StorageListOptions $options): EntryStream $isPreFiltered = $filterResult !== null && $filterResult->isExact; - // Exact is bound by sizeLimit. Inexact is PHP re-evaluated: cap candidate transfer at lookthrough+1. + // Exact is bound by the client sizeLimit; otherwise cap candidate transfer at lookthrough+1. $sqlLimit = match (true) { $isPreFiltered && $options->sizeLimit > 0 => $options->sizeLimit, - !$isPreFiltered && $options->lookthroughLimit > 0 => $options->lookthroughLimit + 1, + $options->lookthroughLimit > 0 => $options->lookthroughLimit + 1, default => null, }; diff --git a/tests/performance/Workload/Worker.php b/tests/performance/Workload/Worker.php index 6481f136..c909504c 100644 --- a/tests/performance/Workload/Worker.php +++ b/tests/performance/Workload/Worker.php @@ -234,7 +234,7 @@ private function doSearchList(LdapClient $client): void { $request = Operations::search(Filters::equal('objectClass', 'inetOrgPerson')) ->base($this->config->writeBase) - ->useSubtreeScope(); + ->useSingleLevelScope(); $client->search($request); } diff --git a/tests/unit/Protocol/ServerProtocolHandler/ServerSearchHandlerTest.php b/tests/unit/Protocol/ServerProtocolHandler/ServerSearchHandlerTest.php index 1ae32485..26933ea3 100644 --- a/tests/unit/Protocol/ServerProtocolHandler/ServerSearchHandlerTest.php +++ b/tests/unit/Protocol/ServerProtocolHandler/ServerSearchHandlerTest.php @@ -236,6 +236,34 @@ public function test_it_should_return_size_limit_exceeded_with_partial_results_w ]); } + public function test_it_should_succeed_when_matches_equal_the_size_limit_exactly(): void + { + $entry1 = Entry::create('dc=foo,dc=bar', ['cn' => 'foo']); + + $search = new LdapMessageRequest( + 2, + (new SearchRequest(Filters::equal('foo', 'bar'))) + ->base('dc=foo,dc=bar') + ->sizeLimit(1), + ); + + $this->mockBackend + ->expects(self::once()) + ->method('search') + ->willReturn(new EntryStream($this->makeGenerator($entry1))); + + $this->mockFilterEvaluator + ->method('evaluate') + ->willReturn(true); + + $this->drive($this->subject, $search); + + $this->assertSentMessages([ + new LdapMessageResponse(2, new SearchResultEntry($entry1)), + new LdapMessageResponse(2, new SearchResultDone(0, 'dc=foo,dc=bar')), + ]); + } + public function test_it_should_not_enforce_size_limit_when_zero(): void { $entry1 = Entry::create('dc=foo,dc=bar', ['cn' => 'foo']); diff --git a/tests/unit/Server/Backend/Storage/Adapter/PdoStorageTest.php b/tests/unit/Server/Backend/Storage/Adapter/PdoStorageTest.php index fc2f045e..37b6f512 100644 --- a/tests/unit/Server/Backend/Storage/Adapter/PdoStorageTest.php +++ b/tests/unit/Server/Backend/Storage/Adapter/PdoStorageTest.php @@ -622,6 +622,38 @@ public function test_search_exact_filter_is_not_subject_to_lookthrough(): void ); } + public function test_search_exact_filter_bounds_transfer_at_lookthrough(): void + { + $storage = PdoStorageFactory::forPcntl(PdoConfig::forSqlite(':memory:')); + $backend = new WritableStorageBackend( + $storage, + new SearchLimits(maxSearchLookthrough: 2), + ); + $backend->add( + new AddCommand(new Entry(new Dn('dc=example,dc=com'), new Attribute('dc', 'example'))), + $this->systemContext(), + ); + foreach (['Ann', 'Bob', 'Cyd', 'Dan', 'Eve'] as $cn) { + $backend->add( + new AddCommand(new Entry( + new Dn("cn={$cn},dc=example,dc=com"), + new Attribute('cn', $cn), + new Attribute('st', 'dup'), + )), + $this->context(), + ); + } + + $request = (new SearchRequest(Filters::equal('st', 'dup'))) + ->base('dc=example,dc=com') + ->useSubtreeScope(); + + self::assertCount( + 3, + iterator_to_array($backend->search($request)->entries), + ); + } + public function test_atomic_rolls_back_on_exception(): void { $threw = false; From 6e27506ec7e1400611f4bc546e752debe6f6a801 Mon Sep 17 00:00:00 2001 From: Chad Sikorra Date: Sat, 18 Jul 2026 16:24:04 -0400 Subject: [PATCH 2/4] Add more options to the load test command. Surface actual server side numbers via the monitor and not just the client side numbers. --- tests/performance/Config.php | 11 ++++++ tests/performance/LoadTestCommand.php | 40 +++++++++++++++++++ tests/performance/Server/ServerManager.php | 2 + tests/performance/Workload/Worker.php | 44 ++++++++++++++++----- tests/support/LdapBackendStorageCommand.php | 27 ++++++++++++- 5 files changed, 113 insertions(+), 11 deletions(-) diff --git a/tests/performance/Config.php b/tests/performance/Config.php index 6ef62cc4..f5bca0dc 100644 --- a/tests/performance/Config.php +++ b/tests/performance/Config.php @@ -66,6 +66,11 @@ final class Config public const DEFAULT_SEARCH_SUB_SIZE_LIMIT = 500; + /** + * Mirrors the realistic ServerOptions default so load tests run bounded rather than with lookthrough disabled. + */ + public const DEFAULT_MAX_SEARCH_LOOKTHROUGH = 5000; + public function __construct( public readonly string $backend, public readonly string $runner, @@ -87,6 +92,10 @@ public function __construct( public readonly bool $jit = true, public readonly int $searchSubSizeLimit = self::DEFAULT_SEARCH_SUB_SIZE_LIMIT, public readonly bool $monitor = false, + public readonly ?string $searchAttributes = null, + public readonly bool $attributesOnly = false, + public readonly int $seedAttributes = 0, + public readonly int $maxSearchLookthrough = self::DEFAULT_MAX_SEARCH_LOOKTHROUGH, ) { $this->assertEnum('backend', $backend, self::BACKENDS); $this->assertEnum('runner', $runner, self::RUNNERS); @@ -97,6 +106,8 @@ public function __construct( $this->assertNonNegative('warmup', $warmup); $this->assertNonNegative('seed-entries', $seedEntries); $this->assertNonNegative('search-sub-size-limit', $searchSubSizeLimit); + $this->assertNonNegative('seed-attributes', $seedAttributes); + $this->assertNonNegative('max-search-lookthrough', $maxSearchLookthrough); if ($duration !== null) { $this->assertPositive('duration', $duration); diff --git a/tests/performance/LoadTestCommand.php b/tests/performance/LoadTestCommand.php index 434b78ee..04a0dadc 100644 --- a/tests/performance/LoadTestCommand.php +++ b/tests/performance/LoadTestCommand.php @@ -211,6 +211,32 @@ protected function configure(): void InputOption::VALUE_REQUIRED, 'Per-request size limit applied to search-sub ops (0 = unlimited)', (string) Config::DEFAULT_SEARCH_SUB_SIZE_LIMIT, + ) + ->addOption( + 'search-attributes', + null, + InputOption::VALUE_REQUIRED, + 'Attributes requested by search ops: a CSV list, ALL (default), or 1.1 for no attributes.', + ) + ->addOption( + 'attributes-only', + null, + InputOption::VALUE_NONE, + 'Request attribute descriptions without values (attributesOnly) on search ops.', + ) + ->addOption( + 'seed-attributes', + null, + InputOption::VALUE_REQUIRED, + 'Filler attributes added to each seeded entry to widen the return path (0 = none).', + '0', + ) + ->addOption( + 'max-search-lookthrough', + null, + InputOption::VALUE_REQUIRED, + 'Max entries examined per search before adminLimitExceeded (0 = unbounded; default mirrors the server).', + (string) Config::DEFAULT_MAX_SEARCH_LOOKTHROUGH, ); } @@ -349,6 +375,7 @@ private function renderMonitorEntry( 'operationsCompleted', 'operationsFailed', 'operationsByType', + 'operationsAvgLatencyMsByType', 'operationsByResultCode', 'bindsByMethod', 'searchesByScope', @@ -410,9 +437,22 @@ private function buildConfig(InputInterface $input): Config jit: !(bool) $input->getOption('no-jit') && $runner !== 'pcntl', searchSubSizeLimit: $this->requireInt($input, 'search-sub-size-limit'), monitor: (bool) $input->getOption('monitor'), + searchAttributes: $this->optionalString($input, 'search-attributes'), + attributesOnly: (bool) $input->getOption('attributes-only'), + seedAttributes: $this->requireInt($input, 'seed-attributes'), + maxSearchLookthrough: $this->requireInt($input, 'max-search-lookthrough'), ); } + private function optionalString(InputInterface $input, string $name): ?string + { + $value = $input->getOption($name); + + return is_string($value) && $value !== '' + ? $value + : null; + } + private function requireString(InputInterface $input, string $name): string { $value = $input->getOption($name); diff --git a/tests/performance/Server/ServerManager.php b/tests/performance/Server/ServerManager.php index b887c4ad..f648c6e3 100644 --- a/tests/performance/Server/ServerManager.php +++ b/tests/performance/Server/ServerManager.php @@ -58,6 +58,8 @@ public function start(): void $command[] = '--runner=' . $this->config->runner; $command[] = '--port=' . $this->config->port; $command[] = '--seed-entries=' . $this->config->seedEntries; + $command[] = '--seed-attributes=' . $this->config->seedAttributes; + $command[] = '--max-search-lookthrough=' . $this->config->maxSearchLookthrough; if ($this->config->monitor) { $command[] = '--monitor'; diff --git a/tests/performance/Workload/Worker.php b/tests/performance/Workload/Worker.php index c909504c..35a55686 100644 --- a/tests/performance/Workload/Worker.php +++ b/tests/performance/Workload/Worker.php @@ -19,6 +19,7 @@ use FreeDSx\Ldap\Entry\Entry; use FreeDSx\Ldap\Exception\OperationException; use FreeDSx\Ldap\LdapClient; +use FreeDSx\Ldap\Operation\Request\SearchRequest; use FreeDSx\Ldap\Operation\ResultCode; use FreeDSx\Ldap\Operations; use FreeDSx\Ldap\Search\Filter\FilterInterface; @@ -197,7 +198,7 @@ private function doBind(LdapClient $client): void private function doSearchRead(LdapClient $client): void { - $request = Operations::search(Filters::present('objectClass')) + $request = $this->newSearch(Filters::present('objectClass')) ->base($this->randomReadDn()) ->useBaseScope(); @@ -206,7 +207,7 @@ private function doSearchRead(LdapClient $client): void private function doSearchEq(LdapClient $client): void { - $request = Operations::search($this->randomEqualityFilter()) + $request = $this->newSearch($this->randomEqualityFilter()) ->base($this->config->baseDn) ->useSubtreeScope(); @@ -219,7 +220,7 @@ private function doSearchSub(LdapClient $client): void ? Filters::startsWith('cn', 'seed-') : Filters::startsWith('cn', ''); - $request = Operations::search($filter) + $request = $this->newSearch($filter) ->base($this->config->baseDn) ->useSubtreeScope(); @@ -232,7 +233,7 @@ private function doSearchSub(LdapClient $client): void private function doSearchList(LdapClient $client): void { - $request = Operations::search(Filters::equal('objectClass', 'inetOrgPerson')) + $request = $this->newSearch(Filters::equal('objectClass', 'inetOrgPerson')) ->base($this->config->writeBase) ->useSingleLevelScope(); @@ -246,7 +247,7 @@ private function doSearchSubstr(LdapClient $client): void : Filters::contains('cn', 'eed'); $client->search( - Operations::search($filter) + $this->newSearch($filter) ->base($this->config->baseDn) ->useSubtreeScope(), ); @@ -259,7 +260,7 @@ private function doSearchSuffix(LdapClient $client): void : Filters::endsWith('cn', 'e'); $client->search( - Operations::search($filter) + $this->newSearch($filter) ->base($this->config->baseDn) ->useSubtreeScope(), ); @@ -272,7 +273,7 @@ private function doSearchRange(LdapClient $client): void : 1000; $client->search( - Operations::search(Filters::greaterThanOrEqual('uidNumber', (string) $threshold)) + $this->newSearch(Filters::greaterThanOrEqual('uidNumber', (string) $threshold)) ->base($this->config->baseDn) ->useSubtreeScope(), ); @@ -289,7 +290,7 @@ private function doSearchAnd(LdapClient $client): void ); $client->search( - Operations::search($filter) + $this->newSearch($filter) ->base($this->config->baseDn) ->useSubtreeScope(), ); @@ -306,12 +307,37 @@ private function doSearchOr(LdapClient $client): void ); $client->search( - Operations::search($filter) + $this->newSearch($filter) ->base($this->config->baseDn) ->useSubtreeScope(), ); } + /** + * Builds a search request, applying the configured attribute selection so the return path can be varied. + */ + private function newSearch(FilterInterface $filter): SearchRequest + { + $request = Operations::search($filter); + $attributes = $this->config->searchAttributes; + + if ($attributes !== null && strcasecmp($attributes, 'ALL') !== 0) { + $request->select(...array_map( + 'trim', + explode( + ',', + $attributes, + ), + )); + } + + if ($this->config->attributesOnly) { + $request->setAttributesOnly(true); + } + + return $request; + } + private function randomSeedIdx(): int { return mt_rand( diff --git a/tests/support/LdapBackendStorageCommand.php b/tests/support/LdapBackendStorageCommand.php index 461e2994..4057d4ba 100644 --- a/tests/support/LdapBackendStorageCommand.php +++ b/tests/support/LdapBackendStorageCommand.php @@ -77,6 +77,13 @@ protected function configure(): void 'Number of additional seed entries to generate', '0', ) + ->addOption( + 'seed-attributes', + null, + InputOption::VALUE_REQUIRED, + 'Filler attributes added to each seed entry to widen the return path (0 = none)', + '0', + ) ->addOption( 'validation-mode', null, @@ -128,6 +135,7 @@ protected function execute( $runner = $this->getStringOption($input, 'runner'); $port = (int) $this->getStringOption($input, 'port'); $seedEntries = (int) $this->getStringOption($input, 'seed-entries'); + $seedAttributes = (int) $this->getStringOption($input, 'seed-attributes'); if (!in_array($storage, ['memory', 'json', 'sqlite', 'mysql'], true)) { $io->error("Invalid --storage value: {$storage}. Expected one of: memory, json, sqlite, mysql."); @@ -147,6 +155,12 @@ protected function execute( return Command::FAILURE; } + if ($seedAttributes < 0) { + $io->error("Invalid --seed-attributes value: {$seedAttributes}. Must be zero or greater."); + + return Command::FAILURE; + } + $validationMode = match ($this->getStringOption($input, 'validation-mode')) { 'strict' => SchemaValidationMode::Strict, 'lenient' => SchemaValidationMode::Lenient, @@ -197,13 +211,22 @@ protected function execute( ]; for ($i = 1; $i <= $seedEntries; $i++) { - $entries[] = new Entry( - new Dn("cn=seed-{$i},ou=people,dc=foo,dc=bar"), + $attributes = [ new Attribute('cn', "seed-{$i}"), new Attribute('objectClass', 'inetOrgPerson', 'extensibleObject'), new Attribute('sn', 'Seeded'), new Attribute('mail', "seed-{$i}@foo.bar"), new Attribute('uidNumber', (string) (1000 + $i)), + ]; + + // Distinct option subtypes of a defined, non-filtered, non-indexed base type widen the return path. + for ($k = 1; $k <= $seedAttributes; $k++) { + $attributes[] = new Attribute("initials;filler-{$k}", "filler-{$i}-{$k}"); + } + + $entries[] = new Entry( + new Dn("cn=seed-{$i},ou=people,dc=foo,dc=bar"), + ...$attributes, ); } From ff201f14cfad60bc0c3e652f077fb3ed0417558c Mon Sep 17 00:00:00 2001 From: Chad Sikorra Date: Sat, 18 Jul 2026 16:50:04 -0400 Subject: [PATCH 3/4] Batch metric roll-ups for cn=monitor so it scales for load properly (otherwise it starts to fail due to backpressure and causes lots of latency). --- .../Rollup/OperationRollupCoordinator.php | 41 +++++++++++++-- .../Rollup/OperationRollupCoordinatorTest.php | 50 +++++++++++++++++-- .../Middleware/MetricsMiddlewareTest.php | 2 + 3 files changed, 84 insertions(+), 9 deletions(-) diff --git a/src/FreeDSx/Ldap/Server/Metrics/Rollup/OperationRollupCoordinator.php b/src/FreeDSx/Ldap/Server/Metrics/Rollup/OperationRollupCoordinator.php index c0264da2..676f56da 100644 --- a/src/FreeDSx/Ldap/Server/Metrics/Rollup/OperationRollupCoordinator.php +++ b/src/FreeDSx/Ldap/Server/Metrics/Rollup/OperationRollupCoordinator.php @@ -23,8 +23,19 @@ */ final class OperationRollupCoordinator { + /** + * Batch sends by op count and elapsed time so a fast workload does not backpressure on the blocking channel. + */ + private const FLUSH_OPS = 256; + + private const FLUSH_INTERVAL_SECONDS = 0.1; + private ?ChildChannel $boundChannel = null; + private int $opsSinceSend = 0; + + private float $lastSendAt = 0.0; + public function __construct( private readonly MetricsRollupInterface $recorder, private readonly ChannelMessageFactory $messageFactory = new MetricsDeltaMessageFactory(), @@ -42,22 +53,28 @@ public function enterChild(ChildChannel $channel): void { $this->recorder->resetDelta(); $this->boundChannel = $channel; + $this->opsSinceSend = 0; + $this->lastSendAt = microtime(true); } /** - * In the child: report the metrics recorded since the last flush. + * In the child: report accumulated metrics once the batch reaches its op count or time bound. */ public function flush(): void { - $this->boundChannel?->send(new MetricsDeltaMessage($this->recorder->takeDelta())); + $this->opsSinceSend++; + + if ($this->batchIsReady()) { + $this->sendDelta(); + } } /** - * In the child: send a final flush, then close the write end so the parent reads EOF. + * In the child: send the final accumulated delta, then close the write end so the parent reads EOF. */ public function finish(): void { - $this->flush(); + $this->sendDelta(); $this->boundChannel?->closeWrite(); } @@ -72,4 +89,20 @@ public function collect(ChildChannel $channel): void } } } + + /** + * Whether the accumulated batch has reached its op-count or time bound and should be sent. + */ + private function batchIsReady(): bool + { + return $this->opsSinceSend >= self::FLUSH_OPS + || (microtime(true) - $this->lastSendAt) >= self::FLUSH_INTERVAL_SECONDS; + } + + private function sendDelta(): void + { + $this->boundChannel?->send(new MetricsDeltaMessage($this->recorder->takeDelta())); + $this->opsSinceSend = 0; + $this->lastSendAt = microtime(true); + } } diff --git a/tests/unit/Server/Metrics/Rollup/OperationRollupCoordinatorTest.php b/tests/unit/Server/Metrics/Rollup/OperationRollupCoordinatorTest.php index 30e9af14..930cbbb8 100644 --- a/tests/unit/Server/Metrics/Rollup/OperationRollupCoordinatorTest.php +++ b/tests/unit/Server/Metrics/Rollup/OperationRollupCoordinatorTest.php @@ -42,7 +42,7 @@ protected function setUp(): void $this->parent = new OperationRollupCoordinator($this->parentRecorder); } - public function test_a_flushed_delta_is_collected_into_the_parent(): void + public function test_a_sub_threshold_flush_defers_the_send_until_finish(): void { $channel = $this->child->openChannel(); $this->child->enterChild($channel); @@ -56,18 +56,27 @@ public function test_a_flushed_delta_is_collected_into_the_parent(): void $this->child->flush(); $this->parent->collect($channel); + self::assertSame( + [], + $this->parentRecorder->snapshot()->operations->counts, + ); + + $this->child->finish(); + $this->parent->collect($channel); + self::assertSame( ['search' => 1], $this->parentRecorder->snapshot()->operations->counts, ); } - public function test_incremental_flushes_accumulate_in_the_parent(): void + public function test_reaching_the_op_batch_threshold_sends_without_finish(): void { $channel = $this->child->openChannel(); $this->child->enterChild($channel); - for ($i = 0; $i < 3; $i++) { + // FLUSH_OPS = 256; the 256th flush crosses the batch and sends the accumulated delta. + for ($i = 0; $i < 256; $i++) { $this->childRecorder->operationObserved(new OperationObservation( OperationType::Search, true, @@ -75,11 +84,42 @@ public function test_incremental_flushes_accumulate_in_the_parent(): void ResultCode::SUCCESS, )); $this->child->flush(); - $this->parent->collect($channel); } + $this->parent->collect($channel); + + self::assertSame( + ['search' => 256], + $this->parentRecorder->snapshot()->operations->counts, + ); + } + + public function test_the_time_interval_triggers_a_send_between_ops(): void + { + $channel = $this->child->openChannel(); + $this->child->enterChild($channel); + + $this->childRecorder->operationObserved(new OperationObservation( + OperationType::Search, + true, + 0.1, + ResultCode::SUCCESS, + )); + $this->child->flush(); + + // Past FLUSH_INTERVAL_SECONDS (0.1s), the next flush sends even though the op count is far below the batch. + usleep(120000); + + $this->childRecorder->operationObserved(new OperationObservation( + OperationType::Search, + true, + 0.1, + ResultCode::SUCCESS, + )); + $this->child->flush(); + $this->parent->collect($channel); self::assertSame( - ['search' => 3], + ['search' => 2], $this->parentRecorder->snapshot()->operations->counts, ); } diff --git a/tests/unit/Server/Middleware/MetricsMiddlewareTest.php b/tests/unit/Server/Middleware/MetricsMiddlewareTest.php index 25cbe0e6..3f3d8361 100644 --- a/tests/unit/Server/Middleware/MetricsMiddlewareTest.php +++ b/tests/unit/Server/Middleware/MetricsMiddlewareTest.php @@ -224,6 +224,8 @@ public function test_it_streams_each_recorded_operation_to_the_rollup_coordinato $this->contextFor(new SearchRequest(Filters::present('objectClass'))), new StubMiddlewareHandler(OperationOutcomeResult::succeeded()), ); + // The coordinator batches sends. force it done for the test. + $coordinator->finish(); $parentRecorder = new InMemoryMetricsRecorder(); (new OperationRollupCoordinator($parentRecorder))->collect($channel); From d8f7a624c330bb5df9c211760758b3002f452a55 Mon Sep 17 00:00:00 2001 From: Chad Sikorra Date: Sat, 18 Jul 2026 16:50:37 -0400 Subject: [PATCH 4/4] Turn on cn=monitor for load tests. --- tests/performance/Config.php | 2 +- tests/performance/LoadTestCommand.php | 21 +++++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/tests/performance/Config.php b/tests/performance/Config.php index f5bca0dc..5d33718c 100644 --- a/tests/performance/Config.php +++ b/tests/performance/Config.php @@ -91,7 +91,7 @@ public function __construct( public readonly string $writeBase = self::DEFAULT_WRITE_BASE, public readonly bool $jit = true, public readonly int $searchSubSizeLimit = self::DEFAULT_SEARCH_SUB_SIZE_LIMIT, - public readonly bool $monitor = false, + public readonly bool $monitor = true, public readonly ?string $searchAttributes = null, public readonly bool $attributesOnly = false, public readonly int $seedAttributes = 0, diff --git a/tests/performance/LoadTestCommand.php b/tests/performance/LoadTestCommand.php index 04a0dadc..ee623834 100644 --- a/tests/performance/LoadTestCommand.php +++ b/tests/performance/LoadTestCommand.php @@ -19,6 +19,7 @@ use FreeDSx\Ldap\Search\Filters; use InvalidArgumentException; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\ConsoleOutputInterface; @@ -202,8 +203,9 @@ protected function configure(): void ->addOption( 'monitor', null, - InputOption::VALUE_NONE, - 'Enable cn=monitor on the spawned server and print its counters after the run for cross-checking.', + InputOption::VALUE_NEGATABLE, + 'Print server-side cn=monitor counters after the run (on by default; use --no-monitor to disable).', + true, ) ->addOption( 'search-sub-size-limit', @@ -384,17 +386,24 @@ private function renderMonitorEntry( 'trafficBytesReceived', 'trafficEntriesReturned', ]; + + $table = new Table($output); + $table->setHeaders([ + 'Stat', + 'Value', + ]); foreach ($attributes as $name) { if (!isset($entry[$name])) { continue; } - $output->writeln(sprintf( - ' %s: %s', + // Multi-valued stats (per-op counts/latencies) read better one pair per line in the cell. + $table->addRow([ $name, - implode(', ', $entry[$name]), - )); + implode("\n", $entry[$name]), + ]); } + $table->render(); } private function buildConfig(InputInterface $input): Config