Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sandbox node graph execution on native targets and attempt recovery from panics on Wasm #1846

Merged
merged 9 commits into from
Jul 28, 2024
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion editor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,17 @@ web-sys = { workspace = true, features = [
"TextMetrics",
] }

# Required dependencies
async-mutex = "1.4.0"
spin = "0.9.8"

# Optional local dependencies
wgpu-executor = { path = "../node-graph/wgpu-executor", optional = true }
gpu-executor = { path = "../node-graph/gpu-executor", optional = true }

# Optional workspace dependencies
wasm-bindgen = { workspace = true, optional = true }
wasm-bindgen-futures = { workspace = true, optional = true }
async-mutex = "1.4.0"

[dev-dependencies]
# Workspace dependencies
Expand Down
10 changes: 5 additions & 5 deletions editor/src/node_graph_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use crate::messages::portfolio::document::node_graph::document_node_types::wrap_
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::prelude::*;

use futures::lock::Mutex;
use graph_craft::concrete;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{generate_uuid, DocumentNodeImplementation, NodeId, NodeNetwork};
Expand All @@ -26,6 +25,7 @@ use interpreted_executor::dynamic_executor::{DynamicExecutor, ResolvedDocumentNo

use glam::{DAffine2, DVec2, UVec2};
use once_cell::sync::Lazy;
use spin::Mutex;
use std::sync::mpsc::{Receiver, Sender};
use std::sync::Arc;

Expand Down Expand Up @@ -120,7 +120,7 @@ impl NodeGraphUpdateSender for InternalNodeGraphUpdateSender {
}
}

pub(crate) static NODE_RUNTIME: Lazy<Mutex<Option<NodeRuntime>>> = Lazy::new(|| Mutex::new(None));
pub static NODE_RUNTIME: Lazy<Mutex<Option<NodeRuntime>>> = Lazy::new(|| Mutex::new(None));

