代码之家  ›  专栏  ›  技术社区  ›  alexander

使用egui、tokio和std::sync::mpsc的无限循环和窗口冻结

  •  0
  • alexander  · 技术社区  · 1 年前

    在通过通道传递消息时,我尝试迭代收到的响应,但不幸的是,我的窗口冻结,当尝试打印调试时 table 对象无限打印。

    事实证明,我挽救局势的努力是徒劳的。

    在sql查询哪个目标是加载表列表及其关系之后,我将其分配给 Diagram.tables 并通过 message channel

    尝试接收频道后 self.handle_responses(); 并尝试通过 tokio runtime,

    我终于迭代了一遍 list of tables

    并且将该列表添加到可以被渲染的正方形向量。

    不幸的是,表的列表一直在循环,甚至挂起了窗口。

    我以为表向量会迭代一次,我可以利用它,但没有成功。

    这是整个代码 //app.rs修改自 eframe template

    use std::sync::mpsc::{self, Receiver, Sender};
    
    use crate::meta::{get_metadata, Table};
    use egui::{Color32, FontId, Sense, Vec2};
    use emath::{Align2, Pos2};
    use std::fmt;
    use tokio::runtime;
    use tokio_postgres::NoTls;
    
    pub const INIT_POS: Pos2 = egui::pos2(10.0, 15.0);
    pub const ATTR_SIZE: Vec2 = egui::vec2(150.0, 25.0);
    
    pub enum TaskMessage {
        //Applicaple to any scenario, behaves almost like a callback
        Generic(Box<dyn FnOnce(&mut Diagram) + Send>),
    }
    
    
    
    #[derive(serde::Deserialize, serde::Serialize)]
    #[serde(default = "Diagram::new")]
    pub struct Diagram {
        pub shapes: Vec<Square>,
        pub tables: Vec<Table>,
        canvas_size: Vec2,
        #[serde(skip)]
        task_reciever: Receiver<TaskMessage>,
    spawing them
    
        #[serde(skip)]
        _task_sender: Sender<TaskMessage>,
    }
    
    impl Diagram {
        fn new() -> Self {
            let (_task_sender, task_reciever) = mpsc::channel::<TaskMessage>();
            Self {
                shapes: vec![],
                tables: vec![],
                canvas_size: Vec2::splat(400.0),
                task_reciever,
                _task_sender,
            }
        }
    
        pub fn handle_responses(&mut self) {
            while let Ok(response) = self.task_reciever.try_recv() {
                match response {
                    TaskMessage::Generic(gen_function) => {
                        gen_function(self);
                    }
                }
            }
        }
    
    
    }
    
    impl egui::Widget for &mut Diagram {
        fn ui(self, ui: &mut egui::Ui) -> egui::Response {
            egui::Frame::canvas(ui.style())
                .show(ui, |ui| {
                    egui::ScrollArea::new([true; 2]).show(ui, |ui| {
                        let sender = self._task_sender.clone();
                        let other_ctx = ui.ctx().clone();
    
                        self.handle_responses();
                        tokio::task::spawn(async {
                            let schema = "public".to_string();
    
                            get_metadata(schema, other_ctx, sender).await
                        });
    
                        eprintln!("{:#?}", self.tables);
                        for table in &self.tables {
                            if let Some(table_name) =
                                table.table.get("table_name").and_then(|v| v.as_str())
                            {
                                // println!("{:#?}", table_name);
                                let square = Square::new(table_name.to_string());
                                self.shapes.push(square);
                            }
                        }
    
                        for shape in self.shapes.iter_mut() {
                            shape.render(ui);
                        }
                        ui.allocate_at_least(self.canvas_size, Sense::hover());
                    });
                    // ui.ctx().set_debug_on_hover(true);
                })
                .response
        }
    }
    
    #[derive(serde::Deserialize, serde::Serialize, Debug)]
    pub struct Square {
        position: egui::Pos2,
        dimension: egui::Vec2,
        label: String,
    }
    
    impl Square {
        fn new(label: String) -> Self {
            Self {
                position: egui::pos2(INIT_POS.x, INIT_POS.y),
                dimension: egui::vec2(ATTR_SIZE.x, ATTR_SIZE.y),
                attributes: vec![InnerSquare::new()],
                label,
            }
        }
        fn render(&mut self, ui: &mut egui::Ui) {
            let square_body = egui::Rect::from_min_size(self.position, self.dimension);
            //"finalized rect" which is offset properly
            let transformed_rect = {
                let resp = ui.allocate_rect(ui.available_rect_before_wrap(), egui::Sense::click());
                let relative_to_screen = egui::emath::RectTransform::from_to(
                    egui::Rect::from_min_size(Pos2::ZERO, resp.rect.size()),
                    resp.rect,
                );
                relative_to_screen.transform_rect(square_body)
            };
    
    
            let frame = {
                let rounding_radius = 2.0;
                let fill = egui::Color32::LIGHT_GREEN;
                let stroke = egui::epaint::Stroke::new(2.0, Color32::DARK_BLUE);
                egui::Frame::none()
                    .rounding(rounding_radius)
                    .fill(fill)
                    .stroke(stroke)
                    .inner_margin(10.0)
            };
            //Creates a new ui where our square is supposed to appear
            ui.allocate_ui_at_rect(transformed_rect, |ui| {
                frame.show(ui, |ui| {
                    //draw each attribute
                    ui.label(
                        egui::RichText::new(&self.label)
                            .heading()
                            .color(egui::Color32::BLACK),
                    );
                    for inner_square in self.attributes.iter_mut() {
                        inner_square.render(ui);
                    }
                });
            });
        }
    }
    
    #[derive(serde::Deserialize, serde::Serialize)]
    #[serde(default)] // if we add new fields, give them default values when deserializing old state
    pub struct TemplateApp {
        diagram: Diagram,
        label: String,
        #[serde(skip)]
        value: f32,
        //this is what the ui thread will just to catch returns of tasks, in this case it's the std
        //mpsc channels, but any channel which has smiliar behaviour works
    }
    
    impl Default for TemplateApp {
        fn default() -> Self {
            Self {
                // Example stuff:
                label: "Hello World!".to_owned(),
                value: 2.7,
                diagram: Diagram::new(),
            }
        }
    }
    
    impl TemplateApp {
        /// Called once before the first frame.
        pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
            Default::default()
        }
    }
    
    impl eframe::App for TemplateApp {
        /// Called by the frame work to save state before shutdown.
        fn save(&mut self, storage: &mut dyn eframe::Storage) {
            eframe::set_value(storage, eframe::APP_KEY, self);
        }
    
        /// Called each time the UI needs repainting, which may be many times per second.
        /// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
        fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
            let Self {
                label,
                diagram,
                value,
            } = self;
    
            // #[cfg(not(target_arch = "wasm32"))] // no File->Quit on web pages!
            egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
                // The top panel is often a good place for a menu bar:
                egui::menu::bar(ui, |ui| {
                    ui.menu_button("File", |ui| {
                        if ui.button("Quit").clicked() {
                            _frame.close();
                        }
                    });
                });
            });
    
    
            egui::CentralPanel::default().show(ctx, |ui| {
                ui.add(&mut self.diagram) // The central panel the region left after adding TopPanel's and SidePanel's
            });
    
            if false {
                egui::Window::new("Window").show(ctx, |ui| {
                    ui.label("Panel Area");
                });
            }
        }
    }
    

    以及负责数据库查询的代码 tokio-postgre

    //图元.rs

    use serde::{Deserialize, Serialize};
    use serde_json::Value;
    use std::error::Error;
    use std::fmt;
    use std::ptr::null;
    use std::sync::mpsc::Sender;
    use std::{thread, time};
    use tokio::runtime::Runtime;
    use tokio_postgres::tls::NoTlsStream;
    use tokio_postgres::{Client, Connection, Error as TokioError, NoTls, Row, Socket};
    
    // Define a custom error enum
    #[derive(Debug)]
    pub enum MetadataError {
        JsonResponseNotFound,
        RowNotFound,
        // PostgresError(PostgresError),
        TokioError(TokioError),
    }
    
    impl fmt::Display for MetadataError {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            match *self {
                MetadataError::JsonResponseNotFound => write!(f, "No JSON response found"),
                MetadataError::RowNotFound => write!(f, "No row found in the result"),
                // MetadataError::PostgresError(ref err) => err.fmt(f),
                MetadataError::TokioError(ref err) => err.fmt(f),
            }
        }
    }
    
    impl From<tokio_postgres::Error> for MetadataError {
        fn from(error: tokio_postgres::Error) -> MetadataError {
            MetadataError::TokioError(error)
        }
    }
    
    impl Error for MetadataError {
        fn source(&self) -> Option<&(dyn Error + 'static)> {
            match *self {
                MetadataError::JsonResponseNotFound | MetadataError::RowNotFound => None,
                // MetadataError::PostgresError(ref err) => Some(err),
                MetadataError::TokioError(ref err) => Some(err),
            }
        }
    }
    
    #[derive(Debug, Serialize, Deserialize)]
    pub struct Table {
        pub table: serde_json::Value,
    }
    
    impl From<Row> for Table {
        fn from(row: Row) -> Self {
            Self { table: row.get(0) }
        }
    }
    
    pub async fn get_metadata(
        schema: String,
        ctx: egui::Context,
        sender: Sender<crate::app::TaskMessage>,
    ) -> Result<(), MetadataError> {
        let qry = r#"
            select row_to_json(Tb) 
        --- table listing query
        "#;
        let (client, connection) = tokio_postgres::connect(
            "postgresql://postgres:chou1979@localhost/authenticate",
            NoTls,
        )
        .await
        .unwrap();
        // The connection object performs the actual communication with the database,
        // so spawn it off to run on its own.
        let conn = tokio::spawn(async move {
            if let Err(e) = connection.await {
                eprintln!("connection error: {}", e);
            }
        });
    
        let stmt = client.prepare(qry).await?;
        let rows = client.query(&stmt, &[&schema]).await?;
    
        let finalized: Vec<Table> = rows.into_iter().map(Table::from).collect();
        conn.abort();
        // println!("{:#?}", finalized);
    
        let gen_function = move |diagram: &mut crate::app::Diagram| {
            diagram.tables = finalized
        };
        // sender.send(Box::new(gen_function)).unwrap();
        sender
            .send(crate::app::TaskMessage::Generic(Box::new(gen_function)))
            .unwrap();
        ctx.request_repaint();
    
        Ok(())
    }
    
    0 回复  |  直到 1 年前
        1
  •  0
  •   Max Meijer    1 年前

    egui 是一个即时模式GUI库,这意味着它将在每一帧重新发送所有内容。他们解释了这是如何工作的 here 。他们特别指出:

    由于即时模式GUI对每帧进行完整布局,因此布局代码需要快速。如果你有一个非常复杂的GUI,这可能会对CPU征税。特别是,在滚动区域中有一个非常大的UI(具有非常长的滚动)可能会很慢,因为内容需要在每一帧中布局。

    如果表的列表特别长,这肯定是一个问题,因此值得考虑的是,您是否想要使用即时模式GUI库。从他们 docs :

    即时模式起源于游戏,屏幕上的所有内容都以显示器刷新率绘制,即每秒60帧以上。在即时模式GUI中,整个界面都以同样高的速率进行布局和绘制。这使得即时模式GUI特别适合高度交互式的应用程序。[…]缺点是:即时模式GUI库更容易使用,但功能较弱。

    尽管如此,您当前面临的问题不一定是必须显示所有表,而是通过调用 get_metadata 以及执行数据库查询。这太过分了,我想知道您是否需要立即刷新元数据。即使您希望不断刷新元数据,也可能希望通过重用相同的数据库连接来实现,而不是每次都创建一个新的连接。

    简而言之,您可能只需要在启动时运行get_metadata函数。或者,您可能希望以不同于刷新率的预定间隔运行它。例如,您可以在一个单独的线程上运行它,如下所示:

    let qry = r#"
            select row_to_json(Tb) 
        --- table listing query
        "#;
        let (client, connection) = tokio_postgres::connect(
            "postgresql://postgres:chou1979@localhost/authenticate",
            NoTls,
        )
        .await
        .unwrap();
        // The connection object performs the actual communication with the database,
        // so spawn it off to run on its own.
        let conn = tokio::spawn(async move {
            if let Err(e) = connection.await {
                eprintln!("connection error: {}", e);
            }
        });
        
        let interval = Duration::from_secs(1);
        let mut next_time = Instant::now() + interval;
        loop {
          let stmt = client.prepare(qry).await?;
          let rows = client.query(&stmt, &[&schema]).await?;
    
          let finalized: Vec<Table> = rows.into_iter().map(Table::from).collect();
          conn.abort();
          // println!("{:#?}", finalized);
    
          let gen_function = move |diagram: &mut crate::app::Diagram| {
              diagram.tables = finalized
          };
          // sender.send(Box::new(gen_function)).unwrap();
          sender
              .send(crate::app::TaskMessage::Generic(Box::new(gen_function)))
              .unwrap();
          ctx.request_repaint();
    
          sleep(next_time - Instant::now());
          next_time += interval;
        }
        Ok(())