diff --git a/Cargo.lock b/Cargo.lock index 4a8c133a616..6e03615ed82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,6 +81,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "anyhow" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" + [[package]] name = "approx" version = "0.4.0" @@ -423,6 +429,9 @@ name = "cc" version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +dependencies = [ + "jobserver", +] [[package]] name = "cesu8" @@ -1018,6 +1027,8 @@ dependencies = [ "glow", "image", "poll-promise", + "puffin", + "puffin_http", "rfd", "three-d", ] @@ -1044,6 +1055,7 @@ dependencies = [ "epi", "glow", "instant", + "puffin", "serde", "tracing", "tts", @@ -1118,6 +1130,7 @@ dependencies = [ "glow", "glutin", "memoffset", + "puffin", "tracing", "wasm-bindgen", "web-sys", @@ -1806,6 +1819,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +dependencies = [ + "libc", +] + [[package]] name = "jpeg-decoder" version = "0.2.2" @@ -2529,6 +2551,43 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "puffin" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78cc1dc2f2e3e4787201dd9404c9b097967fd74b9013ba41e62448b617cb7ac2" +dependencies = [ + "anyhow", + "bincode", + "byteorder", + "once_cell", + "parking_lot 0.12.0", + "ruzstd", + "serde", + "zstd", +] + +[[package]] +name = "puffin_http" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784f1952bda562ec8b76651f0c879b61eb02eaf20fe9acd2a8ca69b4898b13fd" +dependencies = [ + "anyhow", + "crossbeam-channel", + "log", + "puffin", +] + +[[package]] +name = "puffin_profiler" +version = "0.1.0" +dependencies = [ + "eframe", + "puffin", + "puffin_http", +] + [[package]] name = "quote" version = "1.0.15" @@ -2795,6 +2854,16 @@ dependencies = [ "unicode-script", ] +[[package]] +name = "ruzstd" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cada0ef59efa6a5f4dc5e491f93d9f31e3fc7758df421ff1de8a706338e1100" +dependencies = [ + "byteorder", + "twox-hash", +] + [[package]] name = "ryu" version = "1.0.9" @@ -3374,6 +3443,16 @@ dependencies = [ "windows", ] +[[package]] +name = "twox-hash" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee73e6e4924fe940354b8d4d98cad5231175d615cd855b758adc658c0aac6a0" +dependencies = [ + "cfg-if 1.0.0", + "static_assertions", +] + [[package]] name = "unicode-bidi" version = "0.3.7" @@ -4051,6 +4130,35 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zstd" +version = "0.10.0+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b1365becbe415f3f0fcd024e2f7b45bacfb5bdd055f0dc113571394114e7bdd" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "4.1.4+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f7cd17c9af1a4d6c24beb1cc54b17e2ef7b593dc92f19e9d9acad8b182bbaee" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "1.6.3+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc49afa5c8d634e75761feda8c592051e7eeb4683ba827211eb0d731d3402ea8" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "zvariant" version = "3.1.2" diff --git a/Cargo.toml b/Cargo.toml index a8113833166..bb600940a9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,8 @@ members = [ "emath", "epaint", "epi", + + "examples/puffin_profiler", ] [profile.dev] diff --git a/eframe/CHANGELOG.md b/eframe/CHANGELOG.md index 1646e51f9f7..8deb782ff8b 100644 --- a/eframe/CHANGELOG.md +++ b/eframe/CHANGELOG.md @@ -22,6 +22,7 @@ NOTE: [`egui_web`](../egui_web/CHANGELOG.md), [`egui-winit`](../egui-winit/CHANG * `dark-light` (dark mode detection) is now an opt-in feature ([#1437](https://github.com/emilk/egui/pull/1437)). * Fixed potential scale bug when DPI scaling changes (e.g. when dragging a window between different displays) ([#1441](https://github.com/emilk/egui/pull/1441)). * MSRV (Minimum Supported Rust Version) is now `1.60.0` ([#1467](https://github.com/emilk/egui/pull/1467)). +* Added new feature `puffin` to add [`puffin profiler`](https://github.com/EmbarkStudios/puffin) scopes ([#1483](https://github.com/emilk/egui/pull/1483)). ## 0.17.0 - 2022-02-22 diff --git a/eframe/Cargo.toml b/eframe/Cargo.toml index 07413163204..4b1f4569a49 100644 --- a/eframe/Cargo.toml +++ b/eframe/Cargo.toml @@ -37,6 +37,11 @@ persistence = [ "epi/persistence", ] +# Enable profiling with the puffin crate: https://github.com/EmbarkStudios/puffin +# Only enabled on native, because of the low resolution (1ms) of time keeping in browsers. +# eframe will call `puffin::GlobalProfiler::lock().new_frame()` for you +puffin = ["egui_glow/puffin"] + # enable screen reader support (requires `ctx.options().screen_reader = true;`) screen_reader = [ "egui-winit/screen_reader", @@ -74,5 +79,7 @@ image = { version = "0.24", default-features = false, features = [ "png", ] } poll-promise = "0.1" +puffin = "0.13" +puffin_http = "0.10" rfd = "0.8" three-d = { version = "0.11", default-features = false } diff --git a/eframe/README.md b/eframe/README.md index 68581dc9284..b38e0c0b71c 100644 --- a/eframe/README.md +++ b/eframe/README.md @@ -8,9 +8,10 @@ `eframe` is the official framework library for writing apps using [`egui`](https://github.com/emilk/egui). The app can be compiled both to run natively (cross platform) or be compiled to a web app (using WASM). -To get started, go to and follow the instructions there! +To get started, see the [crate examples](https://github.com/emilk/egui/tree/master/examples) and [single-file examples](https://github.com/emilk/egui/tree/master/eframe/examples). +To learn how to set up `eframe` for web and native, go to and follow the instructions there! -You can also take a look at [the `eframe` examples folder](https://github.com/emilk/egui/tree/master/eframe/examples). There is also an excellent tutorial video at . +There is also a tutorial video at . For how to use `egui`, see [the egui docs](https://docs.rs/egui). @@ -37,6 +38,7 @@ Not all rust crates work when compiled to WASM, but here are some useful crates * Audio: [`cpal`](https://github.com/RustAudio/cpal). * HTTP client: [`ehttp`](https://github.com/emilk/ehttp). * Time: [`chrono`](https://github.com/chronotope/chrono). +* WebSockets: [`ewebsock`](https://github.com/rerun-io/ewebsock). ## Name diff --git a/eframe/src/lib.rs b/eframe/src/lib.rs index 6e7a318f738..45ff0f7385f 100644 --- a/eframe/src/lib.rs +++ b/eframe/src/lib.rs @@ -4,9 +4,8 @@ //! and are happy with just using egui for all visuals, //! Then `eframe` is for you! //! -//! To get started, look at . -//! -//! You can also take a look at [the `eframe` examples folder](https://github.com/emilk/egui/tree/master/eframe/examples). +//! To get started, see the [crate examples](https://github.com/emilk/egui/tree/master/examples) and [single-file examples](https://github.com/emilk/egui/tree/master/eframe/examples). +//! To learn how to set up `eframe` for web and native, go to and follow the instructions there! //! //! You write your application code for [`epi`] (implementing [`epi::App`]) and then //! call from [`crate::run_native`] your `main.rs`, and/or call `eframe::start_web` from your `lib.rs`. diff --git a/egui-winit/CHANGELOG.md b/egui-winit/CHANGELOG.md index 296191f13eb..b7c711f635c 100644 --- a/egui-winit/CHANGELOG.md +++ b/egui-winit/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to the `egui-winit` integration will be noted in this file. * Renamed the feature `convert_bytemuck` to `bytemuck` ([#1467](https://github.com/emilk/egui/pull/1467)). * Renamed the feature `serialize` to `serde` ([#1467](https://github.com/emilk/egui/pull/1467)). * MSRV (Minimum Supported Rust Version) is now `1.60.0` ([#1467](https://github.com/emilk/egui/pull/1467)). +* Added new feature `puffin` to add [`puffin profiler`](https://github.com/EmbarkStudios/puffin) scopes ([#1483](https://github.com/emilk/egui/pull/1483)). ## 0.17.0 - 2022-02-22 diff --git a/egui-winit/Cargo.toml b/egui-winit/Cargo.toml index 578e1a390fc..aa835295317 100644 --- a/egui-winit/Cargo.toml +++ b/egui-winit/Cargo.toml @@ -36,6 +36,9 @@ epi_backend = ["epi", "glow"] # enable opening links in a browser when an egui hyperlink is clicked. links = ["webbrowser"] +# Enable profiling with the puffin crate: https://github.com/EmbarkStudios/puffin +puffin = ["dep:puffin"] + # experimental support for a screen reader screen_reader = ["tts"] @@ -57,6 +60,7 @@ epi = { version = "0.17.0", path = "../epi", optional = true } arboard = { version = "2.1", optional = true, default-features = false } dark-light = { version = "0.2.1", optional = true } glow = { version = "0.11", optional = true } +puffin = { version = "0.13", optional = true } serde = { version = "1.0", optional = true, features = ["derive"] } webbrowser = { version = "0.6", optional = true } diff --git a/egui-winit/src/epi.rs b/egui-winit/src/epi.rs index b34d3a5d124..349349c9af6 100644 --- a/egui-winit/src/epi.rs +++ b/egui-winit/src/epi.rs @@ -191,6 +191,7 @@ impl EpiIntegration { } pub fn warm_up(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) { + crate::profile_function!(); let saved_memory: egui::Memory = self.egui_ctx.memory().clone(); self.egui_ctx.memory().set_everything_is_visible(true); let full_output = self.update(app, window); @@ -230,6 +231,7 @@ impl EpiIntegration { let raw_input = self.egui_winit.take_egui_input(window); let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { + crate::profile_scope!("App::update"); app.update(egui_ctx, &mut self.frame); }); self.pending_full_output.append(full_output); @@ -274,7 +276,10 @@ impl EpiIntegration { pub fn save(&mut self, _app: &mut dyn epi::App, _window: &winit::window::Window) { #[cfg(feature = "persistence")] if let Some(storage) = self.frame.storage_mut() { + crate::profile_function!(); + if _app.persist_native_window() { + crate::profile_scope!("native_window"); epi::set_value( storage, STORAGE_WINDOW_KEY, @@ -282,9 +287,15 @@ impl EpiIntegration { ); } if _app.persist_egui_memory() { + crate::profile_scope!("egui_memory"); epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, &*self.egui_ctx.memory()); } - _app.save(storage); + { + crate::profile_scope!("App::save"); + _app.save(storage); + } + + crate::profile_scope!("Storage::flush"); storage.flush(); } } diff --git a/egui-winit/src/lib.rs b/egui-winit/src/lib.rs index 4a3ec05df9d..e0893a40c17 100644 --- a/egui-winit/src/lib.rs +++ b/egui-winit/src/lib.rs @@ -646,3 +646,25 @@ fn translate_cursor(cursor_icon: egui::CursorIcon) -> Option Some(winit::window::CursorIcon::ZoomOut), } } + +// --------------------------------------------------------------------------- + +/// Profiling macro for feature "puffin" +#[doc(hidden)] +#[macro_export] +macro_rules! profile_function { + ($($arg: tt)*) => { + #[cfg(feature = "puffin")] + puffin::profile_function!($($arg)*); + }; +} + +/// Profiling macro for feature "puffin" +#[doc(hidden)] +#[macro_export] +macro_rules! profile_scope { + ($($arg: tt)*) => { + #[cfg(feature = "puffin")] + puffin::profile_scope!($($arg)*); + }; +} diff --git a/egui/examples/README.md b/egui/examples/README.md index 33da866cca6..cdc7a211dad 100644 --- a/egui/examples/README.md +++ b/egui/examples/README.md @@ -2,4 +2,5 @@ There are no stand-alone egui examples, because egui is not stand-alone! There are plenty of examples in [the online demo](https://www.egui.rs/#demo). You can find the source code for it at . -If you are using `eframe`, check out [the `eframe` examples](https://github.com/emilk/egui/tree/master/eframe/examples) and [the `eframe` template repository](https://github.com/emilk/eframe_template/). +If you are using `eframe`, the [crate examples](https://github.com/emilk/egui/tree/master/examples) and [single-file examples](https://github.com/emilk/egui/tree/master/eframe/examples). +To learn how to set up `eframe` for web and native, go to and follow the instructions there! diff --git a/egui_glow/CHANGELOG.md b/egui_glow/CHANGELOG.md index a2852125860..70b62a7e024 100644 --- a/egui_glow/CHANGELOG.md +++ b/egui_glow/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to the `egui_glow` integration will be noted in this file. * Fixed potential scale bug when DPI scaling changes (e.g. when dragging a window between different displays) ([#1441](https://github.com/emilk/egui/pull/1441)). * MSRV (Minimum Supported Rust Version) is now `1.60.0` ([#1467](https://github.com/emilk/egui/pull/1467)). * `clipboard`, `links`, `persistence`, `winit` are now all opt-in features ([#1467](https://github.com/emilk/egui/pull/1467)). +* Added new feature `puffin` to add [`puffin profiler`](https://github.com/EmbarkStudios/puffin) scopes ([#1483](https://github.com/emilk/egui/pull/1483)). ## 0.17.0 - 2022-02-22 diff --git a/egui_glow/Cargo.toml b/egui_glow/Cargo.toml index b2b3c48c5b6..5889477f6e5 100644 --- a/egui_glow/Cargo.toml +++ b/egui_glow/Cargo.toml @@ -47,6 +47,9 @@ persistence = [ "epi?/persistence", ] +# Enable profiling with the puffin crate: https://github.com/EmbarkStudios/puffin +puffin = ["dep:puffin", "egui-winit?/puffin"] + # experimental support for a screen reader screen_reader = ["egui-winit?/screen_reader"] @@ -72,6 +75,7 @@ egui-winit = { version = "0.17.0", path = "../egui-winit", optional = true, defa "epi_backend", ] } glutin = { version = "0.28.0", optional = true } +puffin = { version = "0.13", optional = true } # Web: [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/egui_glow/src/epi_backend.rs b/egui_glow/src/epi_backend.rs index 9c891ea3278..4f50cebda89 100644 --- a/egui_glow/src/epi_backend.rs +++ b/egui_glow/src/epi_backend.rs @@ -11,6 +11,7 @@ fn create_display( glutin::WindowedContext, glow::Context, ) { + crate::profile_function!(); let gl_window = unsafe { glutin::ContextBuilder::new() .with_depth_buffer(native_options.depth_buffer) @@ -77,15 +78,20 @@ pub fn run(app_name: &str, native_options: &epi::NativeOptions, app_creator: epi event_loop.run(move |event, _, control_flow| { let mut redraw = || { + #[cfg(feature = "puffin")] + puffin::GlobalProfiler::lock().new_frame(); + if !is_focused { // On Mac, a minimized Window uses up all CPU: https://github.com/emilk/egui/issues/325 // We can't know if we are minimized: https://github.com/rust-windowing/winit/issues/208 // But we know if we are focused (in foreground). When minimized, we are not focused. // However, a user may want an egui with an animation in the background, // so we still need to repaint quite fast. + crate::profile_scope!("bg_sleep"); std::thread::sleep(std::time::Duration::from_millis(10)); } + crate::profile_scope!("frame"); let screen_size_in_pixels: [u32; 2] = gl_window.window().inner_size().into(); crate::painter::clear(&gl, screen_size_in_pixels, app.clear_color()); @@ -99,7 +105,10 @@ pub fn run(app_name: &str, native_options: &epi::NativeOptions, app_creator: epi integration.handle_platform_output(gl_window.window(), platform_output); - let clipped_primitives = integration.egui_ctx.tessellate(shapes); + let clipped_primitives = { + crate::profile_scope!("tessellate"); + integration.egui_ctx.tessellate(shapes) + }; painter.paint_and_update_textures( screen_size_in_pixels, @@ -108,7 +117,10 @@ pub fn run(app_name: &str, native_options: &epi::NativeOptions, app_creator: epi &textures_delta, ); - gl_window.swap_buffers().unwrap(); + { + crate::profile_scope!("swap_buffers"); + gl_window.swap_buffers().unwrap(); + } { *control_flow = if integration.should_quit() { diff --git a/egui_glow/src/lib.rs b/egui_glow/src/lib.rs index 29a6cf2bf2d..94d155af719 100644 --- a/egui_glow/src/lib.rs +++ b/egui_glow/src/lib.rs @@ -85,3 +85,25 @@ pub fn check_for_gl_error_impl(gl: &glow::Context, file: &str, line: u32, contex } } } + +// --------------------------------------------------------------------------- + +/// Profiling macro for feature "puffin" +#[doc(hidden)] +#[macro_export] +macro_rules! profile_function { + ($($arg: tt)*) => { + #[cfg(feature = "puffin")] + puffin::profile_function!($($arg)*); + }; +} + +/// Profiling macro for feature "puffin" +#[doc(hidden)] +#[macro_export] +macro_rules! profile_scope { + ($($arg: tt)*) => { + #[cfg(feature = "puffin")] + puffin::profile_scope!($($arg)*); + }; +} diff --git a/egui_glow/src/painter.rs b/egui_glow/src/painter.rs index 35b2ff40197..078c028e766 100644 --- a/egui_glow/src/painter.rs +++ b/egui_glow/src/painter.rs @@ -96,6 +96,7 @@ impl Painter { pp_fb_extent: Option<[i32; 2]>, shader_prefix: &str, ) -> Result { + crate::profile_function!(); check_for_gl_error!(&gl, "before Painter::new"); let max_texture_side = unsafe { gl.get_parameter_i32(glow::MAX_TEXTURE_SIZE) } as usize; @@ -297,6 +298,7 @@ impl Painter { clipped_primitives: &[egui::ClippedPrimitive], textures_delta: &egui::TexturesDelta, ) { + crate::profile_function!(); for (id, image_delta) in &textures_delta.set { self.set_texture(*id, image_delta); } @@ -333,6 +335,7 @@ impl Painter { pixels_per_point: f32, clipped_primitives: &[egui::ClippedPrimitive], ) { + crate::profile_function!(); self.assert_not_destroyed(); if let Some(ref mut post_process) = self.post_process { @@ -355,6 +358,7 @@ impl Painter { } Primitive::Callback(callback) => { if callback.rect.is_positive() { + crate::profile_scope!("callback"); // Transform callback rect to physical pixels: let rect_min_x = pixels_per_point * callback.rect.min.x; let rect_min_y = pixels_per_point * callback.rect.min.y; @@ -456,6 +460,8 @@ impl Painter { // ------------------------------------------------------------------------ pub fn set_texture(&mut self, tex_id: egui::TextureId, delta: &egui::epaint::ImageDelta) { + crate::profile_function!(); + self.assert_not_destroyed(); let glow_texture = *self diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000000..7bde00f6116 --- /dev/null +++ b/examples/README.md @@ -0,0 +1 @@ +Examples of how to use [`eframe`](https://github.com/emilk/egui/tree/master/eframe) and [`egui`](https://github.com/emilk/egui/). diff --git a/examples/puffin_profiler/Cargo.toml b/examples/puffin_profiler/Cargo.toml new file mode 100644 index 00000000000..2acf172ff10 --- /dev/null +++ b/examples/puffin_profiler/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "puffin_profiler" +version = "0.1.0" +authors = ["Emil Ernerfeldt "] +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.60" +publish = false + + +[dependencies] +eframe = { path = "../../eframe", features = ["puffin"] } +puffin = "0.13" +puffin_http = "0.10" diff --git a/examples/puffin_profiler/README.md b/examples/puffin_profiler/README.md new file mode 100644 index 00000000000..a2c5085b39f --- /dev/null +++ b/examples/puffin_profiler/README.md @@ -0,0 +1,9 @@ +Example how to use the [puffin profiler](https://github.com/EmbarkStudios/puffin) with an `eframe` app. + + +```sh +cargo run -p puffin_profiler & + +cargo install puffin_viewer +puffin_viewer --url 127.0.0.1:8585 +``` diff --git a/examples/puffin_profiler/src/main.rs b/examples/puffin_profiler/src/main.rs new file mode 100644 index 00000000000..26f7252a1ce --- /dev/null +++ b/examples/puffin_profiler/src/main.rs @@ -0,0 +1,68 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release + +use eframe::egui; + +fn main() { + start_puffin_server(); // NOTE: you may only want to call this if the users specifies some flag or clicks a button! + + let options = eframe::NativeOptions::default(); + eframe::run_native( + "My egui App", + options, + Box::new(|_cc| Box::new(MyApp::default())), + ); +} + +#[derive(Default)] +struct MyApp {} + +impl eframe::App for MyApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + ui.heading("Example of how to use the puffin profiler with egui"); + ui.separator(); + + let cmd = "cargo install puffin_viewer && puffin_viewer --url 127.0.0.1:8585"; + + ui.label("To connect, run this:"); + ui.horizontal(|ui| { + ui.monospace(cmd); + if ui.small_button("📋").clicked() { + ui.output().copied_text = cmd.into(); + } + }); + + ui.separator(); + + ui.label("Note that this app runs in 'reactive' mode, so you must interact with the app for new profile events to be sent. Waving the mouse over this window is enough."); + + if ui + .button( + "Click to sleep a bit. That should be visible as a spike in the profiler view!", + ) + .clicked() + { + puffin::profile_scope!("sleep"); + std::thread::sleep(std::time::Duration::from_millis(50)); + } + }); + } +} + +fn start_puffin_server() { + puffin::set_scopes_on(true); // tell puffin to collect data + + match puffin_http::Server::new("0.0.0.0:8585") { + Ok(puffin_server) => { + eprintln!("Run: cargo install puffin_viewer && puffin_viewer --url 127.0.0.1:8585"); + + // We can store the server if we want, but in this case we just want + // it to keep running. Dropping it closes the server, so let's not drop it! + #[allow(clippy::mem_forget)] + std::mem::forget(puffin_server); + } + Err(err) => { + eprintln!("Failed to start puffin server: {}", err); + } + }; +}