Skip to content

Commit 396cffa

Browse files
authored
Merge pull request #3391 from lightpanda-io/resource-timing
webapi: resource-timing
2 parents 1d66b2f + 1034ed8 commit 396cffa

17 files changed

Lines changed: 1370 additions & 71 deletions

src/browser/Frame.zig

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -410,17 +410,6 @@ pub fn init(self: *Frame, frame_id: u32, page: *Page, opts: InitOpts) !void {
410410
._http_owner = undefined,
411411
};
412412
self._queued_events = &self._queued_events_1;
413-
self._http_owner = .{
414-
.blob_urls = &page.blob_urls,
415-
.origin = &self.origin,
416-
.url = &self.url,
417-
.parent = if (parent) |p| &p._http_owner else null,
418-
.frame_id = frame_id,
419-
.document_frame_id = frame_id,
420-
.loader_id = self._loader_id,
421-
.cookie_jar = &session.cookie_jar,
422-
.notification = session.notification,
423-
};
424413

425414
var screen: *Screen = undefined;
426415
var visual_viewport: *VisualViewport = undefined;
@@ -442,7 +431,7 @@ pub fn init(self: *Frame, frame_id: u32, page: *Page, opts: InitOpts) !void {
442431
._proto = undefined,
443432
._document = self.document,
444433
._location = undefined,
445-
._performance = .init(),
434+
._performance = .init(factory, arena),
446435
._screen = screen,
447436
._visual_viewport = visual_viewport,
448437
._cross_origin_wrapper = undefined,
@@ -458,6 +447,19 @@ pub fn init(self: *Frame, frame_id: u32, page: *Page, opts: InitOpts) !void {
458447
}
459448
self.window._cross_origin_wrapper = .{ .window = self.window };
460449

450+
self._http_owner = .{
451+
.blob_urls = &page.blob_urls,
452+
.origin = &self.origin,
453+
.url = &self.url,
454+
.parent = if (parent) |p| &p._http_owner else null,
455+
.frame_id = frame_id,
456+
.document_frame_id = frame_id,
457+
.loader_id = self._loader_id,
458+
.cookie_jar = &session.cookie_jar,
459+
.notification = session.notification,
460+
.performance = &self.window._performance,
461+
};
462+
461463
self._style_manager = try StyleManager.init(self);
462464
errdefer self._style_manager.deinit();
463465

@@ -472,6 +474,7 @@ pub fn init(self: *Frame, frame_id: u32, page: *Page, opts: InitOpts) !void {
472474
.local_arena = self.local_arena,
473475
});
474476
errdefer browser.env.destroyContext(self.js);
477+
self.window._performance._scheduler = &self.js.scheduler;
475478

476479
const location = try Location.init("about:blank", self);
477480
// We're holding a reference in Zig-side.

src/browser/tests/domexception.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,20 @@
121121
<a id="link" href="foo" class="ok">OK</a>
122122
</div>
123123

124+
<script id=custom_name_outlives_call>
125+
// The constructor's strings come from the call arena: they must be copied.
126+
window.__custom_exception = new DOMException('took too ' + 'long', 'Timeout' + 'Error');
127+
testing.expectEqual('TimeoutError', window.__custom_exception.name);
128+
</script>
129+
130+
<script id=custom_name_outlives_call_2>
131+
const e = window.__custom_exception;
132+
testing.expectEqual('TimeoutError', e.name);
133+
testing.expectEqual('took too long', e.message);
134+
testing.expectEqual(23, e.code);
135+
testing.expectEqual('TimeoutError: took too long', e.toString());
136+
</script>
137+
124138
<script id=hierarchy_error>
125139
let link = $('#link');
126140
let content = $('#content');

src/browser/tests/performance_observer/performance_observer.html

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
</script>
7272

7373
<script>
74-
testing.expectEqual(['mark', 'measure'], PerformanceObserver.supportedEntryTypes);
74+
testing.expectEqual(['mark', 'measure', 'resource'], PerformanceObserver.supportedEntryTypes);
7575
</script>
7676

