From 6d1416479bb4d92c9c7b6b23a4d0903ae3ee6697 Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Mon, 17 Apr 2017 18:35:34 +0200 Subject: [PATCH 1/4] extend profiler toolbar item --- Collector/Collector.php | 10 ++++ Collector/ProfileClient.php | 62 ++++++++++++++++------ Collector/ProfileClientFactory.php | 11 +++- Collector/Stack.php | 21 ++++++++ Resources/config/data-collector.xml | 6 +++ Resources/views/webprofiler.html.twig | 32 +++++++++-- Tests/Unit/Collector/ProfileClientTest.php | 9 +++- 7 files changed, 128 insertions(+), 23 deletions(-) diff --git a/Collector/Collector.php b/Collector/Collector.php index 15d61024..7c1d1213 100644 --- a/Collector/Collector.php +++ b/Collector/Collector.php @@ -108,4 +108,14 @@ public function getClientStacks($client) return $stack->getClient() == $client; }); } + + /** + * @return int + */ + public function getDurationSum() + { + return array_reduce($this->data['stacks'], function ($carry, Stack $stack) { + return $carry + $stack->getDuration(); + }, 0); + } } diff --git a/Collector/ProfileClient.php b/Collector/ProfileClient.php index aaf0c6b2..6f8a00e1 100644 --- a/Collector/ProfileClient.php +++ b/Collector/ProfileClient.php @@ -8,6 +8,8 @@ use Http\Client\HttpClient; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Component\Stopwatch\StopwatchEvent; /** * The ProfileClient decorates any client that implement both HttpClient and HttpAsyncClient interfaces to gather target @@ -34,13 +36,19 @@ class ProfileClient implements HttpClient, HttpAsyncClient */ private $formatter; + /** + * @var Stopwatch + */ + private $stopwatch; + /** * @param HttpClient|HttpAsyncClient $client The client to profile. Client must implement both HttpClient and * HttpAsyncClient interfaces. * @param Collector $collector * @param Formatter $formatter + * @param Stopwatch $stopwatch */ - public function __construct($client, Collector $collector, Formatter $formatter) + public function __construct($client, Collector $collector, Formatter $formatter, Stopwatch $stopwatch) { if (!($client instanceof HttpClient && $client instanceof HttpAsyncClient)) { throw new \RuntimeException(sprintf( @@ -54,6 +62,7 @@ public function __construct($client, Collector $collector, Formatter $formatter) $this->client = $client; $this->collector = $collector; $this->formatter = $formatter; + $this->stopwatch = $stopwatch; } /** @@ -63,16 +72,20 @@ public function sendAsyncRequest(RequestInterface $request) { $stack = $this->collector->getCurrentStack(); $this->collectRequestInformations($request, $stack); + $event = $this->stopwatch->start($this->getStopwatchEventName($request)); - return $this->client->sendAsyncRequest($request)->then(function (ResponseInterface $response) use ($stack) { - $this->collectResponseInformations($response, $stack); + return $this->client->sendAsyncRequest($request)->then( + function (ResponseInterface $response) use ($event, $stack) { + $event->stop(); + $this->collectResponseInformations($response, $event, $stack); - return $response; - }, function (\Exception $exception) use ($stack) { - $this->collectExceptionInformations($exception, $stack); + return $response; + }, function (\Exception $exception) use ($event, $stack) { + $this->collectExceptionInformations($exception, $event, $stack); - throw $exception; - }); + throw $exception; + } + ); } /** @@ -82,15 +95,18 @@ public function sendRequest(RequestInterface $request) { $stack = $this->collector->getCurrentStack(); $this->collectRequestInformations($request, $stack); + $event = $this->stopwatch->start($this->getStopwatchEventName($request)); try { $response = $this->client->sendRequest($request); + $event->stop(); - $this->collectResponseInformations($response, $stack); + $this->collectResponseInformations($response, $event, $stack); return $response; } catch (\Exception $e) { - $this->collectExceptionInformations($e, $stack); + $event->stop(); + $this->collectExceptionInformations($e, $event, $stack); throw $e; } @@ -115,32 +131,48 @@ private function collectRequestInformations(RequestInterface $request, Stack $st /** * @param ResponseInterface $response + * @param StopwatchEvent $event * @param Stack|null $stack */ - private function collectResponseInformations(ResponseInterface $response, Stack $stack = null) + private function collectResponseInformations(ResponseInterface $response, StopwatchEvent $event, Stack $stack = null) { if (!$stack) { return; } + $stack->setDuration($event->getDuration()); $stack->setResponseCode($response->getStatusCode()); $stack->setClientResponse($this->formatter->formatResponse($response)); } /** - * @param \Exception $exception - * @param Stack|null $stack + * @param \Exception $exception + * @param StopwatchEvent $event + * @param Stack|null $stack */ - private function collectExceptionInformations(\Exception $exception, Stack $stack = null) + private function collectExceptionInformations(\Exception $exception, StopwatchEvent $event, Stack $stack = null) { if ($exception instanceof HttpException) { - $this->collectResponseInformations($exception->getResponse(), $stack); + $this->collectResponseInformations($exception->getResponse(), $event, $stack); } if (!$stack) { return; } + $stack->setDuration($event->getDuration()); $stack->setClientException($this->formatter->formatException($exception)); } + + /** + * Generates the event name. + * + * @param RequestInterface $request + * + * @return string + */ + private function getStopwatchEventName(RequestInterface $request) + { + return sprintf('%s %s', $request->getMethod(), $request->getUri()->__toString()); + } } diff --git a/Collector/ProfileClientFactory.php b/Collector/ProfileClientFactory.php index 2fe4b2a3..739eadc1 100644 --- a/Collector/ProfileClientFactory.php +++ b/Collector/ProfileClientFactory.php @@ -6,6 +6,7 @@ use Http\Client\HttpAsyncClient; use Http\Client\HttpClient; use Http\HttplugBundle\ClientFactory\ClientFactory; +use Symfony\Component\Stopwatch\Stopwatch; /** * The ProfileClientFactory decorates any ClientFactory and returns the created client decorated by a ProfileClient. @@ -31,12 +32,17 @@ class ProfileClientFactory implements ClientFactory */ private $formatter; + /** + * @var Stopwatch + */ + private $stopwatch; + /** * @param ClientFactory|callable $factory * @param Collector $collector * @param Formatter $formatter */ - public function __construct($factory, Collector $collector, Formatter $formatter) + public function __construct($factory, Collector $collector, Formatter $formatter, Stopwatch $stopwatch) { if (!$factory instanceof ClientFactory && !is_callable($factory)) { throw new \RuntimeException(sprintf('First argument to ProfileClientFactory::__construct must be a "%s" or a callable.', ClientFactory::class)); @@ -44,6 +50,7 @@ public function __construct($factory, Collector $collector, Formatter $formatter $this->factory = $factory; $this->collector = $collector; $this->formatter = $formatter; + $this->stopwatch = $stopwatch; } /** @@ -57,6 +64,6 @@ public function createClient(array $config = []) $client = new FlexibleHttpClient($client); } - return new ProfileClient($client, $this->collector, $this->formatter); + return new ProfileClient($client, $this->collector, $this->formatter, $this->stopwatch); } } diff --git a/Collector/Stack.php b/Collector/Stack.php index 5431525d..a73a165b 100644 --- a/Collector/Stack.php +++ b/Collector/Stack.php @@ -76,6 +76,11 @@ final class Stack */ private $responseCode; + /** + * @var int + */ + private $duration = 0; + /** * @param string $client * @param string $request @@ -277,4 +282,20 @@ public function setRequestScheme($requestScheme) { $this->requestScheme = $requestScheme; } + + /** + * @return int + */ + public function getDuration() + { + return $this->duration; + } + + /** + * @param int $duration + */ + public function setDuration($duration) + { + $this->duration = $duration; + } } diff --git a/Resources/config/data-collector.xml b/Resources/config/data-collector.xml index 348dce41..ddd6d146 100644 --- a/Resources/config/data-collector.xml +++ b/Resources/config/data-collector.xml @@ -30,31 +30,37 @@ + + + + + + diff --git a/Resources/views/webprofiler.html.twig b/Resources/views/webprofiler.html.twig index 93250cf7..5ff8aec3 100644 --- a/Resources/views/webprofiler.html.twig +++ b/Resources/views/webprofiler.html.twig @@ -5,17 +5,39 @@ {% set icon %} {{ include('@Httplug/Icon/httplug.svg') }} {{ collector.stacks|length }} - req. + in + {{ collector.durationSum|number_format }} + ms {% endset %} {% set text %}
- Successful requests - {{ collector.successfulStacks|length }} + {{ collector.stacks|length }} requests
- Failed requests - {{ collector.failedStacks|length }} + + + + + + + + + + + + {% for stack in collector.stacks %} + + + + {% set target = stack.requestTarget %} + + + + + {% endfor %} + +
ClientMethodTargetTimeStatus
{{ stack.client }}{{ stack.requestMethod }}{{ target|length > 30 ? target[:30] ~ '...' : target }}{{ stack.duration|number_format ~ ' ms'}}{{ stack.failed ? 'FAILED' : stack.responseCode }}
{% endset %} diff --git a/Tests/Unit/Collector/ProfileClientTest.php b/Tests/Unit/Collector/ProfileClientTest.php index 6c1eec53..e1a1a620 100644 --- a/Tests/Unit/Collector/ProfileClientTest.php +++ b/Tests/Unit/Collector/ProfileClientTest.php @@ -12,6 +12,7 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; +use Symfony\Component\Stopwatch\Stopwatch; class ProfileClientTest extends \PHPUnit_Framework_TestCase { @@ -40,6 +41,11 @@ class ProfileClientTest extends \PHPUnit_Framework_TestCase */ private $formatter; + /** + * @var Stopwatch + */ + private $stopwatch; + /** * @var ProfileClient */ @@ -67,7 +73,8 @@ public function setUp() $this->client = $this->getMockBuilder(ClientInterface::class)->getMock(); $this->request = $this->getMockBuilder(RequestInterface::class)->getMock(); $this->formatter = $this->getMockBuilder(Formatter::class)->disableOriginalConstructor()->getMock(); - $this->subject = new ProfileClient($this->client, $this->collector, $this->formatter); + $this->stopwatch = new Stopwatch(); + $this->subject = new ProfileClient($this->client, $this->collector, $this->formatter, $this->stopwatch); $this->response = $this->getMockBuilder(ResponseInterface::class)->getMock(); $this->promise = $this->getMockBuilder(Promise::class)->getMock(); $this->uri = $this->getMockBuilder(UriInterface::class)->getMock(); From 201b11e8a9554278a8b73f8e6e34faa61ab5c907 Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Mon, 24 Apr 2017 18:02:34 +0200 Subject: [PATCH 2/4] removed stopwatch --- DependencyInjection/HttplugExtension.php | 5 ----- Tests/Unit/Collector/ProfileClientFactoryTest.php | 11 +++++++++-- .../Unit/DependencyInjection/HttplugExtensionTest.php | 1 - 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/DependencyInjection/HttplugExtension.php b/DependencyInjection/HttplugExtension.php index 76389ae9..54e52499 100644 --- a/DependencyInjection/HttplugExtension.php +++ b/DependencyInjection/HttplugExtension.php @@ -279,11 +279,6 @@ private function configureClient(ContainerBuilder $container, $clientName, array $pluginClientOptions = []; if ($profiling) { - // Add the stopwatch plugin - if (!in_array('httplug.plugin.stopwatch', $arguments['plugins'])) { - array_unshift($plugins, 'httplug.plugin.stopwatch'); - } - //Decorate each plugin with a ProfilePlugin instance. foreach ($plugins as $pluginServiceId) { $this->decoratePluginWithProfilePlugin($container, $pluginServiceId); diff --git a/Tests/Unit/Collector/ProfileClientFactoryTest.php b/Tests/Unit/Collector/ProfileClientFactoryTest.php index 7c5186ae..aeb58c5e 100644 --- a/Tests/Unit/Collector/ProfileClientFactoryTest.php +++ b/Tests/Unit/Collector/ProfileClientFactoryTest.php @@ -8,6 +8,7 @@ use Http\HttplugBundle\Collector\Formatter; use Http\HttplugBundle\Collector\ProfileClient; use Http\HttplugBundle\Collector\ProfileClientFactory; +use Symfony\Component\Stopwatch\Stopwatch; class ProfileClientFactoryTest extends \PHPUnit_Framework_TestCase { @@ -21,6 +22,11 @@ class ProfileClientFactoryTest extends \PHPUnit_Framework_TestCase */ private $formatter; + /** + * @var Stopwatch + */ + private $stopwatch; + /** * @var HttpClient */ @@ -30,6 +36,7 @@ public function setUp() { $this->collector = $this->getMockBuilder(Collector::class)->disableOriginalConstructor()->getMock(); $this->formatter = $this->getMockBuilder(Formatter::class)->disableOriginalConstructor()->getMock(); + $this->stopwatch = $this->getMockBuilder(Stopwatch::class)->getMock(); $this->client = $this->getMockBuilder(HttpClient::class)->getMock(); } @@ -38,7 +45,7 @@ public function testCreateClientFromClientFactory() $factory = $this->getMockBuilder(ClientFactory::class)->getMock(); $factory->method('createClient')->willReturn($this->client); - $subject = new ProfileClientFactory($factory, $this->collector, $this->formatter); + $subject = new ProfileClientFactory($factory, $this->collector, $this->formatter, $this->stopwatch); $this->assertInstanceOf(ProfileClient::class, $subject->createClient()); } @@ -49,7 +56,7 @@ public function testCreateClientFromCallable() return $this->client; }; - $subject = new ProfileClientFactory($factory, $this->collector, $this->formatter); + $subject = new ProfileClientFactory($factory, $this->collector, $this->formatter, $this->stopwatch); $this->assertInstanceOf(ProfileClient::class, $subject->createClient()); } diff --git a/Tests/Unit/DependencyInjection/HttplugExtensionTest.php b/Tests/Unit/DependencyInjection/HttplugExtensionTest.php index dfa5e279..9f2e989c 100644 --- a/Tests/Unit/DependencyInjection/HttplugExtensionTest.php +++ b/Tests/Unit/DependencyInjection/HttplugExtensionTest.php @@ -117,7 +117,6 @@ public function testClientPlugins() $plugins = [ 'httplug.client.acme.plugin.stack', - 'httplug.plugin.stopwatch', 'httplug.client.acme.plugin.decoder', 'httplug.plugin.redirect', 'httplug.client.acme.plugin.add_host', From f45d808fb4bc03a1f3038a8046e3f90108aa6210 Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Fri, 28 Apr 2017 12:04:15 +0200 Subject: [PATCH 3/4] review feedback --- CHANGELOG.md | 2 ++ Collector/ProfileClient.php | 16 +++++++++++++++- Collector/ProfileClientFactory.php | 1 + DependencyInjection/HttplugExtension.php | 1 + Resources/views/webprofiler.html.twig | 4 ++-- 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfba1034..e83ab5a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,12 @@ - The real request method and target url are now displayed in the profiler. - Support the cache plugin configuration for `respect_response_cache_directives`. +- Extended WebProfilerToolbar item to list request with details. ### Changed - The profiler design has been updated. +- Removed stopwatch-plugin in favor of `ProfileClient`. ### Deprecated diff --git a/Collector/ProfileClient.php b/Collector/ProfileClient.php index 6f8a00e1..45a03d19 100644 --- a/Collector/ProfileClient.php +++ b/Collector/ProfileClient.php @@ -41,6 +41,11 @@ class ProfileClient implements HttpClient, HttpAsyncClient */ private $stopwatch; + /** + * @var array + */ + private $eventNames = []; + /** * @param HttpClient|HttpAsyncClient $client The client to profile. Client must implement both HttpClient and * HttpAsyncClient interfaces. @@ -81,6 +86,7 @@ function (ResponseInterface $response) use ($event, $stack) { return $response; }, function (\Exception $exception) use ($event, $stack) { + $event->stop(); $this->collectExceptionInformations($exception, $event, $stack); throw $exception; @@ -173,6 +179,14 @@ private function collectExceptionInformations(\Exception $exception, StopwatchEv */ private function getStopwatchEventName(RequestInterface $request) { - return sprintf('%s %s', $request->getMethod(), $request->getUri()->__toString()); + $name = sprintf('%s %s', $request->getMethod(), $request->getUri()); + + if (isset($this->eventNames[$name])) { + $name .= sprintf(' [#%d]', ++$this->eventNames[$name]); + } else { + $this->eventNames[$name] = 1; + } + + return $name; } } diff --git a/Collector/ProfileClientFactory.php b/Collector/ProfileClientFactory.php index 739eadc1..613243ea 100644 --- a/Collector/ProfileClientFactory.php +++ b/Collector/ProfileClientFactory.php @@ -41,6 +41,7 @@ class ProfileClientFactory implements ClientFactory * @param ClientFactory|callable $factory * @param Collector $collector * @param Formatter $formatter + * @param Stopwatch $stopwatch */ public function __construct($factory, Collector $collector, Formatter $formatter, Stopwatch $stopwatch) { diff --git a/DependencyInjection/HttplugExtension.php b/DependencyInjection/HttplugExtension.php index 54e52499..3b56ea28 100644 --- a/DependencyInjection/HttplugExtension.php +++ b/DependencyInjection/HttplugExtension.php @@ -556,6 +556,7 @@ private function configureAutoDiscoveryFactory(ContainerBuilder $container, $dis $factory, new Reference('httplug.collector.collector'), new Reference('httplug.collector.formatter'), + new Reference('debug.stopwatch'), ]); $factory = new Reference($factoryServiceId); } diff --git a/Resources/views/webprofiler.html.twig b/Resources/views/webprofiler.html.twig index 5ff8aec3..efc45793 100644 --- a/Resources/views/webprofiler.html.twig +++ b/Resources/views/webprofiler.html.twig @@ -32,8 +32,8 @@ {{ stack.requestMethod }} {% set target = stack.requestTarget %} {{ target|length > 30 ? target[:30] ~ '...' : target }} - {{ stack.duration|number_format ~ ' ms'}} - {{ stack.failed ? 'FAILED' : stack.responseCode }} + {{ stack.duration == 0 ? 'n/a' : stack.duration|number_format ~ ' ms'}} + {{ stack.failed ? 'FAILED' : stack.responseCode|default('n/a') }} {% endfor %} From 52da00b28efb75a67a60cb0ff1b80c3982eee9b0 Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Fri, 28 Apr 2017 18:43:27 +0200 Subject: [PATCH 4/4] rename durationSum to totalDuration --- Collector/Collector.php | 2 +- Resources/views/webprofiler.html.twig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Collector/Collector.php b/Collector/Collector.php index 7c1d1213..ae1f3b8d 100644 --- a/Collector/Collector.php +++ b/Collector/Collector.php @@ -112,7 +112,7 @@ public function getClientStacks($client) /** * @return int */ - public function getDurationSum() + public function getTotalDuration() { return array_reduce($this->data['stacks'], function ($carry, Stack $stack) { return $carry + $stack->getDuration(); diff --git a/Resources/views/webprofiler.html.twig b/Resources/views/webprofiler.html.twig index efc45793..10001f74 100644 --- a/Resources/views/webprofiler.html.twig +++ b/Resources/views/webprofiler.html.twig @@ -6,7 +6,7 @@ {{ include('@Httplug/Icon/httplug.svg') }} {{ collector.stacks|length }} in - {{ collector.durationSum|number_format }} + {{ collector.totalDuration|number_format }} ms {% endset %}