Auto merge of #21825 - jdm:backtrace-helpers, r=Manishearth

Optional backtraces for JS errors and WebGL errors

This adds two new build-time features to enable useful debugging tools when investigating why JS and WebGL content isn't working. They're optional because they're quite heavyweight.

---
- [x] `./mach build -d` does not report any errors
- [x] `./mach test-tidy` does not report any errors
- [x] These changes do not require tests because they're optional developer features.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/servo/servo/21825)
<!-- Reviewable:end -->
This commit is contained in:
bors-servo 2018-10-02 06:57:38 -04:00 committed by GitHub
commit 57053e03bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 131 additions and 16 deletions

1
Cargo.lock generated
View file

@ -2981,6 +2981,7 @@ version = "0.0.1"
dependencies = [ dependencies = [
"app_units 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "app_units 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"audio-video-metadata 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "audio-video-metadata 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
"backtrace 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
"base64 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "base64 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
"bluetooth_traits 0.0.1", "bluetooth_traits 0.0.1",

View file

@ -9,6 +9,9 @@ publish = false
name = "canvas" name = "canvas"
path = "lib.rs" path = "lib.rs"
[features]
webgl_backtrace = ["canvas_traits/webgl_backtrace"]
[dependencies] [dependencies]
azure = {git = "https://github.com/servo/rust-azure"} azure = {git = "https://github.com/servo/rust-azure"}
canvas_traits = {path = "../canvas_traits"} canvas_traits = {path = "../canvas_traits"}

View file

@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use canvas_traits::webgl::{WebGLCommand, WebGLVersion}; use canvas_traits::webgl::{WebGLCommand, WebGLVersion, WebGLCommandBacktrace};
use compositing::compositor_thread::{CompositorProxy, self}; use compositing::compositor_thread::{CompositorProxy, self};
use euclid::Size2D; use euclid::Size2D;
use gleam::gl; use gleam::gl;
@ -144,13 +144,18 @@ impl GLContextWrapper {
} }
} }
pub fn apply_command(&self, cmd: WebGLCommand, state: &mut GLState) { pub fn apply_command(
&self,
cmd: WebGLCommand,
backtrace: WebGLCommandBacktrace,
state: &mut GLState
) {
match *self { match *self {
GLContextWrapper::Native(ref ctx) => { GLContextWrapper::Native(ref ctx) => {
WebGLImpl::apply(ctx, state, cmd); WebGLImpl::apply(ctx, state, cmd, backtrace);
} }
GLContextWrapper::OSMesa(ref ctx) => { GLContextWrapper::OSMesa(ref ctx) => {
WebGLImpl::apply(ctx, state, cmd); WebGLImpl::apply(ctx, state, cmd, backtrace);
} }
} }
} }

View file

