tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 60c72a74ce60a70b97cf0a9d8a858e7b7a09f2fc
parent be98a8d8835c6dd5dad781550c70f87f8b8920b7
Author: Helmut Januschka <helmut@januschka.com>
Date:   Thu,  8 Jan 2026 08:34:19 +0000

Bug 2006340 - Implement Resource Timing Level 3 interim response timestamps r=necko-reviewers,webidl,smaug,valentin

Add finalResponseHeadersStart and firstInterimResponseStart properties
to distinguish interim (1xx) from final HTTP responses per W3C spec.

Uses a simplified 2-timestamp approach: network layer captures
responseStart and finalResponseHeadersStart, Performance API computes
firstInterimResponseStart from the comparison.

Includes HTTP/1.1, HTTP/2, and HTTP/3 support with tests.

Differential Revision: https://phabricator.services.mozilla.com/D276668

Diffstat:
Mdom/performance/PerformanceResourceTiming.h | 4++++
Mdom/performance/PerformanceTiming.cpp | 55++++++++++++++++++++++++++++++++++++++++++++++++-------
Mdom/performance/PerformanceTiming.h | 7+++++++
Mdom/webidl/PerformanceResourceTiming.webidl | 4++++
Mnetwerk/base/nsITimedChannel.idl | 4+++-
Mnetwerk/ipc/NeckoChannelParams.ipdlh | 2++
Mnetwerk/protocol/http/Http2Session.cpp | 17+++++++++++++++--
Mnetwerk/protocol/http/Http3Session.cpp | 14++++++++++++++
Mnetwerk/protocol/http/HttpBaseChannel.cpp | 7+++++++
Mnetwerk/protocol/http/HttpChannelChild.cpp | 1+
Mnetwerk/protocol/http/HttpChannelParent.cpp | 2++
Mnetwerk/protocol/http/HttpTransactionParent.cpp | 4++++
Mnetwerk/protocol/http/HttpTransactionShell.h | 2++
Mnetwerk/protocol/http/NullHttpChannel.cpp | 8++++++++
Mnetwerk/protocol/http/TimingStruct.h | 1+
Mnetwerk/protocol/http/nsHttpChannel.cpp | 10++++++++++
Mnetwerk/protocol/http/nsHttpChannel.h | 2++
Mnetwerk/protocol/http/nsHttpTransaction.cpp | 28+++++++++++++++++++++++++---
Mnetwerk/protocol/http/nsHttpTransaction.h | 2++
Anetwerk/test/unit/test_http3_interim_response_timing.js | 211+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnetwerk/test/unit/xpcshell.toml | 7+++++++
Mtesting/web-platform/meta/resource-timing/idlharness.any.js.ini | 24------------------------
Dtesting/web-platform/meta/resource-timing/interim-response-times.h2.html.ini | 36------------------------------------
Dtesting/web-platform/meta/resource-timing/interim-response-times.html.ini | 36------------------------------------
Mtesting/web-platform/meta/server-timing/idlharness.https.any.js.ini | 24------------------------
25 files changed, 379 insertions(+), 133 deletions(-)

