UI for recording
This commit is contained in:
		
							parent
							
								
									8aab90f024
								
							
						
					
					
						commit
						ce15d22ab1
					
				
							
								
								
									
										10
									
								
								src/app.rs
								
								
								
								
							
							
						
						
									
										10
									
								
								src/app.rs
								
								
								
								
							| 
						 | 
				
			
			@ -116,11 +116,6 @@ impl ApplicationHandler<UserEvent> for Application {
 | 
			
		|||
            server.launch(port);
 | 
			
		||||
            self.open(event_loop, Box::new(server));
 | 
			
		||||
        }
 | 
			
		||||
        if self.init_profiling {
 | 
			
		||||
            let mut profiler = ProfileWindow::new(SimId::Player1, self.client.clone());
 | 
			
		||||
            profiler.launch();
 | 
			
		||||
            self.open(event_loop, Box::new(profiler));
 | 
			
		||||
        }
 | 
			
		||||
        let app = GameWindow::new(
 | 
			
		||||
            self.client.clone(),
 | 
			
		||||
            self.proxy.clone(),
 | 
			
		||||
| 
						 | 
				
			
			@ -129,6 +124,11 @@ impl ApplicationHandler<UserEvent> for Application {
 | 
			
		|||
            SimId::Player1,
 | 
			
		||||
        );
 | 
			
		||||
        self.open(event_loop, Box::new(app));
 | 
			
		||||
        if self.init_profiling {
 | 
			
		||||
            let mut profiler = ProfileWindow::new(SimId::Player1, self.client.clone());
 | 
			
		||||
            profiler.launch();
 | 
			
		||||
            self.open(event_loop, Box::new(profiler));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn window_event(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										255
									
								
								src/profiler.rs
								
								
								
								
							
							
						
						
									
										255
									
								
								src/profiler.rs
								
								
								
								
							| 
						 | 
				
			
			@ -1,13 +1,11 @@
 | 
			
		|||
use std::{
 | 
			
		||||
    collections::HashMap,
 | 
			
		||||
    path::PathBuf,
 | 
			
		||||
    sync::{
 | 
			
		||||
        Arc,
 | 
			
		||||
        atomic::{AtomicBool, Ordering},
 | 
			
		||||
    },
 | 
			
		||||
    sync::{Arc, Mutex},
 | 
			
		||||
    thread,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use anyhow::{Result, bail};
 | 
			
		||||
use tokio::{select, sync::mpsc};
 | 
			
		||||
use wholesym::{SymbolManager, SymbolMap};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -16,7 +14,8 @@ use crate::emulator::{EmulatorClient, EmulatorCommand, ProfileEvent, SimEvent, S
 | 
			
		|||
pub struct Profiler {
 | 
			
		||||
    sim_id: SimId,
 | 
			
		||||
    client: EmulatorClient,
 | 
			
		||||
    running: Arc<AtomicBool>,
 | 
			
		||||
    status: Arc<Mutex<ProfilerStatus>>,
 | 
			
		||||
    action: Option<mpsc::UnboundedSender<RecordingAction>>,
 | 
			
		||||
    killer: Option<oneshot::Sender<()>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -25,21 +24,24 @@ impl Profiler {
 | 
			
		|||
        Self {
 | 
			
		||||
            sim_id,
 | 
			
		||||
            client,
 | 
			
		||||
            running: Arc::new(AtomicBool::new(false)),
 | 
			
		||||
            status: Arc::new(Mutex::new(ProfilerStatus::Disabled)),
 | 
			
		||||
            action: None,
 | 
			
		||||
            killer: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn started(&self) -> bool {
 | 
			
		||||
        self.running.load(Ordering::Relaxed)
 | 
			
		||||
    pub fn status(&self) -> ProfilerStatus {
 | 
			
		||||
        self.status.lock().unwrap().clone()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn start(&mut self) {
 | 
			
		||||
    pub fn enable(&mut self) {
 | 
			
		||||
        let sim_id = self.sim_id;
 | 
			
		||||
        let client = self.client.clone();
 | 
			
		||||
        let running = self.running.clone();
 | 
			
		||||
        let (tx, rx) = oneshot::channel();
 | 
			
		||||
        self.killer = Some(tx);
 | 
			
		||||
        let status = self.status.clone();
 | 
			
		||||
        let (action_tx, action_rx) = mpsc::unbounded_channel();
 | 
			
		||||
        self.action = Some(action_tx);
 | 
			
		||||
        let (killer_tx, killer_rx) = oneshot::channel();
 | 
			
		||||
        self.killer = Some(killer_tx);
 | 
			
		||||
        thread::spawn(move || {
 | 
			
		||||
            tokio::runtime::Builder::new_current_thread()
 | 
			
		||||
                .enable_all()
 | 
			
		||||
| 
						 | 
				
			
			@ -47,48 +49,194 @@ impl Profiler {
 | 
			
		|||
                .unwrap()
 | 
			
		||||
                .block_on(async move {
 | 
			
		||||
                    select! {
 | 
			
		||||
                        _ = run_profile(sim_id, client, running.clone()) => {}
 | 
			
		||||
                        _ = rx => {
 | 
			
		||||
                            running.store(false, Ordering::Relaxed);
 | 
			
		||||
                        _ = run_profile(sim_id, client, status.clone(), action_rx) => {}
 | 
			
		||||
                        _ = killer_rx => {
 | 
			
		||||
                            *status.lock().unwrap() = ProfilerStatus::Disabled;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn stop(&mut self) {
 | 
			
		||||
    pub fn disable(&mut self) {
 | 
			
		||||
        if let Some(killer) = self.killer.take() {
 | 
			
		||||
            let _ = killer.send(());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn start_recording(&mut self) {
 | 
			
		||||
        if let Some(action) = &self.action {
 | 
			
		||||
            let _ = action.send(RecordingAction::Start);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn finish_recording(&mut self) -> oneshot::Receiver<Vec<u8>> {
 | 
			
		||||
        let (tx, rx) = oneshot::channel();
 | 
			
		||||
        if let Some(action) = &self.action {
 | 
			
		||||
            let _ = action.send(RecordingAction::Finish(tx));
 | 
			
		||||
        }
 | 
			
		||||
        rx
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn cancel_recording(&mut self) {
 | 
			
		||||
        if let Some(action) = &self.action {
 | 
			
		||||
            let _ = action.send(RecordingAction::Cancel);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn run_profile(sim_id: SimId, client: EmulatorClient, running: Arc<AtomicBool>) {
 | 
			
		||||
impl Drop for Profiler {
 | 
			
		||||
    fn drop(&mut self) {
 | 
			
		||||
        self.disable();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn run_profile(
 | 
			
		||||
    sim_id: SimId,
 | 
			
		||||
    client: EmulatorClient,
 | 
			
		||||
    status: Arc<Mutex<ProfilerStatus>>,
 | 
			
		||||
    mut action_source: mpsc::UnboundedReceiver<RecordingAction>,
 | 
			
		||||
) {
 | 
			
		||||
    let (profile_sync, mut profile_source) = mpsc::unbounded_channel();
 | 
			
		||||
    client.send_command(EmulatorCommand::StartProfiling(sim_id, profile_sync));
 | 
			
		||||
 | 
			
		||||
    running.store(true, Ordering::Relaxed);
 | 
			
		||||
    *status.lock().unwrap() = ProfilerStatus::Enabled;
 | 
			
		||||
 | 
			
		||||
    let mut session = None;
 | 
			
		||||
    while let Some(event) = profile_source.recv().await {
 | 
			
		||||
        match event {
 | 
			
		||||
            ProfileEvent::Start { file_path } => {
 | 
			
		||||
                session = Some(ProfileSession::new(file_path).await);
 | 
			
		||||
    let mut session = ProfilerSession::new();
 | 
			
		||||
    loop {
 | 
			
		||||
        select! {
 | 
			
		||||
            maybe_event = profile_source.recv() => {
 | 
			
		||||
                let Some(event) = maybe_event else {
 | 
			
		||||
                    break; // emulator thread disconnected
 | 
			
		||||
                };
 | 
			
		||||
                if let Err(error) = handle_event(event, &mut session).await {
 | 
			
		||||
                    *status.lock().unwrap() = ProfilerStatus::Error(error.to_string());
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            ProfileEvent::Update { cycles, event } => {
 | 
			
		||||
                if let Some(session) = &mut session {
 | 
			
		||||
                    session.track_elapsed_cycles(cycles);
 | 
			
		||||
                    if let Some(event) = event {
 | 
			
		||||
                        session.track_event(event);
 | 
			
		||||
            }
 | 
			
		||||
            maybe_action = action_source.recv() => {
 | 
			
		||||
                let Some(action) = maybe_action else {
 | 
			
		||||
                    break; // ui thread disconnected
 | 
			
		||||
                };
 | 
			
		||||
                handle_action(action, &mut session, &status);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    running.store(false, Ordering::Release);
 | 
			
		||||
 | 
			
		||||
    *status.lock().unwrap() = ProfilerStatus::Disabled;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct ProfileSession {
 | 
			
		||||
async fn handle_event(event: ProfileEvent, session: &mut ProfilerSession) -> Result<()> {
 | 
			
		||||
    match event {
 | 
			
		||||
        ProfileEvent::Start { file_path } => session.start_profiling(file_path).await,
 | 
			
		||||
        ProfileEvent::Update { cycles, event } => {
 | 
			
		||||
            session.track_elapsed_cycles(cycles)?;
 | 
			
		||||
            if let Some(event) = event {
 | 
			
		||||
                session.track_event(event)?;
 | 
			
		||||
            }
 | 
			
		||||
            Ok(())
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn handle_action(
 | 
			
		||||
    action: RecordingAction,
 | 
			
		||||
    session: &mut ProfilerSession,
 | 
			
		||||
    status: &Mutex<ProfilerStatus>,
 | 
			
		||||
) {
 | 
			
		||||
    match action {
 | 
			
		||||
        RecordingAction::Start => {
 | 
			
		||||
            session.start_recording();
 | 
			
		||||
            *status.lock().unwrap() = ProfilerStatus::Recording;
 | 
			
		||||
        }
 | 
			
		||||
        RecordingAction::Finish(rx) => {
 | 
			
		||||
            if let Some(bytes) = session.finish_recording() {
 | 
			
		||||
                let _ = rx.send(bytes);
 | 
			
		||||
            }
 | 
			
		||||
            *status.lock().unwrap() = ProfilerStatus::Enabled;
 | 
			
		||||
        }
 | 
			
		||||
        RecordingAction::Cancel => {
 | 
			
		||||
            session.cancel_recording();
 | 
			
		||||
            *status.lock().unwrap() = ProfilerStatus::Enabled;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
pub enum ProfilerStatus {
 | 
			
		||||
    Disabled,
 | 
			
		||||
    Enabled,
 | 
			
		||||
    Recording,
 | 
			
		||||
    Error(String),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ProfilerStatus {
 | 
			
		||||
    pub fn enabled(&self) -> bool {
 | 
			
		||||
        matches!(self, Self::Enabled | Self::Recording)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum RecordingAction {
 | 
			
		||||
    Start,
 | 
			
		||||
    Finish(oneshot::Sender<Vec<u8>>),
 | 
			
		||||
    Cancel,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct Recording {}
 | 
			
		||||
 | 
			
		||||
impl Recording {
 | 
			
		||||
    fn new() -> Self {
 | 
			
		||||
        Self {}
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct ProfilerSession {
 | 
			
		||||
    program: Option<ProgramState>,
 | 
			
		||||
    recording: Option<Recording>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ProfilerSession {
 | 
			
		||||
    fn new() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            program: None,
 | 
			
		||||
            recording: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn start_profiling(&mut self, file_path: PathBuf) -> Result<()> {
 | 
			
		||||
        self.program = Some(ProgramState::new(file_path).await?);
 | 
			
		||||
        self.recording = None;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn track_elapsed_cycles(&mut self, cycles: u32) -> Result<()> {
 | 
			
		||||
        if let Some(program) = &mut self.program {
 | 
			
		||||
            program.track_elapsed_cycles(cycles)?;
 | 
			
		||||
        }
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn track_event(&mut self, event: SimEvent) -> Result<()> {
 | 
			
		||||
        if let Some(program) = &mut self.program {
 | 
			
		||||
            program.track_event(event)?;
 | 
			
		||||
        }
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn start_recording(&mut self) {
 | 
			
		||||
        self.recording = Some(Recording::new());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn finish_recording(&mut self) -> Option<Vec<u8>> {
 | 
			
		||||
        self.recording.take().map(|_| vec![])
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn cancel_recording(&mut self) {
 | 
			
		||||
        self.recording.take();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct ProgramState {
 | 
			
		||||
    symbol_map: SymbolMap,
 | 
			
		||||
    call_stacks: HashMap<u16, Vec<StackFrame>>,
 | 
			
		||||
    context_stack: Vec<u16>,
 | 
			
		||||
| 
						 | 
				
			
			@ -101,13 +249,12 @@ struct StackFrame {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
const RESET_CODE: u16 = 0xfff0;
 | 
			
		||||
impl ProfileSession {
 | 
			
		||||
    async fn new(file_path: PathBuf) -> Self {
 | 
			
		||||
impl ProgramState {
 | 
			
		||||
    async fn new(file_path: PathBuf) -> Result<Self> {
 | 
			
		||||
        let symbol_manager = SymbolManager::with_config(Default::default());
 | 
			
		||||
        let symbol_map = symbol_manager
 | 
			
		||||
            .load_symbol_map_for_binary_at_path(&file_path, None)
 | 
			
		||||
            .await
 | 
			
		||||
            .expect("cannae load symbols");
 | 
			
		||||
            .await?;
 | 
			
		||||
        let mut call_stacks = HashMap::new();
 | 
			
		||||
        call_stacks.insert(
 | 
			
		||||
            RESET_CODE,
 | 
			
		||||
| 
						 | 
				
			
			@ -116,33 +263,34 @@ impl ProfileSession {
 | 
			
		|||
                cycles: 0,
 | 
			
		||||
            }],
 | 
			
		||||
        );
 | 
			
		||||
        Self {
 | 
			
		||||
        Ok(Self {
 | 
			
		||||
            symbol_map,
 | 
			
		||||
            call_stacks,
 | 
			
		||||
            context_stack: vec![RESET_CODE],
 | 
			
		||||
        }
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn track_elapsed_cycles(&mut self, cycles: u32) {
 | 
			
		||||
    fn track_elapsed_cycles(&mut self, cycles: u32) -> Result<()> {
 | 
			
		||||
        let Some(code) = self.context_stack.last() else {
 | 
			
		||||
            return; // program is halted, CPU is idle
 | 
			
		||||
            return Ok(()); // program is halted, CPU is idle
 | 
			
		||||
        };
 | 
			
		||||
        let Some(stack) = self.call_stacks.get_mut(code) else {
 | 
			
		||||
            panic!("missing stack {code:04x}");
 | 
			
		||||
            bail!("missing stack {code:04x}");
 | 
			
		||||
        };
 | 
			
		||||
        for frame in stack {
 | 
			
		||||
            frame.cycles += cycles as u64;
 | 
			
		||||
        }
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn track_event(&mut self, event: SimEvent) {
 | 
			
		||||
    fn track_event(&mut self, event: SimEvent) -> Result<()> {
 | 
			
		||||
        match event {
 | 
			
		||||
            SimEvent::Call(address) => {
 | 
			
		||||
                let Some(code) = self.context_stack.last() else {
 | 
			
		||||
                    panic!("How did we call anything when we're halted?");
 | 
			
		||||
                    bail!("How did we call anything when we're halted?");
 | 
			
		||||
                };
 | 
			
		||||
                let Some(stack) = self.call_stacks.get_mut(code) else {
 | 
			
		||||
                    panic!("missing stack {code:04x}");
 | 
			
		||||
                    bail!("missing stack {code:04x}");
 | 
			
		||||
                };
 | 
			
		||||
                let name = self
 | 
			
		||||
                    .symbol_map
 | 
			
		||||
| 
						 | 
				
			
			@ -152,21 +300,21 @@ impl ProfileSession {
 | 
			
		|||
            }
 | 
			
		||||
            SimEvent::Return => {
 | 
			
		||||
                let Some(code) = self.context_stack.last() else {
 | 
			
		||||
                    panic!("how did we return when we're halted?");
 | 
			
		||||
                    bail!("how did we return when we're halted?");
 | 
			
		||||
                };
 | 
			
		||||
                let Some(stack) = self.call_stacks.get_mut(code) else {
 | 
			
		||||
                    panic!("missing stack {code:04x}");
 | 
			
		||||
                    bail!("missing stack {code:04x}");
 | 
			
		||||
                };
 | 
			
		||||
                if stack.pop().is_none() {
 | 
			
		||||
                    panic!("returned from {code:04x} but stack was empty");
 | 
			
		||||
                    bail!("returned from {code:04x} but stack was empty");
 | 
			
		||||
                }
 | 
			
		||||
                if stack.is_empty() {
 | 
			
		||||
                    panic!("returned to oblivion");
 | 
			
		||||
                    bail!("returned to oblivion");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            SimEvent::Halt => {
 | 
			
		||||
                let Some(RESET_CODE) = self.context_stack.pop() else {
 | 
			
		||||
                    panic!("halted when not in an interrupt");
 | 
			
		||||
                    bail!("halted when not in an interrupt");
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
            SimEvent::Interrupt(code, address) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -181,20 +329,21 @@ impl ProfileSession {
 | 
			
		|||
                    .insert(code, vec![StackFrame { address, cycles: 0 }])
 | 
			
		||||
                    .is_some()
 | 
			
		||||
                {
 | 
			
		||||
                    panic!("{code:04x} fired twice");
 | 
			
		||||
                    bail!("{code:04x} fired twice");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            SimEvent::Reti => {
 | 
			
		||||
                let Some(code) = self.context_stack.pop() else {
 | 
			
		||||
                    panic!("RETI when halted");
 | 
			
		||||
                    bail!("RETI when halted");
 | 
			
		||||
                };
 | 
			
		||||
                if code == RESET_CODE {
 | 
			
		||||
                    panic!("RETI when not in interrupt");
 | 
			
		||||
                    bail!("RETI when not in interrupt");
 | 
			
		||||
                }
 | 
			
		||||
                if self.call_stacks.remove(&code).is_none() {
 | 
			
		||||
                    panic!("{code:04x} popped but never called");
 | 
			
		||||
                    bail!("{code:04x} popped but never called");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,14 +1,19 @@
 | 
			
		|||
use egui::{CentralPanel, ViewportBuilder, ViewportId};
 | 
			
		||||
use std::{fs, time::Duration};
 | 
			
		||||
 | 
			
		||||
use anyhow::Result;
 | 
			
		||||
use egui::{Button, CentralPanel, Checkbox, ViewportBuilder, ViewportId};
 | 
			
		||||
use egui_notify::{Anchor, Toast, Toasts};
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    emulator::{EmulatorClient, SimId},
 | 
			
		||||
    profiler::Profiler,
 | 
			
		||||
    profiler::{Profiler, ProfilerStatus},
 | 
			
		||||
    window::AppWindow,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
pub struct ProfileWindow {
 | 
			
		||||
    sim_id: SimId,
 | 
			
		||||
    profiler: Profiler,
 | 
			
		||||
    toasts: Toasts,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ProfileWindow {
 | 
			
		||||
| 
						 | 
				
			
			@ -16,11 +21,47 @@ impl ProfileWindow {
 | 
			
		|||
        Self {
 | 
			
		||||
            sim_id,
 | 
			
		||||
            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.start();
 | 
			
		||||
        self.profiler.enable();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn finish_recording(&mut self) {
 | 
			
		||||
        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);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn try_finish_recording(&mut self) -> Result<Option<String>> {
 | 
			
		||||
        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)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -40,15 +81,46 @@ impl AppWindow for ProfileWindow {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    fn show(&mut self, ctx: &egui::Context) {
 | 
			
		||||
        let status = self.profiler.status();
 | 
			
		||||
        let recording = matches!(status, ProfilerStatus::Recording);
 | 
			
		||||
        CentralPanel::default().show(ctx, |ui| {
 | 
			
		||||
            let mut started = self.profiler.started();
 | 
			
		||||
            if ui.checkbox(&mut started, "Profiling enabled?").changed() {
 | 
			
		||||
                if started {
 | 
			
		||||
                    self.profiler.start();
 | 
			
		||||
            let mut enabled = status.enabled();
 | 
			
		||||
            let enabled_checkbox = Checkbox::new(&mut enabled, "Profiling enabled?");
 | 
			
		||||
            if ui.add_enabled(!recording, enabled_checkbox).changed() {
 | 
			
		||||
                if enabled {
 | 
			
		||||
                    self.profiler.enable();
 | 
			
		||||
                } else {
 | 
			
		||||
                    self.profiler.stop();
 | 
			
		||||
                    self.profiler.disable();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue