diff --git a/CHANGELOG.md b/CHANGELOG.md index 447f83e43e25..d8338898abab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -244,6 +244,8 @@ - [ToggleButtons can now have tooltips][6035]. - [Rendering of tooltips was improved.][6097] Their text is now more vertically centered and the delay before showing them was extended. +- [Accurate GPU performance measurements have been implemented][6595]. It is + possible now to track both the time spent on both the CPU and the GPU sides. [3857]: https://github.com/enso-org/enso/pull/3857 [3985]: https://github.com/enso-org/enso/pull/3985 @@ -256,6 +258,7 @@ [6366]: https://github.com/enso-org/enso/pull/6366 [6341]: https://github.com/enso-org/enso/pull/6341 [6470]: https://github.com/enso-org/enso/pull/6470 +[6595]: https://github.com/enso-org/enso/pull/6595 [6487]: https://github.com/enso-org/enso/pull/6487 [6512]: https://github.com/enso-org/enso/pull/6512 diff --git a/lib/rust/data-structures/src/lib.rs b/lib/rust/data-structures/src/lib.rs index 3d9daaa69f60..8f7d11d4af06 100644 --- a/lib/rust/data-structures/src/lib.rs +++ b/lib/rust/data-structures/src/lib.rs @@ -29,5 +29,6 @@ pub mod hash_map_tree; pub mod im_list; pub mod index; pub mod opt_vec; +pub mod size_capped_vec_deque; pub use enso_prelude as prelude; diff --git a/lib/rust/data-structures/src/size_capped_vec_deque.rs b/lib/rust/data-structures/src/size_capped_vec_deque.rs new file mode 100644 index 000000000000..97ac8a043c28 --- /dev/null +++ b/lib/rust/data-structures/src/size_capped_vec_deque.rs @@ -0,0 +1,100 @@ +//! A vector with a cap for its size. If the vector is full, adding a new element will remove +//! an old one. +use std::collections::VecDeque; + + + +// ========================== +// === SizeCappedVecDeque === +// ========================== + +/// A vector with a cap for its size. If the vector is full, adding a new element will remove an old +/// one. +#[derive(Clone, Debug)] +pub struct SizeCappedVecDeque { + capacity: usize, + vec: VecDeque, +} + +impl SizeCappedVecDeque { + /// Constructor. + pub fn new(capacity: usize) -> Self { + let vec = VecDeque::with_capacity(capacity); + Self { capacity, vec } + } + + /// Check whether the vector is empty. + pub fn is_empty(&self) -> bool { + self.vec.is_empty() + } + + /// The capacity of the vector. + pub fn len(&self) -> usize { + self.vec.len() + } + + /// Check whether the vector is full. + pub fn is_full(&self) -> bool { + self.len() == self.capacity + } + + /// Push a new element at the beginning of the vector. if the vector is full, the last element + /// will be dropped. + pub fn push_front(&mut self, value: T) { + if self.is_full() { + self.vec.pop_back(); + } + self.vec.push_front(value); + } + + /// Push a new element at the end of the vector. if the vector is full, the first element will + /// be dropped. + pub fn push_back(&mut self, value: T) { + if self.is_full() { + self.vec.pop_front(); + } + self.vec.push_back(value); + } + + /// Pop the first element of the vector. + pub fn pop_front(&mut self) -> Option { + self.vec.pop_front() + } + + /// Pop the last element of the vector. + pub fn pop_back(&mut self) -> Option { + self.vec.pop_back() + } + + /// Get the element at the given index. + pub fn get(&self, index: usize) -> Option<&T> { + self.vec.get(index) + } + + /// Get a mutable reference to the element at the given index. + pub fn get_mut(&mut self, index: usize) -> Option<&mut T> { + self.vec.get_mut(index) + } + + /// Get the last element of the vector, if any. + pub fn last(&self) -> Option<&T> { + self.vec.back() + } + + /// Run the provided function on the last `n` elements of the vector. + pub fn with_last_n_elems(&mut self, n: usize, mut f: impl FnMut(&mut T)) { + let len = self.len(); + let start = len.saturating_sub(n); + for i in start..len { + f(self.vec.get_mut(i).unwrap()); + } + } + + /// Run the provided function on the `n`-th element of the vector counted from back. + pub fn with_last_nth_elem(&mut self, n: usize, f: impl FnOnce(&mut T)) { + let len = self.len(); + if len > n { + f(self.vec.get_mut(len - n - 1).unwrap()); + } + } +} diff --git a/lib/rust/ensogl/core/Cargo.toml b/lib/rust/ensogl/core/Cargo.toml index 4f1eb22e0555..ce7538222f6b 100644 --- a/lib/rust/ensogl/core/Cargo.toml +++ b/lib/rust/ensogl/core/Cargo.toml @@ -68,6 +68,7 @@ features = [ 'WebGlBuffer', 'WebGlFramebuffer', 'WebGlProgram', + 'WebGlQuery', 'WebGlRenderingContext', 'WebGlShader', 'WebGlSync', diff --git a/lib/rust/ensogl/core/src/animation/loops.rs b/lib/rust/ensogl/core/src/animation/loops.rs index d129fa16c8a7..82554f508261 100644 --- a/lib/rust/ensogl/core/src/animation/loops.rs +++ b/lib/rust/ensogl/core/src/animation/loops.rs @@ -173,7 +173,6 @@ crate::define_endpoints_2! { on_after_animations(TimeInfo), on_before_layout(TimeInfo), on_before_rendering(TimeInfo), - frame_end(TimeInfo), } } @@ -188,11 +187,6 @@ pub fn on_frame_start() -> enso_frp::Sampler { LOOP_REGISTRY.with(|registry| registry.on_frame_start.clone_ref()) } -/// Fires at the end of every animation frame. -pub fn frame_end() -> enso_frp::Sampler { - LOOP_REGISTRY.with(|registry| registry.frame_end.clone_ref()) -} - /// Fires before the animations are evaluated. pub fn on_before_animations() -> enso_frp::Sampler { LOOP_REGISTRY.with(|registry| registry.on_before_animations.clone_ref()) @@ -213,8 +207,98 @@ pub fn on_before_rendering() -> enso_frp::Sampler { LOOP_REGISTRY.with(|registry| registry.on_before_rendering.clone_ref()) } -/// A wrapper for JavaScript `requestAnimationFrame` loop. It allows registering callbacks and also -/// exposes FRP endpoints that will emit signals on every loop iteration. +/// A wrapper for JavaScript `requestAnimationFrame` (RAF) loop. It allows registering callbacks and +/// also exposes FRP endpoints that will emit signals on every loop iteration. +/// +/// +/// # Warning, the following information is valid in Chrome only. +/// Handling of events, animation frames, and scheduling tasks is an implementation detail of every +/// browser. We did an in-depth analysis of how Chrome handles these things. Other browsers' +/// behavior might differ. This should not affect the correctness of either application behavior nor +/// performance measurements, but it is important to understand if you are working on +/// performance-related issues. +/// +/// +/// # Events scheduling in Chrome +/// The following graph shows an example order of tasks per frame with the assumptions that all the +/// frame tasks can be executed within that frame and the application runs with native FPS. +/// +/// ```text +/// ║ Frame 1 ║ Frame 2 ║ Frame 3 ║ ... +/// Idle time ║█ ███║██ █████║████ █║ ... +/// Other tasks ║ █ █ ║ █ █ ║ █ █ ║ ... +/// I/O Events ║ ██ █████ ║ ████ ║ █ ████ ║ ... +/// RAF ║ ███ ║ ███ ║ ███ ║ ... +/// GPU ║ ████ ║ ███ ║ ██████ ║ ... +/// ``` +/// +/// A few things should be noted here: +/// +/// - **Frame start idle time.** At the beginning of almost every frame, there will be an idle time +/// of up to a few milliseconds (we've observed idle times between 0 ms and 4 ms). This time is +/// likely caused by frames being synchronized with the screen refresh rate, while the tasks are +/// performed by the Chrome task scheduler, which is not fired at the synchronization points. To +/// learn more, see [this link](https://bugs.chromium.org/p/chromium/issues/detail?id=607650). It +/// is not clear whether Chrome considers this a bug or if they will ever fix it. +/// +/// - **Other tasks.** These are Chrome-internal tasks. For example, before every animation frame, +/// Chrome performs some preparation work, which in most cases is very short (we've observed times +/// between 20 μs and 40 μs). At the end of the frame, Chrome performs pre-paint, layout, +/// layerize, and commit jobs, which, in the case of EnsoGL applications (when no HTML elements +/// are on the stage), are also short, ranging between 0.1 ms and 0.5 ms. +/// +/// - **I/O Events.** Events such as mouse down/up/move can be scheduled before or after the RAF +/// callback. Some of these events might originate in the previous frame, and their handling might +/// be scheduled for the next frame by Chrome. +/// +/// - **RAF.** The RAF (Request Animation Frame) callback triggers WebGL per-frame functions, such +/// as animations, Display Object layout recomputation, WebGL buffer updates, and invoking WebGL +/// draw calls. +/// +/// - **GPU.** The GPU-related work starts at the end of the RAF callback, as soon as the first +/// WebGL calls are issued. It is performed in parallel with the rest of the RAF callback, I/O +/// events, and other tasks performed by Chrome. +/// +/// In case the total time of all the per-frame tasks is longer than the frame duration with the +/// native refresh rate, some frames might be skipped, and a very interesting time report behavior +/// can be observed. Let's consider the following events graph: +/// +/// ```text +/// ║ Real frame 1 work ┆ Real frame 2 work ┆ ... +/// ║ Frame 1 ║ ┆Frame 2 ║ Frame 3 (skip) ║ Frame 4 ║ ... +/// Idle time ║███ ║ ┆ ║ ║ ┆ ║ ... +/// Other tasks ║ █ ║ █┆ ║ ║ █┆ ║ ... +/// RAF ║ ██████████ ║ ┆ ██████████████████████████████ ┆ ██████████ ... +/// I/O Events ║ █████ ┆██ ║ ║ ██ ┆█ ║ ... +/// GPU ║ ███ ┆ ║ ║ █┆ ║ ... +/// ``` +/// +/// A few things should be noted here: +/// +/// - **Frame start idle time.** As previously mentioned, we can observe an idle time at the +/// beginning of the first frame. The Chrome task scheduler rendered the previous frame in time, +/// so there was no pressure to start the next tasks immediately. However, there is no idle time +/// present in the next frames as their computations took longer than the native frame duration. +/// +/// - **Frame times.** Even if the real computation time can exceed the time of a few frames issued +/// with the native refresh rate, the frames are always scheduled with the native refresh rate +/// time. Thus, some of these frames might be skipped, like frame 3 in the example above. +/// +/// - **RAF start time.** When the RAF callback is called, it is provided with the frame start time. +/// However, this time might be smaller than the time reported by performance.now(). For example, +/// in the graph above, the third call to the RAF callback will report the beginning time of frame +/// 4, even though it was performed in the middle of frame 4. If we measured the time at the end +/// of the second RAF callback with performance.now(), this time would be bigger than the time +/// reported by the third RAF callback. +/// +/// +/// # It is impossible to measure correct CPU-time from within JavaScript. +/// In Chrome, it is possible to get precise results of the GPU time (see +/// [ExtDisjointTimerQueryWebgl2]). However, even if we measure the time of all I/O events and the +/// time of the RAF callback, we cannot measure the time of the other tasks performed by Chrome +/// because we cannot register an event at the end of the per-frame job. Thus, when measuring +/// performance within the application, the only reliable way is to base it on the time reported by +/// the RAF callback and the GPU time measurements. #[derive(CloneRef, Derivative, Deref)] #[derivative(Clone(bound = ""))] #[derivative(Debug(bound = ""))] @@ -296,24 +380,23 @@ fn on_frame_closure( before_animations: &callback::registry::Copy1, animations: &callback::registry::Copy1>, ) -> OnFrameClosure { + let output = frp.private.output.clone_ref(); let before_animations = before_animations.clone_ref(); let animations = animations.clone_ref(); - let output = frp.private.output.clone_ref(); let mut time_info = InitializedTimeInfo::default(); let h_cell = Rc::new(Cell::new(callback::Handle::default())); let fixed_fps_sampler = Rc::new(RefCell::new(FixedFrameRateSampler::default())); move |frame_time: Duration| { + let _profiler = profiler::start_debug!(profiler::APP_LIFETIME, "@on_frame"); let time_info = time_info.next_frame(frame_time); let on_frame_start = output.on_frame_start.clone_ref(); let on_before_animations = output.on_before_animations.clone_ref(); let on_after_animations = output.on_after_animations.clone_ref(); let on_before_layout = output.on_before_layout.clone_ref(); let on_before_rendering = output.on_before_rendering.clone_ref(); - let frame_end = output.frame_end.clone_ref(); let before_animations = before_animations.clone_ref(); let animations = animations.clone_ref(); - let _profiler = profiler::start_debug!(profiler::APP_LIFETIME, "@on_frame"); let fixed_fps_sampler = fixed_fps_sampler.clone_ref(); TickPhases::new(&h_cell) @@ -322,12 +405,9 @@ fn on_frame_closure( .then(move || before_animations.run_all(time_info)) .then(move || fixed_fps_sampler.borrow_mut().run(time_info, |t| animations.run_all(t))) .then(move || on_after_animations.emit(time_info)) - .then(move || frame_end.emit(time_info)) .then(move || on_before_layout.emit(time_info)) - .then(move || { - on_before_rendering.emit(time_info); - drop(_profiler); - }) + .then(move || on_before_rendering.emit(time_info)) + .then(move || drop(_profiler)) .schedule(); } } diff --git a/lib/rust/ensogl/core/src/debug/monitor.rs b/lib/rust/ensogl/core/src/debug/monitor.rs index cfa1c979f519..921271035938 100644 --- a/lib/rust/ensogl/core/src/debug/monitor.rs +++ b/lib/rust/ensogl/core/src/debug/monitor.rs @@ -8,6 +8,7 @@ use crate::system::web; use crate::system::web::dom::Shape; use crate::system::web::JsValue; +use enso_data_structures::size_capped_vec_deque::SizeCappedVecDeque; use std::f64; @@ -58,12 +59,14 @@ pub struct ConfigTemplate { pub label_color_ok: Str, pub label_color_warn: Str, pub label_color_err: Str, + pub label_color_missing: Str, pub label_color_ok_selected: Str, pub plot_color_ok: Str, pub plot_color_warn: Str, pub plot_color_err: Str, + pub plot_color_missing: Str, pub plot_background_color: Str, - pub plot_bar_size: Option, + pub plot_bar_size: f64, pub plot_step_size: f64, pub plot_selection_border: f64, pub plot_selection_width: f64, @@ -104,12 +107,14 @@ fn light_theme() -> Config { label_color_ok: "#202124".into(), label_color_warn: "#f58025".into(), label_color_err: "#eb3941".into(), + label_color_missing: "#CCCCCC".into(), label_color_ok_selected: "#008cff".into(), plot_color_ok: "#202124".into(), plot_color_warn: "#f58025".into(), plot_color_err: "#eb3941".into(), + plot_color_missing: "#CCCCCC".into(), plot_background_color: "#f1f1f0".into(), - plot_bar_size: Some(2.0), + plot_bar_size: 2.0, plot_step_size: 1.0, plot_selection_border: 1.0, plot_selection_width: 1.0, @@ -141,12 +146,14 @@ impl Config { label_color_ok: (&self.label_color_ok).into(), label_color_warn: (&self.label_color_warn).into(), label_color_err: (&self.label_color_err).into(), + label_color_missing: (&self.label_color_missing).into(), label_color_ok_selected: (&self.label_color_ok_selected).into(), plot_color_ok: (&self.plot_color_ok).into(), plot_color_warn: (&self.plot_color_warn).into(), plot_color_err: (&self.plot_color_err).into(), + plot_color_missing: (&self.plot_color_missing).into(), plot_background_color: (&self.plot_background_color).into(), - plot_bar_size: self.plot_bar_size.map(|t| t * ratio), + plot_bar_size: self.plot_bar_size * ratio, plot_step_size: self.plot_step_size * ratio, plot_selection_border: self.plot_selection_border * ratio, plot_selection_width: self.plot_selection_width * ratio, @@ -224,6 +231,7 @@ impl DomData { root.set_style_or_warn("z-index", "100"); root.set_style_or_warn("left", format!("{PADDING_LEFT}px")); root.set_style_or_warn("top", format!("{PADDING_TOP}px")); + root.set_style_or_warn("pointer-events", "none"); root.set_style_or_warn("display", "flex"); root.set_style_or_warn("align-items", "stretch"); @@ -244,6 +252,7 @@ impl DomData { details.set_style_or_warn("border-radius", "6px"); details.set_style_or_warn("border", "2px solid #000000c4"); details.set_style_or_warn("background", &config.background_color); + details.set_style_or_warn("pointer-events", "all"); root.append_child(&details).unwrap(); let control_button = ControlButton::default(); @@ -332,6 +341,7 @@ impl MainArea { root.set_style_or_warn("border", "2px solid #000000c4"); root.set_style_or_warn("box-sizing", "content-box"); root.set_style_or_warn("position", "relative"); + root.set_style_or_warn("pointer-events", "all"); let plots = web::document.create_canvas_or_panic(); plots.set_style_or_warn("display", "block"); @@ -449,8 +459,10 @@ impl Default for Monitor { fn default() -> Self { let frp = Frp::new(); let mut renderer = Renderer::new(&frp.public); - renderer.add(sampler::FRAME_TIME); renderer.add(sampler::FPS); + renderer.add(sampler::FRAME_TIME); + renderer.add(sampler::CPU_AND_IDLE_TIME); + renderer.add(sampler::GPU_TIME); renderer.add(sampler::WASM_MEMORY_USAGE); renderer.add(sampler::GPU_MEMORY_USAGE); renderer.add(sampler::DRAW_CALL_COUNT); @@ -479,6 +491,13 @@ impl Monitor { self.renderer.borrow_mut().sample_and_draw(stats); } + /// Redraw the historical data of the monitor. This can be used to update the monitor if the + /// render stats were updated with a few-frames latency. Only the monitor panels marked as + /// `can_be_reported_late` will be redrawn. + pub fn redraw_historical_data(&self, offset: usize) { + self.renderer.borrow_mut().redraw_historical_data(offset); + } + /// Toggle the visibility of the monitor. pub fn toggle(&self) { if !self.initialized.get() { @@ -509,6 +528,11 @@ impl Monitor { } self.renderer.borrow_mut().toggle(); } + + /// Run the provided function with last `n`-th recorded performance sample. + pub fn with_last_nth_sample(&self, n: usize, f: impl FnOnce(&mut StatsData)) { + self.renderer.borrow_mut().samples.with_last_nth_elem(n, f); + } } @@ -519,19 +543,18 @@ impl Monitor { /// Code responsible for drawing [`Monitor`]'s data. #[derive(Debug)] struct Renderer { - frp: api::Public, - user_config: Config, - config: SamplerConfig, - screen_shape: Shape, - width: f64, - height: f64, - dom: Option, - panels: Vec, - selected_panel: Option, - first_draw: bool, - paused: bool, - samples: Vec, - next_sample_index: usize, + frp: api::Public, + user_config: Config, + config: SamplerConfig, + screen_shape: Shape, + width: f64, + height: f64, + dom: Option, + panels: Vec, + selected_panel: Option, + first_draw: bool, + paused: bool, + samples: SizeCappedVecDeque, } impl Renderer { @@ -547,8 +570,7 @@ impl Renderer { let dom = default(); let selected_panel = default(); let paused = default(); - let samples = default(); - let next_sample_index = default(); + let samples = SizeCappedVecDeque::new(config.sample_count); let mut out = Self { frp, user_config, @@ -562,7 +584,6 @@ impl Renderer { first_draw, paused, samples, - next_sample_index, }; out.update_config(); out @@ -644,9 +665,7 @@ impl Renderer { } fn set_focus_sample(&mut self, index: usize) { - let local_index = index + self.next_sample_index; - let local_index = local_index % self.config.sample_count; - let sample = self.samples.get(local_index).or_else(|| self.samples.last()); + let sample = self.samples.get(index).or_else(|| self.samples.last()); if let Some(sample) = sample { for panel in &self.panels { panel.sample_and_postprocess(sample); @@ -659,12 +678,7 @@ impl Renderer { fn sample_and_draw(&mut self, stats: &StatsData) { if !self.paused { - if self.samples.len() < self.config.sample_count { - self.samples.push(stats.clone()); - } else { - self.samples[self.next_sample_index] = stats.clone(); - self.next_sample_index = (self.next_sample_index + 1) % self.config.sample_count; - } + self.samples.push_back(stats.clone()); if self.visible() { for panel in &self.panels { panel.sample_and_postprocess(stats); @@ -676,6 +690,23 @@ impl Renderer { } } + fn redraw_historical_data(&mut self, n: usize) { + if self.visible() && !self.paused { + if let Some(index) = self.samples.len().checked_sub(n + 1) { + if let Some(stats) = self.samples.get(index) { + if let Some(dom) = self.dom.clone() { + self.with_all_panels(&dom, |_selected, panel| { + if panel.can_be_reported_late() { + panel.sample_and_postprocess(stats); + panel.draw_historical_data(&dom, n); + } + }); + } + } + } + } + } + /// Draw the widget and update all of the graphs. fn draw(&mut self, stats: &StatsData) { if let Some(dom) = self.dom.clone() { @@ -820,6 +851,12 @@ impl Panel { self.rc.borrow_mut().draw(selected, dom, stats) } + /// Redraw the historical data of the panel. This can be used to update the panel if the render + /// stats were updated with a few-frames latency. + pub fn draw_historical_data(&self, dom: &Dom, n: usize) { + self.rc.borrow_mut().draw_plot_update(dom, n) + } + /// Display results in the paused state. In this state, the user might dragged the selection /// area, so we need to update the results, but we do not need to update the plots. pub fn draw_paused(&self, selected: bool, dom: &Dom, stats: &StatsData) { @@ -835,6 +872,10 @@ impl Panel { fn first_draw(&self, dom: &Dom) { self.rc.borrow_mut().first_draw(dom) } + + fn can_be_reported_late(&self) -> bool { + self.rc.borrow().sampler.can_be_reported_late + } } @@ -850,7 +891,7 @@ pub struct PanelData { config: SamplerConfig, min_value: f64, max_value: f64, - value: f64, + value: Option, norm_value: f64, value_check: sampler::ValueCheck, precision: usize, @@ -900,33 +941,37 @@ impl PanelData { /// Clamp the measured values to the `max_value` and `min_value`. fn clamp_value(&mut self) { - if let Some(max_value) = self.sampler.max_value { - if self.value > max_value { - self.value = max_value; + if let Some(value) = self.value { + if let Some(max_value) = self.sampler.max_value { + if value > max_value { + self.value = Some(max_value); + } } - } - if let Some(min_value) = self.sampler.min_value { - if self.value > min_value { - self.value = min_value; + if let Some(min_value) = self.sampler.min_value { + if value > min_value { + self.value = Some(min_value); + } + } + if value > self.max_value { + self.max_value = value; + } + if value < self.min_value { + self.min_value = value; } - } - if self.value > self.max_value { - self.max_value = self.value; - } - if self.value < self.min_value { - self.min_value = self.value; } } /// Normalize the value to the monitor's plot size. fn normalize_value(&mut self) { - let mut size = self.max_value - self.min_value; - if let Some(min_size) = self.sampler.min_size() { - if size < min_size { - size = min_size; + if let Some(value) = self.value { + let mut size = self.max_value - self.min_value; + if let Some(min_size) = self.sampler.min_size() { + if size < min_size { + size = min_size; + } } + self.norm_value = (value - self.min_value) / size; } - self.norm_value = (self.value - self.min_value) / size; } } @@ -938,7 +983,7 @@ impl PanelData { pub fn draw(&mut self, selected: bool, dom: &Dom, stats: &StatsData) { self.draw_label(dom, selected); self.draw_value(dom); - self.draw_plot_update(dom); + self.draw_plot_update(dom, 0); self.draw_details(dom, selected, stats); } @@ -984,9 +1029,22 @@ impl PanelData { }) } - fn with_pen_at_new_plot_part(&mut self, dom: &Dom, f: impl FnOnce(&mut Self) -> T) -> T { + /// Move the pen to the new plot part, this is the `right_of_plot - (1 + offset) * + /// plot_step_size`. The `offset` parameter can be used to chose the sample number. For example, + /// if `offset` is `0`, the pen will be moved before the last sample of the plot. + fn with_pen_at_new_plot_part( + &mut self, + dom: &Dom, + offset: usize, + f: impl FnOnce(&mut Self) -> T, + ) -> T { + let offset = 1.0 + offset as f64; self.with_pen_at_plot(dom, |this| { - this.with_moved_pen(dom, this.config.plot_width() - this.config.plot_step_size, f) + this.with_moved_pen( + dom, + this.config.plot_width() - this.config.plot_step_size * offset, + f, + ) }) } @@ -1018,12 +1076,16 @@ impl PanelData { /// Draw the plot text value. fn draw_value(&mut self, dom: &Dom) { self.with_pen_at_value(dom, |this| { - let display_value = format!("{1:.0$}", this.precision, this.value); + let display_value = match this.value { + None => "N/A".to_string(), + Some(value) => format!("{1:.0$}", this.precision, value), + }; let y_pos = this.config.panel_height - this.config.font_vertical_offset; let color = match this.value_check { sampler::ValueCheck::Correct => &this.config.label_color_ok, sampler::ValueCheck::Warning => &this.config.label_color_warn, sampler::ValueCheck::Error => &this.config.label_color_err, + sampler::ValueCheck::Missing => &this.config.label_color_missing, }; dom.plot_area.plots_context.set_fill_style(color); dom.plot_area @@ -1035,29 +1097,30 @@ impl PanelData { /// Draw a single plot point. As the plots shift left on every frame, this function only updates /// the most recent plot value. - fn draw_plot_update(&mut self, dom: &Dom) { - self.with_pen_at_new_plot_part(dom, |this| { - dom.plot_area.plots_context.set_fill_style(&this.config.plot_background_color); + fn draw_plot_update(&mut self, dom: &Dom, offset: usize) { + self.with_pen_at_new_plot_part(dom, offset, |this| { + dom.plot_area.plots_context.set_fill_style(&this.config.background_color); dom.plot_area.plots_context.fill_rect( 0.0, 0.0, this.config.plot_step_size, this.config.panel_height, ); - let value_height = this.norm_value * this.config.panel_height; - let y_pos = this.config.panel_height - value_height; - let bar_height = this.config.plot_bar_size.unwrap_or(value_height); + let panel_space = this.config.panel_height - this.config.plot_bar_size; + let value_height = this.norm_value * panel_space; + let y_pos = panel_space - value_height; let color = match this.value_check { sampler::ValueCheck::Correct => &this.config.plot_color_ok, sampler::ValueCheck::Warning => &this.config.plot_color_warn, sampler::ValueCheck::Error => &this.config.plot_color_err, + sampler::ValueCheck::Missing => &this.config.plot_color_missing, }; dom.plot_area.plots_context.set_fill_style(color); dom.plot_area.plots_context.fill_rect( 0.0, y_pos, this.config.plot_step_size, - bar_height, + this.config.plot_bar_size, ); }) } diff --git a/lib/rust/ensogl/core/src/debug/monitor/sampler.rs b/lib/rust/ensogl/core/src/debug/monitor/sampler.rs index 0bbd36b765f1..94cbb14907c9 100644 --- a/lib/rust/ensogl/core/src/debug/monitor/sampler.rs +++ b/lib/rust/ensogl/core/src/debug/monitor/sampler.rs @@ -6,8 +6,6 @@ use crate::prelude::*; use crate::debug::stats::StatsData; -use num_traits::cast::AsPrimitive; - // ================== @@ -22,6 +20,7 @@ pub enum ValueCheck { Correct, Warning, Error, + Missing, } impl Default for ValueCheck { @@ -34,23 +33,27 @@ impl Default for ValueCheck { #[allow(clippy::collapsible_else_if)] impl ValueCheck { /// Construct the check by comparing the provided value to two threshold values. - pub fn from_threshold(warn_threshold: f64, err_threshold: f64, value: f64) -> Self { - if warn_threshold > err_threshold { - if value >= warn_threshold { - ValueCheck::Correct - } else if value >= err_threshold { - ValueCheck::Warning + pub fn from_threshold(warn_threshold: f64, err_threshold: f64, value: Option) -> Self { + if let Some(value) = value { + if warn_threshold > err_threshold { + if value >= warn_threshold { + ValueCheck::Correct + } else if value >= err_threshold { + ValueCheck::Warning + } else { + ValueCheck::Error + } } else { - ValueCheck::Error + if value <= warn_threshold { + ValueCheck::Correct + } else if value <= err_threshold { + ValueCheck::Warning + } else { + ValueCheck::Error + } } } else { - if value <= warn_threshold { - ValueCheck::Correct - } else if value <= err_threshold { - ValueCheck::Warning - } else { - ValueCheck::Error - } + ValueCheck::Missing } } } @@ -66,25 +69,33 @@ impl ValueCheck { #[derive(Copy, Clone)] pub struct Sampler { /// Label of the sampler to be displayed in the performance monitor window. - pub label: &'static str, - /// Get the newest value of the sampler. The value will be displayed in the monitor panel. - pub expr: fn(&StatsData) -> f64, + pub label: &'static str, + /// Get the newest value of the sampler. The value will be displayed in the monitor panel. In + /// case this function returns [`None`], it means that the value is not available yet. + pub expr: fn(&StatsData) -> Option, /// Get the details to be displayed in the details view. - pub details: Option &[&'static str]>, + pub details: Option &[&'static str]>, /// If the value crosses this threshold, the graph will be drawn in the warning color. - pub warn_threshold: f64, + pub warn_threshold: f64, /// If the value crosses this threshold, the graph will be drawn in the error color. - pub err_threshold: f64, + pub err_threshold: f64, /// The value will be divided by this number before being displayed. - pub value_divisor: f64, + pub value_divisor: f64, /// The minimum expected value in order to set proper scaling of the monitor plots. If the real /// value will be smaller than this parameter, it will be clamped. - pub min_value: Option, + pub min_value: Option, /// The maximum expected value in order to set proper scaling of the monitor plots. If the real /// value will be bigger than this parameter, it will be clamped. - pub max_value: Option, + pub max_value: Option, /// The number of digits after the dot which should be displayed in the monitor panel. - pub precision: usize, + pub precision: usize, + /// The minimum size of the sampler in the performance monitor view. If it is set to [`None`], + /// the size will be computed as the bigger value of the [`Self::warn_threshold`] and + /// [`Self::err_threshold`]. + pub min_size: Option, + /// If set to [`true`], the sampler value can be updated in the subsequent frames and the + /// sampler will be re-drawn if the data updates after the first draw. + pub can_be_reported_late: bool, } impl Debug for Sampler { @@ -96,28 +107,29 @@ impl Debug for Sampler { impl const Default for Sampler { fn default() -> Self { Self { - label: "Unlabeled", - expr: |_| 0.0, - details: None, - warn_threshold: 0.0, - err_threshold: 0.0, - value_divisor: 1.0, - min_value: None, - max_value: None, - precision: 0, + label: "Unlabeled", + expr: |_| None, + details: None, + warn_threshold: 0.0, + err_threshold: 0.0, + value_divisor: 1.0, + min_value: None, + max_value: None, + precision: 0, + min_size: None, + can_be_reported_late: false, } } } impl Sampler { /// The current sampler value. - pub fn value(&self, stats: &StatsData) -> f64 { - let raw_value: f64 = (self.expr)(stats).as_(); - raw_value / self.value_divisor + pub fn value(&self, stats: &StatsData) -> Option { + (self.expr)(stats).map(|t| t / self.value_divisor) } - /// Check the current value in order to draw it with warning or error if it exceeds the allowed - /// thresholds. + /// Check the current value in order to draw it with warning or error color if it exceeds the + /// allowed thresholds. pub fn check(&self, stats: &StatsData) -> ValueCheck { let value = self.value(stats); ValueCheck::from_threshold(self.warn_threshold, self.err_threshold, value) @@ -125,7 +137,7 @@ impl Sampler { /// Minimum size of the size the sampler should occupy in the performance monitor view. pub fn min_size(&self) -> Option { - Some(self.warn_threshold) + Some(self.min_size.unwrap_or_else(|| max(self.warn_threshold, self.err_threshold))) } } @@ -142,28 +154,49 @@ const DEFAULT_SAMPLER: Sampler = Default::default(); #[allow(missing_docs)] pub const FPS: Sampler = Sampler { label: "Frames per second", - expr: |s| s.fps, + expr: |s| Some(s.fps), warn_threshold: 55.0, err_threshold: 25.0, precision: 2, - max_value: Some(60.0), ..DEFAULT_SAMPLER }; #[allow(missing_docs)] pub const FRAME_TIME: Sampler = Sampler { - label: "Frame time (ms)", - expr: |s| s.frame_time, + label: "GPU + CPU + Idle (ms)", + expr: |s| Some(s.frame_time), + warn_threshold: 1000.0 / 55.0, + err_threshold: 1000.0 / 25.0, + precision: 2, + ..DEFAULT_SAMPLER +}; + +#[allow(missing_docs)] +pub const CPU_AND_IDLE_TIME: Sampler = Sampler { + label: "CPU + Idle (ms)", + expr: |s| s.cpu_and_idle_time, + warn_threshold: 1000.0 / 55.0, + err_threshold: 1000.0 / 25.0, + precision: 2, + can_be_reported_late: true, + ..DEFAULT_SAMPLER +}; + +#[allow(missing_docs)] +pub const GPU_TIME: Sampler = Sampler { + label: "GPU time (ms)", + expr: |s| s.gpu_time, warn_threshold: 1000.0 / 55.0, err_threshold: 1000.0 / 25.0, precision: 2, + can_be_reported_late: true, ..DEFAULT_SAMPLER }; #[allow(missing_docs)] pub const WASM_MEMORY_USAGE: Sampler = Sampler { label: "WASM memory usage (Mb)", - expr: |s| s.wasm_memory_usage as f64, + expr: |s| Some(s.wasm_memory_usage as f64), warn_threshold: 50.0, err_threshold: 100.0, precision: 2, @@ -174,7 +207,7 @@ pub const WASM_MEMORY_USAGE: Sampler = Sampler { #[allow(missing_docs)] pub const GPU_MEMORY_USAGE: Sampler = Sampler { label: "GPU memory usage (Mb)", - expr: |s| s.gpu_memory_usage as f64, + expr: |s| Some(s.gpu_memory_usage as f64), warn_threshold: 100.0, err_threshold: 500.0, precision: 2, @@ -185,7 +218,7 @@ pub const GPU_MEMORY_USAGE: Sampler = Sampler { #[allow(missing_docs)] pub const DRAW_CALL_COUNT: Sampler = Sampler { label: "Draw call count", - expr: |s| s.draw_calls.len() as f64, + expr: |s| Some(s.draw_calls.len() as f64), details: Some(|s| &s.draw_calls), warn_threshold: 100.0, err_threshold: 500.0, @@ -195,7 +228,7 @@ pub const DRAW_CALL_COUNT: Sampler = Sampler { #[allow(missing_docs)] pub const BUFFER_COUNT: Sampler = Sampler { label: "Buffer count", - expr: |s| s.buffer_count as f64, + expr: |s| Some(s.buffer_count as f64), warn_threshold: 100.0, err_threshold: 500.0, ..DEFAULT_SAMPLER @@ -204,7 +237,7 @@ pub const BUFFER_COUNT: Sampler = Sampler { #[allow(missing_docs)] pub const DATA_UPLOAD_COUNT: Sampler = Sampler { label: "Data upload count", - expr: |s| s.data_upload_count as f64, + expr: |s| Some(s.data_upload_count as f64), warn_threshold: 100.0, err_threshold: 500.0, ..DEFAULT_SAMPLER @@ -213,7 +246,7 @@ pub const DATA_UPLOAD_COUNT: Sampler = Sampler { #[allow(missing_docs)] pub const DATA_UPLOAD_SIZE: Sampler = Sampler { label: "Data upload size (Mb)", - expr: |s| s.data_upload_size as f64, + expr: |s| Some(s.data_upload_size as f64), warn_threshold: 1.0, err_threshold: 10.0, precision: 2, @@ -224,7 +257,7 @@ pub const DATA_UPLOAD_SIZE: Sampler = Sampler { #[allow(missing_docs)] pub const SPRITE_SYSTEM_COUNT: Sampler = Sampler { label: "Sprite system count", - expr: |s| s.sprite_system_count as f64, + expr: |s| Some(s.sprite_system_count as f64), warn_threshold: 100.0, err_threshold: 500.0, ..DEFAULT_SAMPLER @@ -233,7 +266,7 @@ pub const SPRITE_SYSTEM_COUNT: Sampler = Sampler { #[allow(missing_docs)] pub const SYMBOL_COUNT: Sampler = Sampler { label: "Symbol count", - expr: |s| s.symbol_count as f64, + expr: |s| Some(s.symbol_count as f64), warn_threshold: 100.0, err_threshold: 500.0, ..DEFAULT_SAMPLER @@ -242,7 +275,7 @@ pub const SYMBOL_COUNT: Sampler = Sampler { #[allow(missing_docs)] pub const SPRITE_COUNT: Sampler = Sampler { label: "Sprite count", - expr: |s| s.sprite_count as f64, + expr: |s| Some(s.sprite_count as f64), warn_threshold: 100_000.0, err_threshold: 500_000.0, ..DEFAULT_SAMPLER @@ -251,7 +284,7 @@ pub const SPRITE_COUNT: Sampler = Sampler { #[allow(missing_docs)] pub const SHADER_COUNT: Sampler = Sampler { label: "Shader count", - expr: |s| s.shader_count as f64, + expr: |s| Some(s.shader_count as f64), warn_threshold: 100.0, err_threshold: 500.0, ..DEFAULT_SAMPLER @@ -260,7 +293,7 @@ pub const SHADER_COUNT: Sampler = Sampler { #[allow(missing_docs)] pub const SHADER_COMPILE_COUNT: Sampler = Sampler { label: "Shader compile count", - expr: |s| s.shader_compile_count as f64, + expr: |s| Some(s.shader_compile_count as f64), warn_threshold: 10.0, err_threshold: 100.0, ..DEFAULT_SAMPLER diff --git a/lib/rust/ensogl/core/src/debug/stats.rs b/lib/rust/ensogl/core/src/debug/stats.rs index 0f2401ff808f..e7622496fa2e 100644 --- a/lib/rust/ensogl/core/src/debug/stats.rs +++ b/lib/rust/ensogl/core/src/debug/stats.rs @@ -13,14 +13,14 @@ //! - `gpu_memory_usage` //! - `data_upload_size` -use enso_prelude::*; +use crate::prelude::*; +use enso_web::traits::*; use crate::display::world; use crate::display::SymbolId; use enso_types::unit2::Duration; use enso_web::Performance; -use enso_web::TimeProvider; use js_sys::ArrayBuffer; use js_sys::WebAssembly::Memory; use wasm_bindgen::JsCast; @@ -31,44 +31,24 @@ use wasm_bindgen::JsCast; // === Stats === // ============= -/// Contains all the gathered stats, and provides methods for modifying and retrieving their -/// values. Uses the Web Performance API to access current time for calculating time-dependent -/// stats (e.g. FPS). -pub type Stats = StatsWithTimeProvider; - - - -// ============================= -// === StatsWithTimeProvider === -// ============================= - /// Contains all the gathered stats, and provides methods for modifying and retrieving their /// values. -/// Uses [`T`] to access current time for calculating time-dependent stats (e.g. FPS). -#[derive(Debug, Deref, CloneRef)] -pub struct StatsWithTimeProvider { - rc: Rc>>, +#[derive(Clone, CloneRef, Debug, Deref, Default)] +pub struct Stats { + rc: Rc>, } -impl Clone for StatsWithTimeProvider { - fn clone(&self) -> Self { - Self { rc: self.rc.clone() } - } -} - -impl StatsWithTimeProvider { +impl Stats { /// Constructor. - pub fn new(time_provider: T) -> Self { - let framed_stats_data = FramedStatsData::new(time_provider); - let rc = Rc::new(RefCell::new(framed_stats_data)); - Self { rc } + pub fn new() -> Self { + default() } /// Calculate FPS for the last frame. This function should be called on the very beginning of /// every frame. Please note, that it does not clean the per-frame statistics. You want to run /// the [`reset_per_frame_statistics`] function before running rendering operations. - pub fn calculate_prev_frame_fps(&self, time: Duration) { - self.rc.borrow_mut().calculate_prev_frame_fps(time) + pub fn calculate_prev_frame_stats(&self, time: Duration) { + self.rc.borrow_mut().calculate_prev_frame_stats(time) } /// Clean the per-frame statistics, such as the per-frame number of draw calls. This function @@ -77,12 +57,6 @@ impl StatsWithTimeProvider { self.rc.borrow_mut().reset_per_frame_statistics() } - /// Ends tracking data for the current animation frame. - /// Also, calculates the `frame_time` and `wasm_memory_usage` stats. - pub fn end_frame(&self) { - self.rc.borrow_mut().end_frame(); - } - /// Register a new draw call for the given symbol. pub fn register_draw_call(&self, symbol_id: SymbolId) { let label = world::with_context(|ctx| { @@ -94,49 +68,46 @@ impl StatsWithTimeProvider { -// ======================= -// === FramedStatsData === -// ======================= +// ===================== +// === StatsInternal === +// ===================== -/// Internal representation of [`StatsWithTimeProvider`]. +/// Internal representation of [`Stats`]. #[allow(missing_docs)] #[derive(Debug)] -pub struct FramedStatsData { - time_provider: T, - pub stats_data: StatsData, - frame_begin_time: Option, +pub struct StatsInternal { + time_provider: Performance, + pub stats_data: StatsData, + frame_start: Option, + wasm_memory_usage: u32, } -impl FramedStatsData { +impl StatsInternal { /// Constructor. - fn new(time_provider: T) -> Self { + fn new() -> Self { + let time_provider = enso_web::window.performance_or_panic(); let stats_data = default(); - let frame_begin_time = None; - Self { time_provider, stats_data, frame_begin_time } + let frame_start = None; + let wasm_memory_usage = default(); + Self { time_provider, stats_data, frame_start, wasm_memory_usage } } /// Calculate FPS for the last frame. This function should be called on the very beginning of /// every frame. Please note, that it does not clean the per-frame statistics. You want to run /// the [`reset_per_frame_statistics`] function before running rendering operations. - fn calculate_prev_frame_fps(&mut self, time: Duration) { - let time = time.unchecked_raw() as f64; - if let Some(previous_frame_begin_time) = self.frame_begin_time.replace(time) { - self.stats_data.fps = 1000.0 / (time - previous_frame_begin_time); + fn calculate_prev_frame_stats(&mut self, frame_start: Duration) { + let frame_start = frame_start.unchecked_raw() as f64; + if let Some(prev_frame_start) = self.frame_start.replace(frame_start) { + let prev_frame_time = frame_start - prev_frame_start; + self.stats_data.frame_time = prev_frame_time; + self.stats_data.fps = 1000.0 / prev_frame_time; } - } - - fn end_frame(&mut self) { - if let Some(begin_time) = self.frame_begin_time { - let end_time = self.time_provider.now(); - self.stats_data.frame_time = end_time - begin_time; - } - - // TODO[MC,IB]: drop the `cfg!` (outlier in our codebase) once wasm_bindgen::memory() - // doesn't panic in non-WASM builds (https://www.pivotaltracker.com/story/show/180978631) if cfg!(target_arch = "wasm32") { let memory: Memory = wasm_bindgen::memory().dyn_into().unwrap(); let buffer: ArrayBuffer = memory.buffer().dyn_into().unwrap(); - self.stats_data.wasm_memory_usage = buffer.byte_length(); + let prev_frame_wasm_memory_usage = self.wasm_memory_usage; + self.wasm_memory_usage = buffer.byte_length(); + self.stats_data.wasm_memory_usage = prev_frame_wasm_memory_usage; } } @@ -147,6 +118,14 @@ impl FramedStatsData { self.stats_data.shader_compile_count = 0; self.stats_data.data_upload_count = 0; self.stats_data.data_upload_size = 0; + self.stats_data.cpu_and_idle_time = None; + self.stats_data.gpu_time = None; + } +} + +impl Default for StatsInternal { + fn default() -> Self { + Self::new() } } @@ -164,8 +143,7 @@ macro_rules! emit_if_integer { ($other:ty, $($block:tt)*) => (); } -/// Emits the StatsData struct, and extends StatsWithTimeProvider with accessors to StatsData -/// fields. +/// Emits the [`StatsData`] struct, and extends [`Stats`] with accessors to [`StatsData`]. macro_rules! gen_stats { ($($field:ident : $field_type:ty),* $(,)?) => { paste! { @@ -180,9 +158,9 @@ macro_rules! gen_stats { } - // === StatsWithTimeProvider fields accessors === + // === Stats fields accessors === - impl StatsWithTimeProvider { $( + impl Stats { $( /// Field getter. pub fn $field(&self) -> $field_type { self.rc.borrow().stats_data.$field.clone() @@ -217,8 +195,13 @@ macro_rules! gen_stats { } gen_stats! { - frame_time : f64, fps : f64, + frame_time : f64, + // To learn more why we are not computing CPU-time only, please refer to the docs of + // [`crate::core::animation::loops::LoopRegistry`]. + cpu_and_idle_time : Option, + gpu_time : Option, + idle_time : f64, wasm_memory_usage : u32, gpu_memory_usage : u32, draw_calls : Vec<&'static str>, diff --git a/lib/rust/ensogl/core/src/display/scene.rs b/lib/rust/ensogl/core/src/display/scene.rs index 1e549a51405f..2d6b931ce9be 100644 --- a/lib/rust/ensogl/core/src/display/scene.rs +++ b/lib/rust/ensogl/core/src/display/scene.rs @@ -23,6 +23,7 @@ use crate::display::style::data::DataMatch; use crate::display::symbol::Symbol; use crate::display::world; use crate::system; +use crate::system::gpu::context::profiler::Results; use crate::system::gpu::data::uniform::Uniform; use crate::system::gpu::data::uniform::UniformScope; use crate::system::gpu::shader; @@ -944,6 +945,16 @@ impl SceneData { world::with_context(|t| t.new(label)) } + /// If enabled, the scene will be rendered with 1.0 device pixel ratio, even on high-dpi + /// monitors. + pub fn low_resolution_mode(&self, enabled: bool) { + if enabled { + self.dom.root.override_device_pixel_ratio(Some(1.0)); + } else { + self.dom.root.override_device_pixel_ratio(None); + } + } + fn update_shape(&self) -> bool { if self.dirty.shape.check_all() { let screen = self.dom.shape(); @@ -1021,16 +1032,20 @@ impl SceneData { } pub fn render(&self, update_status: UpdateStatus) { - self.renderer.run(update_status); - // WebGL `flush` should be called when expecting results such as queries, or at completion - // of a rendering frame. Flush tells the implementation to push all pending commands out - // for execution, flushing them out of the queue, instead of waiting for more commands to - // enqueue before sending for execution. - // - // Not flushing commands can sometimes cause context loss. To learn more, see: - // [https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#flush_when_expecting_results]. if let Some(context) = &*self.context.borrow() { - context.flush() + context.profiler.measure_drawing(|| { + self.renderer.run(update_status); + // WebGL `flush` should be called when expecting results such as queries, or at + // completion of a rendering frame. Flush tells the implementation + // to push all pending commands out for execution, flushing them out + // of the queue, instead of waiting for more commands to + // enqueue before sending for execution. + // + // Not flushing commands can sometimes cause context loss. To learn more, see: + // [https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#flush_when_expecting_results]. + + context.flush() + }); } } @@ -1167,6 +1182,18 @@ impl Scene { } } + /// Run the GPU profiler. If the result is [`None`], either the GPU context is not initialized + /// to the profiler is not available at the current platform. In case the resulting vector + /// is empty, the previous frame measurements are not available yet and they will be + /// provided in the future. + pub fn on_frame_start(&self) -> Option> { + if let Some(context) = &*self.context.borrow() { + context.profiler.start_frame() + } else { + None + } + } + pub fn extension(&self) -> T { self.extensions.get(self) } @@ -1296,7 +1323,9 @@ impl Scene { early_status; scene_was_dirty |= self.layers.update(); scene_was_dirty |= self.update_shape(); - scene_was_dirty |= self.update_symbols(); + context.profiler.measure_data_upload(|| { + scene_was_dirty |= self.update_symbols(); + }); self.handle_mouse_over_and_out_events(); scene_was_dirty |= self.shader_compiler.run(context, time); diff --git a/lib/rust/ensogl/core/src/display/symbol/gpu/registry.rs b/lib/rust/ensogl/core/src/display/symbol/gpu/registry.rs index 6a611a6344b7..9ad5eda968ae 100644 --- a/lib/rust/ensogl/core/src/display/symbol/gpu/registry.rs +++ b/lib/rust/ensogl/core/src/display/symbol/gpu/registry.rs @@ -3,10 +3,8 @@ use crate::data::dirty::traits::*; use crate::prelude::*; -use crate::system::web::traits::*; use crate::data::dirty; -use crate::debug; use crate::debug::stats::Stats; use crate::display::camera::Camera2d; use crate::display::scene; @@ -20,7 +18,6 @@ use crate::display::symbol::WeakSymbol; use crate::system::gpu::data::uniform::Uniform; use crate::system::gpu::data::uniform::UniformScope; use crate::system::gpu::Context; -use crate::system::web; @@ -31,6 +28,7 @@ use crate::system::web; pub type Dirty = dirty::SharedSet; + // =============== // === RunMode === // =============== @@ -111,7 +109,7 @@ impl SymbolRegistry { let z_zoom_1 = variables.add_or_panic("z_zoom_1", 1.0); let display_mode = variables.add_or_panic("display_mode", 0); let context = default(); - let stats = debug::stats::Stats::new(web::window.performance_or_panic()); + let stats = default(); let global_id_provider = default(); let next_id = default(); let style_sheet = style::Sheet::new(); diff --git a/lib/rust/ensogl/core/src/display/world.rs b/lib/rust/ensogl/core/src/display/world.rs index 8614d653ce42..17ce3a6ca837 100644 --- a/lib/rust/ensogl/core/src/display/world.rs +++ b/lib/rust/ensogl/core/src/display/world.rs @@ -13,7 +13,6 @@ use crate::control::callback; use crate::data::dirty; use crate::debug; use crate::debug::stats::Stats; -use crate::debug::stats::StatsData; use crate::display; use crate::display::garbage; use crate::display::render; @@ -25,6 +24,7 @@ use crate::display::scene::UpdateStatus; use crate::display::shape::primitive::glsl; use crate::display::symbol::registry::RunMode; use crate::display::symbol::registry::SymbolRegistry; +use crate::system::gpu::context::profiler::Results; use crate::system::gpu::shader; use crate::system::web; @@ -42,6 +42,24 @@ pub use crate::display::symbol::types::*; +// ================= +// === Constants === +// ================= + +/// The number of frames that need to be rendered slow/fast before the resolution mode is switched +/// to low/high one. +const FRAME_COUNT_CHECK_FOR_SWITCHING_RESOLUTION_MODE: usize = 8; + +/// The time threshold for switching to low resolution mode. It will be used on platforms which +/// allow proper GPU time measurements (currently only Chrome). +const LOW_RESOLUTION_MODE_GPU_TIME_THRESHOLD_MS: f64 = 1000.0 / 30.0; + +/// The FPS threshold for switching to low resolution mode. It will be used on platforms which do +/// not allow proper GPU time measurements (currently all browsers but Chrome). +const LOW_RESOLUTION_MODE_FPS_THRESHOLD: usize = 25; + + + // =============== // === Context === // =============== @@ -334,7 +352,11 @@ impl WorldDataWithLoop { let on_before_rendering = animation::on_before_rendering(); let network = frp.network(); crate::frp::extend! {network - eval on_frame_start ((t) data.run_stats(*t)); + eval on_frame_start ([data] (t) { + data.stats.calculate_prev_frame_stats(*t); + let gpu_perf_results = data.default_scene.on_frame_start(); + data.update_stats(*t, gpu_perf_results) + }); layout_update <- on_before_layout.map(f!((t) data.run_next_frame_layout(*t))); _eval <- on_before_rendering.map2(&layout_update, f!((t, early) data.run_next_frame_rendering(*t, *early)) @@ -369,9 +391,8 @@ impl Deref for WorldDataWithLoop { #[derive(Clone, CloneRef, Debug, Default)] #[allow(missing_docs)] pub struct Callbacks { - pub prev_frame_stats: callback::registry::Ref1, - pub before_frame: callback::registry::Copy1, - pub after_frame: callback::registry::Copy1, + pub before_frame: callback::registry::Copy1, + pub after_frame: callback::registry::Copy1, } @@ -409,13 +430,14 @@ pub struct WorldData { display_mode: Rc>, stats: Stats, stats_monitor: debug::monitor::Monitor, - stats_draw_handle: callback::Handle, pub on: Callbacks, debug_hotkeys_handle: Rc>>, update_themes_handle: callback::Handle, garbage_collector: garbage::Collector, emit_measurements_handle: Rc>>, pixel_read_pass_threshold: Rc>>>, + slow_frame_count: Rc>, + fast_frame_count: Rc>, } impl WorldData { @@ -432,14 +454,13 @@ impl WorldData { let uniforms = Uniforms::new(&default_scene.variables); let debug_hotkeys_handle = default(); let garbage_collector = default(); - let stats_draw_handle = on.prev_frame_stats.add(f!([stats_monitor] (stats: &StatsData) { - stats_monitor.sample_and_draw(stats); - })); let themes = with_context(|t| t.theme_manager.clone_ref()); let update_themes_handle = on.before_frame.add(f_!(themes.update())); let emit_measurements_handle = default(); SCENE.with_borrow_mut(|t| *t = Some(default_scene.clone_ref())); let pixel_read_pass_threshold = default(); + let slow_frame_count = default(); + let fast_frame_count = default(); Self { frp, @@ -451,11 +472,12 @@ impl WorldData { on, debug_hotkeys_handle, stats_monitor, - stats_draw_handle, update_themes_handle, garbage_collector, emit_measurements_handle, pixel_read_pass_threshold, + slow_frame_count, + fast_frame_count, } .init() } @@ -534,11 +556,57 @@ impl WorldData { self.default_scene.renderer.set_pipeline(pipeline); } - fn run_stats(&self, time: Duration) { - self.stats.calculate_prev_frame_fps(time); + fn update_stats(&self, _time: Duration, gpu_perf_results: Option>) { { + if let Some(gpu_perf_results) = &gpu_perf_results { + for result in gpu_perf_results { + // The monitor is not updated yet, so the last sample is from the previous + // frame. + let frame_offset = result.frame_offset - 1; + if frame_offset == 0 { + let stats_data = &mut self.stats.borrow_mut().stats_data; + stats_data.gpu_time = Some(result.total); + stats_data.cpu_and_idle_time = Some(stats_data.frame_time - result.total); + } else { + // The last sampler stored in monitor is from 2 frames ago, as the last + // frame stats are not submitted yet. + let sampler_offset = result.frame_offset - 2; + self.stats_monitor.with_last_nth_sample(sampler_offset, |sample| { + sample.gpu_time = Some(result.total); + sample.cpu_and_idle_time = Some(sample.frame_time - result.total); + }); + self.stats_monitor.redraw_historical_data(sampler_offset); + } + } + } + let stats_borrowed = self.stats.borrow(); - self.on.prev_frame_stats.run_all(&stats_borrowed.stats_data); + let stats = &stats_borrowed.stats_data; + self.stats_monitor.sample_and_draw(stats); + + let slow_frame = if let Some(gpu_perf_results) = gpu_perf_results { + gpu_perf_results.last().map(|t| t.total > LOW_RESOLUTION_MODE_GPU_TIME_THRESHOLD_MS) + } else { + Some(stats.fps < LOW_RESOLUTION_MODE_FPS_THRESHOLD as f64) + }; + + if let Some(slow_frame) = slow_frame { + if slow_frame { + self.fast_frame_count.set(0); + self.slow_frame_count.modify(|t| *t += 1); + let count = self.slow_frame_count.get(); + if count == FRAME_COUNT_CHECK_FOR_SWITCHING_RESOLUTION_MODE { + SCENE.with_borrow(|t| t.as_ref().unwrap().low_resolution_mode(true)); + } + } else { + self.slow_frame_count.set(0); + self.fast_frame_count.modify(|t| *t += 1); + let count = self.fast_frame_count.get(); + if count == FRAME_COUNT_CHECK_FOR_SWITCHING_RESOLUTION_MODE { + SCENE.with_borrow(|t| t.as_ref().unwrap().low_resolution_mode(false)); + } + } + } } self.stats.reset_per_frame_statistics(); } @@ -585,7 +653,6 @@ impl WorldData { self.garbage_collector.mouse_events_handled(); self.default_scene.render(update_status); self.on.after_frame.run_all(time); - self.stats.end_frame(); self.after_rendering.emit(()); } diff --git a/lib/rust/ensogl/core/src/system/gpu/context.rs b/lib/rust/ensogl/core/src/system/gpu/context.rs index ec88ccc76f53..eaf4e298893b 100644 --- a/lib/rust/ensogl/core/src/system/gpu/context.rs +++ b/lib/rust/ensogl/core/src/system/gpu/context.rs @@ -17,6 +17,7 @@ use web_sys::WebGl2RenderingContext; pub mod extension; pub mod native; +pub mod profiler; @@ -64,6 +65,7 @@ pub struct Context { pub struct ContextData { #[deref] native: native::ContextWithExtensions, + pub profiler: profiler::Profiler, pub shader_compiler: shader::Compiler, } @@ -76,8 +78,9 @@ impl Context { impl ContextData { fn from_native(native: WebGl2RenderingContext) -> Self { let native = native::ContextWithExtensions::from_native(native); + let profiler = profiler::Profiler::new(&native); let shader_compiler = shader::Compiler::new(&native); - Self { native, shader_compiler } + Self { native, profiler, shader_compiler } } } diff --git a/lib/rust/ensogl/core/src/system/gpu/context/extension.rs b/lib/rust/ensogl/core/src/system/gpu/context/extension.rs index 87f733caab78..314a2eaee6d6 100644 --- a/lib/rust/ensogl/core/src/system/gpu/context/extension.rs +++ b/lib/rust/ensogl/core/src/system/gpu/context/extension.rs @@ -4,8 +4,11 @@ use crate::prelude::*; use crate::system::gpu::data::GlEnum; +use js_sys::Object; +use wasm_bindgen::JsCast; use web_sys::WebGl2RenderingContext; use web_sys::WebGlProgram; +use web_sys::WebGlQuery; use web_sys::WebGlShader; @@ -28,17 +31,19 @@ impl Extensions { } /// Internal representation of [`Extensions`]. -#[derive(Copy, Clone, Debug)] +#[derive(Clone, Debug)] #[allow(missing_docs)] pub struct ExtensionsData { - pub khr_parallel_shader_compile: Option, + pub khr_parallel_shader_compile: Option, + pub ext_disjoint_timer_query_webgl2: Option, } impl ExtensionsData { /// Constructor. fn init(context: &WebGl2RenderingContext) -> Self { let khr_parallel_shader_compile = KhrParallelShaderCompile::try_init(context); - Self { khr_parallel_shader_compile } + let ext_disjoint_timer_query_webgl2 = ExtDisjointTimerQueryWebgl2::try_init(context); + Self { khr_parallel_shader_compile, ext_disjoint_timer_query_webgl2 } } } @@ -87,3 +92,75 @@ impl KhrParallelShaderCompile { context.get_shader_parameter(program, *self.completion_status_khr).as_bool() } } + + + +// =================================== +// === ExtDisjointTimerQueryWebgl2 === +// =================================== + +/// This extension provides a query mechanism that can be used to determine the amount of time it +/// takes to fully complete a set of GL commands, and without stalling the rendering pipeline. It +/// uses the query object mechanisms first introduced in the occlusion query extension, which allow +/// time intervals to be polled asynchronously by the application. +/// +/// # WARNING: WORKS IN CHROME DESKTOP ONLY +/// Currently (2023) this extension is only available in Chrome Desktop. It was removed from all +/// browsers (including Chrome) due to ["GLitch" exploit](https://www.vusec.net/projects/glitch). +/// However, as Site Isolation shipped in Chrome on Desktop, the extension was re-enabled there +/// because the only data that could be read was data that came from the same origin. To learn more, +/// see: https://bugs.chromium.org/p/chromium/issues/detail?id=1230926 +#[derive(Clone, Debug)] +#[allow(missing_docs)] +pub struct ExtDisjointTimerQueryWebgl2 { + ext: Object, + pub time_elapsed_ext: GlEnum, + /// # WARNING: DO NOT USE. + /// Usage of timestamp queries is not supported. To learn more see: + /// - https://bugs.chromium.org/p/chromium/issues/detail?id=1442398 + /// - https://bugs.chromium.org/p/chromium/issues/detail?id=595172 + /// - https://bugs.chromium.org/p/chromium/issues/detail?id=1316254&q=TIMESTAMP_EXT&can=2 + pub timestamp_ext: GlEnum, + pub query_counter_bits_ext: GlEnum, + pub gpu_disjoint_ext: GlEnum, + query_counter_ext_fn: js_sys::Function, +} + +impl ExtDisjointTimerQueryWebgl2 { + /// Try to obtain the extension. + pub fn try_init(context: &WebGl2RenderingContext) -> Option { + let ext = context.get_extension("EXT_disjoint_timer_query_webgl2").ok()??; + let time_elapsed_ext = js_sys::Reflect::get(&ext, &"TIME_ELAPSED_EXT".into()).ok()?; + let time_elapsed_ext = GlEnum(time_elapsed_ext.as_f64()? as u32); + let timestamp_ext = js_sys::Reflect::get(&ext, &"TIMESTAMP_EXT".into()).ok()?; + let timestamp_ext = GlEnum(timestamp_ext.as_f64()? as u32); + let query_counter_bits_ext = + js_sys::Reflect::get(&ext, &"QUERY_COUNTER_BITS_EXT".into()).ok()?; + let query_counter_bits_ext = GlEnum(query_counter_bits_ext.as_f64()? as u32); + let gpu_disjoint_ext = js_sys::Reflect::get(&ext, &"GPU_DISJOINT_EXT".into()).ok()?; + let gpu_disjoint_ext = GlEnum(gpu_disjoint_ext.as_f64()? as u32); + let query_counter_ext_obj = js_sys::Reflect::get(&ext, &"queryCounterEXT".into()).ok()?; + let query_counter_ext_fn = query_counter_ext_obj.dyn_into::().ok()?; + Some(Self { + ext, + time_elapsed_ext, + timestamp_ext, + query_counter_bits_ext, + gpu_disjoint_ext, + query_counter_ext_fn, + }) + } + + /// Query for timestamp at the current GL command queue. + /// + /// # WARNING: DO NOT USE. + /// Usage of timestamp queries is not supported. To learn more see the docs of + /// [`ExtDisjointTimerQueryWebgl2`]. + pub fn query_timestamp(&self, query: &WebGlQuery) { + if let Err(err) = + self.query_counter_ext_fn.call2(&self.ext, query, &(*self.timestamp_ext).into()) + { + warn!("Error while querying timestamp: {:?}", err); + } + } +} diff --git a/lib/rust/ensogl/core/src/system/gpu/context/profiler.rs b/lib/rust/ensogl/core/src/system/gpu/context/profiler.rs new file mode 100644 index 000000000000..a40741a42d4f --- /dev/null +++ b/lib/rust/ensogl/core/src/system/gpu/context/profiler.rs @@ -0,0 +1,225 @@ +//! GPU profiler allowing measurements of various metrics, like draw call time or data upload +//! time. + +use crate::prelude::*; + +use crate::system::gpu::context::extension; +use crate::system::gpu::context::native::ContextWithExtensions; + +use std::collections::VecDeque; +use web_sys::WebGl2RenderingContext; +use web_sys::WebGlQuery; + + + +// ============== +// === Metric === +// ============== + +/// A metric, like the gpu draw call time or data upload time. +/// +/// The metric contains a queue of queries and a queue of results. A query is created on every frame +/// and the results may be received with a few-frame delay. However, it is impossible to receive the +/// results in a different order than the order of the queries. For example, the following scenario +/// is impossible, where "Q2" means a query created on frame 2 and "R1" means a result of a query +/// created on frame 1: +/// +/// ```text +/// | draw calls | data upload | Results emitted from `start_frame` +/// frame 1 | Q1 | Q1 | +/// frame 2 | Q2, R1 | Q2 | +/// frame 3 | Q3 | Q3, R1 | R1 +/// frame 4 | Q4, R2, R3 | Q4, R2, R3 | R2, R3 +/// frame 5 | Q5 | Q5 | +/// frame 6 | Q6 | Q6 | +/// frame 7 | Q7, R4 | Q7 | +/// frame 8 | Q7, R5 | Q7, R4 | R4 +/// ... +/// ``` +#[derive(Debug, Default)] +struct Metric { + queue: VecDeque, + results: VecDeque, +} + + + +// ================ +// === Profiler === +// ================ + +/// The profiler is used to measure the performance of the GPU. It is used to measure the time of +/// draw calls, data uploads, etc. To see what metrics are available, see the [`define_metrics!`] +/// macro. +/// +/// # WARNING: WORKS IN CHROME DESKTOP ONLY +/// The profiler uses the [`WebGL2RenderingContext::EXT_disjoint_timer_query_webgl2`] extension, +/// which currently (2023) is only available in Chrome Desktop. It was removed from all browsers +/// (including Chrome) due to ["GLitch" exploit](https://www.vusec.net/projects/glitch). However, +/// as Site Isolation shipped in Chrome on Desktop, the extension was re-enabled there because the +/// only data that could be read was data that came from the same origin. To learn more, +/// see: https://bugs.chromium.org/p/chromium/issues/detail?id=1230926 +#[derive(Debug)] +pub struct Profiler { + data: Option, +} + +#[derive(Debug)] +struct InitializedProfiler { + ext: extension::ExtDisjointTimerQueryWebgl2, + context: ContextWithExtensions, + metrics: RefCell, + assertions: Assertions, + frame: Cell, + frame_reported: Cell, +} + +#[derive(Debug, Default)] +struct Assertions { + // The WebGL API does not allow nested measurements, so we are checking that it is not + // happening by accident. + during_measurement: Cell, + // We need to be sure that all measurements were fired per frame. Otherwise, we would not get + // the correct results, as we are gathering results from all measurements and adding them + // together. + measurements_per_frame: Cell, +} + +impl Profiler { + /// Constructor. + pub fn new(context: &ContextWithExtensions) -> Self { + let data = context.extensions.ext_disjoint_timer_query_webgl2.as_ref().map(|ext| { + let ext = ext.clone(); + let context = context.clone(); + let metrics = default(); + let assertions = default(); + let frame = default(); + let frame_reported = default(); + InitializedProfiler { ext, context, metrics, assertions, frame, frame_reported } + }); + Self { data } + } +} + +impl InitializedProfiler { + fn update_results_of(&self, target: &mut Metric) { + while let Some(query) = target.queue.front() { + let available_enum = WebGl2RenderingContext::QUERY_RESULT_AVAILABLE; + let disjoint_enum = self.ext.gpu_disjoint_ext; + let available = self.context.get_query_parameter(query, available_enum); + let disjoint = self.context.get_parameter(*disjoint_enum).unwrap(); + let available = available.as_bool().unwrap(); + let disjoint = disjoint.as_bool().unwrap(); + let ready = available && !disjoint; + if ready { + let query_result_enum = WebGl2RenderingContext::QUERY_RESULT; + let result = self.context.get_query_parameter(query, query_result_enum); + let result = result.as_f64().unwrap() / 1_000_000.0; + target.results.push_back(result); + self.context.delete_query(Some(query)); + target.queue.pop_front(); + } else { + break; + } + } + } + + fn measure(&self, target: &mut Metric, f: impl FnOnce() -> T) -> T { + assert!(!self.assertions.during_measurement.get()); + self.assertions.during_measurement.set(true); + self.assertions.measurements_per_frame.modify(|t| *t += 1); + let query = self.context.create_query().unwrap(); + self.context.begin_query(*self.ext.time_elapsed_ext, &query); + let result = f(); + self.context.end_query(*self.ext.time_elapsed_ext); + target.queue.push_back(query); + self.assertions.during_measurement.set(false); + result + } +} + +impl Profiler { + /// Function that should be called on every frame. Gathers results of queries from previous + /// frames. Please note, that not all results may be available yet, or results from multiple + /// previous frames may be returned at once. + pub fn start_frame(&self) -> Option> { + self.data.as_ref().map(|t| t.start_frame()) + } +} + + + +// ====================== +// === Define Metrics === +// ====================== + +macro_rules! define_metrics { + ($($name:ident),*) => { + paste! { + #[derive(Debug, Default)] + struct Metrics { + $(pub $name: Metric),* + } + + /// Results of measurements for each defined metric. + #[allow(missing_docs)] + #[derive(Clone, Copy, Debug, Default)] + pub struct Results { + pub frame_offset: usize, + pub total: f64, + $(pub $name: f64),* + } + + impl Profiler { + $( + /// Measure the time of the given metric. + pub fn [](&self, f: impl FnOnce() -> T) -> T { + if let Some(data) = &self.data { + data.[](f) + } else { + f() + } + } + )* + } + + impl InitializedProfiler { + $( + fn [](&self, f: impl FnOnce() -> T) -> T { + self.measure(&mut self.metrics.borrow_mut().$name, f) + } + )* + + fn start_frame(&self) -> Vec { + #[allow(clippy::redundant_closure_call)] + let target_measurements_per_frame = 0 $(+ (|_| 1)(stringify!($name)) )*; + let measurements_per_frame = self.assertions.measurements_per_frame.take(); + let results = if measurements_per_frame == 0 { + default() + } else if measurements_per_frame == target_measurements_per_frame { + let mut results = vec![]; + let metrics = &mut *self.metrics.borrow_mut(); + $(self.update_results_of(&mut metrics.$name);)* + $(let $name = &mut metrics.$name;)* + while true $(&& $name.results.len() > 0)* { + let frame_offset = self.frame.get() - self.frame_reported.get(); + $(let $name = $name.results.pop_front().unwrap();)* + let total = 0.0 $(+ $name)*; + results.push(Results { frame_offset, total, $($name),* }); + self.frame_reported.modify(|t| *t += 1); + } + results + } else { + error!("Expected {target_measurements_per_frame} metrics per frame, \ + but got {measurements_per_frame}."); + default() + }; + self.frame.modify(|t| *t += 1); + results + } + } + } + }; +} + +define_metrics! { data_upload, drawing } diff --git a/lib/rust/ensogl/core/src/system/gpu/data/attribute.rs b/lib/rust/ensogl/core/src/system/gpu/data/attribute.rs index bd51e9b22209..488598776c45 100644 --- a/lib/rust/ensogl/core/src/system/gpu/data/attribute.rs +++ b/lib/rust/ensogl/core/src/system/gpu/data/attribute.rs @@ -542,8 +542,7 @@ mod allocator_tests { } fn check_allocator(partitions: usize, steps: impl IntoIterator) { - use enso_web::traits::WindowOps; - let stats = Stats::new(enso_web::window.performance_or_panic()); + let stats = Stats::new(); let scope = AttributeScope::new(&stats, || ()); let mut instances: Vec> = default(); instances.resize(partitions, default()); diff --git a/lib/rust/ensogl/core/src/system/web/dom/shape.rs b/lib/rust/ensogl/core/src/system/web/dom/shape.rs index fe5c2f44433d..5383423363b6 100644 --- a/lib/rust/ensogl/core/src/system/web/dom/shape.rs +++ b/lib/rust/ensogl/core/src/system/web/dom/shape.rs @@ -29,8 +29,9 @@ pub struct Shape { impl Shape { /// Constructor. - pub fn new(width: f32, height: f32) -> Self { - Self { width, height, ..default() } + pub fn new(width: f32, height: f32, pixel_ratio: Option) -> Self { + let pixel_ratio = pixel_ratio.unwrap_or_else(|| web::window.device_pixel_ratio() as f32); + Self { width, height, pixel_ratio } } /// Compute shape of the provided element. Note that using it causes a reflow. @@ -59,6 +60,15 @@ impl Shape { pub fn center(&self) -> Vector2 { Vector2::new(self.width / 2.0, self.height / 2.0) } + + /// Create a new shape with the provided device pixel ratio. In case the provided value is + /// [`None`], the device pixel ratio reported by the browser will be used. + pub fn with_device_pixel_ratio(self, ratio: Option) -> Self { + Self { + pixel_ratio: ratio.unwrap_or_else(|| web::window.device_pixel_ratio() as f32), + ..self + } + } } impl Default for Shape { @@ -95,11 +105,12 @@ impl From<&Shape> for Vector2 { #[allow(missing_docs)] pub struct WithKnownShape { #[deref] - dom: T, - network: frp::Network, - pub shape: frp::Sampler, - shape_source: frp::Source, - observer: Rc, + dom: T, + network: frp::Network, + pub shape: frp::Sampler, + shape_source: frp::Source, + observer: Rc, + overridden_pixel_ratio: Rc>>, } impl WithKnownShape { @@ -108,14 +119,26 @@ impl WithKnownShape { where T: Clone + AsRef + Into { let dom = dom.clone(); let element = dom.clone().into(); + let overridden_pixel_ratio: Rc>> = default(); frp::new_network! { network shape_source <- source(); shape <- shape_source.sampler(); }; - let callback = Closure::new(f!((w,h) shape_source.emit(Shape::new(w,h)))); + let callback = Closure::new(f!([shape_source, overridden_pixel_ratio] (w,h) + shape_source.emit(Shape::new(w, h, overridden_pixel_ratio.get())))); let observer = Rc::new(ResizeObserver::new(dom.as_ref(), callback)); shape_source.emit(Shape::new_from_element_with_reflow(&element)); - Self { dom, network, shape, shape_source, observer } + Self { dom, network, shape, shape_source, observer, overridden_pixel_ratio } + } + + /// Override the device pixel ratio. If the provided value is [`None`], the device pixel ratio + /// provided by the browser will be used. + pub fn override_device_pixel_ratio(&self, ratio: Option) { + if ratio != self.overridden_pixel_ratio.get() { + self.overridden_pixel_ratio.set(ratio); + let shape = self.shape.value().with_device_pixel_ratio(ratio); + self.shape_source.emit(shape); + } } /// Get the current shape of the object. @@ -137,7 +160,8 @@ impl From> for WithKnownShape> for WithKnownShape let shape = t.shape; let shape_source = t.shape_source; let observer = t.observer; - Self { dom, network, shape, shape_source, observer } + let overridden_pixel_ratio = t.overridden_pixel_ratio; + Self { dom, network, shape, shape_source, observer, overridden_pixel_ratio } } } diff --git a/lib/rust/web/Cargo.toml b/lib/rust/web/Cargo.toml index 1f76ec83e08e..5c81dd9cc4f0 100644 --- a/lib/rust/web/Cargo.toml +++ b/lib/rust/web/Cargo.toml @@ -40,6 +40,7 @@ features = [ 'CanvasRenderingContext2d', 'WebGlProgram', 'WebGlShader', + 'WebGlQuery', 'Window', 'Navigator', 'console', diff --git a/lib/rust/web/js/resize_observer.js b/lib/rust/web/js/resize_observer.js index 28ba5836e33f..17dd143ace62 100644 --- a/lib/rust/web/js/resize_observer.js +++ b/lib/rust/web/js/resize_observer.js @@ -54,7 +54,9 @@ let resizeObserverPool = new Pool((...args) => new ResizeObserver(...args)) export function resize_observe(target, f) { let id = resizeObserverPool.reserve(resize_observer_update(f)) - resizeObserverPool[id].observe(target) + // We are using the devicePixelContentBoxSize option to get correct results here, as explained in this + // article: https://webglfundamentals.org/webgl/lessons/webgl-resizing-the-canvas.html + resizeObserverPool[id].observe(target, { box: 'content-box' }) return id } diff --git a/lib/rust/web/src/binding/mock.rs b/lib/rust/web/src/binding/mock.rs index db643555c533..df30e9c1e277 100644 --- a/lib/rust/web/src/binding/mock.rs +++ b/lib/rust/web/src/binding/mock.rs @@ -770,6 +770,9 @@ mock_data! { Node => EventTarget ) -> Result; } +// === WebGlQuery === +mock_data! { WebGlQuery => Object } + // =========== diff --git a/lib/rust/web/src/binding/wasm.rs b/lib/rust/web/src/binding/wasm.rs index d50a3cf837f5..1b033caca084 100644 --- a/lib/rust/web/src/binding/wasm.rs +++ b/lib/rust/web/src/binding/wasm.rs @@ -36,6 +36,7 @@ pub use web_sys::MouseEvent; pub use web_sys::Node; pub use web_sys::Performance; pub use web_sys::WebGl2RenderingContext; +pub use web_sys::WebGlQuery; pub use web_sys::WheelEvent; pub use web_sys::Window; diff --git a/lib/rust/web/src/lib.rs b/lib/rust/web/src/lib.rs index de5ec2ba41ac..7c482dc19a59 100644 --- a/lib/rust/web/src/lib.rs +++ b/lib/rust/web/src/lib.rs @@ -937,23 +937,6 @@ pub use async_std::task::sleep; -// ==================== -// === TimeProvider === -// ==================== - -/// Trait for an entity that can retrieve current time. -pub trait TimeProvider { - /// Returns current time, measured in milliseconds. - fn now(&self) -> f64; -} - -impl TimeProvider for Performance { - fn now(&self) -> f64 { - self.now() - } -} - - // ==================== // === FrameCounter === // ====================