Problem/Motivation

Providing server timing would help developers estimate site performance.
The corresponding W3C specification is currently in "Working Draft" status and its already supported by Google Chrome and Firefox.
https://www.w3.org/TR/server-timing/
Symfony adopted it in https://github.com/symfony/symfony/pull/27985

Proposed resolution

Add Server-Timing header with relevant timing information.

Remaining tasks

Decide how enable the Server-Timing.

Proposals

  1. Add it to develoment.services.yml (#2)
  2. Enable it be default, for example by adding it to core.services.yml (#7)
  3. Create a new module for it (#24)
  4. Toggle it via service parameter (#27)
  5. Toggle it via configuration option (#38)

Comments

Chi created an issue. See original summary.

chi’s picture

Status: Active » Needs review
StatusFileSize
new2.01 KB
new114.1 KB

The patch adds a middleware that is supposed to be enabled in develoment.services.yml. Currently it only logs total execution time. I am not sure if it's feasible to provide more detailed information, like bootstrap time, DB queries time, rendering time and so on.
Server timing

chi’s picture

Note that Server-Timing permits metrics without duration. We can supply some additional information related to site performance, like cache hit/miss.

Status: Needs review » Needs work

The last submitted patch, 2: server-timing-3104566-2.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

chi’s picture

Status: Needs work » Needs review
StatusFileSize
new2.62 KB

Fixed tests.

wim leers’s picture

wim leers’s picture

Status: Needs review » Needs work

I love this!

We could add more detail, which is what #2851733: Expose collected metrics via Server-Timing response header suggested. But … that also means much more bikeshedding. This is simple. We can have follow-ups to add more detail. But that detail could potentially be used in an adversarial way (e.g. exposing DB query time would enable malicious users to figure out the most expensive route, and then hit that route in a DDoS attack).

That risk doesn't exist in this patch.

I know you're only adding it to development.services.yml so the aforementioned risks don't really apply, but wouldn't it be great to expose at least the "total" timing always, also on production sites? That's a key goal of the Server Timing spec: https://www.w3.org/TR/server-timing/#server-timing-for-automated-analytics


  1. +++ b/core/lib/Drupal/Core/StackMiddleware/ServerTiming.php
    @@ -0,0 +1,44 @@
    +   * The kernel.
    

    Übernit: The decorated kernel.

  2. +++ b/core/lib/Drupal/Core/StackMiddleware/ServerTiming.php
    @@ -0,0 +1,44 @@
    +    $response->headers->set('Server-Timing', 'total;desc="Total";dur=' . $duration);
    

    Nit: this is not setting the $replace parameter. Which means its default is used: $replace = TRUE. That means this call is overwriting any pre-existing Server-Timing response header. We should instead be merging these.

    (And ideally we'd have test coverage proving that this appends to any existing Server-Timing response header, instead of overwriting it.)

    P.S.: I touched on something similar last week: https://git.drupalcode.org/project/http2_server_push/commit/fc0cc00.

chi’s picture

Status: Needs work » Needs review
StatusFileSize
new3.99 KB
new2.33 KB

Addressed issues #7.

chi’s picture

StatusFileSize
new4.01 KB
new532 bytes

Added missing test group.

chi’s picture

StatusFileSize
new3.54 KB
new3.37 KB

Moved service definition to core.services.yml to expose total time on production as it was suggested in #7.

Also changed the description from "Total" to "Page execution time" as the latter seems more meaningful.

I did not change the timing name because it appears that "total" has a special meaning for Google Chrome though I could find any references about this in the specification.
From my observation if server timing is named "total" Google Chrome applies the following formatting rules to it.

  1. It always appears at the end of the group regardless of the order it came
  2. It has no indentation
  3. The color of the measurement bar is always gray

The last submitted patch, 10: server-timing-3104566-10.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

chi’s picture

StatusFileSize
new4.67 KB
new1.13 KB

Fixed entity resource tests.

The last submitted patch, 12: server-timing-3104566-12.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

Exciting progress, @Chi!

  1. +++ b/core/tests/Drupal/Tests/Core/StackMiddleware/ServerTimingTest.php
    @@ -0,0 +1,37 @@
    +    ClockMock::register(__CLASS__);
    +    ClockMock::withClockMock($_SERVER['REQUEST_TIME_FLOAT'] + 0.123123);
    

    FANCY!

  2. +++ b/core/tests/Drupal/Tests/Core/StackMiddleware/ServerTimingTest.php
    @@ -0,0 +1,37 @@
    +    $http_kernel = $this->createMock('Symfony\Component\HttpKernel\HttpKernelInterface');
    

    $this→prophesize(HttpKernelInterface::class)→reveal() is nowadays the preferred way of doing this.

  3. +++ b/core/tests/Drupal/Tests/Core/StackMiddleware/ServerTimingTest.php
    @@ -0,0 +1,37 @@
    +    self::assertEquals('total;desc="Page execution time";dur=123.12', $response->headers->get('Server-Timing'));
    

    $this→assertSame() is nowadays preferred.

chi’s picture

StatusFileSize
new5.37 KB
new2.32 KB

Addressed #14 and fixed JSON API tests.

chi’s picture

Does this need a change record?

The last submitted patch, 15: server-timing-3104566-15.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

Yes, I think this should have a change record, to make as many people aware as possible :)

I think it'd be great to add test coverage for the edge case I described in #7.2 — now that there is a unit test, it's easy to test that 🥳

chi’s picture

Issue tags: +Needs change record
StatusFileSize
new6.78 KB
new3.42 KB
  • Added test coverage for the pre-existing Server-Timing header
  • Fixed UncaughtExceptionTest test
  • Fixed a couple CS issues
chi’s picture

The change record is ready for review.
https://www.drupal.org/node/3105166

chi’s picture

Issue summary: View changes

The support of Server Timing has been added to Firefox 71 though apparently it only works through HTTPS.
https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/71

wim leers’s picture

Component: base system » request processing system
Status: Needs review » Reviewed & tested by the community
Issue tags: -Needs change record
  1. 👍 Change record looks great!
  2. +++ b/core/tests/Drupal/Tests/Core/StackMiddleware/ServerTimingTest.php
    @@ -0,0 +1,49 @@
    +   * Tests Server-Timing header.
    

    🤔 Nit: Can change to just @covers ::handle I think?

  3. +++ b/core/tests/Drupal/Tests/Core/StackMiddleware/ServerTimingTest.php
    @@ -0,0 +1,49 @@
    +  public function testServerTiming() {
    +
    +    ClockMock::register(__CLASS__);
    

    🤓 Nit: extraneous empty line.

Those are übertrivial nits though, and not worth holding this patch up for.

chi’s picture

StatusFileSize
new6.77 KB
new621 bytes

Fixed #22.

catch’s picture

Status: Reviewed & tested by the community » Needs review

I understand not making this development only since it's viable to want it enabled on production, but do we really want to enable it on production for every site? It's a syscall to get the microtime for the request, and even if you want this on production, you'd have to make a conscious effort to use it I think? Wondering if we want a tiny module to do this instead.

wim leers’s picture

you'd have to make a conscious effort to use it I think

I'm not sure what you mean by "conscious effort" here. It's available to any end user (which includes developers of the site) as well as analytics (Google Analytics or otherwise).

longwave’s picture

It does bloat every single response with an extra header, which some sites might not want? There is also perhaps a better chance of something like a crypto-related timing attack if you can more accurately measure how long a request took?

jhedstrom’s picture

I think a small module makes sense, or if not, perhaps the behavior could be toggled on and off in a similar manner to the way cache tag headers ( X-Drupal-Cache-Tags)?

chi’s picture

Having a separate module for such a small task does not seem reasonable unless we have more features to include into it.

Can we just add a configuration option for this?

$server_timing = $this->configFactory->get('system.performance')->get('server_timing');
if ($server_timing['total']) {
  // The request stack is already empty at this point so that
  // \Drupal::time()->getRequestMicroTime() cannot be used here.
  $duration = round(1000 * (microtime(TRUE) - $_SERVER['REQUEST_TIME_FLOAT']), 2);
  $response->headers->set('Server-Timing', 'total;desc="Page execution time";dur=' . $duration, FALSE);
}

That could have a checkbox on Administration -> Configuration -> Development -> Performance form.

chi’s picture

Issue summary: View changes

I summarized possible implementations in the issue summary.

catch’s picture

Issue summary: View changes

@WimLeers you have to make a conscious effort to use analytics no?

andypost’s picture

+++ b/core/lib/Drupal/Core/StackMiddleware/ServerTiming.php
@@ -0,0 +1,44 @@
+  public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
+    $response = $this->httpKernel->handle($request, $type, $catch);

+++ b/core/tests/Drupal/Tests/Core/StackMiddleware/ServerTimingTest.php
@@ -0,0 +1,48 @@
+    $http_kernel->handle(Argument::type(Request::class), HttpKernelInterface::MASTER_REQUEST, TRUE)
+      ->willReturn($response);

It should not fire on subrequests and that needs test coverage

wim leers’s picture

#26:
RE: extra header: it compresses to nearly nothing and we've got far worse bloat in our responses, such as X-Generator which has definitely no value at all. You're right in absolute terms of course! :) But in relative terms, I don't see significant cost here.
RE: crypto-related timing attacks relate to response size and very precise differences in timing. Drupal response times do not depend on crypto logic, but on data retrieval and assembling a response. If Drupal were extremely tightly optimized and had extremely low-latency I/O for its data retrieval, then I would share this concern. Right now, this is a concern I would love to have 🤓😄

#30: Good point. But if analytics have already been set up, then this enables them to provide richer insights without any additional conscious effort.

daffie’s picture

+1 for me.

borisson_’s picture

Status: Needs review » Reviewed & tested by the community

#28: I agree, it doesn't seem needed to add a new module for this. I also don't think we need to add a configuration option for this in the UI. That could alway be added later.

#31: That does seem like something we can add, I'm not sure if we need to add this in this patch, I think we can add this in the future as a followup to this?

I reviewed the patch and it looks great, I'm sitting next to @Wim Leers at the global sprint weekend and he explained why we should implement this. +1.

andypost’s picture

btw Probably it needs review from project manager pov

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/lib/Drupal/Core/StackMiddleware/ServerTiming.php
@@ -0,0 +1,44 @@
+    // The request stack is already empty at this point so that
+    // \Drupal::time()->getRequestMicroTime() cannot be used here.
+    $duration = round(1000 * (microtime(TRUE) - $_SERVER['REQUEST_TIME_FLOAT']), 2);

The request stack is empty but we have the request - so we can do $request->server->get('REQUEST_TIME_FLOAT') instead of $_SERVER['REQUEST_TIME_FLOAT']

It'd be great to get a +1 from @catch

FWIW I think this patch and approach is okay. If you don't want this information exposed you can remove the service but it seems helpful to expose by default and it is not exposing anything that represents a security risk.

chi’s picture

Status: Needs work » Needs review
StatusFileSize
new6.69 KB
new2.38 KB

Addressed #36 and moved ClockMock close to $middleware->handle($request);.

andypost’s picture

@Chi maybe this middleware should check that only for master request?

andypost’s picture

Issue summary: View changes

Added https://github.com/symfony/symfony/pull/27985/files to summary - SF adopted this header too

+++ b/core/lib/Drupal/Core/StackMiddleware/ServerTiming.php
@@ -0,0 +1,42 @@
+    $response->headers->set('Server-Timing', 'total;desc="Page execution time";dur=' . $duration, FALSE);

I suggest to use kinda "Request execution time" because not all requests produce a Page

chi’s picture

Request execution time

What about "Total execution time"?

chi’s picture

StatusFileSize
new6.75 KB
new1.66 KB

Addressed #38 and #39.

johnwebdev’s picture

Status: Needs review » Reviewed & tested by the community

Code looks good, and this is nice.

To address the concerns from previous comments, perhaps we could add a link to the change record on how to remove this.

The last submitted patch, 41: server-timing-3104566-41.patch, failed testing. View results

neslee canil pinto’s picture

Status: Reviewed & tested by the community » Needs work

Test Failed to #41, Moving to Needs Work

andypost’s picture

Version: 8.9.x-dev » 9.1.x-dev

I bet it proper branch

neslee canil pinto’s picture

StatusFileSize
new6.75 KB
new1.78 KB
chi’s picture

Version: 9.1.x-dev » 8.9.x-dev
Status: Needs work » Reviewed & tested by the community

I think that was a random test failure, back to RTBC as #41 passing tests now.

alexpott’s picture

Version: 8.9.x-dev » 9.1.x-dev
Status: Reviewed & tested by the community » Needs work

I think we should only be adding this in 9.1.x now as both 8.9.x and 9.0.x are in beta.
#46 is failing because \Symfony\Component\HttpFoundation\HeaderBag::get() with 3 arguments is deprecated in Symfony 4.

So that means

+++ b/core/tests/Drupal/Tests/Core/StackMiddleware/ServerTimingTest.php
@@ -0,0 +1,51 @@
+    self::assertSame($expected_headers, $response->headers->get('Server-Timing', NULL, FALSE));

Should be:
self::assertSame($expected_headers, $response->headers->all('Server-Timing'));

alexpott’s picture

+++ b/core/lib/Drupal/Core/StackMiddleware/ServerTiming.php
@@ -0,0 +1,44 @@
+    $response = $this->httpKernel->handle($request, $type, $catch);
+    if ($type == self::MASTER_REQUEST) {
+      $duration = round(1000 * (microtime(TRUE) - $request->server->get('REQUEST_TIME_FLOAT')), 2);
+      $response->headers->set('Server-Timing', 'total;desc="Request execution time";dur=' . $duration, FALSE);
+    }
+    return $response;

There's no test coverage of the condition if ($type == self::MASTER_REQUEST) { as far as I can see.

neslee canil pinto’s picture

Status: Needs work » Needs review
StatusFileSize
new6.74 KB
new603 bytes
johnwebdev’s picture

StatusFileSize
new7.27 KB
new851 bytes

Status: Needs review » Needs work

The last submitted patch, 51: 3104566-51.patch, failed testing. View results

johnwebdev’s picture

Status: Needs work » Needs review
StatusFileSize
new7.3 KB
new604 bytes
andypost’s picture

Status: Needs review » Reviewed & tested by the community

Great tests! Back to rtbc

xjm’s picture

Nice work on this; it's an interesting proposal.

I think we need an explicit signoff from the framework managers here: @alexpott indicated he was OK with it, but @catch had some questions and I think we still need some final consensus/signoff.

catch’s picture

Status: Reviewed & tested by the community » Needs review

If we're going to add this without an additional module, I think we need profiling to see if the call to microtime() makes a measurable difference with the internal page cache (this is also a reason not to use the config system since that'd need to be checked in the internal page cache too, unless we introduce a dependency between the configuration value and the container).

microtime() results in a syscall that we otherwise usually avoid via the various request time superglobals.

chi’s picture

Status: Needs review » Needs work
Issue tags: +Needs reroll
rajandro’s picture

Status: Needs work » Needs review
StatusFileSize
new7.28 KB

Rerolled the last patch (3104566-52.patch) as per the issue tag. Please review it.

rajandro’s picture

Issue tags: -Needs reroll

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

moshe weitzman’s picture

I think this is a worthwhile change, even considering the new microtime() call on page cached pages. If thats going to be a blocker, could we remove this feature when page cache is successful for a given page or even when its enabled on the site? Just looking for a compromise.

chi’s picture

Status: Needs review » Needs work

I think the compromise could be reversion to the original approach. which is having server timing only in development environment. In this case performance of microtime() would not be so important. Actually, I think on development environment it would be hard to notice the difference because of null backends for cache bins.

Other points are:

  1. Not all people are happy about exposing this metric by default.
  2. The analytics use case seems too narrow.
  3. In case this metrics is really needed on production. It's quite easy to get it by copying the service definition from development.services.yml to services.yml.
andypost’s picture

other side of "disabled by default" is that just few people will use it as new setting for sure will be skipped

longwave’s picture

Proposal 3, making a separate module for it, allows it to be discovered easily while solving the other problems?

andypost’s picture

@longwave do you mean experimental telemetry module and initiative?

longwave’s picture

Why not just a small core module? page_cache is one service and a cache bin, automated_cron is one service and some config. server_timing module could just add this one service?

chi’s picture

Re #64. It'll be enable by default but only for development environment.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

moshe weitzman’s picture

I think the compromise could be reversion to the original approach. which is having server timing only in development environment.

That sounds good to me.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

andypost’s picture

For development time there's better tools now like https://github.com/brettmc/opentelemetry-php-contrib/blob/main/src/Instr...

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.