7777
<script id="list_get_entries_by_type">
@@ -174,3 +174,36 @@
174174
});
175175
}
176176
</script>
177+
178+
<script id="observe_mode_is_fixed">
179+
{
180+
const observer = new PerformanceObserver(() => {});
181+
observer.observe({ entryTypes: ["mark"] });
182+
testing.expectError("InvalidModificationError", () => observer.observe({ type: "mark" }));
183+
observer.disconnect();
184+
185+
const single = new PerformanceObserver(() => {});
186+
single.observe({ type: "mark" });
187+
testing.expectError("InvalidModificationError", () => single.observe({ entryTypes: ["mark"] }));
188+
testing.expectError("TypeError", () => single.observe({ type: "mark", entryTypes: ["mark"] }));
189+
testing.expectError("TypeError", () => single.observe({}));
190+
single.disconnect();
191+
}
192+
</script>
193+
194+
<script id="buffered_ignored_with_entry_types" type=module>
195+
{
196+
const state = await testing.async();
197+
performance.mark("before_observer");
198+
// Like every engine (and unlike the spec's TypeError), buffered is
199+
// ignored alongside entryTypes: no past entry is delivered.
200+
let delivered = 0;
201+
const observer = new PerformanceObserver((list) => { delivered += list.getEntries().length; });
202+
observer.observe({ entryTypes: ["mark"], buffered: true });
203+
setTimeout(() => state.resolve(), 20);
204+
await state.done(() => {
205+
observer.disconnect();
206+
testing.expectEqual(0, delivered);
207+
});
208+
}
209+
</script>
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
<!DOCTYPE html>
2+
<script src="testing.js"></script>
3+
4+
<script id=parser_loaded_script>
5+
// testing.js itself was fetched by the parser.
6+
const url = testing.BASE_URL + 'testing.js';
7+
const entries = performance.getEntriesByName(url);
8+
testing.expectEqual(1, entries.length);
9+
const e = entries[0];
10+
testing.expectEqual(true, e instanceof PerformanceResourceTiming);
11+
testing.expectEqual(true, e instanceof PerformanceEntry);
12+
testing.expectEqual('resource', e.entryType);
13+
testing.expectEqual(url, e.name);
14+
testing.expectEqual('script', e.initiatorType);
15+
testing.expectEqual(200, e.responseStatus);
16+
testing.expectEqual(true, e.decodedBodySize > 1000);
17+
testing.expectEqual(e.decodedBodySize, e.encodedBodySize);
18+
testing.expectEqual(true, e.transferSize > e.encodedBodySize);
19+
testing.expectEqual(1, performance.getEntriesByType('resource').length);
20+
testing.expectEqual(e, performance.getEntries()[0]);
21+
</script>
22+
23+
<script id=fetch_entry type=module>
24+
{
25+
const state = await testing.async();
26+
const url = testing.ORIGIN + '/xhr';
27+
const before = performance.now();
28+
await (await fetch(url)).text();
29+
const after = performance.now();
30+
state.resolve();
31+
await state.done(() => {
32+
const list = performance.getEntriesByName(url, 'resource');
33+
testing.expectEqual(1, list.length);
34+
const e = list[0];
35+
testing.expectEqual('fetch', e.initiatorType);
36+
testing.expectEqual('http/1.1', e.nextHopProtocol);
37+
testing.expectEqual('', e.deliveryType);
38+
testing.expectEqual(0, e.workerStart);
39+
testing.expectEqual(0, e.redirectStart);
40+
testing.expectEqual(0, e.redirectEnd);
41+
testing.expectEqual(true, e.startTime >= before);
42+
testing.expectEqual(e.startTime, e.fetchStart);
43+
testing.expectEqual(true, e.fetchStart <= e.domainLookupStart);
44+
testing.expectEqual(true, e.domainLookupStart <= e.domainLookupEnd);
45+
testing.expectEqual(true, e.domainLookupEnd <= e.connectStart);
46+
testing.expectEqual(true, e.connectStart <= e.connectEnd);
47+
testing.expectEqual(true, e.connectEnd <= e.requestStart);
48+
testing.expectEqual(true, e.requestStart <= e.responseStart);
49+
testing.expectEqual(true, e.responseStart <= e.responseEnd);
50+
testing.expectEqual(true, e.responseEnd <= after);
51+
testing.expectEqual(0, e.secureConnectionStart);
52+
testing.expectEqual(e.responseEnd - e.startTime, e.duration);
53+
testing.expectEqual(100, e.encodedBodySize);
54+
testing.expectEqual(100, e.decodedBodySize);
55+
testing.expectEqual(true, e.transferSize > 100);
56+
testing.expectEqual(200, e.responseStatus);
57+
testing.expectEqual('text/html', e.contentType);
58+
testing.expectEqual('', e.contentEncoding);
59+
testing.expectEqual(0, e.firstInterimResponseStart);
60+
testing.expectEqual(e.responseStart, e.finalResponseHeadersStart);
61+
62+
const json = e.toJSON();
63+
testing.expectEqual(url, json.name);
64+
testing.expectEqual('resource', json.entryType);
65+
testing.expectEqual('fetch', json.initiatorType);
66+
testing.expectEqual(e.responseEnd, json.responseEnd);
67+
testing.expectEqual(true, JSON.stringify(e).includes('"encodedBodySize":100'));
68+
});
69+
}
70+
</script>
71+
72+
<script id=xhr_entry type=module>
73+
{
74+
const state = await testing.async();
75+
const url = testing.ORIGIN + '/xhr/json';
76+
await new Promise((resolve) => {
77+
const xhr = new XMLHttpRequest();
78+
xhr.onload = resolve;
79+
xhr.open('GET', url);
80+
xhr.send();
81+
});
82+
state.resolve();
83+
await state.done(() => {
84+
const e = performance.getEntriesByName(url)[0];
85+
testing.expectEqual('xmlhttprequest', e.initiatorType);
86+
testing.expectEqual('application/json', e.contentType);
87+
testing.expectEqual(true, JSON.stringify(e).includes('"contentEncoding":""'));
88+
testing.expectEqual(200, e.responseStatus);
89+
});
90+
}
91+
</script>
92+
93+
<script id=redirect type=module>
94+
{
95+
const state = await testing.async();
96+
const url = testing.ORIGIN + '/resource-timing/redirect';
97+
const res = await fetch(url);
98+
await res.text();
99+
state.resolve();
100+
await state.done(() => {
101+
testing.expectEqual(testing.ORIGIN + '/resource-timing/plain', res.url);
102+
// Named after the URL as requested, not the one it landed on.
103+
testing.expectEqual(0, performance.getEntriesByName(res.url).length);
104+
const list = performance.getEntriesByName(url);
105+
testing.expectEqual(1, list.length);
106+
const e = list[0];
107+
testing.expectEqual(e.startTime, e.redirectStart);
108+
testing.expectEqual(true, e.redirectEnd >= e.redirectStart);
109+
testing.expectEqual(e.redirectEnd, e.fetchStart);
110+
testing.expectEqual(true, e.responseStart >= e.fetchStart);
111+
testing.expectEqual(true, e.responseEnd >= e.responseStart);
112+
testing.expectEqual(200, e.responseStatus);
113+
testing.expectEqual('window.__rt_plain = true;'.length, e.decodedBodySize);
114+
});
115+
}
116+
</script>
117+
118+
<script id=cross_origin_opaque type=module>
119+
{
120+
const state = await testing.async();
121+
// localhost is a different origin than the 127.0.0.1 test page; without
122+
// Timing-Allow-Origin only the start, end and duration are exposed.
123+
const url = 'http://localhost:9582/resource-timing/plain';
124+
const before = performance.now();
125+
await new Promise((resolve, reject) => {
126+
const s = document.createElement('script');
127+
s.src = url;
128+
s.onload = resolve;
129+
s.onerror = reject;
130+
document.head.appendChild(s);
131+
});
132+
state.resolve();
133+
await state.done(() => {
134+
testing.expectEqual(true, window.__rt_plain);
135+
const e = performance.getEntriesByName(url)[0];
136+
testing.expectEqual('script', e.initiatorType);
137+
testing.expectEqual(true, e.startTime >= before);
138+
testing.expectEqual(true, e.responseEnd > e.startTime);
139+
testing.expectEqual(e.responseEnd - e.startTime, e.duration);
140+
testing.expectEqual(e.startTime, e.fetchStart);
141+
testing.expectEqual(0, e.domainLookupStart);
142+
testing.expectEqual(0, e.domainLookupEnd);
143+
testing.expectEqual(0, e.connectStart);
144+
testing.expectEqual(0, e.connectEnd);
145+
testing.expectEqual(0, e.requestStart);
146+
testing.expectEqual(0, e.responseStart);
147+
testing.expectEqual(0, e.transferSize);
148+
testing.expectEqual(0, e.encodedBodySize);
149+
testing.expectEqual(0, e.decodedBodySize);
150+
testing.expectEqual(0, e.responseStatus);
151+
testing.expectEqual('', e.nextHopProtocol);
152+
testing.expectEqual('', e.contentType);
153+
});
154+
}
155+
</script>
156+
157+
<script id=cross_origin_timing_allow type=module>
158+
{
159+
const state = await testing.async();
160+
const url = 'http://localhost:9582/resource-timing/tao';
161+
await new Promise((resolve, reject) => {
162+
const s = document.createElement('script');
163+
s.src = url;
164+
s.onload = resolve;
165+
s.onerror = reject;
166+
document.head.appendChild(s);
167+
});
168+
state.resolve();
169+
await state.done(() => {
170+
testing.expectEqual(true, window.__rt_tao);
171+
const e = performance.getEntriesByName(url)[0];
172+
testing.expectEqual(true, e.requestStart >= e.fetchStart);
173+
testing.expectEqual(true, e.responseStart >= e.requestStart);
174+
testing.expectEqual(true, e.transferSize > e.encodedBodySize);
175+
testing.expectEqual('window.__rt_tao = true;'.length, e.decodedBodySize);
176+
testing.expectEqual(200, e.responseStatus);
177+
testing.expectEqual('http/1.1', e.nextHopProtocol);
178+
testing.expectEqual('text/javascript', e.contentType);
179+
});
180+
}
181+
</script>
182+
183+
<script id=failed_fetch type=module>
184+
{
185+
const state = await testing.async();
186+
// Nothing listens on port 1: the fetch fails without a response.
187+
const url = 'http://127.0.0.1:1/nope';
188+
let failed = false;
189+
try {
190+
await fetch(url);
191+
} catch (err) {
192+
failed = true;
193+
}
194+
state.resolve();
195+
await state.done(() => {
196+
testing.expectEqual(true, failed);
197+
const list = performance.getEntriesByName(url);
198+
testing.expectEqual(1, list.length);
199+
const e = list[0];
200+
testing.expectEqual('fetch', e.initiatorType);
201+
testing.expectEqual(true, e.startTime > 0);
202+
testing.expectEqual(true, e.responseEnd >= e.startTime);
203+
testing.expectEqual(e.startTime, e.fetchStart);
204+
testing.expectEqual(0, e.responseStatus);
205+
testing.expectEqual(0, e.transferSize);
206+
testing.expectEqual(0, e.responseStart);
207+
testing.expectEqual('', e.contentType);
208+
});
209+
}
210+
</script>
211+
212+
<script id=iframe_entry type=module>
213+
{
214+
const state = await testing.async();
215+
const url = testing.ORIGIN + '/redirect-target';
216+
const frame = document.createElement('iframe');
217+
await new Promise((resolve) => {
218+
frame.src = url;
219+
frame.onload = resolve;
220+
document.body.appendChild(frame);
221+
});
222+
state.resolve();
223+
await state.done(() => {
224+
// A frame's document fetch is a resource of its parent, not of itself.
225+
const e = performance.getEntriesByName(url)[0];
226+
testing.expectEqual('iframe', e.initiatorType);
227+
testing.expectEqual('text/html', e.contentType);
228+
testing.expectEqual(200, e.responseStatus);
229+
testing.expectEqual(0, frame.contentWindow.performance.getEntriesByType('resource').length);
230+
});
231+
}
232+
</script>
233+
234+
<script id=observer type=module>
235+
{
236+
const state = await testing.async();
237+
const url = testing.ORIGIN + '/xhr/xml';
238+
const observed = [];
239+
const po = new PerformanceObserver((list) => {
240+
for (const e of list.getEntries()) {
241+
observed.push(e);
242+
}
243+
});
244+
po.observe({ type: 'resource' });
245+
246+
const buffered = [];
247+
const po2 = new PerformanceObserver((list) => {
248+
for (const e of list.getEntries()) {
249+
buffered.push(e);
250+
}
251+
});
252+
po2.observe({ type: 'resource', buffered: true });
253+
254+
await (await fetch(url)).text();
255+
// Observers are notified from a queued task.
256+
await new Promise((resolve) => setTimeout(resolve, 20));
257+
state.resolve();
258+
await state.done(() => {
259+
po.disconnect();
260+
po2.disconnect();
261+
testing.expectEqual(true, PerformanceObserver.supportedEntryTypes.includes('resource'));
262+
testing.expectEqual(true, observed.some((e) => e.name === url && e.entryType === 'resource'));
263+
// The wire type is application/xml; the API reports the minimized essence.
264+
testing.expectEqual('application/xml', performance.getEntriesByName(url)[0].contentType);
265+
testing.expectEqual(false, observed.some((e) => e.name.endsWith('/testing.js')));
266+
testing.expectEqual(true, buffered.some((e) => e.name.endsWith('/testing.js')));
267+
testing.expectEqual(true, buffered.some((e) => e.name === url));
268+
});
269+
}
270+
</script>

0 commit comments

Comments
 (0)