diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9574b89d..039183c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,7 @@ jobs: run: | sudo apt-get update sudo apt-get -yq --no-install-suggests --no-install-recommends install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libxkbcommon-dev # egui_glium dependencies + sudo apt-get install libgtk-3-dev # rfd dependencies # make sure all code has been formatted with rustfmt - run: rustup component add rustfmt @@ -66,6 +67,7 @@ jobs: run: | sudo apt-get update sudo apt-get -yq --no-install-suggests --no-install-recommends install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libxkbcommon-dev # egui_glium dependencies + sudo apt-get install libgtk-3-dev # rfd dependencies - name: cargo test build uses: actions-rs/cargo@v1 with: diff --git a/Cargo.lock b/Cargo.lock index bf96f4ec..52917d9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,6 +110,18 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a61eb019cb8f415d162cb9f12130ee6bbe9168b7d953c17f4ad049e4051ca00" +[[package]] +name = "atk-sys" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "badcf670157c84bb8b1cf6b5f70b650fed78da2033c9eed84c4e49b11cbe83ea" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "atomic_refcell" version = "0.1.7" @@ -223,6 +235,16 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "cairo-sys-rs" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c9c3928781e8a017ece15eace05230f04b647457d170d2d9641c94a444ff80" +dependencies = [ + "libc", + "system-deps", +] + [[package]] name = "calloop" version = "0.6.5" @@ -248,6 +270,15 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" +[[package]] +name = "cfg-expr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b412e83326147c2bb881f8b40edfbf9905b9b8abaebd0e47ca190ba62fda8f0e" +dependencies = [ + "smallvec 1.6.1", +] + [[package]] name = "cfg-if" version = "0.1.10" @@ -729,8 +760,7 @@ checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" [[package]] name = "eframe" version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3677cb27e360092b5078c5aaae1edf4b84d900b2e5ee61e93c71c4f2574e97a" +source = "git+https://github.com/emilk/egui?rev=f8a304225851165c817f7ceb3a0c2b9c78b2b177#f8a304225851165c817f7ceb3a0c2b9c78b2b177" dependencies = [ "egui", "egui_glium", @@ -741,8 +771,7 @@ dependencies = [ [[package]] name = "egui" version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77f9b394ccacc0cccb30f6de7371038a116931aa3186acd908b9ec61f405f15c" +source = "git+https://github.com/emilk/egui?rev=f8a304225851165c817f7ceb3a0c2b9c78b2b177#f8a304225851165c817f7ceb3a0c2b9c78b2b177" dependencies = [ "epaint", "ron", @@ -775,8 +804,7 @@ dependencies = [ [[package]] name = "egui_glium" version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da94629a664a360708c91495dbae0a55c0e05acec5e05fcc6c99beabd6036036" +source = "git+https://github.com/emilk/egui?rev=f8a304225851165c817f7ceb3a0c2b9c78b2b177#f8a304225851165c817f7ceb3a0c2b9c78b2b177" dependencies = [ "copypasta", "directories-next", @@ -791,8 +819,7 @@ dependencies = [ [[package]] name = "egui_web" version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce8d91b33af1d26ba3f7b22819e7bfdcb596e2d3a382eacec2363960eacdf643" +source = "git+https://github.com/emilk/egui?rev=f8a304225851165c817f7ceb3a0c2b9c78b2b177#f8a304225851165c817f7ceb3a0c2b9c78b2b177" dependencies = [ "egui", "epi", @@ -813,8 +840,7 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "emath" version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80eea7508c08a7b4e2a041adcdca6f400d8606622c6bb980ecbbdc5f1aff9a4c" +source = "git+https://github.com/emilk/egui?rev=f8a304225851165c817f7ceb3a0c2b9c78b2b177#f8a304225851165c817f7ceb3a0c2b9c78b2b177" dependencies = [ "serde", ] @@ -822,8 +848,7 @@ dependencies = [ [[package]] name = "epaint" version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "132e30d483d9fa252fe06aa112de777d068503c379fa51bc5834204ee3258fce" +source = "git+https://github.com/emilk/egui?rev=f8a304225851165c817f7ceb3a0c2b9c78b2b177#f8a304225851165c817f7ceb3a0c2b9c78b2b177" dependencies = [ "ab_glyph", "ahash 0.7.4", @@ -836,8 +861,7 @@ dependencies = [ [[package]] name = "epi" version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b04a4a240553fa9254d0569cf3aa22d9dbe77a66025e1f04b0785744629c9ec" +source = "git+https://github.com/emilk/egui?rev=f8a304225851165c817f7ceb3a0c2b9c78b2b177#f8a304225851165c817f7ceb3a0c2b9c78b2b177" dependencies = [ "egui", "ron", @@ -891,6 +915,36 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +[[package]] +name = "gdk-pixbuf-sys" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f097c0704201fbc8f69c1762dc58c6947c8bb188b8ed0bc7e65259f1894fe590" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e091b3d3d6696949ac3b3fb3c62090e5bfd7bd6850bef5c3c5ea701de1b1f1e" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + [[package]] name = "getrandom" version = "0.2.3" @@ -908,6 +962,19 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7" +[[package]] +name = "gio-sys" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0a41df66e57fcc287c4bcf74fc26b884f31901ea9792ec75607289b456f48fa" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi 0.3.9", +] + [[package]] name = "gl_generator" version = "0.14.0" @@ -925,6 +992,16 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "333928d5eb103c5d4050533cec0384302db6be8ef7d3cebd30ec6a35350353da" +[[package]] +name = "glib-sys" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c1d60554a212445e2a858e42a0e48cece1bd57b311a19a9468f70376cf554ae" +dependencies = [ + "libc", + "system-deps", +] + [[package]] name = "glium" version = "0.29.1" @@ -1056,6 +1133,35 @@ dependencies = [ "gl_generator", ] +[[package]] +name = "gobject-sys" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa92cae29759dae34ab5921d73fff5ad54b3d794ab842c117e36cafc7994c3f5" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk-sys" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c14c8d3da0545785a7c5a120345b3abb534010fb8ae0f2ef3f47c027fba303e" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + [[package]] name = "half" version = "1.7.1" @@ -1812,6 +1918,18 @@ dependencies = [ "ttf-parser 0.12.3", ] +[[package]] +name = "pango-sys" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2367099ca5e761546ba1d501955079f097caa186bb53ce0f718dca99ac1942fe" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "parking_lot" version = "0.11.1" @@ -1979,6 +2097,7 @@ dependencies = [ "puffin", "puffin_egui", "puffin_http", + "rfd", "simple_logger", ] @@ -2109,6 +2228,29 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9c17925a9027d298a4603d286befe3f9dc0e8ed02523141914eb628798d6e5b" +[[package]] +name = "rfd" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cede43618603a102f37bb58244534a33de7daa6a3b77f00277675eef5f8174fe" +dependencies = [ + "block", + "dispatch", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "lazy_static", + "objc", + "objc-foundation", + "objc_id", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winapi 0.3.9", +] + [[package]] name = "ron" version = "0.6.4" @@ -2388,6 +2530,24 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" +[[package]] +name = "strum" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2" + +[[package]] +name = "strum_macros" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "syn" version = "1.0.75" @@ -2399,6 +2559,24 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "system-deps" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "480c269f870722b3b08d2f13053ce0c2ab722839f472863c3e2d61ff3a1c2fa6" +dependencies = [ + "anyhow", + "cfg-expr", + "heck", + "itertools", + "pkg-config", + "strum", + "strum_macros", + "thiserror", + "toml", + "version-compare", +] + [[package]] name = "takeable-option" version = "0.5.0" @@ -2503,6 +2681,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "version-compare" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" + [[package]] name = "version_check" version = "0.9.3" diff --git a/Cargo.toml b/Cargo.toml index 757c3d77..01f6073e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,7 @@ members = [ "puffin-imgui", "puffin_viewer", ] + +[patch.crates-io] +eframe = { git = "https://github.com/emilk/egui", rev = "f8a304225851165c817f7ceb3a0c2b9c78b2b177" } +egui = { git = "https://github.com/emilk/egui", rev = "f8a304225851165c817f7ceb3a0c2b9c78b2b177" } diff --git a/puffin_egui/src/lib.rs b/puffin_egui/src/lib.rs index 55ded08a..c1a86daa 100644 --- a/puffin_egui/src/lib.rs +++ b/puffin_egui/src/lib.rs @@ -321,6 +321,14 @@ fn latest_frames() -> Frames { } impl ProfilerUi { + pub fn reset(&mut self) { + let options = self.options; + *self = Self { + options, + ..Default::default() + }; + } + /// Show an [`egui::Window`] with the profiler contents. /// /// If you want to control the window yourself, use [`Self::ui`] instead. diff --git a/puffin_viewer/CHANGELOG.md b/puffin_viewer/CHANGELOG.md index def0a7eb..459233ae 100644 --- a/puffin_viewer/CHANGELOG.md +++ b/puffin_viewer/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to `puffin_viewer` will be documented in this file. +## Unreleased +* Load and save recordings as `.puffin` files. + + ## 0.4.0 * Add support for compressed TCP stream (up to 75% bandwidth reduction). diff --git a/puffin_viewer/Cargo.toml b/puffin_viewer/Cargo.toml index 73f5a6e9..400a6373 100644 --- a/puffin_viewer/Cargo.toml +++ b/puffin_viewer/Cargo.toml @@ -20,4 +20,5 @@ puffin_http = { version = "0.4.0", path = "../puffin_http" } argh = "0.1" eframe = { version = "0.13", features = ["persistence"] } log = "0.4" +rfd = "0.4.3" simple_logger = "1.11" diff --git a/puffin_viewer/src/main.rs b/puffin_viewer/src/main.rs index cc931a86..a26f21af 100644 --- a/puffin_viewer/src/main.rs +++ b/puffin_viewer/src/main.rs @@ -71,14 +71,22 @@ // END - Embark standard lints v0.4 // crate-specific exceptions: #![deny(missing_crate_level_docs)] +#![allow(clippy::exit)] use eframe::{egui, epi}; +use puffin::GlobalProfiler; +use std::path::PathBuf; -/// puffin remote profile viewer. +/// puffin profile viewer. /// -/// Connect to a puffin server and show its profile data. +/// Can either connect remotely to a puffin server +/// or open a .puffin recording file. #[derive(argh::FromArgs)] struct Arguments { + /// what .puffin file to open, e.g. `my/recording.puffin`. + #[argh(option)] + file: Option, + /// which server to connect to, e.g. `127.0.0.1:8585`. #[argh(option, default = "default_url()")] url: String, @@ -97,15 +105,163 @@ fn main() { .ok(); puffin::set_scopes_on(true); // quiet warning in `puffin_egui`. - let client = puffin_http::Client::new(opt.url); - let app = PuffinViewer { client }; + let app = if let Some(file) = opt.file { + let path = PathBuf::from(file); + match GlobalProfiler::load_path(&path) { + Ok(profiler) => { + *GlobalProfiler::lock() = profiler; + PuffinViewer { + profiler_ui: Default::default(), + source: Source::FilePath(path), + error: None, + } + } + Err(err) => { + log::error!("Failed to load {}: {}", path.display(), err); + std::process::exit(1); + } + } + } else { + PuffinViewer { + profiler_ui: Default::default(), + source: Source::Http(puffin_http::Client::new(opt.url)), + error: None, + } + }; + let options = Default::default(); eframe::run_native(Box::new(app), options); } +pub enum Source { + Http(puffin_http::Client), + FilePath(PathBuf), + FileName(String), +} + +impl Source { + fn ui(&self, ui: &mut egui::Ui) { + match self { + Source::Http(http_client) => { + if http_client.connected() { + ui.label(format!("Connected to {}", http_client.addr())); + } else { + ui.label(format!("Connecting to {}…", http_client.addr())); + } + } + Source::FilePath(path) => { + ui.label(format!("Viewing {}", path.display())); + } + Source::FileName(name) => { + ui.label(format!("Viewing {}", name)); + } + } + } +} + pub struct PuffinViewer { - client: puffin_http::Client, + profiler_ui: puffin_egui::ProfilerUi, + source: Source, + error: Option, +} + +impl PuffinViewer { + fn open_puffin_path(&mut self, path: std::path::PathBuf) { + match GlobalProfiler::load_path(&path) { + Ok(profiler) => { + *GlobalProfiler::lock() = profiler; + self.profiler_ui.reset(); + self.source = Source::FilePath(path); + self.error = None; + } + Err(err) => { + self.error = Some(format!("Failed to load {}: {}", path.display(), err)); + } + } + } + + fn open_puffin_bytes(&mut self, name: String, bytes: &[u8]) { + let mut reader = std::io::Cursor::new(bytes); + match GlobalProfiler::load_reader(&mut reader) { + Ok(profiler) => { + *GlobalProfiler::lock() = profiler; + self.profiler_ui.reset(); + self.source = Source::FileName(name); + self.error = None; + } + Err(err) => { + self.error = Some(format!("Failed to load file {:?}: {}", name, err)); + } + } + } + + fn ui_menu_bar(&mut self, ctx: &egui::CtxRef, frame: &mut epi::Frame<'_>) { + egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { + egui::menu::bar(ui, |ui| { + egui::menu::menu(ui, "File", |ui| { + if ui.button("Open…").clicked() { + if let Some(path) = rfd::FileDialog::new() + .add_filter("puffin", &["puffin"]) + .pick_file() + { + self.open_puffin_path(path); + } + } + + if ui.button("Save as…").clicked() { + if let Some(path) = rfd::FileDialog::new() + .add_filter("puffin", &["puffin"]) + .save_file() + { + if let Err(error) = GlobalProfiler::lock().save_to_path(&path) { + self.error = Some(format!("Failed to export: {}", error)); + } else { + self.error = None; + } + } + } + + if ui.button("Quit").clicked() { + frame.quit(); + } + }); + }); + }); + } + + fn ui_file_drag_and_drop(&mut self, ctx: &egui::CtxRef) { + use egui::*; + + // Preview hovering files: + if !ctx.input().raw.hovered_files.is_empty() { + let painter = + ctx.layer_painter(LayerId::new(Order::Foreground, Id::new("file_drop_target"))); + + let screen_rect = ctx.input().screen_rect(); + painter.rect_filled(screen_rect, 0.0, Color32::from_black_alpha(192)); + painter.text( + screen_rect.center(), + Align2::CENTER_CENTER, + "Drop to open .puffin file", + TextStyle::Heading, + Color32::WHITE, + ); + } + + // Collect dropped files: + if !ctx.input().raw.dropped_files.is_empty() { + for file in &ctx.input().raw.dropped_files { + if let Some(path) = &file.path { + self.open_puffin_path(path.clone()); + break; + } else if let Some(bytes) = &file.bytes { + self.open_puffin_bytes(file.name.clone(), bytes); + break; + } + } + } + } } impl epi::App for PuffinViewer { @@ -113,15 +269,22 @@ impl epi::App for PuffinViewer { "puffin http client viewer" } - fn update(&mut self, ctx: &egui::CtxRef, _frame: &mut epi::Frame<'_>) { - egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { - if self.client.connected() { - ui.label(format!("Connected to {}", self.client.addr())); - } else { - ui.label(format!("Connecting to {}…", self.client.addr())); + fn update(&mut self, ctx: &egui::CtxRef, frame: &mut epi::Frame<'_>) { + self.ui_menu_bar(ctx, frame); + + egui::TopBottomPanel::bottom("info_bar").show(ctx, |ui| { + if let Some(error) = &self.error { + ui.colored_label(egui::Color32::RED, error); + ui.separator(); } + + self.source.ui(ui); + }); + + egui::CentralPanel::default().show(ctx, |ui| { + self.profiler_ui.ui(ui); }); - egui::CentralPanel::default().show(ctx, puffin_egui::profiler_ui); + self.ui_file_drag_and_drop(ctx); } }