@ -137,8 +137,8 @@ impl<VR: WebVRRenderHandler + 'static> WebGLThread<VR> {
WebGLMsg::RemoveContext(ctx_id) => { WebGLMsg::RemoveContext(ctx_id) => {
self.remove_webgl_context(ctx_id); self.remove_webgl_context(ctx_id);
}, },
WebGLMsg::WebGLCommand(ctx_id, command) => { WebGLMsg::WebGLCommand(ctx_id, command, backtrace) => {
self.handle_webgl_command(ctx_id, command); self.handle_webgl_command(ctx_id, command, backtrace);
}, },
WebGLMsg::WebVRCommand(ctx_id, command) => { WebGLMsg::WebVRCommand(ctx_id, command) => {
self.handle_webvr_command(ctx_id, command); self.handle_webvr_command(ctx_id, command);
@ -164,10 +164,15 @@ impl<VR: WebVRRenderHandler + 'static> WebGLThread<VR> {
} }
/// Handles a WebGLCommand for a specific WebGLContext /// Handles a WebGLCommand for a specific WebGLContext
fn handle_webgl_command(&mut self, context_id: WebGLContextId, command: WebGLCommand) { fn handle_webgl_command(
&mut self,
context_id: WebGLContextId,
command: WebGLCommand,
backtrace: WebGLCommandBacktrace,
) {
let data = Self::make_current_if_needed_mut(context_id, &mut self.contexts, &mut self.bound_context_id); let data = Self::make_current_if_needed_mut(context_id, &mut self.contexts, &mut self.bound_context_id);
if let Some(data) = data { if let Some(data) = data {
data.ctx.apply_command(command, &mut data.state); data.ctx.apply_command(command, backtrace, &mut data.state);
} }
} }
@ -672,7 +677,8 @@ impl WebGLImpl {
pub fn apply<Native: NativeGLContextMethods>( pub fn apply<Native: NativeGLContextMethods>(
ctx: &GLContext<Native>, ctx: &GLContext<Native>,
state: &mut GLState, state: &mut GLState,
command: WebGLCommand command: WebGLCommand,
_backtrace: WebGLCommandBacktrace,
) { ) {
match command { match command {
WebGLCommand::GetContextAttributes(ref sender) => WebGLCommand::GetContextAttributes(ref sender) =>
@ -1193,7 +1199,14 @@ impl WebGLImpl {
// TODO: update test expectations in order to enable debug assertions // TODO: update test expectations in order to enable debug assertions
let error = ctx.gl().get_error(); let error = ctx.gl().get_error();
if error != gl::NO_ERROR { if error != gl::NO_ERROR {
error!("Last GL operation failed: {:?}", command) error!("Last GL operation failed: {:?}", command);
#[cfg(feature = "webgl_backtrace")]
{
error!("Backtrace from failed WebGL API:\n{}", _backtrace.backtrace);
if let Some(backtrace) = _backtrace.js_backtrace {
error!("JS backtrace from failed WebGL API:\n{}", backtrace);
}
}
} }
assert_eq!(error, gl::NO_ERROR, "Unexpected WebGL error: 0x{:x} ({})", error, error); assert_eq!(error, gl::NO_ERROR, "Unexpected WebGL error: 0x{:x} ({})", error, error);
} }

View file

@ -9,6 +9,9 @@ publish = false
name = "canvas_traits" name = "canvas_traits"
path = "lib.rs" path = "lib.rs"
[features]
webgl_backtrace = []
[dependencies] [dependencies]
cssparser = "0.24.0" cssparser = "0.24.0"
euclid = "0.19" euclid = "0.19"

View file

@ -24,6 +24,14 @@ pub use ::webgl_channel::WebGLPipeline;
/// Entry point channel type used for sending WebGLMsg messages to the WebGL renderer. /// Entry point channel type used for sending WebGLMsg messages to the WebGL renderer.
pub use ::webgl_channel::WebGLChan; pub use ::webgl_channel::WebGLChan;
#[derive(Clone, Deserialize, Serialize)]
pub struct WebGLCommandBacktrace {
#[cfg(feature = "webgl_backtrace")]
pub backtrace: String,
#[cfg(feature = "webgl_backtrace")]
pub js_backtrace: Option<String>,
}
/// WebGL Message API /// WebGL Message API
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub enum WebGLMsg { pub enum WebGLMsg {
@ -35,7 +43,7 @@ pub enum WebGLMsg {
/// Drops a WebGLContext. /// Drops a WebGLContext.
RemoveContext(WebGLContextId), RemoveContext(WebGLContextId),
/// Runs a WebGLCommand in a specific WebGLContext. /// Runs a WebGLCommand in a specific WebGLContext.
WebGLCommand(WebGLContextId, WebGLCommand), WebGLCommand(WebGLContextId, WebGLCommand, WebGLCommandBacktrace),
/// Runs a WebVRCommand in a specific WebGLContext. /// Runs a WebVRCommand in a specific WebGLContext.
WebVRCommand(WebGLContextId, WebVRCommand), WebVRCommand(WebGLContextId, WebVRCommand),
/// Locks a specific WebGLContext. Lock messages are used for a correct synchronization /// Locks a specific WebGLContext. Lock messages are used for a correct synchronization
@ -121,8 +129,8 @@ impl WebGLMsgSender {
/// Send a WebGLCommand message /// Send a WebGLCommand message
#[inline] #[inline]
pub fn send(&self, command: WebGLCommand) -> WebGLSendResult { pub fn send(&self, command: WebGLCommand, backtrace: WebGLCommandBacktrace) -> WebGLSendResult {
self.sender.send(WebGLMsg::WebGLCommand(self.ctx_id, command)) self.sender.send(WebGLMsg::WebGLCommand(self.ctx_id, command, backtrace))
} }
/// Send a WebVRCommand message /// Send a WebVRCommand message

View file

@ -16,6 +16,8 @@ debugmozjs = ['mozjs/debugmozjs']
unstable = [] unstable = []
unrooted_must_root_lint = ["script_plugins/unrooted_must_root_lint"] unrooted_must_root_lint = ["script_plugins/unrooted_must_root_lint"]
default = ["unrooted_must_root_lint"] default = ["unrooted_must_root_lint"]
webgl_backtrace = ["backtrace", "canvas_traits/webgl_backtrace"]
js_backtrace = ["backtrace"]
[build-dependencies] [build-dependencies]
cmake = "0.1" cmake = "0.1"
@ -29,6 +31,7 @@ tinyfiledialogs = "3.0"
[dependencies] [dependencies]
app_units = "0.7" app_units = "0.7"
audio-video-metadata = "0.1.4" audio-video-metadata = "0.1.4"
backtrace = {version = "0.3", optional = true}
base64 = "0.6" base64 = "0.6"
bitflags = "1.0" bitflags = "1.0"
bluetooth_traits = {path = "../bluetooth_traits"} bluetooth_traits = {path = "../bluetooth_traits"}

View file

@ -4,6 +4,10 @@
//! Utilities to throw exceptions from Rust bindings. //! Utilities to throw exceptions from Rust bindings.
#[cfg(feature = "js_backtrace")]
use backtrace::Backtrace;
#[cfg(feature = "js_backtrace")]
use dom::bindings::cell::DomRefCell;
use dom::bindings::codegen::Bindings::DOMExceptionBinding::DOMExceptionMethods; use dom::bindings::codegen::Bindings::DOMExceptionBinding::DOMExceptionMethods;
use dom::bindings::codegen::PrototypeList::proto_id_to_name; use dom::bindings::codegen::PrototypeList::proto_id_to_name;
use dom::bindings::conversions::{ConversionResult, FromJSValConvertible, ToJSValConvertible}; use dom::bindings::conversions::{ConversionResult, FromJSValConvertible, ToJSValConvertible};
@ -24,6 +28,11 @@ use js::rust::wrappers::JS_SetPendingException;
use libc::c_uint; use libc::c_uint;
use std::slice::from_raw_parts; use std::slice::from_raw_parts;
/// An optional stringified JS backtrace and stringified native backtrace from the
/// the last DOM exception that was reported.
#[cfg(feature = "js_backtrace")]
thread_local!(static LAST_EXCEPTION_BACKTRACE: DomRefCell<Option<(Option<String>, String)>> = DomRefCell::new(None));
/// DOM exceptions that can be thrown by a native DOM method. /// DOM exceptions that can be thrown by a native DOM method.
#[derive(Clone, Debug, MallocSizeOf)] #[derive(Clone, Debug, MallocSizeOf)]
pub enum Error { pub enum Error {
@ -90,6 +99,16 @@ pub type ErrorResult = Fallible<()>;
/// Set a pending exception for the given `result` on `cx`. /// Set a pending exception for the given `result` on `cx`.
pub unsafe fn throw_dom_exception(cx: *mut JSContext, global: &GlobalScope, result: Error) { pub unsafe fn throw_dom_exception(cx: *mut JSContext, global: &GlobalScope, result: Error) {
#[cfg(feature = "js_backtrace")]
{
capture_stack!(in(cx) let stack);
let js_stack = stack.and_then(|s| s.as_string(None));
let rust_stack = Backtrace::new();
LAST_EXCEPTION_BACKTRACE.with(|backtrace| {
*backtrace.borrow_mut() = Some((js_stack, format!("{:?}", rust_stack)));
});
}
let code = match result { let code = match result {
Error::IndexSize => DOMErrorName::IndexSizeError, Error::IndexSize => DOMErrorName::IndexSizeError,
Error::NotFound => DOMErrorName::NotFoundError, Error::NotFound => DOMErrorName::NotFoundError,
@ -244,6 +263,17 @@ pub unsafe fn report_pending_exception(cx: *mut JSContext, dispatch_event: bool)
"Error at {}:{}:{} {}", "Error at {}:{}:{} {}",
error_info.filename, error_info.lineno, error_info.column, error_info.message error_info.filename, error_info.lineno, error_info.column, error_info.message
); );
#[cfg(feature = "js_backtrace")]
{
LAST_EXCEPTION_BACKTRACE.with(|backtrace| {
if let Some((js_backtrace, rust_backtrace)) = backtrace.borrow_mut().take() {
if let Some(stack) = js_backtrace {
eprintln!("JS backtrace:\n{}", stack);
}
eprintln!("Rust backtrace:\n{}", rust_backtrace);
}
});
}
if dispatch_event { if dispatch_event {
GlobalScope::from_context(cx).report_an_error(error_info, value.handle()); GlobalScope::from_context(cx).report_an_error(error_info, value.handle());

View file

@ -2,9 +2,11 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#[cfg(feature = "webgl_backtrace")]
use backtrace::Backtrace;
use byteorder::{ByteOrder, NativeEndian, WriteBytesExt}; use byteorder::{ByteOrder, NativeEndian, WriteBytesExt};
use canvas_traits::canvas::{byte_swap, multiply_u8_pixel}; use canvas_traits::canvas::{byte_swap, multiply_u8_pixel};
use canvas_traits::webgl::{DOMToTextureCommand, Parameter}; use canvas_traits::webgl::{DOMToTextureCommand, Parameter, WebGLCommandBacktrace};
use canvas_traits::webgl::{TexParameter, WebGLCommand, WebGLContextShareMode, WebGLError}; use canvas_traits::webgl::{TexParameter, WebGLCommand, WebGLContextShareMode, WebGLError};
use canvas_traits::webgl::{WebGLFramebufferBindingRequest, WebGLMsg, WebGLMsgSender}; use canvas_traits::webgl::{WebGLFramebufferBindingRequest, WebGLMsg, WebGLMsgSender};
use canvas_traits::webgl::{WebGLProgramId, WebGLResult, WebGLSLVersion, WebGLSender}; use canvas_traits::webgl::{WebGLProgramId, WebGLResult, WebGLSLVersion, WebGLSender};
@ -316,7 +318,7 @@ impl WebGLRenderingContext {
#[inline] #[inline]
pub fn send_command(&self, command: WebGLCommand) { pub fn send_command(&self, command: WebGLCommand) {
self.webgl_sender.send(command).unwrap(); self.webgl_sender.send(command, capture_webgl_backtrace(self)).unwrap();
} }
#[inline] #[inline]
@ -1189,6 +1191,25 @@ impl WebGLRenderingContext {
} }
} }
#[cfg(not(feature = "webgl_backtrace"))]
#[inline]
pub fn capture_webgl_backtrace<T: DomObject>(_: &T) -> WebGLCommandBacktrace {
WebGLCommandBacktrace {}
}
#[cfg(feature = "webgl_backtrace")]
#[cfg_attr(feature = "webgl_backtrace", allow(unsafe_code))]
pub fn capture_webgl_backtrace<T: DomObject>(obj: &T) -> WebGLCommandBacktrace {
let bt = Backtrace::new();
unsafe {
capture_stack!(in(obj.global().get_cx()) let stack);
WebGLCommandBacktrace {
backtrace: format!("{:?}", bt),
js_backtrace: stack.and_then(|s| s.as_string(None)),
}
}
}
impl Drop for WebGLRenderingContext { impl Drop for WebGLRenderingContext {
fn drop(&mut self) { fn drop(&mut self) {
let _ = self.webgl_sender.send_remove(); let _ = self.webgl_sender.send_remove();
@ -1521,9 +1542,10 @@ impl WebGLRenderingContextMethods for WebGLRenderingContext {
let (sender, receiver) = webgl_channel().unwrap(); let (sender, receiver) = webgl_channel().unwrap();
// If the send does not succeed, assume context lost // If the send does not succeed, assume context lost
let backtrace = capture_webgl_backtrace(self);
if self if self
.webgl_sender .webgl_sender
.send(WebGLCommand::GetContextAttributes(sender)) .send(WebGLCommand::GetContextAttributes(sender), backtrace)
.is_err() .is_err()
{ {
return None; return None;

View file

@ -19,6 +19,8 @@
extern crate app_units; extern crate app_units;
extern crate audio_video_metadata; extern crate audio_video_metadata;
#[cfg(any(feature = "webgl_backtrace", feature = "js_backtrace"))]
extern crate backtrace;
extern crate base64; extern crate base64;
#[macro_use] #[macro_use]
extern crate bitflags; extern crate bitflags;

View file

@ -17,6 +17,7 @@ webdriver = ["webdriver_server"]
energy-profiling = ["profile_traits/energy-profiling"] energy-profiling = ["profile_traits/energy-profiling"]
debugmozjs = ["script/debugmozjs"] debugmozjs = ["script/debugmozjs"]
googlevr = ["webvr/googlevr"] googlevr = ["webvr/googlevr"]
js_backtrace = ["script/js_backtrace"]
webrender_debugger = ["webrender/debugger"] webrender_debugger = ["webrender/debugger"]
oculusvr = ["webvr/oculusvr"] oculusvr = ["webvr/oculusvr"]
unstable = [ unstable = [
@ -24,6 +25,11 @@ unstable = [
"profile/unstable", "profile/unstable",
"script/unstable", "script/unstable",
] ]
webgl_backtrace = [
"script/webgl_backtrace",
"canvas/webgl_backtrace",
"canvas_traits/webgl_backtrace",
]
[dependencies] [dependencies]
bluetooth_traits = {path = "../bluetooth_traits"} bluetooth_traits = {path = "../bluetooth_traits"}

View file

@ -42,3 +42,5 @@ debugmozjs = ["libservo/debugmozjs"]
unstable = ["libservo/unstable"] unstable = ["libservo/unstable"]
googlevr = ["libservo/googlevr"] googlevr = ["libservo/googlevr"]
oculusvr = ["libservo/oculusvr"] oculusvr = ["libservo/oculusvr"]
webgl_backtrace = ["libservo/webgl_backtrace"]
js_backtrace = ["libservo/js_backtrace"]

View file

@ -30,6 +30,8 @@ webdriver = ["libservo/webdriver"]
energy-profiling = ["libservo/energy-profiling"] energy-profiling = ["libservo/energy-profiling"]
debugmozjs = ["libservo/debugmozjs"] debugmozjs = ["libservo/debugmozjs"]
unstable = ["libservo/unstable"] unstable = ["libservo/unstable"]
webgl_backtrace = ["libservo/webgl_backtrace"]
js_backtrace = ["libservo/js_backtrace"]
[target.'cfg(not(target_os = "android"))'.dependencies] [target.'cfg(not(target_os = "android"))'.dependencies]
backtrace = "0.3" backtrace = "0.3"

View file

@ -264,6 +264,11 @@ class MachCommands(CommandBase):
if debug_mozjs: if debug_mozjs:
features += ["debugmozjs"] features += ["debugmozjs"]
if self.config["build"]["webgl-backtrace"]:
features += ["webgl-backtrace"]
if self.config["build"]["dom-backtrace"]:
features += ["dom-backtrace"]
if features: if features:
opts += ["--features", "%s" % ' '.join(features)] opts += ["--features", "%s" % ' '.join(features)]

View file

@ -288,6 +288,8 @@ class CommandBase(object):
self.config["build"].setdefault("rustflags", "") self.config["build"].setdefault("rustflags", "")
self.config["build"].setdefault("incremental", None) self.config["build"].setdefault("incremental", None)
self.config["build"].setdefault("thinlto", False) self.config["build"].setdefault("thinlto", False)
self.config["build"].setdefault("webgl-backtrace", False)
self.config["build"].setdefault("dom-backtrace", False)
self.config.setdefault("android", {}) self.config.setdefault("android", {})
self.config["android"].setdefault("sdk", "") self.config["android"].setdefault("sdk", "")

View file

@ -32,6 +32,14 @@ android = false
# Set "debug-mozjs" or use `mach build --debug-mozjs` to build a debug spidermonkey. # Set "debug-mozjs" or use `mach build --debug-mozjs` to build a debug spidermonkey.
debug-mozjs = false debug-mozjs = false
# When a GL error occurs as a result of a WebGL operation, print the stack trace for the content
# JS and native Rust code that triggered the failed operation. Warning: very slow.
webgl-backtrace = false
# When a DOM exception is reported, print the stack trace for the content JS and native Rust code
# that triggered it.
dom-backtrace = false
# Set to the path to your ccache binary to enable caching of compiler outputs # Set to the path to your ccache binary to enable caching of compiler outputs
#ccache = "/usr/local/bin/ccache" #ccache = "/usr/local/bin/ccache"