0

Expose contentEncoding in PerformanceResourceTiming

This CL introduce a contentEncoding field to Performance resource timing
object. This field is behind a feature flag.

PR to resource timing specification:
https://github.com/w3c/resource-timing/pull/411
PR to fetch specification:
https://github.com/whatwg/fetch/pull/1796

Bug: 327941462
Change-Id: I70cad190fe658fb3dbf8b401ff8393bc1d0782f0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6098321
Commit-Queue: Guohui Deng <guohuideng@microsoft.com>
Reviewed-by: Noam Rosenthal <nrosenthal@chromium.org>
Reviewed-by: Matthew Denton <mpdenton@chromium.org>
Reviewed-by: Yoav Weiss (@Shopify) <yoavweiss@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1407331}
This commit is contained in:
Guohui Deng
2025-01-16 08:04:50 -08:00
committed by Chromium LUCI CQ
parent 2561f1c080
commit 2841e06cd2
27 changed files with 276 additions and 0 deletions

@ -6989,6 +6989,7 @@ interface PerformanceResourceTiming : PerformanceEntry
attribute @@toStringTag
getter connectEnd
getter connectStart
getter contentEncoding
getter contentType
getter decodedBodySize
getter deliveryType

@ -125,6 +125,10 @@ struct ResourceTimingInfo {
// PerformanceResourceTiming (https://w3c.github.io/resource-timing/).
string content_type;
// Holds the string corresponding to |contentEncoding| in
// PerformanceResourceTiming (https://w3c.github.io/resource-timing/).
string content_encoding;
// Holds the source type of ServiceWorker static routing API. It keeps track
// of |matchedSourceType| and |finalSourceType|. This is a proposed field in
// https://github.com/WICG/service-worker-static-routing-api, not in the

@ -35,6 +35,7 @@ CrossThreadCopier<blink::mojom::blink::ResourceTimingInfoPtr>::Copy(
info->allow_timing_details, info->allow_negative_values,
CloneServerTimingInfoArray(info->server_timing),
info->render_blocking_status, info->response_status, info->content_type,
info->content_encoding,
info->service_worker_router_info
? info->service_worker_router_info->Clone()
: nullptr,

@ -154,6 +154,10 @@ AtomicString PerformanceResourceTiming::contentType() const {
return AtomicString(info_->content_type);
}
AtomicString PerformanceResourceTiming::contentEncoding() const {
return AtomicString(info_->content_encoding);
}
uint16_t PerformanceResourceTiming::responseStatus() const {
return info_->response_status;
}
@ -509,6 +513,9 @@ void PerformanceResourceTiming::BuildJSONValue(V8ObjectBuilder& builder) const {
if (RuntimeEnabledFeatures::ResourceTimingContentTypeEnabled()) {
builder.AddString("contentType", contentType());
}
if (RuntimeEnabledFeatures::ResourceTimingContentEncodingEnabled()) {
builder.AddString("contentEncoding", contentEncoding());
}
builder.AddNumber("workerStart", workerStart());
if (RuntimeEnabledFeatures::ServiceWorkerStaticRouterTimingInfoEnabled(
ExecutionContext::From(builder.GetScriptState()))) {

@ -73,6 +73,7 @@ class CORE_EXPORT PerformanceResourceTiming : public PerformanceEntry {
AtomicString nextHopProtocol() const;
virtual V8RenderBlockingStatusType renderBlockingStatus() const;
virtual AtomicString contentType() const;
virtual AtomicString contentEncoding() const;
DOMHighResTimeStamp workerStart() const;
DOMHighResTimeStamp workerRouterEvaluationStart() const;
DOMHighResTimeStamp workerCacheLookupStart() const;

@ -77,6 +77,11 @@ interface PerformanceResourceTiming : PerformanceEntry {
[RuntimeEnabled=ResourceTimingContentType]
readonly attribute DOMString contentType;
// PerformanceResourceTiming#contentEncoding
// see: https://github.com/guohuideng2024/resource-timing/blob/gh-pages/Explainers/Content_Encoding.md
[RuntimeEnabled=ResourceTimingContentEncoding]
readonly attribute DOMString contentEncoding;
[RuntimeEnabled=ResourceTimingFinalResponseHeadersStart]
readonly attribute DOMHighResTimeStamp finalResponseHeadersStart;
readonly attribute DOMHighResTimeStamp firstInterimResponseStart;

@ -1,4 +1,5 @@
include_rules = [
"+base/containers/fixed_flat_set.h",
"+base/containers/flat_set.h",
"+net/base/auth.h",
"+net/base/ip_endpoint.h",

@ -27,7 +27,10 @@
#include "third_party/blink/renderer/platform/loader/fetch/resource_response.h"
#include <string>
#include <string_view>
#include "base/containers/contains.h"
#include "base/containers/fixed_flat_set.h"
#include "base/memory/scoped_refptr.h"
#include "net/http/structured_headers.h"
#include "net/ssl/ssl_info.h"
@ -51,6 +54,18 @@ namespace blink {
namespace {
constexpr auto kSupportedContentEncodingsValues =
base::MakeFixedFlatSet<std::string_view>({
"",
"br",
"dcb",
"dcz",
"deflate",
"gzip",
"identity",
"zstd",
});
template <typename Interface>
Vector<Interface> IsolatedCopy(const Vector<Interface>& src) {
Vector<Interface> result;
@ -443,6 +458,18 @@ AtomicString ResourceResponse::HttpContentType() const {
HttpHeaderField(http_names::kContentType).LowerASCII());
}
AtomicString ResourceResponse::GetFilteredHttpContentEncoding() const {
AtomicString content_encoding =
HttpHeaderField(http_names::kContentEncoding).LowerASCII();
if (content_encoding.IsNull()) {
return g_empty_atom;
}
if (kSupportedContentEncodingsValues.contains(content_encoding.Ascii())) {
return content_encoding;
}
return AtomicString("unknown");
}
bool ResourceResponse::WasCached() const {
return was_cached_;
}

@ -50,6 +50,7 @@
#include "third_party/blink/renderer/platform/wtf/allocator/allocator.h"
#include "third_party/blink/renderer/platform/wtf/date_math.h"
#include "third_party/blink/renderer/platform/wtf/ref_counted.h"
#include "third_party/blink/renderer/platform/wtf/text/atomic_string.h"
#include "third_party/blink/renderer/platform/wtf/vector.h"
namespace blink {
@ -158,6 +159,7 @@ class PLATFORM_EXPORT ResourceResponse final {
bool IsAttachment() const;
AtomicString HttpContentType() const;
AtomicString GetFilteredHttpContentEncoding() const;
// These functions return parsed values of the corresponding response headers.
// NaN means that the header was not present or had invalid value.

@ -18,6 +18,7 @@
#include "third_party/blink/renderer/platform/network/http_parsers.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/wtf/text/atomic_string.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
namespace blink {
@ -95,12 +96,15 @@ mojom::blink::ResourceTimingInfoPtr CreateResourceTimingInfo(
bool allow_response_details = response->IsCorsSameOrigin();
info->content_type = g_empty_string;
info->content_encoding = g_empty_string;
if (allow_response_details) {
info->response_status = response->HttpStatusCode();
if (!response->HttpContentType().IsNull()) {
info->content_type = MinimizedMIMEType(response->HttpContentType());
}
info->content_encoding = response->GetFilteredHttpContentEncoding();
}
bool expose_body_sizes =

@ -3587,6 +3587,10 @@
name: "ResetInputTypeToNoneBeforeCharacterInput",
status: "stable",
},
{
name: "ResourceTimingContentEncoding",
status: "experimental",
},
{
name: "ResourceTimingContentType",
status: "experimental",

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="variant" , content="?pipe=gzip">
<title>contentEncoding in navigation timing</title>
<link rel="author" title="Microsoft" href="http://www.microsoft.com/" />
<link rel="help" href="http://www.w3.org/TR/navigation-timing/#sec-navigation-timing-interface" />
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
async_test(function (t) {
var observer = new PerformanceObserver(
t.step_func(function (entryList) {
assert_equals(entryList.getEntries()[0].contentEncoding, "gzip",
"Expected contentEncoding to be gzip.");
observer.disconnect();
t.done();
})
);
observer.observe({ entryTypes: ["navigation"] });
}, "contentEncoding should be gzip.");
</script>
</head>
<body>
<h1>
Description</h1>
<p>
This test validates that when a html is compressed with gzip, navigation timing reports contentEncoding as gzip</p>
</body>
</html>

@ -0,0 +1,107 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>contentEncoding in resource timing</title>
<link rel="author" title="Microsoft" href="http://www.microsoft.com/" />
<link rel="help" href="https://www.w3.org/TR/resource-timing-2/#sec-performanceresourcetiming" />
<script src="/common/get-host-info.sub.js"></script>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/entry-invariants.js"></script>
<script src="resources/resource-loaders.js"></script>
<script>
const { ORIGIN, REMOTE_ORIGIN } = get_host_info();
const run_same_origin_test = (path, contentEncoding) => {
const url = new URL(path, ORIGIN);
attribute_test(
fetch, url,
entry => {
assert_equals(entry.contentEncoding, contentEncoding,
`run_same_origin_test failed, unexpected contentEncoding value.`);
});
}
run_same_origin_test("/resource-timing/resources/compressed-data.py?content_encoding=dcb", "dcb");
run_same_origin_test("/resource-timing/resources/compressed-data.py?content_encoding=dcz", "dcz");
run_same_origin_test("/resource-timing/resources/gzip_xml.py", "gzip");
run_same_origin_test("/resource-timing/resources/foo.text.br", "br");
run_same_origin_test("/resource-timing/resources/foo.text.gz", "gzip");
run_same_origin_test("/resource-timing/resources/foo.text.zst", "zstd");
run_same_origin_test("/resource-timing/resources/compressed-js.py?content_encoding=deflate", "deflate");
run_same_origin_test("/resource-timing/resources/compressed-js.py?content_encoding=gzip", "gzip");
run_same_origin_test("/resource-timing/resources/compressed-js.py?content_encoding=identity", "identity");
// Unrecognized content encoding value should be transformed to "unknown".
run_same_origin_test("/resource-timing/resources/compressed-js.py?content_encoding=unrecognizedname", "unknown");
const run_cross_origin_test = (path) => {
const url = new URL(path, REMOTE_ORIGIN);
attribute_test(
load.xhr_async, url,
entry => {
assert_equals(entry.contentEncoding, "",
`run_cross_origin_test failed, contentEncoding should be empty.`);
});
}
run_cross_origin_test("/resource-timing/resources/compressed-data.py?content_encoding=dcb");
run_cross_origin_test("/resource-timing/resources/gzip_xml.py");
run_cross_origin_test("/resource-timing/resources/compressed-data.py?content_encoding=dcz");
run_cross_origin_test("/resource-timing/resources/foo.text.br");
run_cross_origin_test("/resource-timing/resources/foo.text.gz");
run_cross_origin_test("/resource-timing/resources/foo.text.zst");
run_cross_origin_test("/resource-timing/resources/compressed-js.py?content_encoding=deflate");
run_cross_origin_test("/resource-timing/resources/compressed-js.py?content_encoding=gzip");
const run_cross_origin_allowed_test = (path, contentEncoding) => {
const url = new URL(path, REMOTE_ORIGIN);
url.searchParams.set("allow_origin", ORIGIN);
attribute_test(
load.xhr_async, url,
entry => {
assert_equals(entry.contentEncoding, contentEncoding,
`run_cross_origin_allowed_test failed, unexpected contentEncoding value.`);
});
}
run_cross_origin_allowed_test("/resource-timing/resources/compressed-data.py?content_encoding=dcb", "dcb");
run_cross_origin_allowed_test("/resource-timing/resources/compressed-data.py?content_encoding=dcz", "dcz");
run_cross_origin_allowed_test("/resource-timing/resources/gzip_xml.py", "gzip");
run_cross_origin_allowed_test("/resource-timing/resources/compressed-js.py?content_encoding=deflate", "deflate");
run_cross_origin_allowed_test("/resource-timing/resources/compressed-js.py?content_encoding=gzip", "gzip");
// Content-Encoding for iframes is empty when cross origin redirects are present.
const multiRedirect = new URL(`${ORIGIN}/resource-timing/resources/multi_redirect.py`);
multiRedirect.searchParams.append("page_origin", ORIGIN);
multiRedirect.searchParams.append("cross_origin", REMOTE_ORIGIN);
multiRedirect.searchParams.append("final_resource", "/resource-timing/resources/compressed-js.py?content_encoding=gzip");
attribute_test(load.iframe, multiRedirect, (entry) => {
assert_equals(entry.contentEncoding, "",
`content-encoding should be empty for iframes having cross origin redirects`);
});
// Content-Encoding for iframes is exposed for same origin redirects.
const redirectCORS = new URL(`${ORIGIN}/resource-timing/resources/redirect-cors.py`);
const dest = `${ORIGIN}/resource-timing/resources/compressed-js.py?content_encoding=gzip`;
redirectCORS.searchParams.append("location", dest)
attribute_test(load.iframe, redirectCORS, (entry) => {
assert_equals(entry.contentEncoding, "gzip",
`content-encoding should be exposed for iframes having only same origin redirects`);
});
</script>
</head>
<body>
<h1>
Description</h1>
<p>
This test validates contentEncoding in resource timing.</p>
</body>
</html>

@ -0,0 +1,33 @@
def main(request, response):
response.headers.set(b"Content-Type", b"text/plain")
# `dcb_data` and `dcz_data` are generated using the following commands:
#
# $ echo "This is a test dictionary." > /tmp/dict
# $ echo -n "This is compressed test data using a test dictionary" \
# > /tmp/data
#
# $ echo -en '\xffDCB' > /tmp/out.dcb
# $ openssl dgst -sha256 -binary /tmp/dict >> /tmp/out.dcb
# $ brotli --stdout -D /tmp/dict /tmp/data >> /tmp/out.dcb
# $ xxd -p /tmp/out.dcb | tr -d '\n' | sed 's/\(..\)/\\x\1/g'
dcb_data = b"\xff\x44\x43\x42\x53\x96\x9b\xcf\x5e\x96\x0e\x0e\xdb\xf0\xa4\xbd\xde\x6b\x0b\x3e\x93\x81\xe1\x56\xde\x7f\x5b\x91\xce\x83\x91\x62\x42\x70\xf4\x16\xa1\x98\x01\x80\x62\xa4\x4c\x1d\xdf\x12\x84\x8c\xae\xc2\xca\x60\x22\x07\x6e\x81\x05\x14\xc9\xb7\xc3\x44\x8e\xbc\x16\xe0\x15\x0e\xec\xc1\xee\x34\x33\x3e\x0d"
# $ echo -en '\x5e\x2a\x4d\x18\x20\x00\x00\x00' > /tmp/out.dcz
# $ openssl dgst -sha256 -binary /tmp/dict >> /tmp/out.dcz
# $ zstd -D /tmp/dict -f -o /tmp/tmp.zstd /tmp/data
# $ cat /tmp/tmp.zstd >> /tmp/out.dcz
# $ xxd -p /tmp/out.dcz | tr -d '\n' | sed 's/\(..\)/\\x\1/g'
dcz_data = b"\x5e\x2a\x4d\x18\x20\x00\x00\x00\x53\x96\x9b\xcf\x5e\x96\x0e\x0e\xdb\xf0\xa4\xbd\xde\x6b\x0b\x3e\x93\x81\xe1\x56\xde\x7f\x5b\x91\xce\x83\x91\x62\x42\x70\xf4\x16\x28\xb5\x2f\xfd\x24\x34\xf5\x00\x00\x98\x63\x6f\x6d\x70\x72\x65\x73\x73\x65\x64\x61\x74\x61\x20\x75\x73\x69\x6e\x67\x03\x00\x59\xf9\x73\x54\x46\x27\x26\x10\x9e\x99\xf2\xbc"
if b'content_encoding' in request.GET:
content_encoding = request.GET.first(b"content_encoding")
response.headers.set(b"Content-Encoding", content_encoding)
if content_encoding == b"dcb":
# Send the pre compressed file
response.content = dcb_data
if content_encoding == b"dcz":
# Send the pre compressed file
response.content = dcz_data
if b'allow_origin' in request.GET:
response.headers.set(b'access-control-allow-origin', request.GET.first(b'allow_origin'))

@ -0,0 +1,29 @@
import os.path
import zlib
import gzip
def read(file):
path = os.path.join(os.path.dirname(__file__), file)
return open(path, u"rb").read()
def main(request, response):
response.headers.set(b"Content-Type", b"text/javascript")
if b'allow_origin' in request.GET:
response.headers.set(
b'access-control-allow-origin',
request.GET.first(b'allow_origin'))
if b'content_encoding' in request.GET:
content_encoding = request.GET.first(b"content_encoding")
response.headers.set(b"Content-Encoding", content_encoding)
if content_encoding == b"deflate":
response.content = zlib.compress(read(u"./dummy.js"))
if content_encoding == b"gzip":
response.content = gzip.compress(read(u"./dummy.js"))
if content_encoding == b"identity":
# Uncompressed
response.content = read(u"./dummy.js")
if content_encoding == b"unrecognizedname":
# Just return something
response.content = gzip.compress(read(u"./dummy.js"))

@ -0,0 +1 @@
// A dummy js file to be compressed and transferred.

@ -0,0 +1,2 @@
Content-type: text/plain
Content-Encoding: br

@ -0,0 +1,2 @@
Content-type: text/plain
Content-Encoding: gzip

@ -0,0 +1,2 @@
Content-type: text/plain
Content-Encoding: zstd

@ -20,4 +20,7 @@ def main(request, response):
(b"Content-Encoding", b"gzip"),
(b"Content-Length", len(output))]
if b'allow_origin' in request.GET:
headers.append((b'access-control-allow-origin', request.GET.first(b'allow_origin')))
return headers, output

@ -1663,6 +1663,7 @@ interface PerformanceResourceTiming : PerformanceEntry
attribute @@toStringTag
getter connectEnd
getter connectStart
getter contentEncoding
getter contentType
getter decodedBodySize
getter deliveryType

@ -1743,6 +1743,7 @@ Starting worker: resources/global-interface-listing-worker.js
[Worker] attribute @@toStringTag
[Worker] getter connectEnd
[Worker] getter connectStart
[Worker] getter contentEncoding
[Worker] getter contentType
[Worker] getter decodedBodySize
[Worker] getter deliveryType

@ -7358,6 +7358,7 @@ interface PerformanceResourceTiming : PerformanceEntry
attribute @@toStringTag
getter connectEnd
getter connectStart
getter contentEncoding
getter contentType
getter decodedBodySize
getter deliveryType

@ -1578,6 +1578,7 @@ Starting worker: resources/global-interface-listing-worker.js
[Worker] attribute @@toStringTag
[Worker] getter connectEnd
[Worker] getter connectStart
[Worker] getter contentEncoding
[Worker] getter contentType
[Worker] getter decodedBodySize
[Worker] getter deliveryType