fix(performance): record Window fetch resource timing

Retain request-time script attribution and publish Fetch entries at buffered or streaming body completion. Apply CORS and Timing-Allow-Origin visibility independently, and cover network failures, async provenance, lazy publication and cache states.
This commit is contained in:
ldm0
2026-11-12 00:44:00 +08:00
committed by Donough Liu
parent 77dd61766c
commit e4a8931249
16 changed files with 526 additions and 11 deletions
@@ -96,21 +96,28 @@ async fn resource_timing_distinguishes_network_local_and_revalidated_responses()
let browser = Browser::new(config)?;
let mut page = browser.fetch(&format!("http://{addr}/")).await?;
let fixture = include_str!("../fixtures/cache-resource-timing.js");
for context in ["top", "child"] {
for (context, transport) in [
("top", "xhr"),
("child", "xhr"),
("top", "fetch"),
("child", "fetch"),
] {
let fixture = format!(
"(() => {{globalThis.cacheTimingContext = '{context}-{transport}';globalThis.cacheTimingTransport = '{transport}';return {fixture};}})()"
);
let script = if context == "top" {
fixture.to_owned()
fixture.clone()
} else {
format!(
r#"(async () => {{
const frame = document.createElement('iframe');
frame.srcdoc = '<!doctype html><body>child';
await new Promise(resolve => {{ frame.onload=resolve; document.body.append(frame); }});
frame.contentWindow.cacheTimingContext = 'child';
const result = await frame.contentWindow.eval({});
frame.remove();
return result;
}})()"#,
serde_json::to_string(fixture)?
serde_json::to_string(&fixture)?
)
};
let result = tokio::time::timeout(
@@ -155,7 +162,7 @@ async fn resource_timing_distinguishes_network_local_and_revalidated_responses()
{"padding":0,"transfer":306,"encoded":6},
{"padding":4096,"transfer":306,"encoded":6}
]}),
"{context}"
"{context}, {transport}"
);
}
let _ = shutdown.send(());
+4 -2
View File
@@ -7,6 +7,8 @@
request.send();
});
const context = globalThis.cacheTimingContext || 'top';
const request = globalThis.cacheTimingTransport === 'fetch'
? url => fetch(url).then(response => response.text()) : xhr;
const resourceUrl = (path, parameters) => {
const url = new URL(path, document.baseURI);
url.search = new URLSearchParams({...parameters, context});
@@ -17,7 +19,7 @@
const url = resourceUrl('/probe-cache', {mode});
const observations = [];
for (let i = 0; i < 3; i++) {
const body = await xhr(url);
const body = await request(url);
await new Promise(resolve => setTimeout(resolve, 0));
const entries = performance.getEntriesByName(url);
const entry = entries[entries.length - 1];
@@ -36,7 +38,7 @@
const headers = [];
for (const padding of [0, 4096]) {
const url = resourceUrl('/probe-cache', {mode: 'padding', padding});
await xhr(url);
await request(url);
await new Promise(resolve => setTimeout(resolve, 0));
const entry = performance.getEntriesByName(url)[0];
headers.push({padding, transfer: entry.transferSize, encoded: entry.encodedBodySize});
+4 -4
View File
@@ -447,10 +447,10 @@ pub(crate) use self::notification_runtime::{
notification_options_payload,
};
pub(crate) use self::performance_runtime::{
ResourcePerformanceEntry, bind_document_performance, bind_window_performance_seed,
current_performance_time_origin, increment_performance_event_count,
project_document_lifecycle_timing, record_resource_performance_entry,
run_resource_timing_buffer_full_task,
FetchResourceTiming, ResourcePerformanceEntry, bind_document_performance,
bind_window_performance_seed, current_performance_time_origin,
increment_performance_event_count, project_document_lifecycle_timing,
record_resource_performance_entry, run_resource_timing_buffer_full_task,
};
use self::range_live::{
update_live_ranges_for_character_data_edit, update_live_ranges_for_character_data_reset,
@@ -2,6 +2,8 @@ use super::*;
use crate::util::{get_private_value, set_private_value};
mod entries;
mod fetch_timing;
pub(crate) use fetch_timing::FetchResourceTiming;
mod install;
mod lazy_subobjects;
mod marks_measures;
@@ -0,0 +1,91 @@
use super::ResourcePerformanceEntry;
use crate::types::AsyncSubresourceFetchResponseFilter;
use moli_fetch::{FetchUrlList, RequestMode, ResponseCacheState, ResponseHead};
use moli_url::WebOrigin;
use url::Url;
/// Request-time metadata, independent of the callback or Realm that happens to
/// be active when the body finishes. This must not retain any V8 objects.
#[derive(Clone)]
pub(crate) struct FetchResourceTiming {
start_unix_millis: f64,
initiator_url: String,
}
impl FetchResourceTiming {
pub(crate) fn capture(scope: &mut v8::PinScope<'_, '_>, origin: &WebOrigin) -> Self {
Self {
start_unix_millis: crate::context_bootstrap::unix_epoch_millis(),
initiator_url: crate::resource_initiator::current_url(scope, origin),
}
}
pub(crate) fn completed(
&self,
url: &Url,
origin: &WebOrigin,
mode: RequestMode,
head: &ResponseHead,
body_size: usize,
filter: Option<AsyncSubresourceFetchResponseFilter>,
) -> ResourcePerformanceEntry {
let filter =
filter.or_else(|| crate::network_host::network_response_filter(origin, head, mode));
let readable = filter.is_none_or(|filter| filter.is_readable());
let mut entry = ResourcePerformanceEntry::from_streaming_network_response(
url.as_str(),
"fetch",
Some(self.start_unix_millis),
head,
body_size,
);
// CORS controls response metadata and script attribution. TAO controls
// timing/body sizes separately, including for opaque responses.
if readable && head.cache_state == ResponseCacheState::None {
entry.initiator_url.clone_from(&self.initiator_url);
}
if !readable {
entry.response_status = 0.0;
entry.content_type.clear();
}
if !timing_allowed(origin, head) {
entry.transfer_size = 0.0;
entry.encoded_body_size = 0.0;
entry.decoded_body_size = 0.0;
}
entry
}
pub(crate) fn failed(&self, url: &Url) -> ResourcePerformanceEntry {
ResourcePerformanceEntry::from_network_failure(
url.as_str(),
"fetch",
Some(self.start_unix_millis),
)
}
}
fn timing_allowed(origin: &WebOrigin, head: &ResponseHead) -> bool {
// Once a redirect has failed TAO, a later response cannot restore access.
// Likewise, returning home does not reset cross-origin response tainting.
let mut cross_origin = false;
for (index, redirect) in head.redirect_chain.iter().enumerate() {
cross_origin |= !origin.same_origin_url(&redirect.from_url);
let urls = FetchUrlList::new(&redirect.from_url, &head.redirect_chain[..index]);
if cross_origin && !allows_timing(&redirect.headers, &urls.serialized_origin(origin)) {
return false;
}
}
cross_origin |= !origin.same_origin_url(&head.final_url);
!cross_origin || allows_timing(&head.headers, &head.url_list().serialized_origin(origin))
}
fn allows_timing(headers: &[(String, Vec<u8>)], origin: &str) -> bool {
headers
.iter()
.filter(|(name, _)| name.eq_ignore_ascii_case("timing-allow-origin"))
.filter_map(|(_, value)| std::str::from_utf8(value).ok())
.flat_map(|value| value.split(','))
.map(|value| value.trim_matches(|ch: char| ch.is_ascii_whitespace()))
.any(|value| value == "*" || value == origin)
}
@@ -601,6 +601,7 @@ impl JsContextHost {
&mut self,
fetch_context: super::WindowFetchContext,
resolver: v8::Global<v8::PromiseResolver>,
resource_timing: crate::context_bootstrap::FetchResourceTiming,
keepalive: bool,
connect_policy: crate::document_runtime::DocumentConnectPolicySnapshot,
csp_report_context: crate::network_host::WindowCspReportRequestContext,
@@ -641,6 +642,7 @@ impl JsContextHost {
continuation: PendingSubresourceContinuation::Fetch(
crate::types::PendingWindowFetchContinuation::new(
resolver,
resource_timing,
keepalive,
cache_mode,
connect_policy,
@@ -886,6 +888,7 @@ impl JsContextHost {
&mut self,
fetch_context: super::WindowFetchContext,
resolver: v8::Global<v8::PromiseResolver>,
resource_timing: crate::context_bootstrap::FetchResourceTiming,
keepalive: bool,
connect_policy: crate::document_runtime::DocumentConnectPolicySnapshot,
csp_report_context: crate::network_host::WindowCspReportRequestContext,
@@ -931,6 +934,7 @@ impl JsContextHost {
continuation: PendingSubresourceContinuation::Fetch(
crate::types::PendingWindowFetchContinuation::new(
resolver,
resource_timing,
keepalive,
cache_mode,
connect_policy,
@@ -23,6 +23,7 @@ pub(super) fn record_intercepted_fetch(
host.record_pending_subresource_fetch(
prepared.fetch_context,
v8::Global::new(scope, resolver),
prepared.resource_timing,
prepared.keepalive,
prepared.connect_policy,
prepared.csp_report_context,
@@ -243,6 +244,7 @@ pub(super) fn spawn_network_fetch(
let internal_id = host.record_async_subresource_fetch(
prepared.fetch_context,
v8::Global::new(scope, resolver),
prepared.resource_timing,
prepared.keepalive,
prepared.connect_policy,
prepared.csp_report_context,
@@ -59,6 +59,7 @@ pub(super) struct PreparedWindowFetchRequest {
pub(super) csp_report_context: crate::network_host::WindowCspReportRequestContext,
pub(super) document_url: url::Url,
pub(super) request_origin: moli_url::WebOrigin,
pub(super) resource_timing: crate::context_bootstrap::FetchResourceTiming,
pub(super) network_partition_key: Option<String>,
pub(super) document_referrer_policy: Option<String>,
pub(super) policy_context: crate::types::SubresourcePolicyContext,
@@ -130,6 +131,10 @@ pub(super) fn prepare_window_fetch_request<'s>(
let resolved_url = resolve_context_url(&base_url, &parsed.url, None)?;
Ok(PreparedWindowFetchRequest {
resource_timing: crate::context_bootstrap::FetchResourceTiming::capture(
scope,
&request_origin,
),
frame_id,
fetch_context,
resource_loader,
@@ -52,6 +52,7 @@ pub(super) fn dispatch_service_worker_fetch(
let internal_id = host.record_async_subresource_fetch(
prepared.fetch_context.duplicate(scope),
v8::Global::new(scope, resolver),
prepared.resource_timing.clone(),
prepared.keepalive,
prepared.connect_policy.clone(),
prepared.csp_report_context.clone(),
@@ -657,6 +657,21 @@ impl ScriptVm {
trace_fields,
record_started,
);
if let PendingSubresourceContinuation::Fetch(fetch) = &pending.continuation {
crate::context_bootstrap::record_resource_performance_entry(
scope,
fetch.resource_timing().completed(
&pending.info.url,
&pending.request_origin,
pending.request_mode,
&response.head(),
response.body_bytes().len(),
opaque_response_blocked
.then_some(AsyncSubresourceFetchResponseFilter::Opaque)
.or(response_filter),
),
);
}
let mut observable_response = response;
if matches!(
pending.info.resource_type,
@@ -878,6 +893,10 @@ impl ScriptVm {
moli_trace::cdp_runtime_trace_enabled().then(Instant::now);
match pending.continuation {
PendingSubresourceContinuation::Fetch(fetch) => {
crate::context_bootstrap::record_resource_performance_entry(
scope,
fetch.resource_timing().failed(&pending.info.url),
);
let resolver = fetch
.into_resolver()
.expect("detached keepalive failure is handled before V8 entry");
@@ -727,6 +727,10 @@ impl ScriptVm {
.record_subresource_network(network_record);
match &pending.continuation {
PendingSubresourceContinuation::Fetch(fetch) => {
crate::context_bootstrap::record_resource_performance_entry(
scope,
fetch.resource_timing().failed(&pending.info.url),
);
let resolver = fetch
.resolver()
.expect("detached keepalive stream is handled before V8 entry");
@@ -1799,6 +1803,19 @@ impl ScriptVm {
trace_fields,
record_started,
);
if let PendingSubresourceContinuation::Fetch(fetch) = &streaming.pending.continuation {
crate::context_bootstrap::record_resource_performance_entry(
scope,
fetch.resource_timing().completed(
&streaming.pending.info.url,
&streaming.pending.request_origin,
streaming.pending.request_mode,
&streaming.head,
response_body_size,
streaming.response_filter,
),
);
}
if let PendingSubresourceContinuation::Xhr { xhr, initiator_url, .. } =
streaming.pending.continuation
&& let Some(response_body) = xhr_delivery_body
@@ -1851,6 +1868,12 @@ impl ScriptVm {
}
}
Err(error_text) => {
if let PendingSubresourceContinuation::Fetch(fetch) = &streaming.pending.continuation {
crate::context_bootstrap::record_resource_performance_entry(
scope,
fetch.resource_timing().failed(&streaming.pending.info.url),
);
}
let error_started =
moli_trace::cdp_runtime_trace_enabled().then(Instant::now);
crate::network_host::error_pending_network_body_stream(
@@ -157,6 +157,10 @@ fn pending_fetch_continuation<'s>(
crate::types::PendingSubresourceContinuation::Fetch(
crate::types::PendingWindowFetchContinuation::new(
v8::Global::new(scope, resolver),
crate::context_bootstrap::FetchResourceTiming::capture(
scope,
&host.document_url().into(),
),
false,
moli_fetch::RequestCacheMode::Default,
crate::document_runtime::DocumentConnectPolicySnapshot::from_policy_container(
@@ -327,6 +327,7 @@ fn register_pending_window_fetch_for_test(
let internal_id = host.record_async_subresource_fetch(
fetch_context,
v8::Global::new(scope, resolver),
crate::context_bootstrap::FetchResourceTiming::capture(scope, &(&url).into()),
keepalive,
connect_policy,
csp_report_context,
@@ -500,6 +501,7 @@ fn register_pending_window_fetch_with_connect_policy_for_test(
let internal_id = host.record_async_subresource_fetch(
fetch_context,
v8::Global::new(scope, resolver),
crate::context_bootstrap::FetchResourceTiming::capture(scope, &(&document_url).into()),
keepalive,
crate::document_runtime::DocumentConnectPolicySnapshot::from_policy_container(&policy),
csp_report_context,
@@ -2,6 +2,7 @@ use super::*;
use crate::script_provenance::CompiledStringProvenance;
mod event_handler_getter;
mod fetch;
mod geolocation_watch;
mod realm_lifetime;
@@ -0,0 +1,345 @@
use super::*;
fn finish_fetch(
vm: &mut ScriptVm,
request: &crate::types::PendingSubresourceFetchInfo,
response: crate::protocol_types::NavigationResponse,
streaming: bool,
) {
if streaming {
let body_source_id = crate::network_host::new_network_body_source_id();
vm.start_streaming_async_subresource_fetch(
crate::types::AsyncSubresourceStreamingStarted {
internal_id: request.internal_id,
request_url: request.url.clone(),
request_method: request.method.clone(),
request_headers: request.request_headers.clone(),
request_body: None,
skip_fetch_security_validation: false,
response_filter: None,
body_source_id,
head: response.head(),
network_request_headers: None,
},
)
.unwrap();
assert_eq!(
vm.eval("String(performance.getEntriesByType('resource').length)")
.unwrap(),
"0",
"headers must not publish an unfinished Fetch"
);
vm.append_streaming_async_subresource_fetch_chunk(
body_source_id,
response.body_bytes().to_vec(),
);
assert_eq!(
vm.eval("String(performance.getEntriesByType('resource').length)")
.unwrap(),
"0",
"body chunks must not publish an unfinished Fetch"
);
vm.finish_streaming_async_subresource_fetch(request.internal_id, body_source_id, Ok(()))
.unwrap();
} else {
vm.complete_async_subresource_fetch(crate::types::AsyncSubresourceFetchCompletion {
internal_id: request.internal_id,
request_url: request.url.clone(),
request_method: request.method.clone(),
request_headers: request.request_headers.clone(),
request_body: None,
response_status_text: None,
skip_fetch_security_validation: false,
response_filter: None,
network_error_text: None,
result: Ok(response).into(),
})
.unwrap();
}
}
#[test]
fn fetch_resource_timing_publishes_once_after_buffered_and_streaming_body_completion() {
let document_url = Url::parse("https://fetch-timing.test/page").unwrap();
let caller = document_url.join("caller.js").unwrap();
for streaming in [false, true] {
let mut vm = new_storage_test_vm(document_url.as_str());
vm.set_fetch_subresource_interception(
true,
Some(crate::types::SubresourceResourceType::Fetch),
);
execute_with_source(
&mut vm,
r#"
globalThis.observed = [];
globalThis.observer = new PerformanceObserver(list => observed.push(...list.getEntries()));
observer.observe({type:'resource'});
globalThis.fetchResponse = null;
globalThis.started = performance.now();
fetch('/asset').then(response => fetchResponse = response);
"#,
&caller,
&document_url,
);
let requests = vm.take_pending_subresource_fetch_infos();
assert_eq!(requests.len(), 1);
let request = &requests[0];
finish_fetch(
&mut vm,
request,
crate::protocol_types::NavigationResponse::from_text_body(
request.url.clone(),
200,
vec![("Content-Type".to_owned(), b"text/plain".to_vec())],
"body".to_owned(),
),
streaming,
);
assert_eq!(vm.eval("String(fetchResponse.status)").unwrap(), "200");
assert_eq!(vm.eval(r#"JSON.stringify({
entries:performance.getEntriesByType('resource').map(entry => ({
name:entry.name, type:entry.initiatorType, source:entry.initiatorUrl,
jsonSource:entry.toJSON().initiatorUrl, status:entry.responseStatus,
encoded:entry.encodedBodySize, decoded:entry.decodedBodySize,
transfer:entry.transferSize, native:entry instanceof PerformanceResourceTiming,
start:entry.startTime >= started && entry.startTime <= performance.now()
})),
observer:[...observed, ...observer.takeRecords()].map(entry => entry.name)
})"#).unwrap(), serde_json::json!({
"entries":[{"name":request.url.as_str(),"type":"fetch","source":caller.as_str(),
"jsonSource":caller.as_str(),"status":200,"encoded":4,"decoded":4,"transfer":304,"native":true,"start":true}],
"observer":[request.url.as_str()]
}).to_string(), "streaming={streaming}");
}
}
#[tokio::test]
async fn fetch_resource_timing_retains_initiators_through_real_network_and_lazy_performance() {
let server = StaticHttpServer::spawn_with_responder(
3,
"Content-Type: text/plain\r\nCache-Control: no-store\r\n",
|_, _| "ok".to_owned(),
)
.await;
let base = server.base_url();
let loader = static_http_loader(std::iter::empty::<String>());
let mut vm = new_storage_page_task_executor_test_vm_with_loader(base.as_str(), &loader);
execute_with_source(
&mut vm,
r#"
globalThis.fetchDone = 0;
globalThis.get = path => fetch(path).then(response => response.text()).then(() => fetchDone++);
"#,
&base.join("library.js").unwrap(),
&base,
);
let caller = base.join("caller.js?version=2").unwrap();
execute_with_source(
&mut vm,
r#"
get('/direct');
Promise.resolve().then(() => get('/promise'));
setTimeout(() => get('/timer'), 0);
"#,
&caller,
&base,
);
execute_with_source(
&mut vm,
"globalThis.laterScript = true;",
&base.join("later.js").unwrap(),
&base,
);
advance_page_task_executor_until_eval_equals(
&mut vm,
&loader,
"String(fetchDone)",
"3",
"real Fetch body completion",
)
.await;
assert_eq!(vm.eval(r#"JSON.stringify(performance.getEntriesByType('resource')
.map(entry => [new URL(entry.name).pathname, entry.initiatorType, entry.initiatorUrl, entry.toJSON().initiatorUrl]).sort())"#).unwrap(),
serde_json::json!([
["/direct","fetch",caller.as_str(),caller.as_str()],
["/promise","fetch",caller.as_str(),caller.as_str()],
["/timer","fetch",caller.as_str(),caller.as_str()]
]).to_string());
assert_eq!(server.finish().await.len(), 3);
}
#[test]
fn fetch_resource_timing_reports_failed_network_requests() {
let base = Url::parse("https://failed-fetch-timing.test/page").unwrap();
let mut vm = new_storage_test_vm(base.as_str());
vm.set_fetch_subresource_interception(true, Some(crate::types::SubresourceResourceType::Fetch));
execute_with_source(
&mut vm,
"fetch('/failed').catch(error => globalThis.fetchError = error.name);",
&base,
&base,
);
let request = vm.take_pending_subresource_fetch_infos().pop().unwrap();
vm.complete_async_subresource_fetch(crate::types::AsyncSubresourceFetchCompletion {
internal_id: request.internal_id,
request_url: request.url.clone(),
request_method: request.method,
request_headers: request.request_headers,
request_body: None,
response_status_text: None,
skip_fetch_security_validation: false,
response_filter: None,
network_error_text: None,
result: Err("network failed".to_owned()).into(),
})
.unwrap();
assert_eq!(vm.eval("fetchError").unwrap(), "TypeError");
assert_eq!(
vm.eval(
r#"JSON.stringify(performance.getEntriesByType('resource')
.map(entry => [entry.name,entry.initiatorType,entry.responseStatus,entry.transferSize]))"#
)
.unwrap(),
serde_json::json!([[request.url.as_str(), "fetch", 0, 0]]).to_string()
);
}
#[test]
fn fetch_resource_timing_separates_cors_attribution_from_timing_allow_origin() {
let document = Url::parse("https://fetch-timing.test/page").unwrap();
let caller = document.join("caller.js").unwrap();
let streaming_modes: &[bool] = &[false, true];
// The production transport buffers no-cors responses for ORB body sniffing.
let buffered: &[bool] = &[false];
for (modes, mode, tao, expected_source, expected_size, expected_status) in [
(streaming_modes, "cors", None, caller.as_str(), 0, 200),
(streaming_modes, "cors", Some("*"), caller.as_str(), 4, 200),
(
streaming_modes,
"cors",
Some("https://fetch-timing.test"),
caller.as_str(),
4,
200,
),
(
streaming_modes,
"cors",
Some("https://other.test"),
caller.as_str(),
0,
200,
),
(buffered, "no-cors", None, "", 0, 0),
(buffered, "no-cors", Some("*"), "", 4, 0),
] {
for &streaming in modes {
let mut vm = new_storage_test_vm(document.as_str());
vm.set_fetch_subresource_interception(
true,
Some(crate::types::SubresourceResourceType::Fetch),
);
execute_with_source(
&mut vm,
&format!(
"fetch('https://cross-origin.test/asset', {{mode:'{mode}'}}).then(response => globalThis.fetchResponse = response);"
),
&caller,
&document,
);
let request = vm.take_pending_subresource_fetch_infos().pop().unwrap();
let mut headers = vec![
("Content-Type".to_owned(), b"text/plain".to_vec()),
("Access-Control-Allow-Origin".to_owned(), b"*".to_vec()),
];
if let Some(tao) = tao {
headers.push(("Timing-Allow-Origin".to_owned(), tao.as_bytes().to_vec()));
}
finish_fetch(
&mut vm,
&request,
crate::protocol_types::NavigationResponse::from_text_body(
request.url.clone(),
200,
headers,
"body".to_owned(),
),
streaming,
);
assert_eq!(
vm.eval(
r#"JSON.stringify(performance.getEntriesByType('resource').map(entry => [
entry.name, entry.initiatorType, entry.initiatorUrl, entry.responseStatus,
entry.encodedBodySize, entry.decodedBodySize, entry.transferSize, entry.contentType
]))"#
)
.unwrap(),
serde_json::json!([[
request.url.as_str(),
"fetch",
expected_source,
expected_status,
expected_size,
expected_size,
if expected_size == 0 { 0 } else { 304 },
if mode == "cors" { "text/plain" } else { "" }
]])
.to_string(),
"streaming={streaming}, mode={mode}, tao={tao:?}"
);
}
}
}
#[test]
fn fetch_resource_timing_keeps_cache_hits_and_unknown_script_sources_unattributed() {
let document = Url::parse("https://fetch-cache-timing.test/page").unwrap();
let caller = document.join("caller.js").unwrap();
for streaming in [false, true] {
for (cache, transfer) in [
(moli_fetch::ResponseCacheState::None, 304),
(moli_fetch::ResponseCacheState::Local, 0),
(moli_fetch::ResponseCacheState::Validated, 300),
] {
for known_source in [false, true] {
let mut vm = new_storage_test_vm(document.as_str());
vm.set_fetch_subresource_interception(
true,
Some(crate::types::SubresourceResourceType::Fetch),
);
let script =
"fetch('/asset').then(response => globalThis.fetchResponse = response);";
if known_source {
execute_with_source(&mut vm, script, &caller, &document);
} else {
vm.exec(script, None).unwrap();
}
let request = vm.take_pending_subresource_fetch_infos().pop().unwrap();
let mut response = crate::protocol_types::NavigationResponse::from_text_body(
request.url.clone(),
200,
Vec::new(),
"body".to_owned(),
);
response.cache_state = cache;
response.from_cache = cache != moli_fetch::ResponseCacheState::None;
finish_fetch(&mut vm, &request, response, streaming);
let source = if known_source && cache == moli_fetch::ResponseCacheState::None {
caller.as_str()
} else {
""
};
assert_eq!(
vm.eval(
r#"JSON.stringify(performance.getEntriesByType('resource').map(entry => [
entry.initiatorUrl, entry.encodedBodySize, entry.transferSize
]))"#
)
.unwrap(),
serde_json::json!([[source, 4, transfer]]).to_string(),
"streaming={streaming}, cache={cache:?}, known_source={known_source}"
);
}
}
}
}
+7
View File
@@ -246,6 +246,7 @@ impl PendingSubresourceContinuation {
pub(super) struct PendingWindowFetchContinuation {
promise: PendingWindowFetchPromise,
resource_timing: crate::context_bootstrap::FetchResourceTiming,
keepalive: bool,
cache_mode: moli_fetch::RequestCacheMode,
connect_policy: crate::document_runtime::DocumentConnectPolicySnapshot,
@@ -260,6 +261,7 @@ enum PendingWindowFetchPromise {
impl PendingWindowFetchContinuation {
pub(super) fn new(
resolver: v8::Global<v8::PromiseResolver>,
resource_timing: crate::context_bootstrap::FetchResourceTiming,
keepalive: bool,
cache_mode: moli_fetch::RequestCacheMode,
connect_policy: crate::document_runtime::DocumentConnectPolicySnapshot,
@@ -267,6 +269,7 @@ impl PendingWindowFetchContinuation {
) -> Self {
Self {
promise: PendingWindowFetchPromise::Active(resolver),
resource_timing,
keepalive,
cache_mode,
connect_policy,
@@ -278,6 +281,10 @@ impl PendingWindowFetchContinuation {
self.cache_mode
}
pub(super) fn resource_timing(&self) -> &crate::context_bootstrap::FetchResourceTiming {
&self.resource_timing
}
pub(super) fn keepalive(&self) -> bool {
self.keepalive
}