tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

mod.rs (20851B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 mod debug_flags;
      6 mod profiler;
      7 mod shell;
      8 mod textures;
      9 mod composite_view;
     10 mod draw_calls;
     11 mod timeline;
     12 
     13 use eframe::egui;
     14 use webrender_api::{DebugFlags, RenderCommandInfo};
     15 use webrender_api::debugger::{DebuggerMessage, DebuggerTextureContent, ProfileCounterId, CompositorDebugInfo};
     16 use crate::{command, net};
     17 use std::collections::{HashMap, BTreeMap, VecDeque};
     18 use std::fs;
     19 use std::io::Write;
     20 use std::sync::mpsc;
     21 
     22 use profiler::Graph;
     23 
     24 #[allow(dead_code)]
     25 enum ApplicationEvent {
     26    RunCommand(String),
     27    NetworkEvent(net::NetworkEvent),
     28 }
     29 
     30 struct LoggedFrame {
     31    pub render_commands: Option<Vec<RenderCommandInfo>>,
     32 }
     33 
     34 struct FrameLog {
     35    pub frames: VecDeque<LoggedFrame>,
     36    pub enabled: bool,
     37    pub frames_end: usize,
     38 }
     39 
     40 impl FrameLog {
     41    pub fn new() -> Self {
     42        FrameLog {
     43            frames: VecDeque::with_capacity(100),
     44            enabled: false,
     45            frames_end: 0,
     46        }
     47    }
     48 
     49    pub fn first_frame_index(&self) -> usize {
     50        self.frames_end - self.frames.len()
     51    }
     52 
     53    pub fn last_frame_index(&self) -> usize {
     54        self.frames_end.max(1) - 1
     55    }
     56 
     57    pub fn frame(&self, idx: usize) -> Option<&LoggedFrame> {
     58        let i = idx - self.first_frame_index();
     59        if i < self.frames.len() {
     60            return Some(&self.frames[i]);
     61        }
     62 
     63        None
     64    }
     65 }
     66 
     67 struct DataModel {
     68    is_connected: bool,
     69    debug_flags: DebugFlags,
     70    cmd: String,
     71    log: Vec<String>,
     72    documents: Vec<Document>,
     73    preview_doc_index: Option<usize>,
     74    profile_graphs: HashMap<ProfileCounterId, Graph>,
     75    frame_log: FrameLog,
     76    timeline: timeline::Timeline,
     77 }
     78 
     79 impl DataModel {
     80    fn new() -> Self {
     81        DataModel {
     82            is_connected: false,
     83            debug_flags: DebugFlags::empty(),
     84            cmd: String::new(),
     85            log: Vec::new(),
     86            documents: Vec::new(),
     87            preview_doc_index: None,
     88            profile_graphs: HashMap::new(),
     89            frame_log: FrameLog::new(),
     90            timeline: timeline::Timeline::new(),
     91        }
     92    }
     93 }
     94 
     95 #[derive(serde::Serialize, serde::Deserialize)]
     96 pub enum Tool {
     97    DebugFlags,
     98    Profiler,
     99    Shell,
    100    Documents,
    101    Preview,
    102    DrawCalls,
    103    Timeline,
    104 }
    105 
    106 impl egui_tiles::Behavior<Tool> for Gui {
    107    fn tab_title_for_pane(&mut self, tool: &Tool) -> egui::WidgetText {
    108        let title = match tool {
    109            Tool::DebugFlags => { "Debug flags" }
    110            Tool::Profiler => { "Profiler" }
    111            Tool::Shell => { "Shell" }
    112            Tool::Documents => { "Documents" }
    113            Tool::Preview => { "Preview" }
    114            Tool::DrawCalls => { "Draw calls" }
    115            Tool::Timeline => { "Timeline" }
    116        };
    117 
    118        title.into()
    119    }
    120 
    121    fn pane_ui(&mut self, ui: &mut egui::Ui, _id: egui_tiles::TileId, tool: &mut Tool) -> egui_tiles::UiResponse {
    122        // Add a bit of margin around the panes, otherwise their content hugs the
    123        // border in an awkward way. There may be a setting somewhere in egui_tiles
    124        // rather than doing it manually.
    125        egui::Frame::new().inner_margin(egui::Margin::symmetric(5, 5)).show(ui, |ui| {
    126            match tool {
    127                Tool::Documents => { do_documents_ui(self, ui); }
    128                Tool::Preview => { do_preview_ui(self, ui); }
    129                Tool::DebugFlags => { debug_flags::ui(self, ui); }
    130                Tool::Profiler => { profiler::ui(self, ui); }
    131                Tool::Shell => { shell::ui(self, ui); }
    132                Tool::DrawCalls => { draw_calls::ui(self, ui); }
    133                Tool::Timeline => { timeline::ui(self, ui); }
    134            }
    135        });
    136 
    137        Default::default()
    138    }
    139 
    140    fn simplification_options(&self) -> egui_tiles::SimplificationOptions {
    141        let mut options = egui_tiles::SimplificationOptions::default();
    142        options.all_panes_must_have_tabs = true;
    143        options
    144    }
    145 
    146    fn tab_title_spacing(&self, _visuals: &egui::Visuals) -> f32 {
    147        50.0
    148    }
    149 
    150    fn gap_width(&self, _style: &egui::Style) -> f32 {
    151        2.0
    152    }
    153 
    154    fn tab_outline_stroke(
    155        &self,
    156        _visuals: &egui::Visuals,
    157        _tiles: &egui_tiles::Tiles<Tool>,
    158        _tile_id: egui_tiles::TileId,
    159        _state: &egui_tiles::TabState,
    160    ) -> egui::Stroke {
    161        egui::Stroke::NONE
    162    }
    163 }
    164 
    165 pub struct Gui {
    166    data_model: DataModel,
    167    net: net::HttpConnection,
    168    cmd_list: command::CommandList,
    169    cmd_history: Vec<String>,
    170    cmd_history_index: usize,
    171    doc_id: usize,
    172    event_receiver: mpsc::Receiver<ApplicationEvent>,
    173    ui_tiles: Option<egui_tiles::Tree<Tool>>,
    174 }
    175 
    176 impl Gui {
    177    pub fn new(host: &str, cmd_list: command::CommandList) -> Self {
    178        let net = net::HttpConnection::new(host);
    179        let data_model = DataModel::new();
    180 
    181        let (event_sender, event_receiver) = mpsc::channel();
    182 
    183        // Spawn network event thread
    184        let host_clone = host.to_string();
    185        std::thread::spawn(move || {
    186            net::NetworkEventStream::spawn(&host_clone, move |event| {
    187                let _ = event_sender.send(ApplicationEvent::NetworkEvent(event));
    188            });
    189        });
    190 
    191        // Try to load saved ui_tiles state, or create default layout
    192        let save = fs::read_to_string(config_path())
    193            .ok()
    194            .and_then(|content| {
    195                serde_json::from_str::<GuiSavedState>(&content)
    196                    .map_err(|e| {
    197                        eprintln!("Failed to deserialize ui_tiles state: {}", e);
    198                        e
    199                    })
    200                    .ok()
    201            }).and_then(|save| {
    202                if save.version == GuiSavedState::VERSION {
    203                    Some(save)
    204                } else {
    205                    None
    206                }
    207            }).unwrap_or_else(|| {
    208                // Create default layout
    209                let mut tiles = egui_tiles::Tiles::default();
    210                let tabs = vec![
    211                    tiles.insert_pane(Tool::Profiler),
    212                    tiles.insert_pane(Tool::Preview),
    213                    tiles.insert_pane(Tool::DrawCalls),
    214                ];
    215                let side = vec![
    216                    tiles.insert_pane(Tool::DebugFlags),
    217                    tiles.insert_pane(Tool::Documents),
    218                ];
    219                let side = tiles.insert_vertical_tile(side);
    220                let main_tile = tiles.insert_tab_tile(tabs);
    221                let main_and_side = vec![
    222                    side,
    223                    main_tile,
    224                ];
    225                let shell_and_timeline = vec![
    226                    tiles.insert_pane(Tool::Shell),
    227                    tiles.insert_pane(Tool::Timeline),
    228                ];
    229                let shell_and_timeline = tiles.insert_tab_tile(shell_and_timeline);
    230                let main_and_side = tiles.insert_horizontal_tile(main_and_side);
    231                let root = tiles.insert_vertical_tile(vec![
    232                    main_and_side,
    233                    shell_and_timeline,
    234                ]);
    235 
    236                GuiSavedState {
    237                    version: GuiSavedState::VERSION,
    238                    cmd_history: Vec::new(),
    239                    ui_tiles: egui_tiles::Tree::new("WR debugger", root, tiles),
    240                }
    241            });
    242 
    243        Gui {
    244            data_model,
    245            net,
    246            cmd_list,
    247            cmd_history: save.cmd_history,
    248            cmd_history_index: 0,
    249            doc_id: 0,
    250            event_receiver,
    251            ui_tiles: Some(save.ui_tiles),
    252        }
    253    }
    254 
    255    pub fn run(self) {
    256        let native_options = eframe::NativeOptions {
    257            viewport: egui::ViewportBuilder::default()
    258                .with_inner_size([1280.0, 720.0])
    259                .with_title("WebRender Debug UI"),
    260            ..Default::default()
    261        };
    262 
    263        let _ = eframe::run_native(
    264            "WebRender Debug UI",
    265            native_options,
    266            Box::new(|cc| {
    267                // Load fonts
    268                let mut fonts = egui::FontDefinitions::default();
    269 
    270                fonts.font_data.insert(
    271                    "FiraSans".to_owned(),
    272                    egui::FontData::from_static(include_bytes!("../../res/FiraSans-Regular.ttf")).into(),
    273                );
    274                fonts.font_data.insert(
    275                    "FiraCode".to_owned(),
    276                    egui::FontData::from_static(include_bytes!("../../res/FiraCode-Regular.ttf")).into(),
    277                );
    278 
    279                fonts.families.get_mut(&egui::FontFamily::Proportional).unwrap()
    280                    .insert(0, "FiraSans".to_owned());
    281                fonts.families.get_mut(&egui::FontFamily::Monospace).unwrap()
    282                    .insert(0, "FiraCode".to_owned());
    283 
    284                cc.egui_ctx.set_fonts(fonts);
    285 
    286                // Load layout settings if available
    287                if let Ok(_ini) = fs::read_to_string("default-layout.ini") {
    288                    // egui uses a different state persistence mechanism
    289                    // This would need to be adapted for egui's state system
    290                }
    291 
    292                Ok(Box::new(self))
    293            }),
    294        );
    295    }
    296 }
    297 
    298 impl eframe::App for Gui {
    299    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
    300        // Process pending events
    301        while let Ok(event) = self.event_receiver.try_recv() {
    302            match event {
    303                ApplicationEvent::RunCommand(cmd_name) => {
    304                    self.handle_command(&cmd_name);
    305                }
    306                ApplicationEvent::NetworkEvent(net_event) => {
    307                    self.handle_network_event(net_event);
    308                }
    309            }
    310        }
    311 
    312        textures::prepare(self, ctx);
    313 
    314        // Main menu bar
    315        egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
    316            egui::MenuBar::new().ui(ui, |ui| {
    317                ui.menu_button("File", |ui| {
    318                    if ui.button("Exit").clicked() {
    319                        ctx.send_viewport_cmd(egui::ViewportCommand::Close);
    320                    }
    321                });
    322                ui.menu_button("Help", |ui| {
    323                    ui.label("About");
    324                });
    325 
    326                // Connection status on the right
    327                ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
    328                    let (msg, text_color, bg_color) = if self.data_model.is_connected {
    329                        ("Connected", egui::Color32::from_rgb(48, 48, 48), egui::Color32::from_rgb(0, 255, 0))
    330                    } else {
    331                        ("Disconnected", egui::Color32::WHITE, egui::Color32::from_rgb(255, 0, 0))
    332                    };
    333 
    334                    // TODO: measure the text instead.
    335                    let mut area = ui.max_rect();
    336                    area.min.x = area.max.x - 85.0;
    337                    area.max.x += 10.0;
    338 
    339                    ui.painter().rect_filled(area, 0, bg_color);
    340 
    341                    let label = egui::Label::new(
    342                        egui::RichText::new(msg).color(text_color)
    343                    );
    344                    ui.add(label).on_hover_text("Connection status");
    345                });
    346            });
    347        });
    348 
    349        let mut ui_tiles = self.ui_tiles.take().unwrap();
    350        egui::CentralPanel::default().show(ctx, |ui| {
    351            ui.spacing_mut().window_margin = egui::Margin::ZERO;
    352            ui_tiles.ui(self, ui);
    353        });
    354        self.ui_tiles = Some(ui_tiles);
    355    }
    356 }
    357 
    358 impl Gui {
    359    fn handle_command(&mut self, cmd_name: &str) {
    360        match self.cmd_list.get_mut(cmd_name) {
    361            Some(cmd) => {
    362                let mut ctx = command::CommandContext::new(
    363                    BTreeMap::new(),
    364                    &mut self.net,
    365                );
    366                let output = cmd.run(&mut ctx);
    367 
    368                match output {
    369                    command::CommandOutput::Log(msg) => {
    370                        self.data_model.log.push(msg);
    371                    }
    372                    command::CommandOutput::Err(msg) => {
    373                        self.data_model.log.push(msg);
    374                    }
    375                    command::CommandOutput::TextDocument { title, content } => {
    376                        let title = format!("{} [id {}]", title, self.doc_id);
    377                        self.doc_id += 1;
    378                        self.data_model.preview_doc_index = Some(self.data_model.documents.len());
    379                        self.data_model.documents.push(
    380                            Document {
    381                                title,
    382                                kind: DocumentKind::Text {
    383                                    content,
    384                                }
    385                            }
    386                        );
    387                    }
    388                    command::CommandOutput::SerdeDocument { kind, ref content } => {
    389                        let title = format!("Compositor [id {}]", self.doc_id);
    390                        self.doc_id += 1;
    391                        self.data_model.preview_doc_index = Some(self.data_model.documents.len());
    392 
    393                        let kind = match kind.as_str() {
    394                            "composite-view" => {
    395                                let info = serde_json::from_str(content).unwrap();
    396                                DocumentKind::Compositor {
    397                                    info,
    398                                }
    399                            }
    400                            _ => {
    401                                unreachable!("unknown content");
    402                            }
    403                        };
    404 
    405                        self.data_model.documents.push(
    406                            Document {
    407                                title,
    408                                kind,
    409                            }
    410                        );
    411                    }
    412                    command::CommandOutput::Textures(textures) => {
    413                        textures::add_textures(self, textures);
    414                    }
    415                }
    416            }
    417            None => {
    418                self.data_model.log.push(
    419                    format!("Unknown command '{}'", cmd_name)
    420                );
    421            }
    422        }
    423    }
    424 
    425    fn handle_network_event(&mut self, event: net::NetworkEvent) {
    426        match event {
    427            net::NetworkEvent::Connected => {
    428                self.data_model.is_connected = true;
    429            }
    430            net::NetworkEvent::Disconnected => {
    431                self.data_model.is_connected = false;
    432            }
    433            net::NetworkEvent::Message(msg) => {
    434                match msg {
    435                    DebuggerMessage::SetDebugFlags(info) => {
    436                        self.data_model.debug_flags = info.flags;
    437                    }
    438                    DebuggerMessage::InitProfileCounters(info) => {
    439                        let selected_counters = [
    440                            "Frame building",
    441                            "Renderer",
    442                        ];
    443 
    444                        for counter in info.counters {
    445                            if selected_counters.contains(&counter.name.as_str()) {
    446                                println!("Add profile counter {:?}", counter.name);
    447                                self.data_model.profile_graphs.insert(
    448                                    counter.id,
    449                                    Graph::new(&counter.name, 512),
    450                                );
    451                            }
    452                        }
    453                    }
    454                    DebuggerMessage::UpdateFrameLog(info) => {
    455                        if let Some(updates) = info.profile_counters {
    456                            for counter in &updates {
    457                                if let Some(graph) = self.data_model
    458                                    .profile_graphs
    459                                    .get_mut(&counter.id) {
    460                                    graph.push(counter.value);
    461                                }
    462                            }
    463                        }
    464                        let frame_log = &mut self.data_model.frame_log;
    465                        if frame_log.frames.len() == frame_log.frames.capacity() {
    466                            frame_log.frames.pop_front();
    467                        }
    468                        frame_log.frames.push_back(LoggedFrame {
    469                            render_commands: info.render_commands.clone(),
    470                        });
    471                        frame_log.frames_end += 1;
    472                        let first = frame_log.first_frame_index();
    473                        let last = frame_log.last_frame_index();
    474 
    475                        if self.data_model.timeline.current_frame < first  {
    476                            self.data_model.timeline.current_frame = first;
    477                        }
    478 
    479                        if self.data_model.timeline.current_frame >= last - 1  {
    480                            self.data_model.timeline.current_frame = last;
    481                        }
    482                    }
    483                }
    484            }
    485        }
    486    }
    487 }
    488 
    489 impl Drop for Gui {
    490    fn drop(&mut self) {
    491        // Serialize and save UI state
    492        const MAX_HISTORY_SAVED: usize = 50;
    493        let hist_len = self.cmd_history.len();
    494        let range = if hist_len > MAX_HISTORY_SAVED {
    495            hist_len - MAX_HISTORY_SAVED..hist_len
    496        } else {
    497            0..hist_len
    498        };
    499 
    500        let save = GuiSavedState {
    501            version: GuiSavedState::VERSION,
    502            ui_tiles: self.ui_tiles.take().unwrap(),
    503            cmd_history: self.cmd_history.drain(range).collect(),
    504        };
    505 
    506        match serde_json::to_string(&save) {
    507            Ok(serialized) => {
    508                if let Err(e) = fs::File::create(config_path())
    509                    .and_then(|mut file| file.write_all(serialized.as_bytes()))
    510                {
    511                    eprintln!("Failed to save ui_tiles state: {}", e);
    512                }
    513            }
    514            Err(e) => {
    515                eprintln!("Failed to serialize ui_tiles: {}", e);
    516            }
    517        }
    518    }
    519 }
    520 
    521 pub enum DocumentKind {
    522    Text {
    523        content: String,
    524    },
    525    Compositor {
    526        info: CompositorDebugInfo,
    527    },
    528    Texture {
    529        content: DebuggerTextureContent,
    530        handle: Option<egui::TextureHandle>,
    531    }
    532 }
    533 
    534 pub struct Document {
    535    pub title: String,
    536    pub kind: DocumentKind,
    537 }
    538 
    539 fn do_documents_ui(app: &mut Gui, ui: &mut egui::Ui) {
    540    let width = ui.available_width();
    541    for (i, doc) in app.data_model.documents.iter().enumerate() {
    542        if let DocumentKind::Texture { .. } = doc.kind {
    543            // Handle textures separately below.
    544            continue;
    545        }
    546 
    547        let item = egui::Button::selectable(
    548            app.data_model.preview_doc_index == Some(i),
    549            &doc.title,
    550        ).min_size(egui::vec2(width, 20.0));
    551 
    552        if ui.add(item).clicked() {
    553            app.data_model.preview_doc_index = Some(i);
    554        }
    555    }
    556 
    557    textures::texture_list_ui(app, ui);
    558 }
    559 
    560 fn do_preview_ui(app: &mut Gui, ui: &mut egui::Ui) {
    561    if let Some(idx) = app.data_model.preview_doc_index {
    562        if idx >= app.data_model.documents.len() {
    563            app.data_model.preview_doc_index = None;
    564            return;
    565        }
    566 
    567        let doc = &app.data_model.documents[idx];
    568 
    569        match &doc.kind {
    570            DocumentKind::Text { content } => {
    571                egui::ScrollArea::both().show(ui, |ui| {
    572                    ui.label(egui::RichText::new(content).monospace());
    573                });
    574            }
    575            DocumentKind::Compositor { .. } => {
    576                // We need to handle compositor separately due to borrow checker
    577                if let Some(Document { kind: DocumentKind::Compositor { info }, .. }) =
    578                    app.data_model.documents.get_mut(idx) {
    579                    composite_view::ui(ui, info);
    580                }
    581            }
    582            DocumentKind::Texture { content, handle } => {
    583                if let Some(handle) = handle {
    584                    textures::texture_viewer_ui(ui, &content, &handle);
    585                }
    586            }
    587        }
    588    }
    589 }
    590 
    591 
    592 // TODO: It would be better to use confy::load/store instead of loading
    593 // files manually but for some reason serialization of the ui tiles panics
    594 // in confy.
    595 fn config_path() -> std::path::PathBuf {
    596    confy::get_configuration_file_path("wr_debugger", None).unwrap()
    597 }
    598 
    599 #[derive(serde::Serialize, serde::Deserialize)]
    600 struct GuiSavedState {
    601    version: u32,
    602    cmd_history: Vec<String>,
    603    ui_tiles: egui_tiles::Tree<Tool>,
    604 }
    605 
    606 impl GuiSavedState {
    607    /// Update this number to reset the configuration. This ensures that new
    608    /// panels are added.
    609    const VERSION: u32 = 2;
    610 }