use std::{fs, time::Duration}; use anyhow::Result; use egui::{Button, CentralPanel, Checkbox, Label, ViewportBuilder, ViewportId}; use egui_notify::{Anchor, Toast, Toasts}; use crate::{ emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId}, profiler::{Profiler, ProfilerStatus}, window::AppWindow, }; pub struct ProfileWindow { sim_id: SimId, client: EmulatorClient, profiler: Profiler, toasts: Toasts, } impl ProfileWindow { pub fn new(sim_id: SimId, client: EmulatorClient) -> Self { Self { sim_id, client: client.clone(), profiler: Profiler::new(sim_id, client), toasts: Toasts::new() .with_anchor(Anchor::BottomLeft) .with_margin((10.0, 10.0).into()) .reverse(true), } } pub fn launch(&mut self) { self.profiler.enable(); } fn finish_recording(&mut self) { let pause = matches!(self.client.emulator_state(), EmulatorState::Running); if pause { self.client.send_command(EmulatorCommand::Pause); } match self.try_finish_recording() { Ok(Some(path)) => { let mut toast = Toast::info(format!("Saved to {path}")); toast.duration(Some(Duration::from_secs(5))); self.toasts.add(toast); } Ok(None) => {} Err(error) => { let mut toast = Toast::error(format!("{error:#}")); toast.duration(Some(Duration::from_secs(5))); self.toasts.add(toast); } } if pause { self.client.send_command(EmulatorCommand::Resume); } } fn try_finish_recording(&mut self) -> Result> { let bytes_receiver = self.profiler.finish_recording(); let file = rfd::FileDialog::new() .add_filter("Profiler files", &["json"]) .set_file_name("profile.json") .save_file(); if let Some(path) = file { let bytes = pollster::block_on(bytes_receiver)?; fs::write(&path, bytes)?; Ok(Some(path.display().to_string())) } else { self.profiler.cancel_recording(); Ok(None) } } } impl AppWindow for ProfileWindow { fn viewport_id(&self) -> ViewportId { ViewportId::from_hash_of(format!("Profile-{}", self.sim_id)) } fn sim_id(&self) -> SimId { self.sim_id } fn initial_viewport(&self) -> ViewportBuilder { ViewportBuilder::default() .with_title(format!("Profiler ({})", self.sim_id)) .with_inner_size((300.0, 200.0)) } fn show(&mut self, ctx: &egui::Context) { let status = self.profiler.status(); let recording = matches!(status, ProfilerStatus::Recording); CentralPanel::default().show(ctx, |ui| { ui.horizontal_wrapped(|ui| { ui.spacing_mut().item_spacing.x = 0.0; ui.add( Label::new( "Use this tool to record performance profiles of your game, for use in ", ) .wrap_mode(egui::TextWrapMode::Wrap), ); ui.hyperlink("https://profiler.firefox.com"); ui.label("."); }); ui.horizontal_wrapped(|ui| { ui.spacing_mut().item_spacing.x = 0.0; ui.add( Label::new("For more instructions, see ").wrap_mode(egui::TextWrapMode::Wrap), ); ui.hyperlink_to( "the Lemur wiki", "https://git.virtual-boy.com/PVB/lemur/wiki/Profiling-with-Lemur", ); ui.label("."); }); ui.separator(); let mut enabled = status.enabled(); let enabled_checkbox = Checkbox::new(&mut enabled, "Enable profiling"); if ui.add_enabled(!recording, enabled_checkbox).changed() { if enabled { self.profiler.enable(); } else { self.profiler.disable(); } } if !enabled { ui.label("Enabling profiling will restart your current game."); } else { ui.horizontal(|ui| { if !recording { let record_button = Button::new("Record"); let can_record = matches!(status, ProfilerStatus::Enabled); if ui.add_enabled(can_record, record_button).clicked() { self.profiler.start_recording(); } } else { if ui.button("Finish recording").clicked() { self.finish_recording(); } if ui.button("Cancel recording").clicked() { self.profiler.cancel_recording(); } } }); } match &status { ProfilerStatus::Recording => { ui.label("Recording..."); } ProfilerStatus::Error(message) => { ui.label(message); } _ => {} } }); self.toasts.show(ctx); } }