Implement debug crash handler dialog (#139)

This commit is contained in:
Samuel 2024-04-25 16:55:44 -03:00 committed by GitHub
parent 4b49046999
commit 9672858b48
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 459 additions and 57 deletions

View File

@ -14,6 +14,7 @@
- Adds `zng::app::crash_handler`.
- Can be used to easily implement crash reporting, stacktrace and minidump
collection, app restart on crash.
- Call `zng::app::crash_handler::init_debug()` to quickly setup panic and minidump collection.
* Fix view-process kill by user not working after respawn.
* Fix view-process assuming any signal kill was requested by the user.
* Fix potential issue retrieving current_exe trough symbolic links.

View File

@ -9,7 +9,7 @@ use std::{
fmt,
io::{BufRead, BufReader},
path::{Path, PathBuf},
time::{Duration, SystemTime},
time::SystemTime,
};
use zng_layout::unit::TimeUnits as _;
@ -184,18 +184,12 @@ pub struct CrashError {
/// Minidump file.
pub minidump: Option<PathBuf>,
}
/// Alternate mode `{:#}` prints plain stdout and stderr (no ANSI escape sequences).
impl fmt::Display for CrashError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"timestamp: {}",
self.timestamp
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_secs()
)?;
writeln!(f, "timestamp: {}", self.unix_time())?;
if let Some(c) = self.code {
writeln!(f, "exit code: {c:#x}")?
writeln!(f, "exit code: {c:#X}")?
}
if let Some(c) = self.signal {
writeln!(f, "exit signal: {c}")?
@ -203,7 +197,11 @@ impl fmt::Display for CrashError {
if let Some(p) = self.minidump.as_ref() {
writeln!(f, "minidump: {}", p.display())?
}
write!(f, "\nSTDOUT:\n{}\nSTDERR:\n{}\n", self.stdout, self.stderr)
if f.alternate() {
write!(f, "\nSTDOUT:\n{}\nSTDERR:\n{}\n", self.stdout_plain(), self.stderr_plain())
} else {
write!(f, "\nSTDOUT:\n{}\nSTDERR:\n{}\n", self.stdout, self.stderr)
}
}
}
impl CrashError {
@ -233,13 +231,56 @@ impl CrashError {
}
}
/// Seconds since Unix epoch.
pub fn unix_time(&self) -> u64 {
self.timestamp.duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs()
}
/// Gets if `stdout` does not contain any ANSI scape sequences.
pub fn is_stdout_plain(&self) -> bool {
!self.stdout.contains(CSI)
}
/// Gets if `stderr` does not contain any ANSI scape sequences.
pub fn is_stderr_plain(&self) -> bool {
!self.stderr.contains(CSI)
}
/// Get `stdout` without any ANSI escape sequences (CSI).
pub fn stdout_plain(&self) -> Txt {
remove_ansi_csi(&self.stdout)
}
/// Get `stderr` without any ANSI escape sequences (CSI).
pub fn stderr_plain(&self) -> Txt {
remove_ansi_csi(&self.stderr)
}
/// Gets if `stderr` contains a crash panic.
pub fn has_panic(&self) -> bool {
if self.code == Some(101) {
CrashPanic::contains(&self.stderr_plain())
} else {
false
}
}
/// Gets if `stderr` contains a crash panic that traced widget/window path.
pub fn has_panic_widget(&self) -> bool {
if self.code == Some(101) {
CrashPanic::contains_widget(&self.stderr_plain())
} else {
false
}
}
/// Try parse `stderr` for the crash panic.
///
/// Only reliably works if the panic fully printed correctly and was formatted by the panic
/// hook installed by `crash_handler` or by the display print of [`CrashPanic`].
pub fn find_panic(&self) -> Option<CrashPanic> {
if self.code == Some(101) {
CrashPanic::find(&self.stderr)
CrashPanic::find(&self.stderr_plain())
} else {
None
}
@ -254,15 +295,17 @@ impl CrashError {
} else if let Some(msg) = self.minidump_message() {
msg
} else {
"Error.".into()
"".into()
};
use std::fmt::Write as _;
if let Some(c) = self.code {
write!(&mut msg, " Code: {c:#x}.").unwrap();
let sep = if msg.is_empty() { "" } else { "\n" };
write!(&mut msg, "{sep}Code: {c:#X}").unwrap();
}
if let Some(c) = self.signal {
write!(&mut msg, " Signal: {c}.").unwrap();
let sep = if msg.is_empty() { "" } else { "\n" };
write!(&mut msg, "{sep}Signal: {c}").unwrap();
}
msg.end_mut();
msg
@ -300,6 +343,29 @@ impl CrashError {
}
}
const CSI: &str = "\x1b[";
/// Remove ANSI escape sequences (CSI) from `s`.
pub fn remove_ansi_csi(mut s: &str) -> Txt {
fn is_esc_end(byte: u8) -> bool {
(0x40..=0x7e).contains(&byte)
}
let mut r = String::new();
while let Some(i) = s.find(CSI) {
r.push_str(&s[..i]);
s = &s[i + CSI.len()..];
let mut esc_end = 0;
while esc_end < s.len() && !is_esc_end(s.as_bytes()[esc_end]) {
esc_end += 1;
}
esc_end += 1;
s = &s[esc_end..];
}
r.push_str(s);
r.into()
}
/// Panic parsed from a `stderr` dump.
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct CrashPanic {
@ -332,11 +398,32 @@ impl fmt::Display for CrashPanic {
}
}
impl CrashPanic {
/// Gets if `stderr` contains a panic that can be parsed by [`find`].
///
/// [`find`]: Self::find
pub fn contains(stderr: &str) -> bool {
Self::find_impl(stderr, false).is_some()
}
/// Gets if `stderr` contains a panic that can be parsed by [`find`] and traced a widget/window path.
///
/// [`find`]: Self::find
pub fn contains_widget(stderr: &str) -> bool {
match Self::find_impl(stderr, false) {
Some(p) => !p.widget_path.is_empty(),
None => false,
}
}
/// Try parse `stderr` for the crash panic.
///
/// Only reliably works if the panic fully printed correctly and was formatted by the panic
/// hook installed by `crash_handler` or by the display print of this type.
pub fn find(stderr: &str) -> Option<Self> {
Self::find_impl(stderr, true)
}
fn find_impl(stderr: &str, parse: bool) -> Option<Self> {
let mut panic_at = usize::MAX;
let mut widget_path = usize::MAX;
let mut stack_backtrace = usize::MAX;
@ -358,6 +445,22 @@ impl CrashPanic {
return None;
}
if !parse {
return Some(Self {
thread: Txt::from(""),
message: Txt::from(""),
file: Txt::from(""),
line: 0,
column: 0,
widget_path: if widget_path < stderr.len() {
Txt::from("true")
} else {
Txt::from("")
},
backtrace: Txt::from(""),
});
}
let panic_str = stderr[panic_at..].lines().next().unwrap();
let (thread, location) = panic_str.strip_prefix("thread '").unwrap().split_once("' panicked at ").unwrap();
let mut location = location.split(':');
@ -382,7 +485,7 @@ impl CrashPanic {
}
let widget_path = if widget_path < stderr.len() {
stderr[widget_path..].lines().nth(1).unwrap().trim()
stderr[widget_path..].lines().next().unwrap().trim()
} else {
""
};
@ -482,7 +585,7 @@ fn crash_handler_monitor_process(mut cfg_app: ConfigProcess, mut cfg_dialog: Con
}
tracing::error!(
"app-process crashed with exit code ({:#x}), signal ({:#?}), {} crashes previously",
"app-process crashed with exit code ({:#X}), signal ({:#?}), {} crashes previously",
code.unwrap_or(0),
signal.unwrap_or(0),
dialog_args.app_crashes.len()

View File

@ -411,7 +411,7 @@ impl Controller {
if !killed_by_us {
let code = code.unwrap_or(0);
let signal = signal.unwrap_or(0);
tracing::error!(target: "vp_respawn", "view-process exit code: {code:#x}, signal: {signal}");
tracing::error!(target: "vp_respawn", "view-process exit code: {code:#X}, signal: {signal}");
}
let stderr = match String::from_utf8(c.stderr) {

View File

@ -537,4 +537,256 @@ pub use zng_ext_single_instance::{is_single_instance, single_instance, single_in
pub mod crash_handler {
#[cfg(feature = "crash_handler")]
pub use zng_app::crash_handler::{init, restart_count, CrashArgs, CrashConfig, CrashError, CrashPanic};
/// Init a crash-handler with dialog that shows detailed debug info.
pub fn init_debug() {
init(debug_config())
}
/// Config used by [`init_debug`]
pub fn debug_config() -> CrashConfig {
CrashConfig::new(super::crash_handler_dbg::dialog)
}
}
mod crash_handler_dbg {
use std::path::PathBuf;
use crate::{
ansi_text::AnsiText,
app::crash_handler::*,
color::ColorScheme,
prelude::*,
widget::node::{presenter, BoxedUiNode},
};
pub fn dialog(args: CrashArgs) -> ! {
if let Some(c) = &args.dialog_crash {
eprintln!("DEBUG CRASH DIALOG CRASHED");
eprintln!(" {}", c.message());
eprintln!("APP CRASH");
eprintln!(" {}", args.latest().message());
args.exit(0xBADC0DE)
}
// Just in case no view-process was inited before the crash_handler setup.
// Multiple view-process inits are ok because they either do nothing or never return.
#[cfg(feature = "view_prebuilt")]
crate::view_process::prebuilt::init();
#[cfg(feature = "view")]
crate::view_process::default::init();
APP.defaults().run_window(async_clmv!(args, {
let error = args.latest();
Window! {
title = APP.about().map(|a| formatx!("{}App Crashed", a.title_prefix()));
start_position = zng::window::StartPosition::CenterMonitor;
color_scheme = ColorScheme::Dark;
on_load = hn_once!(|_| {
// force to foreground
let _ = WINDOWS.focus(WINDOW.id());
});
on_close = hn_once!(args, |_| {
args.exit(0);
});
padding = 5;
child_top = header(error), 5;
child = panels(error);
child_bottom = commands(args), 5;
}
}));
args.exit(0);
}
fn header(error: &CrashError) -> impl UiNode {
SelectableText! {
txt = error.message();
layout::margin = 10;
}
}
fn panels(error: &CrashError) -> impl UiNode {
let mut options = vec![ErrorPanel::Summary];
let mut active = ErrorPanel::Summary;
if !error.stdout.is_empty() {
options.push(ErrorPanel::StdoutPlain);
active = ErrorPanel::StdoutPlain;
if !error.is_stdout_plain() {
options.push(ErrorPanel::Stdout);
active = ErrorPanel::Stdout;
}
}
if !error.stderr.is_empty() {
options.push(ErrorPanel::StderrPlain);
active = ErrorPanel::StderrPlain;
if !error.is_stderr_plain() {
options.push(ErrorPanel::Stderr);
active = ErrorPanel::Stderr;
}
}
if error.has_panic() {
options.push(ErrorPanel::Panic);
active = ErrorPanel::Panic;
}
if error.has_panic_widget() {
options.push(ErrorPanel::Widget);
}
if error.minidump.is_some() {
options.push(ErrorPanel::Minidump);
active = ErrorPanel::Minidump;
}
let active = var(active);
Container! {
child_top = Wrap! {
toggle::selector = toggle::Selector::single(active.clone());
children = options.iter().map(|p| Toggle! {
child = Text!(p.title());
value = *p;
}).collect::<UiNodeVec>();
toggle::style_fn = Style! {
layout::padding = (2, 4);
widget::corner_radius = 2;
};
spacing = 5;
}, 5;
child = presenter(active, wgt_fn!(error, |p: ErrorPanel| p.panel(&error)));
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ErrorPanel {
Summary,
Stdout,
Stderr,
StdoutPlain,
StderrPlain,
Panic,
Widget,
Minidump,
}
impl ErrorPanel {
fn title(&self) -> Txt {
match self {
ErrorPanel::Summary => "Summary",
ErrorPanel::Stdout => "Stdout",
ErrorPanel::Stderr => "Stderr",
ErrorPanel::StdoutPlain => "Stdout (plain)",
ErrorPanel::StderrPlain => "Stderr (plain)",
ErrorPanel::Panic => "Panic",
ErrorPanel::Widget => "Widget",
ErrorPanel::Minidump => "Minidump",
}
.into()
}
fn panel(&self, error: &CrashError) -> BoxedUiNode {
match self {
ErrorPanel::Summary => summary_panel(error).boxed(),
ErrorPanel::Stdout => std_panel(error.stdout.clone()).boxed(),
ErrorPanel::Stderr => std_panel(error.stderr.clone()).boxed(),
ErrorPanel::StdoutPlain => std_plain_panel(error.stdout_plain()).boxed(),
ErrorPanel::StderrPlain => std_plain_panel(error.stderr_plain()).boxed(),
ErrorPanel::Panic => panic_panel(error.find_panic().unwrap()).boxed(),
ErrorPanel::Widget => widget_panel(error.find_panic().unwrap().widget_path).boxed(),
ErrorPanel::Minidump => minidump_panel(error.minidump.clone().unwrap()).boxed(),
}
}
}
fn summary_panel(error: &CrashError) -> impl UiNode {
let s = formatx!(
"Timestamp: {}\nExit Code: {}\nSignal: {}\nStderr: {} bytes\nStdout: {} bytes\nPanic: {}\nMinidump: {}\n\nArgs: {:?}\n",
error.unix_time(),
match error.code {
Some(c) => format!("{c:#x}"),
None => "<none>".to_owned(),
},
match error.signal {
Some(c) => format!("{c}"),
None => "<none>".to_owned(),
},
error.stderr.len(),
error.stdout.len(),
error.find_panic().is_some(),
match &error.minidump {
Some(p) => {
let path = p.display().to_string();
let path = path.trim_start_matches(r"\\?\");
path.to_owned()
}
None => "<none>".to_owned(),
},
error.args,
);
plain_panel(s)
}
fn std_panel(std: Txt) -> impl UiNode {
Scroll! {
child_align = Align::TOP_START;
widget::background_color = colors::BLACK;
layout::padding = 5;
child = AnsiText! {
txt = std;
font_size = 0.9.em();
}
}
}
fn std_plain_panel(std: Txt) -> impl UiNode {
plain_panel(std)
}
fn panic_panel(panic: CrashPanic) -> impl UiNode {
plain_panel(panic.to_txt())
}
fn widget_panel(widget_path: Txt) -> impl UiNode {
plain_panel(widget_path)
}
fn minidump_panel(path: PathBuf) -> impl UiNode {
let path = path.display().to_string();
let path = path.trim_start_matches(r"\\?\");
plain_panel(path.to_txt())
}
fn plain_panel(txt: Txt) -> impl UiNode {
Scroll! {
child_align = Align::TOP_START;
widget::background_color = colors::BLACK;
layout::padding = 5;
child = SelectableText! {
txt;
font_size = 0.9.em();
// same as AnsiText
font_family = ["JetBrains Mono", "Consolas", "monospace"];
}
}
}
fn commands(args: CrashArgs) -> impl UiNode {
Stack! {
spacing = 5;
direction = StackDirection::start_to_end();
layout::align = Align::END;
children = ui_vec![
Button! {
child = Text!("Restart App");
on_click = hn_once!(args, |_| {
args.restart();
});
},
Button! {
child = Text!("Exit App");
on_click = hn_once!(|_| {
args.exit(0);
});
}
];
}
}
}

View File

@ -198,7 +198,7 @@ Source: [respawn.rs](./respawn.rs)
cargo do run respawn
```
Demonstrates the view-process respawn error recovery feature.
Demonstrates app-process crash handler and view-process respawn.
### `scroll`

View File

@ -27,6 +27,7 @@ use zng::view_process::prebuilt as view_process;
fn main() {
examples_util::print_info();
view_process::init();
zng::app::crash_handler::init_debug();
// let rec = examples_util::record_profile("animation");

View File

@ -11,6 +11,7 @@ use zng::view_process::prebuilt as view_process;
fn main() {
examples_util::print_info();
view_process::init();
zng::app::crash_handler::init_debug();
//let rec = examples_util::record_profile("border");

View File

@ -17,6 +17,7 @@ use zng::view_process::prebuilt as view_process;
fn main() {
examples_util::print_info();
view_process::init();
zng::app::crash_handler::init_debug();
// let rec = examples_util::record_profile("button");

View File

@ -12,6 +12,7 @@ use zng::view_process::prebuilt as view_process;
fn main() {
examples_util::print_info();
view_process::init();
zng::app::crash_handler::init_debug();
//let rec = examples_util::record_profile("calculator");

View File

@ -17,6 +17,7 @@ use zng::view_process::prebuilt as view_process;
fn main() {
examples_util::print_info();
view_process::init();
zng::app::crash_handler::init_debug();
// let rec = examples_util::record_profile("button");

View File

@ -8,6 +8,7 @@ use zng::view_process::prebuilt as view_process;
fn main() {
examples_util::print_info();
view_process::init();
zng::app::crash_handler::init_debug();
// view_process::run_same_process(app_main);
app_main();

View File

@ -15,6 +15,7 @@ use zng::view_process::prebuilt as view_process;
fn main() {
examples_util::print_info();
view_process::init();
zng::app::crash_handler::init_debug();
// view_process::run_same_process(app_main);
app_main();

View File

@ -11,6 +11,7 @@ use zng_view::extensions::ViewExtensions;
fn main() {
examples_util::print_info();
zng::app::crash_handler::init_debug();
// zng_view::init_extended(view_extensions);
// app_main();

View File

@ -25,6 +25,7 @@ use zng::view_process::prebuilt as view_process;
fn main() {
examples_util::print_info();
view_process::init();
zng::app::crash_handler::init_debug();
// let rec = examples_util::record_profile("focus");

View File

@ -18,6 +18,7 @@ use zng::view_process::prebuilt as view_process;
fn main() {
examples_util::print_info();
view_process::init();
zng::app::crash_handler::init_debug();
// view_process::run_same_process(app_main);
app_main();

View File

@ -27,6 +27,7 @@ use zng::view_process::prebuilt as view_process;
fn main() {
examples_util::print_info();
view_process::init();
zng::app::crash_handler::init_debug();
// let rec = examples_util::record_profile("icon");

View File

@ -27,6 +27,7 @@ use zng_wgt_webrender_debug as wr;
fn main() {
examples_util::print_info();
// view_process::init();
zng::app::crash_handler::init_debug();
// let rec = examples_util::record_profile("image");

View File

@ -17,6 +17,7 @@ use zng::view_process::prebuilt as view_process;
fn main() {
examples_util::print_info();
view_process::init();
zng::app::crash_handler::init_debug();
// let rec = examples_util::record_profile("layer");

View File

@ -26,6 +26,7 @@ use zng::view_process::prebuilt as view_process;
fn main() {
examples_util::print_info();
view_process::init();
zng::app::crash_handler::init_debug();
// let rec = examples_util::record_profile("localize");

View File

@ -15,6 +15,7 @@ use zng::view_process::prebuilt as view_process;
fn main() {
examples_util::print_info();
view_process::init();
zng::app::crash_handler::init_debug();
// let rec = examples_util::record_profile("markdown");

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -15,7 +15,8 @@ fn main() {
examples_util::print_info();
// init crash-handler before view to use different view for the crash dialog app.
zng::app::crash_handler::init(zng::app::crash_handler::CrashConfig::new(app_crash_dialog));
zng::app::crash_handler::init_debug();
// zng::app::crash_handler::init(zng::app::crash_handler::CrashConfig::new(app_crash_dialog));
// this is the normal app-process:
@ -32,29 +33,49 @@ fn main() {
child = Stack! {
direction = StackDirection::top_to_bottom();
layout::margin = 10;
spacing = 5;
spacing = 10;
children_align = Align::TOP;
children = ui_vec![
Markdown! {
layout::margin = (20, 0, 0, 0);
txt = "The renderer and OS windows are created in separate process, the `view-process`. \
It automatically respawns in case of a graphics driver crash or other similar fatal error.";
},
view_respawn(),
view_crash(),
Wrap! {
spacing = 5;
children = ui_vec![
view_respawn(),
view_crash(),
],
},
Markdown! {
layout::margin = (20, 0, 0, 0);
txt = "When the app is instantiated the crash handler takes over the process, becoming the `monitor-process`. \
It spawns the `app-process` that is the normal execution. If the `app-process` crashes it spawns the \
`dialog-process` that runs a different app that shows an error message.";
},
app_crash(true),
app_crash(false),
Wrap! {
spacing = 5;
children = ui_vec![
app_crash("panic"),
app_crash("access violation"),
app_crash("stack overflow"),
app_crash("custom exit"),
],
},
Markdown! {
layout::margin = (20, 0, 0, 0);
txt = "The states of these buttons is only preserved for `view-process` crashes. \
use `CONFIG` or some other state saving to better recover from `app-process` crashes.";
},
click_counter(),
click_counter(),
image(),
Wrap! {
spacing = 5;
children = ui_vec![
click_counter(),
image(),
click_counter(),
],
},
];
};
}
@ -62,33 +83,28 @@ fn main() {
}
// Crash dialog app, runs in the dialog-process.
#[allow(unused)]
fn app_crash_dialog(args: zng::app::crash_handler::CrashArgs) -> ! {
zng::view_process::prebuilt::init();
APP.defaults().run_window(async move {
Window! {
title = "Respawn Example - App Crashed";
icon = WindowIcon::render(icon);
start_position = zng::window::StartPosition::CenterMonitor;
auto_size = window::AutoSize::CONTENT;
min_size = (300, 100);
on_load = hn_once!(|_| {
// force to foreground
let _ = WINDOWS.focus(WINDOW.id());
});
on_close = hn_once!(args, |_| {
args.exit(0);
});
padding = 5;
child_top = Markdown!(
"The Respawn Example app has crashed.\n\n{}\n\n**Details:**\n",
child = Markdown!(
"The Respawn Example app has crashed.\n\n{}\n\n",
args.latest().message(),
), 5;
child = Scroll! {
padding = 5;
child_align = Align::TOP_START;
child = zng::ansi_text::AnsiText!(args.latest().to_txt());
widget::background_color = colors::BLACK;
};
);
child_bottom = Stack! {
spacing = 5;
direction = StackDirection::start_to_end();
@ -117,7 +133,7 @@ fn app_crash_dialog(args: zng::app::crash_handler::CrashArgs) -> ! {
}, 5;
}
});
panic!("dialog app did not respond correctly")
std::process::exit(0)
}
fn view_respawn() -> impl UiNode {
@ -143,18 +159,32 @@ fn view_crash() -> impl UiNode {
}
}
fn app_crash(panic: bool) -> impl UiNode {
fn app_crash(crash_name: &'static str) -> impl UiNode {
Button! {
child = Text!("Crash App-Process ({})", if panic { "panic" } else { "access violation" });
child = Text!("Crash ({crash_name})");
on_click = hn!(|_| {
if panic {
panic!("Test app-process crash!");
} else {
// SAFETY: deliberate access violation
#[allow(deref_nullptr)]
unsafe {
*std::ptr::null_mut() = true;
match crash_name {
"panic" => panic!("Test app-process crash!"),
"access violation" => {
// SAFETY: deliberate access violation
#[allow(deref_nullptr)]
unsafe {
*std::ptr::null_mut() = true;
}
}
"stack overflow" => {
fn overflow(c: u64) {
if c < u64::MAX {
overflow(c + 1)
}
}
overflow(0)
}
"custom exit" => {
eprintln!("custom error");
std::process::exit(0xBAD);
}
n => panic!("Unknown crash '{n}'"),
}
});
}
@ -175,13 +205,9 @@ fn click_counter() -> impl UiNode {
}
fn image() -> impl UiNode {
Stack! {
direction = StackDirection::top_to_bottom();
spacing = 3;
children = ui_vec![
text::Strong!("Image:"),
Image! { source = include_bytes!("res/window/icon-bytes.png"); size = (32, 32); },
];
Image! {
source = include_bytes!("res/window/icon-bytes.png"); size = (32, 32);
tooltip = Tip!(Text!("Image reloads after respawn"));
}
}

View File

@ -15,6 +15,7 @@ use rand::SeedableRng;
fn main() {
examples_util::print_info();
view_process::init();
zng::app::crash_handler::init_debug();
// let rec = examples_util::record_profile("scroll");
// view_process::run_same_process(app_main);

View File

@ -9,6 +9,7 @@ use zng::view_process::prebuilt as view_process;
fn main() {
examples_util::print_info();
view_process::init();
zng::app::crash_handler::init_debug();
// let rec = examples_util::record_profile("shortcuts");

View File

@ -32,6 +32,7 @@ use zng::view_process::prebuilt as view_process;
fn main() {
examples_util::print_info();
view_process::init();
zng::app::crash_handler::init_debug();
//let rec = examples_util::record_profile("text");

View File

@ -21,6 +21,7 @@ use zng::view_process::prebuilt as view_process;
fn main() {
examples_util::print_info();
view_process::init();
zng::app::crash_handler::init_debug();
// let rec = examples_util::record_profile("transform");

View File

@ -29,6 +29,7 @@ use zng::view_process::default as view_process;
fn main() {
examples_util::print_info();
// view_process::init();
zng::app::crash_handler::init_debug();
// let rec = examples_util::record_profile("window");