impl NodeRuntime {
pub fn new(receiver: Receiver<NodeRuntimeMessage>, sender: Sender<NodeGraphUpdate>) -> Self {
Expand Down Expand Up @@ -377,22 +377,22 @@ impl NodeRuntime {
}

pub async fn introspect_node(path: &[NodeId]) -> Option<Arc<dyn std::any::Any>> {
let runtime = NODE_RUNTIME.lock().await;
let runtime = NODE_RUNTIME.lock();
if let Some(ref mut runtime) = runtime.as_ref() {
return runtime.executor.introspect(path).flatten();
}
None
}

pub async fn run_node_graph() {
let mut runtime = NODE_RUNTIME.lock().await;
let Some(mut runtime) = NODE_RUNTIME.try_lock() else { return };
if let Some(ref mut runtime) = runtime.as_mut() {
runtime.run().await;
}
}

pub async fn replace_node_runtime(runtime: NodeRuntime) -> Option<NodeRuntime> {
let mut node_runtime = NODE_RUNTIME.lock().await;
let mut node_runtime = NODE_RUNTIME.lock();
node_runtime.replace(runtime)
}

Expand Down
2 changes: 1 addition & 1 deletion frontend/wasm/.cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ rustflags = [
]

[unstable]
build-std = ["panic_abort", "std"]
build-std = ["std"]
5 changes: 4 additions & 1 deletion frontend/wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ web-sys = { workspace = true, features = [
# Optional workspace dependencies
ron = { workspace = true, optional = true }

[profile.release]
debug = true

[package.metadata.wasm-pack.profile.dev]
wasm-opt = false

Expand All @@ -61,7 +64,7 @@ demangle-name-section = true
dwarf-debug-info = true

[package.metadata.wasm-pack.profile.release]
wasm-opt = ["-Os"]
wasm-opt = ["-Os", "-g"]

[package.metadata.wasm-pack.profile.release.wasm-bindgen]
debug-js-glue = false
Expand Down
55 changes: 30 additions & 25 deletions frontend/wasm/src/editor_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ impl EditorHandle {
pub fn new(frontend_message_handler_callback: js_sys::Function) -> Self {
let editor = Editor::new();
let editor_handle = EditorHandle { frontend_message_handler_callback };
if EDITOR.with(|editor_cell| editor_cell.set(RefCell::new(editor))).is_err() {
if EDITOR.with(|handle| handle.lock().ok().map(|mut guard| *guard = Some(editor))).is_none() {
log::error!("Attempted to initialize the editor more than once");
}
if EDITOR_HANDLE.with(|handle_cell| handle_cell.set(RefCell::new(editor_handle.clone()))).is_err() {
if EDITOR_HANDLE.with(|handle| handle.lock().ok().map(|mut guard| *guard = Some(editor_handle.clone()))).is_none() {
log::error!("Attempted to initialize the editor handle more than once");
}
editor_handle
Expand Down Expand Up @@ -143,18 +143,20 @@ impl EditorHandle {
*g.borrow_mut() = Some(Closure::new(move |timestamp| {
wasm_bindgen_futures::spawn_local(poll_node_graph_evaluation());

editor_and_handle(|editor, handle| {
let micros: f64 = timestamp * 1000.;
let timestamp = Duration::from_micros(micros.round() as u64);
if !EDITOR_HAS_CRASHED.load(Ordering::SeqCst) {
editor_and_handle(|editor, handle| {
let micros: f64 = timestamp * 1000.;
let timestamp = Duration::from_micros(micros.round() as u64);

for message in editor.handle_message(InputPreprocessorMessage::FrameTimeAdvance { timestamp }) {
handle.send_frontend_message_to_js(message);
}
for message in editor.handle_message(InputPreprocessorMessage::FrameTimeAdvance { timestamp }) {
handle.send_frontend_message_to_js(message);
}

for message in editor.handle_message(BroadcastMessage::TriggerEvent(BroadcastEvent::AnimationFrame)) {
handle.send_frontend_message_to_js(message);
}
});
for message in editor.handle_message(BroadcastMessage::TriggerEvent(BroadcastEvent::AnimationFrame)) {
handle.send_frontend_message_to_js(message);
}
});
}

// Schedule ourself for another requestAnimationFrame callback
request_animation_frame(f.borrow().as_ref().unwrap());
Expand Down Expand Up @@ -893,29 +895,27 @@ fn set_timeout(f: &Closure<dyn FnMut()>, delay: Duration) {
}

/// Provides access to the `Editor` by calling the given closure with it as an argument.
fn editor<T: Default>(callback: impl FnOnce(&mut editor::application::Editor) -> T) -> T {
fn editor<T>(callback: impl FnOnce(&mut editor::application::Editor) -> T) -> T {
EDITOR.with(|editor| {
let Some(Ok(mut editor)) = editor.get().map(RefCell::try_borrow_mut) else {
// TODO: Investigate if this should just panic instead, and if not doing so right now may be the cause of silent crashes that don't inform the user that the app has panicked
log::error!("Failed to borrow the editor");
return T::default();
};
let mut guard = editor.lock();
let Ok(Some(ref mut editor)) = guard.as_deref_mut() else { panic!("Failed to borrow the editor") };

callback(&mut editor)
callback(editor)
})
}

/// Provides access to the `Editor` and its `EditorHandle` by calling the given closure with them as arguments.
fn editor_and_handle(mut callback: impl FnMut(&mut Editor, &mut EditorHandle)) {
editor(|editor| {
EDITOR_HANDLE.with(|editor_handle| {
let Some(Ok(mut handle)) = editor_handle.get().map(RefCell::try_borrow_mut) else {
pub(crate) fn editor_and_handle(mut callback: impl FnMut(&mut Editor, &mut EditorHandle)) {
EDITOR_HANDLE.with(|editor_handle| {
editor(|editor| {
let mut guard = editor_handle.lock();
let Ok(Some(ref mut editor_handle)) = guard.as_deref_mut() else {
log::error!("Failed to borrow editor handle");
return;
};

// Call the closure with the editor and its handle
callback(editor, &mut handle);
callback(editor, editor_handle);
})
});
}
Expand All @@ -937,13 +937,18 @@ async fn poll_node_graph_evaluation() {
}
}

// Clear the error display if there are no more errors
if !messages.is_empty() {
crate::NODE_GRAPH_ERROR_DISPLAYED.store(false, Ordering::SeqCst);
}

// Send each `FrontendMessage` to the JavaScript frontend
for response in messages.into_iter().flat_map(|message| editor.handle_message(message)) {
handle.send_frontend_message_to_js(response);
}

// If the editor cannot be borrowed then it has encountered a panic - we should just ignore new dispatches
})
});
}

fn auto_save_all_documents() {
Expand Down
55 changes: 45 additions & 10 deletions frontend/wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,19 @@ pub mod helpers;

use editor::messages::prelude::*;

use std::cell::{OnceCell, RefCell};
use std::panic;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
use wasm_bindgen::prelude::*;

// Set up the persistent editor backend state
pub static EDITOR_HAS_CRASHED: AtomicBool = AtomicBool::new(false);
pub static NODE_GRAPH_ERROR_DISPLAYED: AtomicBool = AtomicBool::new(false);
pub static LOGGER: WasmLog = WasmLog;

thread_local! {
pub static EDITOR: OnceCell<RefCell<editor::application::Editor>> = const { OnceCell::new() };
pub static EDITOR_HANDLE: OnceCell<RefCell<editor_api::EditorHandle>> = const { OnceCell::new() };
pub static EDITOR: Mutex<Option<editor::application::Editor>> = const { Mutex::new(None) };
pub static EDITOR_HANDLE: Mutex<Option<editor_api::EditorHandle>> = const { Mutex::new(None) };
}

/// Initialize the backend
Expand All @@ -35,16 +37,44 @@ pub fn init_graphite() {

/// When a panic occurs, notify the user and log the error to the JS console before the backend dies
pub fn panic_hook(info: &panic::PanicInfo) {
EDITOR_HAS_CRASHED.store(true, Ordering::SeqCst);
let info = info.to_string();
let backtrace = Error::new("stack").stack().to_string();
if backtrace.contains("DynAnyNode") {
log::error!("Node graph evaluation panicked {info}");

// When the graph panics, the node runtime lock may not be released properly
if editor::node_graph_executor::NODE_RUNTIME.try_lock().is_none() {
unsafe { editor::node_graph_executor::NODE_RUNTIME.force_unlock() };
}

error!("{info}");
if !NODE_GRAPH_ERROR_DISPLAYED.load(Ordering::SeqCst) {
NODE_GRAPH_ERROR_DISPLAYED.store(true, Ordering::SeqCst);
editor_api::editor_and_handle(|_, handle| {
let error = r#"
<rect x="50%" y="50%" width="600" height="100" transform="translate(-300 -50)" rx="4" fill="var(--color-error-red)" />
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-size="18" fill="var(--color-2-mildblack)">
<tspan x="50%" dy="-24" font-weight="bold">The document crashed while being rendered in its current state.</tspan>
<tspan x="50%" dy="24">The editor is now unstable! Undo your last action to restore the artwork,</tspan>
<tspan x="50%" dy="24">then save your document and restart the editor before continuing work.</tspan>
/text>"#
// It's a mystery why the `/text>` tag above needs to be missing its `<`, but when it exists it prints the `<` character in the text. However this works with it removed.
.to_string();
handle.send_frontend_message_to_js_rust_proxy(FrontendMessage::UpdateDocumentArtwork { svg: error });
});
}

return;
} else {
EDITOR_HAS_CRASHED.store(true, Ordering::SeqCst);
}

log::error!("{info}");

EDITOR_HANDLE.with(|editor_handle| {
editor_handle.get().map(|handle| {
handle
.borrow_mut()
.send_frontend_message_to_js_rust_proxy(FrontendMessage::DisplayDialogPanic { panic_info: info.to_string() })
})
let mut guard = editor_handle.lock();
if let Ok(Some(ref mut handle)) = guard.as_deref_mut() {
handle.send_frontend_message_to_js_rust_proxy(FrontendMessage::DisplayDialogPanic { panic_info: info.to_string() });
}
});
}

Expand All @@ -56,6 +86,9 @@ extern "C" {

#[wasm_bindgen(constructor)]
pub fn new(msg: &str) -> Error;

#[wasm_bindgen(structural, method, getter)]
fn stack(error: &Error) -> String;
}

/// Logging to the JS console
Expand All @@ -69,6 +102,8 @@ extern "C" {
fn warn(msg: &str, format: &str);
#[wasm_bindgen(js_namespace = console)]
fn error(msg: &str, format: &str);
#[wasm_bindgen(js_namespace = console)]
fn trace(msg: &str, format: &str);
}

#[derive(Default)]
Expand Down
20 changes: 17 additions & 3 deletions node-graph/interpreted-executor/src/dynamic_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use graph_craft::Type;

use std::collections::{HashMap, HashSet};
use std::error::Error;
use std::panic::UnwindSafe;
use std::sync::Arc;

/// An executor of a node graph that does not require an online compilation server, and instead uses `Box<dyn ...>`.
Expand Down Expand Up @@ -102,9 +103,22 @@ impl DynamicExecutor {
}
}

impl<'a, I: StaticType + 'static + Send + Sync> Executor<I, TaggedValue> for &'a DynamicExecutor {
impl<'a, I: StaticType + 'static + Send + Sync + std::panic::UnwindSafe> Executor<I, TaggedValue> for &'a DynamicExecutor {
fn execute(&self, input: I) -> LocalFuture<Result<TaggedValue, Box<dyn Error>>> {
Box::pin(async move { self.tree.eval_tagged_value(self.output, input).await.map_err(|e| e.into()) })
Box::pin(async move {
use futures::FutureExt;

let result = self.tree.eval_tagged_value(self.output, input);
let wrapped_result = std::panic::AssertUnwindSafe(result).catch_unwind().await;

match wrapped_result {
Ok(result) => result.map_err(|e| e.into()),
Err(e) => {
Box::leak(e);
Err("Node graph execution paniced".into())
}
}
})
}
}

Expand Down Expand Up @@ -176,7 +190,7 @@ impl BorrowTree {
}
/// Evaluate the output node of the [`BorrowTree`] and cast it to a tagged value.
/// This ensures that no borrowed data can escape the node graph.
pub async fn eval_tagged_value<I: StaticType + 'static + Send + Sync>(&self, id: NodeId, input: I) -> Result<TaggedValue, String> {
pub async fn eval_tagged_value<I: StaticType + 'static + Send + Sync + UnwindSafe>(&self, id: NodeId, input: I) -> Result<TaggedValue, String> {
let node = self.nodes.get(&id).cloned().ok_or("Output node not found in executor")?;
let output = node.eval(Box::new(input));
TaggedValue::try_from_any(output.await)
Expand Down
Loading