diff --git a/Cargo.toml b/Cargo.toml index 9c45b2f588..2cedb1b6e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,14 @@ smol = ["iced_futures/smol"] palette = ["iced_core/palette"] # Enables querying system information system = ["iced_winit/system"] +# Enables tracing with Chrome +trace_chrome = [ + "iced_winit/trace", + "iced_wgpu?/trace", + "iced_glow?/trace", + "iced_glutin?/trace", + "iced_profiling/tracing-chrome" +] [badges] maintenance = { status = "actively-developed" } @@ -51,6 +59,7 @@ members = [ "glow", "glutin", "lazy", + "profiling", "native", "style", "wgpu", @@ -66,6 +75,7 @@ iced_graphics = { version = "0.5", path = "graphics" } iced_winit = { version = "0.6", path = "winit", features = ["application"] } iced_glutin = { version = "0.5", path = "glutin", optional = true } iced_glow = { version = "0.5", path = "glow", optional = true } +iced_profiling = { version = "0.1.0", path = "profiling", optional = true } thiserror = "1.0" [dependencies.image_rs] diff --git a/docs/images/perfetto.png b/docs/images/perfetto.png new file mode 100644 index 0000000000..93e9df1512 Binary files /dev/null and b/docs/images/perfetto.png differ diff --git a/examples/integration_wgpu/Cargo.toml b/examples/integration_wgpu/Cargo.toml index eaa1df7e17..9380f65977 100644 --- a/examples/integration_wgpu/Cargo.toml +++ b/examples/integration_wgpu/Cargo.toml @@ -8,6 +8,7 @@ publish = false [dependencies] iced_winit = { path = "../../winit" } iced_wgpu = { path = "../../wgpu", features = ["webgl"] } +iced_profiling = { path = "../../profiling", features = ["tracing-chrome"]} env_logger = "0.8" [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/examples/integration_wgpu/src/main.rs b/examples/integration_wgpu/src/main.rs index 70f9a48b43..e3dfb6fc64 100644 --- a/examples/integration_wgpu/src/main.rs +++ b/examples/integration_wgpu/src/main.rs @@ -16,6 +16,8 @@ use winit::{ event_loop::{ControlFlow, EventLoop}, }; +use iced_profiling::info_span; + #[cfg(target_arch = "wasm32")] use wasm_bindgen::JsCast; #[cfg(target_arch = "wasm32")] @@ -39,6 +41,9 @@ pub fn main() { #[cfg(not(target_arch = "wasm32"))] env_logger::init(); + // Initialize tracing + let _guard = iced_profiling::init(); + // Initialize winit let event_loop = EventLoop::new(); @@ -198,6 +203,8 @@ pub fn main() { } } Event::RedrawRequested(_) => { + let _ = info_span!("Integration_WGPU", "DRAW").entered(); + if resized { let size = window.inner_size(); diff --git a/glow/Cargo.toml b/glow/Cargo.toml index f586d24d96..6c150a74bf 100644 --- a/glow/Cargo.toml +++ b/glow/Cargo.toml @@ -8,6 +8,7 @@ license = "MIT AND OFL-1.1" repository = "https://github.com/iced-rs/iced" [features] +trace = ["iced_profiling"] svg = ["iced_graphics/svg"] image = ["iced_graphics/image"] png = ["iced_graphics/png"] @@ -42,6 +43,11 @@ version = "0.5" path = "../graphics" features = ["font-fallback", "font-icons", "opengl"] +[dependencies.iced_profiling] +version = "0.1.0" +path = "../profiling" +optional = true + [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] all-features = true diff --git a/glow/src/image.rs b/glow/src/image.rs index 521a01e7bc..351b98fac5 100644 --- a/glow/src/image.rs +++ b/glow/src/image.rs @@ -21,6 +21,9 @@ use glow::HasContext; use std::cell::RefCell; +#[cfg(feature = "trace")] +use iced_profiling::info_span; + #[derive(Debug)] pub(crate) struct Pipeline { program: ::Program, @@ -148,6 +151,9 @@ impl Pipeline { images: &[layer::Image], layer_bounds: Rectangle, ) { + #[cfg(feature = "trace")] + let _ = info_span!("Glow::Image", "DRAW").entered(); + unsafe { gl.use_program(Some(self.program)); gl.bind_vertex_array(Some(self.vertex_array)); diff --git a/glow/src/quad.rs b/glow/src/quad.rs index d9f1c6ae70..fc918bf27d 100644 --- a/glow/src/quad.rs +++ b/glow/src/quad.rs @@ -7,6 +7,9 @@ use glow::HasContext; use iced_graphics::layer; use iced_native::Rectangle; +#[cfg(feature = "trace")] +use iced_profiling::info_span; + #[derive(Debug)] pub enum Pipeline { Core(core::Pipeline), @@ -42,6 +45,9 @@ impl Pipeline { scale: f32, bounds: Rectangle, ) { + #[cfg(feature = "trace")] + let _ = info_span!("Glow::Quad", "DRAW").enter(); + match self { Pipeline::Core(pipeline) => { pipeline.draw( diff --git a/glow/src/triangle.rs b/glow/src/triangle.rs index d0205e0818..11d2434922 100644 --- a/glow/src/triangle.rs +++ b/glow/src/triangle.rs @@ -9,6 +9,9 @@ use iced_graphics::triangle::{ColoredVertex2D, Vertex2D}; use glow::HasContext; use std::marker::PhantomData; +#[cfg(feature = "trace")] +use iced_profiling::info_span; + const DEFAULT_VERTICES: usize = 1_000; const DEFAULT_INDICES: usize = 1_000; @@ -58,6 +61,9 @@ impl Pipeline { transformation: Transformation, scale_factor: f32, ) { + #[cfg(feature = "trace")] + let _ = info_span!("Glow::Triangle", "DRAW").enter(); + unsafe { gl.enable(glow::MULTISAMPLE); gl.enable(glow::SCISSOR_TEST); diff --git a/glutin/Cargo.toml b/glutin/Cargo.toml index 022457b186..6749cb963a 100644 --- a/glutin/Cargo.toml +++ b/glutin/Cargo.toml @@ -11,6 +11,7 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] [features] +trace = ["iced_profiling/tracing-chrome"] debug = ["iced_winit/debug"] system = ["iced_winit/system"] @@ -35,3 +36,8 @@ features = ["application"] version = "0.5" path = "../graphics" features = ["opengl"] + +[dependencies.iced_profiling] +version = "0.1.0" +path = "../profiling" +optional = true diff --git a/glutin/src/application.rs b/glutin/src/application.rs index 3e9d11f9ff..21051f168e 100644 --- a/glutin/src/application.rs +++ b/glutin/src/application.rs @@ -17,6 +17,9 @@ use iced_winit::{Clipboard, Command, Debug, Proxy, Settings}; use glutin::window::Window; use std::mem::ManuallyDrop; +#[cfg(feature = "trace")] +use iced_profiling::{info_span, instrument::Instrument}; + /// Runs an [`Application`] with an executor, compositor, and the provided /// settings. pub fn run( @@ -35,9 +38,15 @@ where use glutin::platform::run_return::EventLoopExtRunReturn; use glutin::ContextBuilder; + #[cfg(feature = "trace")] + let _guard = iced_profiling::init(); + let mut debug = Debug::new(); debug.startup_started(); + #[cfg(feature = "trace")] + let _ = info_span!("Application::Glutin", "RUN").entered(); + let mut event_loop = EventLoopBuilder::with_user_event().build(); let proxy = event_loop.create_proxy(); @@ -124,18 +133,26 @@ where let (mut sender, receiver) = mpsc::unbounded(); - let mut instance = Box::pin(run_instance::( - application, - compositor, - renderer, - runtime, - proxy, - debug, - receiver, - context, - init_command, - settings.exit_on_close_request, - )); + let mut instance = Box::pin({ + let run_instance = run_instance::( + application, + compositor, + renderer, + runtime, + proxy, + debug, + receiver, + context, + init_command, + settings.exit_on_close_request, + ); + + #[cfg(feature = "trace")] + let run_instance = + run_instance.instrument(info_span!("Application", "LOOP")); + + run_instance + }); let mut context = task::Context::from_waker(task::noop_waker_ref()); @@ -333,6 +350,9 @@ async fn run_instance( messages.push(message); } event::Event::RedrawRequested(_) => { + #[cfg(feature = "trace")] + let _ = info_span!("Application", "FRAME").entered(); + debug.render_started(); #[allow(unsafe_code)] diff --git a/profiling/Cargo.toml b/profiling/Cargo.toml new file mode 100644 index 0000000000..29ee8c9c8c --- /dev/null +++ b/profiling/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "iced_profiling" +authors = ["Bingus "] +version = "0.1.0" +edition = "2021" +description = "Profiling backend implementations for Iced" + +[dependencies] +tracing = { version = "0.1.37", default-features = false, features = ["std"] } +tracing-core = "0.1.30" +tracing-subscriber = { version = "0.3.16", features = ["registry", "env-filter"] } + +[dependencies.tracing-chrome] +version = "0.7.0" +optional = true diff --git a/profiling/README.md b/profiling/README.md new file mode 100644 index 0000000000..f150bcecd4 --- /dev/null +++ b/profiling/README.md @@ -0,0 +1,61 @@ +# `iced_profiling` +[![Documentation](https://docs.rs/iced_profiling/badge.svg)] +[![Crates.io](https://img.shields.io/crates/v/iced_profiling.svg)](https://crates.io/crates/iced_profiling) +[![License](https://img.shields.io/crates/l/iced_profiling.svg)](https://github.com/iced-rs/iced/blob/master/LICENSE) +[![Discord Server](https://img.shields.io/discord/628993209984614400?label=&labelColor=6A7EC2&logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/3xZJ65GAhd) + +`iced_profiling` is a crate which implements various tracing backends for Iced. + +It relies on the [tracing](https://crates.io/crates/tracing) crate to collect diagnostics. We currently only support +tracing with `tracing`'s `info_span!` macro, but will consider different logging levels in the future. PRs welcome! + +## Trace backends + +We currently support only Chrome JSON traces using the [tracing-chrome](https://crates.io/crates/tracing-chrome) crate. + +There are plans to add support for [Tracy](https://github.com/wolfpld/tracy) in the near future! + +## Generating a trace file + +### Using Iced's `Application` + +Simply enable your tracing backend of choice (e.g. `trace_chrome`) feature in Iced & run your project. + +```shell +cargo run --features iced/trace_chrome +``` +### Standalone dependency + +You can enable tracing by enabling your tracing backend of choice as a feature of `iced_profiling`. + +```toml +iced_profiling = { version = "0.1.0", features = ["tracing-chrome"]} +``` + +Doing so will require you to initialize the profiler manually like so: + +```rust +let _guard = iced_profiling::init(); +``` + +This reference must be kept alive for the entire duration of your application that you wish to profile. + +## Chrome + +By default, Chrome trace files will be generated in the current working directory: +```shell +path/to/your/project/project_trace_{timestamp}.json +``` + +You also set a specific path by setting the `CHROME_TRACE_FILE` env variable: +```shell +CHROME_TRACE_FILE = ~/Desktop/trace.json cargo run +``` + +If you cannot find your trace file, there may have been a permission issue when trying to generate your file. Be sure to check your cargo manifest directory! + +Once your file is generated, you can view it in Google Chrome at either [ui.perfetto.dev](ui.perfetto.dev) (new) or [chrome://trace](chrome://trace) (old). + +