diff --git a/dom/performance/PerformanceResourceTiming.h b/dom/performance/PerformanceResourceTiming.h @@ -132,6 +132,10 @@ class PerformanceResourceTiming : public PerformanceEntry { IMPL_RESOURCE_TIMING_TAO_PROTECTED_TIMING_PROP(ResponseStart) + IMPL_RESOURCE_TIMING_TAO_PROTECTED_TIMING_PROP(FirstInterimResponseStart) + + IMPL_RESOURCE_TIMING_TAO_PROTECTED_TIMING_PROP(FinalResponseHeadersStart) + DOMHighResTimeStamp ResponseEnd() const { return mTimingData->ResponseEndHighRes(mPerformance); } diff --git a/dom/performance/PerformanceTiming.cpp b/dom/performance/PerformanceTiming.cpp @@ -182,6 +182,7 @@ PerformanceTimingData::PerformanceTimingData(nsITimedChannel* aChannel, aChannel->GetConnectEnd(&mConnectEnd); aChannel->GetRequestStart(&mRequestStart); aChannel->GetResponseStart(&mResponseStart); + aChannel->GetFinalResponseHeadersStart(&mFinalResponseHeadersStart); aChannel->GetCacheReadStart(&mCacheReadStart); aChannel->GetResponseEnd(&mResponseEnd); aChannel->GetCacheReadEnd(&mCacheReadEnd); @@ -741,22 +742,62 @@ DOMHighResTimeStamp PerformanceTimingData::ResponseStartHighRes( if (!StaticPrefs::dom_enable_performance() || !IsInitialized()) { return mZeroTime; } - if (mResponseStart.IsNull() || - (!mCacheReadStart.IsNull() && mCacheReadStart < mResponseStart)) { - mResponseStart = mCacheReadStart; + + // responseStart is already set correctly in the network layer: + // - For 1xx responses: set on first interim response + // - For final responses: set to the same value as finalResponseHeadersStart + TimeStamp effectiveResponseStart = mResponseStart; + + if (effectiveResponseStart.IsNull() || + (!mCacheReadStart.IsNull() && mCacheReadStart < effectiveResponseStart)) { + effectiveResponseStart = mCacheReadStart; } - if (mResponseStart.IsNull() || - (!mRequestStart.IsNull() && mResponseStart < mRequestStart)) { - mResponseStart = mRequestStart; + if (effectiveResponseStart.IsNull() || + (!mRequestStart.IsNull() && effectiveResponseStart < mRequestStart)) { + effectiveResponseStart = mRequestStart; } - return TimeStampToReducedDOMHighResOrFetchStart(aPerformance, mResponseStart); + return TimeStampToReducedDOMHighResOrFetchStart(aPerformance, + effectiveResponseStart); } DOMTimeMilliSec PerformanceTiming::ResponseStart() { return static_cast<int64_t>(mTimingData->ResponseStartHighRes(mPerformance)); } +DOMHighResTimeStamp PerformanceTimingData::FirstInterimResponseStartHighRes( + Performance* aPerformance) { + MOZ_ASSERT(aPerformance); + + if (!StaticPrefs::dom_enable_performance() || !IsInitialized()) { + return mZeroTime; + } + + // If responseStart < finalResponseHeadersStart, then responseStart was set + // from a 1xx interim response. Otherwise, there was no interim response. + if (!mResponseStart.IsNull() && !mFinalResponseHeadersStart.IsNull() && + mResponseStart < mFinalResponseHeadersStart) { + return TimeStampToReducedDOMHighResOrFetchStart(aPerformance, + mResponseStart); + } + + return 0; // No interim response +} + +DOMHighResTimeStamp PerformanceTimingData::FinalResponseHeadersStartHighRes( + Performance* aPerformance) { + MOZ_ASSERT(aPerformance); + + if (!StaticPrefs::dom_enable_performance() || !IsInitialized()) { + return mZeroTime; + } + if (mFinalResponseHeadersStart.IsNull()) { + return 0; + } + return TimeStampToReducedDOMHighResOrFetchStart(aPerformance, + mFinalResponseHeadersStart); +} + DOMHighResTimeStamp PerformanceTimingData::ResponseEndHighRes( Performance* aPerformance) { MOZ_ASSERT(aPerformance); diff --git a/dom/performance/PerformanceTiming.h b/dom/performance/PerformanceTiming.h @@ -151,6 +151,10 @@ class PerformanceTimingData final : public CacheablePerformanceTimingData { DOMHighResTimeStamp ConnectEndHighRes(Performance* aPerformance); DOMHighResTimeStamp RequestStartHighRes(Performance* aPerformance); DOMHighResTimeStamp ResponseStartHighRes(Performance* aPerformance); + DOMHighResTimeStamp FirstInterimResponseStartHighRes( + Performance* aPerformance); + DOMHighResTimeStamp FinalResponseHeadersStartHighRes( + Performance* aPerformance); DOMHighResTimeStamp ResponseEndHighRes(Performance* aPerformance); DOMHighResTimeStamp ZeroTime() const { return mZeroTime; } @@ -180,6 +184,7 @@ class PerformanceTimingData final : public CacheablePerformanceTimingData { TimeStamp mConnectEnd; TimeStamp mRequestStart; TimeStamp mResponseStart; + TimeStamp mFinalResponseHeadersStart; TimeStamp mCacheReadStart; TimeStamp mResponseEnd; TimeStamp mCacheReadEnd; @@ -412,6 +417,7 @@ struct ParamTraits<mozilla::dom::PerformanceTimingData> { WriteParam(aWriter, aParam.mConnectEnd); WriteParam(aWriter, aParam.mRequestStart); WriteParam(aWriter, aParam.mResponseStart); + WriteParam(aWriter, aParam.mFinalResponseHeadersStart); WriteParam(aWriter, aParam.mCacheReadStart); WriteParam(aWriter, aParam.mResponseEnd); WriteParam(aWriter, aParam.mCacheReadEnd); @@ -447,6 +453,7 @@ struct ParamTraits<mozilla::dom::PerformanceTimingData> { ReadParam(aReader, &aResult->mConnectEnd) && ReadParam(aReader, &aResult->mRequestStart) && ReadParam(aReader, &aResult->mResponseStart) && + ReadParam(aReader, &aResult->mFinalResponseHeadersStart) && ReadParam(aReader, &aResult->mCacheReadStart) && ReadParam(aReader, &aResult->mResponseEnd) && ReadParam(aReader, &aResult->mCacheReadEnd) && diff --git a/dom/webidl/PerformanceResourceTiming.webidl b/dom/webidl/PerformanceResourceTiming.webidl @@ -40,6 +40,10 @@ interface PerformanceResourceTiming : PerformanceEntry [NeedsSubjectPrincipal] readonly attribute DOMHighResTimeStamp requestStart; [NeedsSubjectPrincipal] + readonly attribute DOMHighResTimeStamp finalResponseHeadersStart; + [NeedsSubjectPrincipal] + readonly attribute DOMHighResTimeStamp firstInterimResponseStart; + [NeedsSubjectPrincipal] readonly attribute DOMHighResTimeStamp responseStart; readonly attribute DOMHighResTimeStamp responseEnd; diff --git a/netwerk/base/nsITimedChannel.idl b/netwerk/base/nsITimedChannel.idl @@ -25,7 +25,7 @@ interface nsIServerTiming : nsISupports { [ref] native nsServerTimingArrayRef(nsTArray<nsCOMPtr<nsIServerTiming>>); // All properties return zero if the value is not available -[scriptable, builtinclass, uuid(ca63784d-959c-4c3a-9a59-234a2a520de0)] +[scriptable, builtinclass, uuid(8f2c89e7-d654-4b9a-b8f1-3c7e6a9d2f4e)] interface nsITimedChannel : nsISupports { // The number of redirects attribute uint8_t redirectCount; @@ -57,6 +57,7 @@ interface nsITimedChannel : nsISupports { [noscript] readonly attribute TimeStamp connectEnd; [noscript] readonly attribute TimeStamp requestStart; [noscript] readonly attribute TimeStamp responseStart; + [noscript] readonly attribute TimeStamp finalResponseHeadersStart; [noscript] readonly attribute TimeStamp responseEnd; // The redirect attributes timings must be writeble, se we can transfer @@ -125,6 +126,7 @@ interface nsITimedChannel : nsISupports { readonly attribute PRTime connectEndTime; readonly attribute PRTime requestStartTime; readonly attribute PRTime responseStartTime; + readonly attribute PRTime finalResponseHeadersStartTime; readonly attribute PRTime responseEndTime; readonly attribute PRTime cacheReadStartTime; readonly attribute PRTime cacheReadEndTime; diff --git a/netwerk/ipc/NeckoChannelParams.ipdlh b/netwerk/ipc/NeckoChannelParams.ipdlh @@ -587,6 +587,7 @@ struct TimingStructArgs { TimeStamp connectEnd; TimeStamp requestStart; TimeStamp responseStart; + TimeStamp finalResponseHeadersStart; TimeStamp responseEnd; TimeStamp transactionPending; }; @@ -600,6 +601,7 @@ struct ResourceTimingStructArgs { TimeStamp connectEnd; TimeStamp requestStart; TimeStamp responseStart; + TimeStamp finalResponseHeadersStart; TimeStamp responseEnd; TimeStamp fetchStart; TimeStamp redirectStart; diff --git a/netwerk/protocol/http/Http2Session.cpp b/netwerk/protocol/http/Http2Session.cpp @@ -1703,8 +1703,21 @@ nsresult Http2Session::ResponseHeadersComplete() { } // allow more headers in the case of 1xx - if (((httpResponseCode / 100) == 1) && didFirstSetAllRecvd) { - mInputFrameDataStream->UnsetAllHeadersReceived(); + if (didFirstSetAllRecvd) { + RefPtr<nsAHttpTransaction> trans = mInputFrameDataStream->Transaction(); + nsHttpTransaction* httpTrans = + trans ? trans->QueryHttpTransaction() : nullptr; + auto now = TimeStamp::Now(); + if (httpTrans) { + // Set responseStart only the first time + httpTrans->SetResponseStart(now, true); + } + + if ((httpResponseCode / 100) == 1) { + mInputFrameDataStream->UnsetAllHeadersReceived(); + } else if (httpTrans) { // For non interim responses + httpTrans->SetFinalResponseHeadersStart(now, true); + } } ChangeDownstreamState(PROCESSING_COMPLETE_HEADERS); diff --git a/netwerk/protocol/http/Http3Session.cpp b/netwerk/protocol/http/Http3Session.cpp @@ -555,6 +555,20 @@ nsresult Http3Session::ProcessEvents() { stream->SetResponseHeaders(data, event.header_ready.fin, event.header_ready.interim); + // Capture response timing + RefPtr<Http3Stream> http3Stream = stream->GetHttp3Stream(); + MOZ_RELEASE_ASSERT(http3Stream, "This must be a Http3Stream"); + RefPtr<nsAHttpTransaction> trans = http3Stream->Transaction(); + nsHttpTransaction* httpTrans = + trans ? trans->QueryHttpTransaction() : nullptr; + if (httpTrans) { + auto now = TimeStamp::Now(); + httpTrans->SetResponseStart(now, true); // Won't overwrite if set + if (!event.header_ready.interim) { + httpTrans->SetFinalResponseHeadersStart(now, true); + } + } + rv = ProcessTransactionRead(stream); if (NS_FAILED(rv)) { diff --git a/netwerk/protocol/http/HttpBaseChannel.cpp b/netwerk/protocol/http/HttpBaseChannel.cpp @@ -5929,6 +5929,12 @@ HttpBaseChannel::GetResponseStart(TimeStamp* _retval) { } NS_IMETHODIMP +HttpBaseChannel::GetFinalResponseHeadersStart(TimeStamp* _retval) { + *_retval = mTransactionTimings.finalResponseHeadersStart; + return NS_OK; +} + +NS_IMETHODIMP HttpBaseChannel::GetResponseEnd(TimeStamp* _retval) { *_retval = mTransactionTimings.responseEnd; return NS_OK; @@ -5995,6 +6001,7 @@ IMPL_TIMING_ATTR(SecureConnectionStart) IMPL_TIMING_ATTR(ConnectEnd) IMPL_TIMING_ATTR(RequestStart) IMPL_TIMING_ATTR(ResponseStart) +IMPL_TIMING_ATTR(FinalResponseHeadersStart) IMPL_TIMING_ATTR(ResponseEnd) IMPL_TIMING_ATTR(CacheReadStart) IMPL_TIMING_ATTR(CacheReadEnd) diff --git a/netwerk/protocol/http/HttpChannelChild.cpp b/netwerk/protocol/http/HttpChannelChild.cpp @@ -367,6 +367,7 @@ static void ResourceTimingStructArgsToTimingsStruct( aTimings.connectEnd = aArgs.connectEnd(); aTimings.requestStart = aArgs.requestStart(); aTimings.responseStart = aArgs.responseStart(); + aTimings.finalResponseHeadersStart = aArgs.finalResponseHeadersStart(); aTimings.responseEnd = aArgs.responseEnd(); aTimings.transactionPending = aArgs.transactionPending(); } diff --git a/netwerk/protocol/http/HttpChannelParent.cpp b/netwerk/protocol/http/HttpChannelParent.cpp @@ -1094,6 +1094,8 @@ static ResourceTimingStructArgs GetTimingAttributes(HttpBaseChannel* aChannel) { args.requestStart() = timeStamp; aChannel->GetResponseStart(&timeStamp); args.responseStart() = timeStamp; + aChannel->GetFinalResponseHeadersStart(&timeStamp); + args.finalResponseHeadersStart() = timeStamp; aChannel->GetResponseEnd(&timeStamp); args.responseEnd() = timeStamp; aChannel->GetAsyncOpen(&timeStamp); diff --git a/netwerk/protocol/http/HttpTransactionParent.cpp b/netwerk/protocol/http/HttpTransactionParent.cpp @@ -317,6 +317,10 @@ mozilla::TimeStamp HttpTransactionParent::GetResponseStart() { return mTimings.responseStart; } +mozilla::TimeStamp HttpTransactionParent::GetFinalResponseHeadersStart() { + return mTimings.finalResponseHeadersStart; +} + mozilla::TimeStamp HttpTransactionParent::GetResponseEnd() { return mTimings.responseEnd; } diff --git a/netwerk/protocol/http/HttpTransactionShell.h b/netwerk/protocol/http/HttpTransactionShell.h @@ -134,6 +134,7 @@ class HttpTransactionShell : public nsISupports { virtual mozilla::TimeStamp GetConnectEnd() = 0; virtual mozilla::TimeStamp GetRequestStart() = 0; virtual mozilla::TimeStamp GetResponseStart() = 0; + virtual mozilla::TimeStamp GetFinalResponseHeadersStart() = 0; virtual mozilla::TimeStamp GetResponseEnd() = 0; virtual void SetDomainLookupStart(mozilla::TimeStamp timeStamp, @@ -216,6 +217,7 @@ class HttpTransactionShell : public nsISupports { virtual mozilla::TimeStamp GetConnectEnd() override; \ virtual mozilla::TimeStamp GetRequestStart() override; \ virtual mozilla::TimeStamp GetResponseStart() override; \ + virtual mozilla::TimeStamp GetFinalResponseHeadersStart() override; \ virtual mozilla::TimeStamp GetResponseEnd() override; \ virtual void SetDomainLookupStart(mozilla::TimeStamp timeStamp, \ bool onlyIfNull = false) override; \ diff --git a/netwerk/protocol/http/NullHttpChannel.cpp b/netwerk/protocol/http/NullHttpChannel.cpp @@ -671,6 +671,13 @@ NullHttpChannel::GetResponseStart(mozilla::TimeStamp* aResponseStart) { } NS_IMETHODIMP +NullHttpChannel::GetFinalResponseHeadersStart( + mozilla::TimeStamp* aFinalResponseHeadersStart) { + *aFinalResponseHeadersStart = mAsyncOpenTime; + return NS_OK; +} + +NS_IMETHODIMP NullHttpChannel::GetResponseEnd(mozilla::TimeStamp* aResponseEnd) { *aResponseEnd = mAsyncOpenTime; return NS_OK; @@ -885,6 +892,7 @@ IMPL_TIMING_ATTR(SecureConnectionStart) IMPL_TIMING_ATTR(ConnectEnd) IMPL_TIMING_ATTR(RequestStart) IMPL_TIMING_ATTR(ResponseStart) +IMPL_TIMING_ATTR(FinalResponseHeadersStart) IMPL_TIMING_ATTR(ResponseEnd) IMPL_TIMING_ATTR(CacheReadStart) IMPL_TIMING_ATTR(CacheReadEnd) diff --git a/netwerk/protocol/http/TimingStruct.h b/netwerk/protocol/http/TimingStruct.h @@ -20,6 +20,7 @@ struct TimingStruct { TimeStamp connectEnd; TimeStamp requestStart; TimeStamp responseStart; + TimeStamp finalResponseHeadersStart; TimeStamp responseEnd; TimeStamp transactionPending; }; diff --git a/netwerk/protocol/http/nsHttpChannel.cpp b/netwerk/protocol/http/nsHttpChannel.cpp @@ -8538,6 +8538,16 @@ nsHttpChannel::GetResponseStart(TimeStamp* _retval) { } NS_IMETHODIMP +nsHttpChannel::GetFinalResponseHeadersStart(TimeStamp* _retval) { + if (mTransaction) { + *_retval = mTransaction->GetFinalResponseHeadersStart(); + } else { + *_retval = mTransactionTimings.finalResponseHeadersStart; + } + return NS_OK; +} + +NS_IMETHODIMP nsHttpChannel::GetResponseEnd(TimeStamp* _retval) { if (mTransaction) { *_retval = mTransaction->GetResponseEnd(); diff --git a/netwerk/protocol/http/nsHttpChannel.h b/netwerk/protocol/http/nsHttpChannel.h @@ -180,6 +180,8 @@ class nsHttpChannel final : public HttpBaseChannel, NS_IMETHOD GetConnectEnd(mozilla::TimeStamp* aConnectEnd) override; NS_IMETHOD GetRequestStart(mozilla::TimeStamp* aRequestStart) override; NS_IMETHOD GetResponseStart(mozilla::TimeStamp* aResponseStart) override; + NS_IMETHOD GetFinalResponseHeadersStart( + mozilla::TimeStamp* aFinalResponseHeadersStart) override; NS_IMETHOD GetResponseEnd(mozilla::TimeStamp* aResponseEnd) override; NS_IMETHOD GetTransactionPending( diff --git a/netwerk/protocol/http/nsHttpTransaction.cpp b/netwerk/protocol/http/nsHttpTransaction.cpp @@ -814,9 +814,6 @@ nsresult nsHttpTransaction::WritePipeSegment(nsIOutputStream* stream, if (trans->mTransactionDone) return NS_BASE_STREAM_CLOSED; // stop iterating - // Set the timestamp to Now(), only if it null - trans->SetResponseStart(TimeStamp::Now(), true); - // Bug 1153929 - add checks to fix windows crash MOZ_ASSERT(trans->mWriter); if (!trans->mWriter) { @@ -840,6 +837,9 @@ nsresult nsHttpTransaction::WritePipeSegment(nsIOutputStream* stream, trans->mReceivedData = true; trans->mTransferSize += *countWritten; + // Set the timestamp to Now(), only if it is null + trans->SetResponseStart(TimeStamp::Now(), true); + // Let the transaction "play" with the buffer. It is free to modify // the contents of the buffer and/or modify countWritten. // - Bytes in HTTP headers don't count towards countWritten, so the input @@ -2074,6 +2074,14 @@ nsresult nsHttpTransaction::ParseLineSegment(char* segment, uint32_t len) { mLineBuf.Truncate(); // discard this response if it is a 100 continue or other 1xx status. uint16_t status = mResponseHead->Status(); + + // Capture timing for interim (1xx) vs final responses + auto now = TimeStamp::Now(); + SetResponseStart(now, true); // Won't overwrite if already set from 1xx + if (status / 100 != 1) { + SetFinalResponseHeadersStart(now, true); + } + if (status == 103 && (StaticPrefs::network_early_hints_over_http_v1_1_enabled() || mResponseHead->Version() != HttpVersion::v1_1)) { @@ -2903,6 +2911,15 @@ void nsHttpTransaction::SetResponseEnd(mozilla::TimeStamp timeStamp, mTimings.responseEnd = timeStamp; } +void nsHttpTransaction::SetFinalResponseHeadersStart( + mozilla::TimeStamp timeStamp, bool onlyIfNull) { + mozilla::MutexAutoLock lock(mLock); + if (onlyIfNull && !mTimings.finalResponseHeadersStart.IsNull()) { + return; + } + mTimings.finalResponseHeadersStart = timeStamp; +} + mozilla::TimeStamp nsHttpTransaction::GetDomainLookupStart() { mozilla::MutexAutoLock lock(mLock); return mTimings.domainLookupStart; @@ -2948,6 +2965,11 @@ mozilla::TimeStamp nsHttpTransaction::GetResponseEnd() { return mTimings.responseEnd; } +mozilla::TimeStamp nsHttpTransaction::GetFinalResponseHeadersStart() { + mozilla::MutexAutoLock lock(mLock); + return mTimings.finalResponseHeadersStart; +} + //----------------------------------------------------------------------------- // nsHttpTransaction deletion event //----------------------------------------------------------------------------- diff --git a/netwerk/protocol/http/nsHttpTransaction.h b/netwerk/protocol/http/nsHttpTransaction.h @@ -154,6 +154,8 @@ class nsHttpTransaction final : public nsAHttpTransaction, void SetConnectEnd(mozilla::TimeStamp timeStamp, bool onlyIfNull = false); void SetRequestStart(mozilla::TimeStamp timeStamp, bool onlyIfNull = false); void SetResponseStart(mozilla::TimeStamp timeStamp, bool onlyIfNull = false); + void SetFinalResponseHeadersStart(mozilla::TimeStamp timeStamp, + bool onlyIfNull = false); void SetResponseEnd(mozilla::TimeStamp timeStamp, bool onlyIfNull = false); [[nodiscard]] bool Do0RTT() override; diff --git a/netwerk/test/unit/test_http3_interim_response_timing.js b/netwerk/test/unit/test_http3_interim_response_timing.js @@ -0,0 +1,211 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test Resource Timing Level 3 interim response timestamps with HTTP/3 +// https://www.w3.org/TR/resource-timing/#interim-response-times + +var { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +const earlyhintspath = "/103_response"; + +add_setup(async function () { + await http3_setup_tests("h3"); +}); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }).QueryInterface(Ci.nsIHttpChannel); + return chan; +} + +function channelOpenPromise(chan, flags) { + return new Promise(resolve => { + function finish(req, buffer) { + resolve([req, buffer]); + } + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +// Test HTTP/3 request without interim response (no 103 Early Hints) +add_task(async function test_http3_no_interim_response() { + Services.obs.notifyObservers(null, "net:prune-all-connections"); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + + let chan = makeChan(`https://foo.example.com`); + let [req] = await channelOpenPromise(chan); + + // Verify it's HTTP/3 + let httpVersion = ""; + try { + httpVersion = req.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3", "Request should use HTTP/3"); + + let timing = req.QueryInterface(Ci.nsITimedChannel); + + // When there's no interim response: + // - firstInterimResponseStart should be 0 (computed) + // - responseStart should equal finalResponseHeadersStart + + // Compute firstInterimResponseStart the same way Performance API does + let firstInterimResponseStart = 0; + if ( + timing.responseStartTime > 0 && + timing.finalResponseHeadersStartTime > 0 && + timing.responseStartTime < timing.finalResponseHeadersStartTime + ) { + firstInterimResponseStart = timing.responseStartTime; + } + + Assert.equal( + firstInterimResponseStart, + 0, + "firstInterimResponseStart should be 0 when no interim response" + ); + + Assert.greater(timing.responseStartTime, 0, "responseStart should be set"); + Assert.greater( + timing.finalResponseHeadersStartTime, + 0, + "finalResponseHeadersStart should be set" + ); + + // responseStart should approximately equal finalResponseHeadersStart + // (they're set to the same TimeStamp in the code) + let timeDiff = Math.abs( + timing.responseStartTime - timing.finalResponseHeadersStartTime + ); + Assert.less( + timeDiff, + 10, + "responseStart and finalResponseHeadersStart should be approximately equal when no interim response" + ); +}); + +// Test HTTP/3 request with interim response (103 Early Hints) +add_task(async function test_http3_with_interim_response() { + Services.obs.notifyObservers(null, "net:prune-all-connections"); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + + let chan = makeChan(`https://foo.example.com${earlyhintspath}`); + // Request 103 Early Hints + chan.setRequestHeader( + "link-to-set", + "</style.css>; rel=preload; as=style", + false + ); + + let [req] = await channelOpenPromise(chan); + + // Verify it's HTTP/3 + let httpVersion = ""; + try { + httpVersion = req.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3", "Request should use HTTP/3"); + + let timing = req.QueryInterface(Ci.nsITimedChannel); + + // When there's an interim response (103 Early Hints): + // - firstInterimResponseStart should be > 0 (computed) + // - responseStart should equal firstInterimResponseStart + // - finalResponseHeadersStart should be >= firstInterimResponseStart + + // Compute firstInterimResponseStart the same way Performance API does + let firstInterimResponseStart = 0; + if ( + timing.responseStartTime > 0 && + timing.finalResponseHeadersStartTime > 0 && + timing.responseStartTime < timing.finalResponseHeadersStartTime + ) { + firstInterimResponseStart = timing.responseStartTime; + } + + Assert.greater( + firstInterimResponseStart, + 0, + "firstInterimResponseStart should be set when interim response received" + ); + + Assert.greater(timing.responseStartTime, 0, "responseStart should be set"); + Assert.greater( + timing.finalResponseHeadersStartTime, + 0, + "finalResponseHeadersStart should be set" + ); + + // responseStart should equal firstInterimResponseStart (computed from responseStart) + Assert.equal( + timing.responseStartTime, + firstInterimResponseStart, + "responseStart should equal firstInterimResponseStart when interim response received" + ); + + // Timing order should be: firstInterimResponseStart <= finalResponseHeadersStart + Assert.lessOrEqual( + firstInterimResponseStart, + timing.finalResponseHeadersStartTime, + "firstInterimResponseStart should be before or equal to finalResponseHeadersStart" + ); +}); + +// Test timing ordering with HTTP/3 and interim response +add_task(async function test_http3_timing_order() { + Services.obs.notifyObservers(null, "net:prune-all-connections"); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + + let chan = makeChan(`https://foo.example.com${earlyhintspath}`); + chan.setRequestHeader( + "link-to-set", + "</img.png>; rel=preload; as=image", + false + ); + + let [req] = await channelOpenPromise(chan); + let timing = req.QueryInterface(Ci.nsITimedChannel); + + // Compute firstInterimResponseStart + let firstInterimResponseStart = 0; + if ( + timing.responseStartTime > 0 && + timing.finalResponseHeadersStartTime > 0 && + timing.responseStartTime < timing.finalResponseHeadersStartTime + ) { + firstInterimResponseStart = timing.responseStartTime; + } + + // Verify complete timing order: + // requestStart <= firstInterimResponseStart <= finalResponseHeadersStart <= responseEnd + Assert.greater(timing.requestStartTime, 0, "requestStart should be set"); + Assert.lessOrEqual( + timing.requestStartTime, + firstInterimResponseStart, + "requestStart should be before or equal to firstInterimResponseStart" + ); + Assert.lessOrEqual( + firstInterimResponseStart, + timing.finalResponseHeadersStartTime, + "firstInterimResponseStart should be before or equal to finalResponseHeadersStart" + ); + Assert.lessOrEqual( + timing.finalResponseHeadersStartTime, + timing.responseEndTime, + "finalResponseHeadersStart should be before or equal to responseEnd" + ); +}); + +registerCleanupFunction(async () => { + http3_clear_prefs(); +}); diff --git a/netwerk/test/unit/xpcshell.toml b/netwerk/test/unit/xpcshell.toml @@ -871,6 +871,13 @@ skip-if = [ "os == 'win' && os_version == '11.26100' && arch == 'x86_64' && msix", # Bug 1807931 ] +["test_http3_interim_response_timing.js"] +run-sequentially = ["true"] # node server exceptions dont replay well +skip-if = [ + "os == 'android'", # H3 servers don't work well on Android + "msix", # H3 servers don't work well on MSIX +] + ["test_http3_kyber.js"] skip-if = [ "os == 'android' && os_version == '14' && arch == 'x86_64'", diff --git a/testing/web-platform/meta/resource-timing/idlharness.any.js.ini b/testing/web-platform/meta/resource-timing/idlharness.any.js.ini @@ -8,12 +8,6 @@ [PerformanceResourceTiming interface: resource must inherit property "deliveryType" with the proper type] expected: FAIL - [PerformanceResourceTiming interface: attribute firstInterimResponseStart] - expected: FAIL - - [PerformanceResourceTiming interface: resource must inherit property "firstInterimResponseStart" with the proper type] - expected: FAIL - [PerformanceResourceTiming interface: attribute responseStatus] expected: if (os == "android") and not debug: [PASS, FAIL] @@ -22,12 +16,6 @@ expected: if (os == "android") and not debug: [PASS, FAIL] - [PerformanceResourceTiming interface: attribute finalResponseHeadersStart] - expected: FAIL - - [PerformanceResourceTiming interface: resource must inherit property "finalResponseHeadersStart" with the proper type] - expected: FAIL - [PerformanceResourceTiming interface: attribute contentEncoding] expected: FAIL @@ -69,12 +57,6 @@ [PerformanceResourceTiming interface: resource must inherit property "deliveryType" with the proper type] expected: FAIL - [PerformanceResourceTiming interface: attribute firstInterimResponseStart] - expected: FAIL - - [PerformanceResourceTiming interface: resource must inherit property "firstInterimResponseStart" with the proper type] - expected: FAIL - [PerformanceResourceTiming interface: attribute responseStatus] expected: if (os == "android") and not debug: [PASS, FAIL] @@ -83,12 +65,6 @@ expected: if (os == "android") and not debug: [PASS, FAIL] - [PerformanceResourceTiming interface: attribute finalResponseHeadersStart] - expected: FAIL - - [PerformanceResourceTiming interface: resource must inherit property "finalResponseHeadersStart" with the proper type] - expected: FAIL - [PerformanceResourceTiming interface: attribute contentEncoding] expected: FAIL diff --git a/testing/web-platform/meta/resource-timing/interim-response-times.h2.html.ini b/testing/web-platform/meta/resource-timing/interim-response-times.h2.html.ini @@ -1,36 +0,0 @@ -[interim-response-times.h2.html] - [Fetch from same-origin with early hints, with 100 response] - expected: FAIL - - [Fetch from cross-origin with early hints, with 100 response] - expected: FAIL - - [Fetch from cross-origin-with-TAO with early hints, with 100 response] - expected: FAIL - - [Fetch from same-origin with early hints, without 100 response] - expected: FAIL - - [Fetch from cross-origin with early hints, without 100 response] - expected: FAIL - - [Fetch from cross-origin-with-TAO with early hints, without 100 response] - expected: FAIL - - [Fetch from same-origin without early hints, with 100 response] - expected: FAIL - - [Fetch from cross-origin without early hints, with 100 response] - expected: FAIL - - [Fetch from cross-origin-with-TAO without early hints, with 100 response] - expected: FAIL - - [Fetch from same-origin without early hints, without 100 response] - expected: FAIL - - [Fetch from cross-origin without early hints, without 100 response] - expected: FAIL - - [Fetch from cross-origin-with-TAO without early hints, without 100 response] - expected: FAIL diff --git a/testing/web-platform/meta/resource-timing/interim-response-times.html.ini b/testing/web-platform/meta/resource-timing/interim-response-times.html.ini @@ -1,36 +0,0 @@ -[interim-response-times.html] - [Fetch from same-origin with early hints, with 100 response] - expected: FAIL - - [Fetch from cross-origin with early hints, with 100 response] - expected: FAIL - - [Fetch from cross-origin-with-TAO with early hints, with 100 response] - expected: FAIL - - [Fetch from same-origin with early hints, without 100 response] - expected: FAIL - - [Fetch from cross-origin with early hints, without 100 response] - expected: FAIL - - [Fetch from cross-origin-with-TAO with early hints, without 100 response] - expected: FAIL - - [Fetch from same-origin without early hints, with 100 response] - expected: FAIL - - [Fetch from cross-origin without early hints, with 100 response] - expected: FAIL - - [Fetch from cross-origin-with-TAO without early hints, with 100 response] - expected: FAIL - - [Fetch from same-origin without early hints, without 100 response] - expected: FAIL - - [Fetch from cross-origin without early hints, without 100 response] - expected: FAIL - - [Fetch from cross-origin-with-TAO without early hints, without 100 response] - expected: FAIL diff --git a/testing/web-platform/meta/server-timing/idlharness.https.any.js.ini b/testing/web-platform/meta/server-timing/idlharness.https.any.js.ini @@ -14,12 +14,6 @@ [PerformanceResourceTiming interface: resource must inherit property "deliveryType" with the proper type] expected: FAIL - [PerformanceResourceTiming interface: attribute firstInterimResponseStart] - expected: FAIL - - [PerformanceResourceTiming interface: resource must inherit property "firstInterimResponseStart" with the proper type] - expected: FAIL - [PerformanceResourceTiming interface: resource must inherit property "responseStatus" with the proper type] expected: if (os == "android") and not debug: [PASS, FAIL] @@ -28,12 +22,6 @@ expected: if (os == "android") and not debug: [PASS, FAIL] - [PerformanceResourceTiming interface: attribute finalResponseHeadersStart] - expected: FAIL - - [PerformanceResourceTiming interface: resource must inherit property "finalResponseHeadersStart" with the proper type] - expected: FAIL - [PerformanceResourceTiming interface: attribute contentEncoding] expected: FAIL @@ -81,12 +69,6 @@ [PerformanceResourceTiming interface: resource must inherit property "deliveryType" with the proper type] expected: FAIL - [PerformanceResourceTiming interface: attribute firstInterimResponseStart] - expected: FAIL - - [PerformanceResourceTiming interface: resource must inherit property "firstInterimResponseStart" with the proper type] - expected: FAIL - [PerformanceResourceTiming interface: resource must inherit property "responseStatus" with the proper type] expected: if (os == "android") and not debug: [PASS, FAIL] @@ -95,12 +77,6 @@ expected: if (os == "android") and not debug: [PASS, FAIL] - [PerformanceResourceTiming interface: attribute finalResponseHeadersStart] - expected: FAIL - - [PerformanceResourceTiming interface: resource must inherit property "finalResponseHeadersStart" with the proper type] - expected: FAIL - [PerformanceResourceTiming interface: attribute contentEncoding] expected: FAIL