/src/serenity/Userland/Libraries/LibWeb/HighResolutionTime/Performance.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> |
3 | | * Copyright (c) 2023, Luke Wilde <lukew@serenityos.org> |
4 | | * |
5 | | * SPDX-License-Identifier: BSD-2-Clause |
6 | | */ |
7 | | |
8 | | #include <LibWeb/Bindings/PerformancePrototype.h> |
9 | | #include <LibWeb/DOM/Document.h> |
10 | | #include <LibWeb/DOM/Event.h> |
11 | | #include <LibWeb/DOM/EventDispatcher.h> |
12 | | #include <LibWeb/HTML/StructuredSerialize.h> |
13 | | #include <LibWeb/HTML/Window.h> |
14 | | #include <LibWeb/HighResolutionTime/Performance.h> |
15 | | #include <LibWeb/HighResolutionTime/TimeOrigin.h> |
16 | | #include <LibWeb/NavigationTiming/EntryNames.h> |
17 | | #include <LibWeb/NavigationTiming/PerformanceNavigation.h> |
18 | | #include <LibWeb/NavigationTiming/PerformanceTiming.h> |
19 | | #include <LibWeb/PerformanceTimeline/EntryTypes.h> |
20 | | |
21 | | namespace Web::HighResolutionTime { |
22 | | |
23 | | JS_DEFINE_ALLOCATOR(Performance); |
24 | | |
25 | | Performance::Performance(JS::Realm& realm) |
26 | 0 | : DOM::EventTarget(realm) |
27 | 0 | , m_timer(Core::TimerType::Precise) |
28 | 0 | { |
29 | 0 | m_timer.start(); |
30 | 0 | } |
31 | | |
32 | 0 | Performance::~Performance() = default; |
33 | | |
34 | | void Performance::initialize(JS::Realm& realm) |
35 | 0 | { |
36 | 0 | Base::initialize(realm); |
37 | 0 | WEB_SET_PROTOTYPE_FOR_INTERFACE(Performance); |
38 | 0 | } |
39 | | |
40 | | void Performance::visit_edges(Cell::Visitor& visitor) |
41 | 0 | { |
42 | 0 | Base::visit_edges(visitor); |
43 | 0 | visitor.visit(m_navigation); |
44 | 0 | visitor.visit(m_timing); |
45 | 0 | } |
46 | | |
47 | | JS::GCPtr<NavigationTiming::PerformanceTiming> Performance::timing() |
48 | 0 | { |
49 | 0 | auto& realm = this->realm(); |
50 | 0 | if (!m_timing) |
51 | 0 | m_timing = heap().allocate<NavigationTiming::PerformanceTiming>(realm, realm); |
52 | 0 | return m_timing; |
53 | 0 | } |
54 | | |
55 | | JS::GCPtr<NavigationTiming::PerformanceNavigation> Performance::navigation() |
56 | 0 | { |
57 | 0 | auto& realm = this->realm(); |
58 | 0 | if (!m_navigation) { |
59 | | // FIXME actually determine values for these |
60 | 0 | u16 type = 0; |
61 | 0 | u16 redirect_count = 0; |
62 | |
|
63 | 0 | m_navigation = heap().allocate<NavigationTiming::PerformanceNavigation>(realm, realm, type, redirect_count); |
64 | 0 | } |
65 | 0 | return m_navigation; |
66 | 0 | } |
67 | | |
68 | | // https://w3c.github.io/hr-time/#timeorigin-attribute |
69 | | double Performance::time_origin() const |
70 | 0 | { |
71 | | // FIXME: The timeOrigin attribute MUST return the number of milliseconds in the duration returned by get time origin timestamp for the relevant global object of this. |
72 | 0 | return static_cast<double>(m_timer.origin_time().nanoseconds()) / 1e6; |
73 | 0 | } |
74 | | |
75 | | // https://w3c.github.io/hr-time/#now-method |
76 | | double Performance::now() const |
77 | 0 | { |
78 | | // The now() method MUST return the number of milliseconds in the current high resolution time given this's relevant global object (a duration). |
79 | 0 | return current_high_resolution_time(HTML::relevant_global_object(*this)); |
80 | 0 | } |
81 | | |
82 | | // https://w3c.github.io/user-timing/#mark-method |
83 | | WebIDL::ExceptionOr<JS::NonnullGCPtr<UserTiming::PerformanceMark>> Performance::mark(String const& mark_name, UserTiming::PerformanceMarkOptions const& mark_options) |
84 | 0 | { |
85 | 0 | auto& realm = this->realm(); |
86 | | |
87 | | // 1. Run the PerformanceMark constructor and let entry be the newly created object. |
88 | 0 | auto entry = TRY(UserTiming::PerformanceMark::construct_impl(realm, mark_name, mark_options)); |
89 | | |
90 | | // 2. Queue entry. |
91 | 0 | window_or_worker().queue_performance_entry(entry); |
92 | | |
93 | | // 3. Add entry to the performance entry buffer. |
94 | | // FIXME: This seems to be a holdover from moving to the `queue` structure for PerformanceObserver, as this would cause a double append. |
95 | | |
96 | | // 4. Return entry. |
97 | 0 | return entry; |
98 | 0 | } |
99 | | |
100 | | void Performance::clear_marks(Optional<String> mark_name) |
101 | 0 | { |
102 | | // 1. If markName is omitted, remove all PerformanceMark objects from the performance entry buffer. |
103 | 0 | if (!mark_name.has_value()) { |
104 | 0 | window_or_worker().clear_performance_entry_buffer({}, PerformanceTimeline::EntryTypes::mark); |
105 | 0 | return; |
106 | 0 | } |
107 | | |
108 | | // 2. Otherwise, remove all PerformanceMark objects listed in the performance entry buffer whose name is markName. |
109 | 0 | window_or_worker().remove_entries_from_performance_entry_buffer({}, PerformanceTimeline::EntryTypes::mark, mark_name.value()); |
110 | | |
111 | | // 3. Return undefined. |
112 | 0 | } |
113 | | |
114 | | WebIDL::ExceptionOr<HighResolutionTime::DOMHighResTimeStamp> Performance::convert_name_to_timestamp(JS::Realm& realm, String const& name) |
115 | 0 | { |
116 | 0 | auto& vm = realm.vm(); |
117 | | |
118 | | // 1. If the global object is not a Window object, throw a TypeError. |
119 | 0 | if (!is<HTML::Window>(realm.global_object())) |
120 | 0 | return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, TRY_OR_THROW_OOM(vm, String::formatted("'{}' is an attribute in the PerformanceTiming interface and thus can only be used in a Window context", name)) }; |
121 | | |
122 | | // 2. If name is navigationStart, return 0. |
123 | 0 | if (name == NavigationTiming::EntryNames::navigationStart) |
124 | 0 | return 0.0; |
125 | | |
126 | 0 | auto timing_interface = timing(); |
127 | 0 | VERIFY(timing_interface); |
128 | | |
129 | | // 3. Let startTime be the value of navigationStart in the PerformanceTiming interface. |
130 | 0 | auto start_time = timing_interface->navigation_start(); |
131 | | |
132 | | // 4. Let endTime be the value of name in the PerformanceTiming interface. |
133 | 0 | u64 end_time { 0 }; |
134 | |
|
135 | 0 | #define __ENUMERATE_NAVIGATION_TIMING_ENTRY_NAME(camel_case_name, snake_case_name) \ |
136 | 0 | if (name == NavigationTiming::EntryNames::camel_case_name) \ |
137 | 0 | end_time = timing_interface->snake_case_name(); |
138 | 0 | ENUMERATE_NAVIGATION_TIMING_ENTRY_NAMES |
139 | 0 | #undef __ENUMERATE_NAVIGATION_TIMING_ENTRY_NAME |
140 | | |
141 | | // 5. If endTime is 0, throw an InvalidAccessError. |
142 | 0 | if (end_time == 0) |
143 | 0 | return WebIDL::InvalidAccessError::create(realm, MUST(String::formatted("The '{}' entry in the PerformanceTiming interface is equal to 0, meaning it hasn't happened yet", name))); |
144 | | |
145 | | // 6. Return result of subtracting startTime from endTime. |
146 | 0 | return static_cast<HighResolutionTime::DOMHighResTimeStamp>(end_time - start_time); |
147 | 0 | } |
148 | | |
149 | | // https://w3c.github.io/user-timing/#dfn-convert-a-mark-to-a-timestamp |
150 | | WebIDL::ExceptionOr<HighResolutionTime::DOMHighResTimeStamp> Performance::convert_mark_to_timestamp(JS::Realm& realm, Variant<String, HighResolutionTime::DOMHighResTimeStamp> mark) |
151 | 0 | { |
152 | 0 | if (mark.has<String>()) { |
153 | 0 | auto const& mark_string = mark.get<String>(); |
154 | | |
155 | | // 1. If mark is a DOMString and it has the same name as a read only attribute in the PerformanceTiming interface, let end |
156 | | // time be the value returned by running the convert a name to a timestamp algorithm with name set to the value of mark. |
157 | 0 | #define __ENUMERATE_NAVIGATION_TIMING_ENTRY_NAME(name, _) \ |
158 | 0 | if (mark_string == NavigationTiming::EntryNames::name) \ |
159 | 0 | return convert_name_to_timestamp(realm, mark_string); |
160 | 0 | ENUMERATE_NAVIGATION_TIMING_ENTRY_NAMES |
161 | 0 | #undef __ENUMERATE_NAVIGATION_TIMING_ENTRY_NAME |
162 | | |
163 | | // 2. Otherwise, if mark is a DOMString, let end time be the value of the startTime attribute from the most recent occurrence |
164 | | // of a PerformanceMark object in the performance entry buffer whose name is mark. If no matching entry is found, throw a |
165 | | // SyntaxError. |
166 | 0 | auto& tuple = window_or_worker().relevant_performance_entry_tuple(PerformanceTimeline::EntryTypes::mark); |
167 | 0 | auto& performance_entry_buffer = tuple.performance_entry_buffer; |
168 | |
|
169 | 0 | auto maybe_entry = performance_entry_buffer.last_matching([&mark_string](JS::Handle<PerformanceTimeline::PerformanceEntry> const& entry) { |
170 | 0 | return entry->name() == mark_string; |
171 | 0 | }); |
172 | |
|
173 | 0 | if (!maybe_entry.has_value()) |
174 | 0 | return WebIDL::SyntaxError::create(realm, MUST(String::formatted("No PerformanceMark object with name '{}' found in the performance timeline", mark_string))); |
175 | | |
176 | 0 | return maybe_entry.value()->start_time(); |
177 | 0 | } |
178 | | |
179 | | // 3. Otherwise, if mark is a DOMHighResTimeStamp: |
180 | 0 | auto mark_time_stamp = mark.get<HighResolutionTime::DOMHighResTimeStamp>(); |
181 | | |
182 | | // 1. If mark is negative, throw a TypeError. |
183 | 0 | if (mark_time_stamp < 0.0) |
184 | 0 | return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Cannot have negative time values in PerformanceMark"sv }; |
185 | | |
186 | | // 2. Otherwise, let end time be mark. |
187 | 0 | return mark_time_stamp; |
188 | 0 | } |
189 | | |
190 | | // https://w3c.github.io/user-timing/#dom-performance-measure |
191 | | WebIDL::ExceptionOr<JS::NonnullGCPtr<UserTiming::PerformanceMeasure>> Performance::measure(String const& measure_name, Variant<String, UserTiming::PerformanceMeasureOptions> const& start_or_measure_options, Optional<String> end_mark) |
192 | 0 | { |
193 | 0 | auto& realm = this->realm(); |
194 | 0 | auto& vm = this->vm(); |
195 | | |
196 | | // 1. If startOrMeasureOptions is a PerformanceMeasureOptions object and at least one of start, end, duration, and detail |
197 | | // are present, run the following checks: |
198 | 0 | auto const* start_or_measure_options_dictionary_object = start_or_measure_options.get_pointer<UserTiming::PerformanceMeasureOptions>(); |
199 | 0 | if (start_or_measure_options_dictionary_object |
200 | 0 | && (start_or_measure_options_dictionary_object->start.has_value() |
201 | 0 | || start_or_measure_options_dictionary_object->end.has_value() |
202 | 0 | || start_or_measure_options_dictionary_object->duration.has_value() |
203 | 0 | || !start_or_measure_options_dictionary_object->detail.is_undefined())) { |
204 | | // 1. If endMark is given, throw a TypeError. |
205 | 0 | if (end_mark.has_value()) |
206 | 0 | return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Cannot provide PerformanceMeasureOptions and endMark at the same time"sv }; |
207 | | |
208 | | // 2. If startOrMeasureOptions's start and end members are both omitted, throw a TypeError. |
209 | 0 | if (!start_or_measure_options_dictionary_object->start.has_value() && !start_or_measure_options_dictionary_object->end.has_value()) |
210 | 0 | return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "PerformanceMeasureOptions must contain one or both of 'start' and 'end'"sv }; |
211 | | |
212 | | // 3. If startOrMeasureOptions's start, duration, and end members are all present, throw a TypeError. |
213 | 0 | if (start_or_measure_options_dictionary_object->start.has_value() && start_or_measure_options_dictionary_object->end.has_value() && start_or_measure_options_dictionary_object->duration.has_value()) |
214 | 0 | return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "PerformanceMeasureOptions cannot contain 'start', 'duration' and 'end' properties all at once"sv }; |
215 | 0 | } |
216 | | |
217 | | // 2. Compute end time as follows: |
218 | 0 | HighResolutionTime::DOMHighResTimeStamp end_time { 0.0 }; |
219 | | |
220 | | // 1. If endMark is given, let end time be the value returned by running the convert a mark to a timestamp algorithm passing |
221 | | // in endMark. |
222 | 0 | if (end_mark.has_value()) { |
223 | 0 | end_time = TRY(convert_mark_to_timestamp(realm, end_mark.value())); |
224 | 0 | } |
225 | | // 2. Otherwise, if startOrMeasureOptions is a PerformanceMeasureOptions object, and if its end member is present, let end |
226 | | // time be the value returned by running the convert a mark to a timestamp algorithm passing in startOrMeasureOptions's end. |
227 | 0 | else if (start_or_measure_options_dictionary_object && start_or_measure_options_dictionary_object->end.has_value()) { |
228 | 0 | end_time = TRY(convert_mark_to_timestamp(realm, start_or_measure_options_dictionary_object->end.value())); |
229 | 0 | } |
230 | | // 3. Otherwise, if startOrMeasureOptions is a PerformanceMeasureOptions object, and if its start and duration members are |
231 | | // both present: |
232 | 0 | else if (start_or_measure_options_dictionary_object && start_or_measure_options_dictionary_object->start.has_value() && start_or_measure_options_dictionary_object->duration.has_value()) { |
233 | | // 1. Let start be the value returned by running the convert a mark to a timestamp algorithm passing in start. |
234 | 0 | auto start = TRY(convert_mark_to_timestamp(realm, start_or_measure_options_dictionary_object->start.value())); |
235 | | |
236 | | // 2. Let duration be the value returned by running the convert a mark to a timestamp algorithm passing in duration. |
237 | 0 | auto duration = TRY(convert_mark_to_timestamp(realm, start_or_measure_options_dictionary_object->duration.value())); |
238 | | |
239 | | // 3. Let end time be start plus duration. |
240 | 0 | end_time = start + duration; |
241 | 0 | } |
242 | | // 4. Otherwise, let end time be the value that would be returned by the Performance object's now() method. |
243 | 0 | else { |
244 | 0 | end_time = now(); |
245 | 0 | } |
246 | | |
247 | | // 3. Compute start time as follows: |
248 | 0 | HighResolutionTime::DOMHighResTimeStamp start_time { 0.0 }; |
249 | | |
250 | | // 1. If startOrMeasureOptions is a PerformanceMeasureOptions object, and if its start member is present, let start time be |
251 | | // the value returned by running the convert a mark to a timestamp algorithm passing in startOrMeasureOptions's start. |
252 | 0 | if (start_or_measure_options_dictionary_object && start_or_measure_options_dictionary_object->start.has_value()) { |
253 | 0 | start_time = TRY(convert_mark_to_timestamp(realm, start_or_measure_options_dictionary_object->start.value())); |
254 | 0 | } |
255 | | // 2. Otherwise, if startOrMeasureOptions is a PerformanceMeasureOptions object, and if its duration and end members are |
256 | | // both present: |
257 | 0 | else if (start_or_measure_options_dictionary_object && start_or_measure_options_dictionary_object->duration.has_value() && start_or_measure_options_dictionary_object->end.has_value()) { |
258 | | // 1. Let duration be the value returned by running the convert a mark to a timestamp algorithm passing in duration. |
259 | 0 | auto duration = TRY(convert_mark_to_timestamp(realm, start_or_measure_options_dictionary_object->duration.value())); |
260 | | |
261 | | // 2. Let end be the value returned by running the convert a mark to a timestamp algorithm passing in end. |
262 | 0 | auto end = TRY(convert_mark_to_timestamp(realm, start_or_measure_options_dictionary_object->end.value())); |
263 | | |
264 | | // 3. Let start time be end minus duration. |
265 | 0 | start_time = end - duration; |
266 | 0 | } |
267 | | // 3. Otherwise, if startOrMeasureOptions is a DOMString, let start time be the value returned by running the convert a mark |
268 | | // to a timestamp algorithm passing in startOrMeasureOptions. |
269 | 0 | else if (start_or_measure_options.has<String>()) { |
270 | 0 | start_time = TRY(convert_mark_to_timestamp(realm, start_or_measure_options.get<String>())); |
271 | 0 | } |
272 | | // 4. Otherwise, let start time be 0. |
273 | 0 | else { |
274 | 0 | start_time = 0.0; |
275 | 0 | } |
276 | | |
277 | | // NOTE: Step 4 (creating the entry) is done after determining values, as we set the values once during creation and never |
278 | | // change them after. |
279 | | |
280 | | // 5. Set entry's name attribute to measureName. |
281 | | // NOTE: Will be done during construction. |
282 | | |
283 | | // 6. Set entry's entryType attribute to DOMString "measure". |
284 | | // NOTE: Already done via the `entry_type` virtual function. |
285 | | |
286 | | // 7. Set entry's startTime attribute to start time. |
287 | | // NOTE: Will be done during construction. |
288 | | |
289 | | // 8. Set entry's duration attribute to the duration from start time to end time. The resulting duration value MAY be negative. |
290 | 0 | auto duration = end_time - start_time; |
291 | | |
292 | | // 9. Set entry's detail attribute as follows: |
293 | 0 | JS::Value detail { JS::js_null() }; |
294 | | |
295 | | // 1. If startOrMeasureOptions is a PerformanceMeasureOptions object and startOrMeasureOptions's detail member is present: |
296 | 0 | if (start_or_measure_options_dictionary_object && !start_or_measure_options_dictionary_object->detail.is_undefined()) { |
297 | | // 1. Let record be the result of calling the StructuredSerialize algorithm on startOrMeasureOptions's detail. |
298 | 0 | auto record = TRY(HTML::structured_serialize(vm, start_or_measure_options_dictionary_object->detail)); |
299 | | |
300 | | // 2. Set entry's detail to the result of calling the StructuredDeserialize algorithm on record and the current realm. |
301 | 0 | detail = TRY(HTML::structured_deserialize(vm, record, realm, Optional<HTML::DeserializationMemory> {})); |
302 | 0 | } |
303 | | |
304 | | // 2. Otherwise, set it to null. |
305 | | // NOTE: Already the default value of `detail`. |
306 | | |
307 | | // 4. Create a new PerformanceMeasure object (entry) with this's relevant realm. |
308 | 0 | auto entry = realm.heap().allocate<UserTiming::PerformanceMeasure>(realm, realm, measure_name, start_time, duration, detail); |
309 | | |
310 | | // 10. Queue entry. |
311 | 0 | window_or_worker().queue_performance_entry(entry); |
312 | | |
313 | | // 11. Add entry to the performance entry buffer. |
314 | | // FIXME: This seems to be a holdover from moving to the `queue` structure for PerformanceObserver, as this would cause a double append. |
315 | | |
316 | | // 12. Return entry. |
317 | 0 | return entry; |
318 | 0 | } |
319 | | |
320 | | // https://w3c.github.io/user-timing/#dom-performance-clearmeasures |
321 | | void Performance::clear_measures(Optional<String> measure_name) |
322 | 0 | { |
323 | | // 1. If measureName is omitted, remove all PerformanceMeasure objects in the performance entry buffer. |
324 | 0 | if (!measure_name.has_value()) { |
325 | 0 | window_or_worker().clear_performance_entry_buffer({}, PerformanceTimeline::EntryTypes::measure); |
326 | 0 | return; |
327 | 0 | } |
328 | | |
329 | | // 2. Otherwise remove all PerformanceMeasure objects listed in the performance entry buffer whose name is measureName. |
330 | 0 | window_or_worker().remove_entries_from_performance_entry_buffer({}, PerformanceTimeline::EntryTypes::measure, measure_name.value()); |
331 | | |
332 | | // 3. Return undefined. |
333 | 0 | } |
334 | | |
335 | | // https://www.w3.org/TR/performance-timeline/#getentries-method |
336 | | WebIDL::ExceptionOr<Vector<JS::Handle<PerformanceTimeline::PerformanceEntry>>> Performance::get_entries() const |
337 | 0 | { |
338 | 0 | auto& vm = this->vm(); |
339 | | |
340 | | // Returns a PerformanceEntryList object returned by the filter buffer map by name and type algorithm with name and |
341 | | // type set to null. |
342 | 0 | return TRY_OR_THROW_OOM(vm, window_or_worker().filter_buffer_map_by_name_and_type(/* name= */ Optional<String> {}, /* type= */ Optional<String> {})); |
343 | 0 | } |
344 | | |
345 | | // https://www.w3.org/TR/performance-timeline/#dom-performance-getentriesbytype |
346 | | WebIDL::ExceptionOr<Vector<JS::Handle<PerformanceTimeline::PerformanceEntry>>> Performance::get_entries_by_type(String const& type) const |
347 | 0 | { |
348 | 0 | auto& vm = this->vm(); |
349 | | |
350 | | // Returns a PerformanceEntryList object returned by filter buffer map by name and type algorithm with name set to null, |
351 | | // and type set to the method's input type parameter. |
352 | 0 | return TRY_OR_THROW_OOM(vm, window_or_worker().filter_buffer_map_by_name_and_type(/* name= */ Optional<String> {}, type)); |
353 | 0 | } |
354 | | |
355 | | // https://www.w3.org/TR/performance-timeline/#dom-performance-getentriesbyname |
356 | | WebIDL::ExceptionOr<Vector<JS::Handle<PerformanceTimeline::PerformanceEntry>>> Performance::get_entries_by_name(String const& name, Optional<String> type) const |
357 | 0 | { |
358 | 0 | auto& vm = this->vm(); |
359 | | |
360 | | // Returns a PerformanceEntryList object returned by filter buffer map by name and type algorithm with name set to the |
361 | | // method input name parameter, and type set to null if optional entryType is omitted, or set to the method's input type |
362 | | // parameter otherwise. |
363 | 0 | return TRY_OR_THROW_OOM(vm, window_or_worker().filter_buffer_map_by_name_and_type(name, type)); |
364 | 0 | } |
365 | | |
366 | | HTML::WindowOrWorkerGlobalScopeMixin& Performance::window_or_worker() |
367 | 0 | { |
368 | 0 | auto* window_or_worker = dynamic_cast<HTML::WindowOrWorkerGlobalScopeMixin*>(&realm().global_object()); |
369 | 0 | VERIFY(window_or_worker); |
370 | 0 | return *window_or_worker; |
371 | 0 | } |
372 | | |
373 | | HTML::WindowOrWorkerGlobalScopeMixin const& Performance::window_or_worker() const |
374 | 0 | { |
375 | 0 | return const_cast<Performance*>(this)->window_or_worker(); |
376 | 0 | } |
377 | | |
378 | | } |