+ The native target +

\ No newline at end of file diff --git a/profiling/src/lib.rs b/profiling/src/lib.rs new file mode 100644 index 0000000000..8fc58fb841 --- /dev/null +++ b/profiling/src/lib.rs @@ -0,0 +1,100 @@ +use std::time::Duration; +use tracing_subscriber::prelude::*; +use tracing_subscriber::Registry; + +#[cfg(feature = "tracing-chrome")] +use { + tracing_chrome::FlushGuard, + tracing_subscriber::fmt::{format::DefaultFields, FormattedFields}, +}; + +pub use tracing::{info_span, instrument}; + +/// Profiler state. This will likely need to be updated or reworked when adding new tracing backends. +pub struct Profiler { + #[cfg(feature = "tracing-chrome")] + /// [`FlushGuard`] must not be dropped until the application scope is dropped for accurate tracing. + _guard: FlushGuard, +} + +pub fn init() -> Profiler { + // Registry stores the spans & generates unique span IDs + let subscriber = Registry::default(); + + #[cfg(feature = "tracing-chrome")] + let (chrome_layer, guard) = { + let mut layer = tracing_chrome::ChromeLayerBuilder::new(); + + // Optional configurable env var: CHROME_TRACE_FILE=/path/to/trace_file/file.json, + // for uploading to chrome://tracing (old) or ui.perfetto.dev (new). + if let Ok(path) = std::env::var("CHROME_TRACE_FILE") { + layer = layer.file(path); + } else if let Ok(current_dir) = std::env::current_dir() { + let time = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or(Duration::from_millis(0)) + .as_millis(); + + let trace_file_name = current_dir + .file_name() + .map(|file_dir| { + format!( + "{}_trace_{}.json", + file_dir.to_str().unwrap_or("trace"), + time + ) + }) + .unwrap_or_else(|| "trace.json".to_string()); + + let path = format!( + "{}/{}", + current_dir.to_str().expect("Invalid path"), + trace_file_name + ); + + layer = layer.file(path); + } else { + layer = layer.file(env!("CARGO_MANIFEST_DIR")) + } + + let (chrome_layer, guard) = layer + .name_fn(Box::new(|event_or_span| match event_or_span { + tracing_chrome::EventOrSpan::Event(event) => { + event.metadata().name().into() + } + tracing_chrome::EventOrSpan::Span(span) => { + if let Some(fields) = span + .extensions() + .get::>() + { + format!( + "{}: {}", + span.metadata().name(), + fields.fields.as_str() + ) + } else { + span.metadata().name().into() + } + } + })) + .build(); + + (chrome_layer, guard) + }; + + let fmt_layer = tracing_subscriber::fmt::Layer::default(); + let subscriber = subscriber.with(fmt_layer); + + #[cfg(feature = "tracing-chrome")] + let subscriber = subscriber.with(chrome_layer); + + // create dispatcher which will forward span events to the subscriber + // this can only be set once or will panic + tracing::subscriber::set_global_default(subscriber) + .expect("Profiler could not set the global default subscriber."); + + Profiler { + #[cfg(feature = "tracing-chrome")] + _guard: guard, + } +} diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index a40d996714..ca89cc2653 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -8,24 +8,25 @@ license = "MIT AND OFL-1.1" repository = "https://github.com/iced-rs/iced" [features] -svg = ["iced_graphics/svg"] +bmp = ["iced_graphics/bmp"] +canvas = ["iced_graphics/canvas"] +dds = ["iced_graphics/dds"] +default_system_font = ["iced_graphics/font-source"] +farbfeld = ["iced_graphics/farbfeld"] +gif = ["iced_graphics/gif"] +hdr = ["iced_graphics/hdr"] +ico = ["iced_graphics/ico"] image = ["iced_graphics/image"] -png = ["iced_graphics/png"] jpeg = ["iced_graphics/jpeg"] jpeg_rayon = ["iced_graphics/jpeg_rayon"] -gif = ["iced_graphics/gif"] -webp = ["iced_graphics/webp"] +png = ["iced_graphics/png"] pnm = ["iced_graphics/pnm"] -ico = ["iced_graphics/ico"] -bmp = ["iced_graphics/bmp"] -hdr = ["iced_graphics/hdr"] -dds = ["iced_graphics/dds"] -farbfeld = ["iced_graphics/farbfeld"] -canvas = ["iced_graphics/canvas"] qr_code = ["iced_graphics/qr_code"] -default_system_font = ["iced_graphics/font-source"] spirv = ["wgpu/spirv"] +svg = ["iced_graphics/svg"] +trace = ["iced_profiling"] webgl = ["wgpu/webgl"] +webp = ["iced_graphics/webp"] [dependencies] wgpu = "0.14" @@ -50,6 +51,11 @@ version = "0.5" path = "../graphics" features = ["font-fallback", "font-icons"] +[dependencies.iced_profiling] +version = "0.1.0" +path = "../profiling" +optional = true + [dependencies.encase] version = "0.3.0" features = ["glam"] diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 946eb71290..dda9aee18a 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -10,6 +10,9 @@ use iced_graphics::{Primitive, Viewport}; use iced_native::alignment; use iced_native::{Font, Size}; +#[cfg(feature = "trace")] +use iced_profiling::info_span; + #[cfg(any(feature = "image", feature = "svg"))] use crate::image; @@ -77,6 +80,8 @@ impl Backend { overlay_text: &[T], ) { log::debug!("Drawing"); + #[cfg(feature = "trace")] + let _ = info_span!("Wgpu::Backend", "PRESENT").entered(); let target_size = viewport.physical_size(); let scale_factor = viewport.scale_factor() as f32; diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index 390bad90df..ed81e0c6d8 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -23,6 +23,9 @@ use iced_native::image; #[cfg(feature = "svg")] use iced_native::svg; +#[cfg(feature = "trace")] +use iced_profiling::info_span; + #[derive(Debug)] pub struct Pipeline { #[cfg(feature = "image")] @@ -289,6 +292,9 @@ impl Pipeline { target: &wgpu::TextureView, _scale: f32, ) { + #[cfg(feature = "trace")] + let _ = info_span!("Wgpu::Image", "DRAW").entered(); + let instances: &mut Vec = &mut Vec::new(); #[cfg(feature = "image")] diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index 027a34bebe..702122e14a 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -6,6 +6,9 @@ use bytemuck::{Pod, Zeroable}; use std::mem; use wgpu::util::DeviceExt; +#[cfg(feature = "trace")] +use iced_profiling::info_span; + #[derive(Debug)] pub struct Pipeline { pipeline: wgpu::RenderPipeline, @@ -173,6 +176,9 @@ impl Pipeline { bounds: Rectangle, target: &wgpu::TextureView, ) { + #[cfg(feature = "trace")] + let _ = info_span!("Wgpu::Quad", "DRAW").entered(); + let uniforms = Uniforms::new(transformation, scale); { @@ -207,6 +213,9 @@ impl Pipeline { instance_buffer.copy_from_slice(instance_bytes); + #[cfg(feature = "trace")] + let _ = info_span!("Wgpu::Quad", "BEGIN_RENDER_PASS").enter(); + { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 061154b622..b7c63167c6 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -8,6 +8,8 @@ use crate::Transformation; use iced_graphics::layer::mesh::{self, Mesh}; use iced_graphics::triangle::ColoredVertex2D; use iced_graphics::Size; +#[cfg(feature = "trace")] +use iced_profiling::info_span; #[derive(Debug)] pub struct Pipeline { @@ -53,6 +55,9 @@ impl Pipeline { scale_factor: f32, meshes: &[Mesh<'_>], ) { + #[cfg(feature = "trace")] + let _ = info_span!("Wgpu::Triangle", "DRAW").entered(); + // Count the total amount of vertices & indices we need to handle let count = mesh::attribute_count_of(meshes); @@ -247,6 +252,9 @@ impl Pipeline { (target, None, wgpu::LoadOp::Load) }; + #[cfg(feature = "trace")] + let _ = info_span!("Wgpu::Triangle", "BEGIN_RENDER_PASS").enter(); + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("iced_wgpu::triangle render pass"), diff --git a/winit/Cargo.toml b/winit/Cargo.toml index ebbadb12dc..22b40f708b 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -11,6 +11,7 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] [features] +trace = ["iced_profiling"] debug = ["iced_native/debug"] system = ["sysinfo"] application = [] @@ -37,6 +38,11 @@ path = "../graphics" version = "0.5" path = "../futures" +[dependencies.iced_profiling] +version = "0.1.0" +path = "../profiling" +optional = true + [target.'cfg(target_os = "windows")'.dependencies.winapi] version = "0.3.6" diff --git a/winit/src/application.rs b/winit/src/application.rs index 0f9b562e67..b567559b40 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -24,6 +24,9 @@ pub use iced_native::application::{Appearance, StyleSheet}; use std::mem::ManuallyDrop; +#[cfg(feature = "trace")] +use iced_profiling::{info_span, instrument::Instrument}; + /// An interactive, native cross-platform application. /// /// This trait is the main entrypoint of Iced. Once implemented, you can run @@ -111,9 +114,15 @@ where use futures::Future; use winit::event_loop::EventLoopBuilder; + #[cfg(feature = "trace")] + let _guard = iced_profiling::init(); + let mut debug = Debug::new(); debug.startup_started(); + #[cfg(feature = "trace")] + let _ = info_span!("Application", "RUN").entered(); + let event_loop = EventLoopBuilder::with_user_event().build(); let proxy = event_loop.create_proxy(); @@ -175,18 +184,26 @@ where let (mut sender, receiver) = mpsc::unbounded(); - let mut instance = Box::pin(run_instance::( - application, - compositor, - renderer, - runtime, - proxy, - debug, - receiver, - init_command, - window, - settings.exit_on_close_request, - )); + let mut instance = Box::pin({ + let run_instance = run_instance::( + application, + compositor, + renderer, + runtime, + proxy, + debug, + receiver, + init_command, + window, + settings.exit_on_close_request, + ); + + #[cfg(feature = "trace")] + let run_instance = + run_instance.instrument(info_span!("Application", "LOOP")); + + run_instance + }); let mut context = task::Context::from_waker(task::noop_waker_ref()); @@ -391,6 +408,9 @@ async fn run_instance( messages.push(message); } event::Event::RedrawRequested(_) => { + #[cfg(feature = "trace")] + let _ = info_span!("Application", "FRAME").entered(); + let physical_size = state.physical_size(); if physical_size.width == 0 || physical_size.height == 0 { @@ -529,12 +549,24 @@ pub fn build_user_interface<'a, A: Application>( where ::Theme: StyleSheet, { + #[cfg(feature = "trace")] + let view_span = info_span!("Application", "VIEW").entered(); + debug.view_started(); let view = application.view(); + + #[cfg(feature = "trace")] + let _ = view_span.exit(); debug.view_finished(); + #[cfg(feature = "trace")] + let layout_span = info_span!("Application", "LAYOUT").entered(); + debug.layout_started(); let user_interface = UserInterface::build(view, size, cache, renderer); + + #[cfg(feature = "trace")] + let _ = layout_span.exit(); debug.layout_finished(); user_interface @@ -559,10 +591,16 @@ pub fn update( ::Theme: StyleSheet, { for message in messages.drain(..) { + #[cfg(feature = "trace")] + let update_span = info_span!("Application", "UPDATE").entered(); + debug.log_message(&message); debug.update_started(); let command = runtime.enter(|| application.update(message)); + + #[cfg(feature = "trace")] + let _ = update_span.exit(); debug.update_finished(); run_command(