Auto merge of #25803 - ferjm:layout_debug, r=SimonSapin

Layout viewer for layout 2020

- [X] `./mach build -d` does not report any errors
- [X] `./mach test-tidy` does not report any errors

This PR makes layout 2020 dump the box and fragment tree states into json files that can be visualized with the layout viewer tool. This tool has not much functionality other than displaying these trees and allowing to inspect each node additional data, so there is a lot of room for improvements. Some ideas for follow-ups:

- Make the tool create and display diffs between tree states.
- Actually allow creating new debug scopes during box tree and fragment tree construction. Right now there is a single scope created after constructing both trees, which is not ideal as it only allows looking at the reflow result.
-  Right now an independent JSON file is created per reflow. It would be nice to unify the data obtained on each reflow on a single JSON, so diffs between reflows can be displayed as well.
- Dump and display the DOM tree. Link boxes to DOM nodes.
- #23339
This commit is contained in:
bors-servo 2020-02-24 01:12:15 -05:00 committed by GitHub
commit 2d1ec68d31
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 622 additions and 43 deletions

3
.gitignore vendored
View file

@ -60,3 +60,6 @@ support/hololens/ServoApp/Release/
support/hololens/packages/
support/hololens/AppPackages/
support/hololens/.vs/
# Layout debugger trace files
layout_trace*

1
Cargo.lock generated
View file

@ -2929,6 +2929,7 @@ dependencies = [
"script_layout_interface",
"script_traits",
"serde",
"serde_json",
"servo_arc",
"servo_geometry",
"servo_url",

View file

@ -33,6 +33,7 @@ rayon_croissant = "0.2.0"
script_layout_interface = {path = "../script_layout_interface"}
script_traits = {path = "../script_traits"}
serde = "1.0"
serde_json = "1.0"
servo_arc = { path = "../servo_arc" }
servo_geometry = {path = "../geometry"}
servo_url = {path = "../url"}

View file

@ -10,7 +10,7 @@ use crate::style_ext::{ComputedValuesExt, DisplayInside};
use servo_arc::Arc;
use style::properties::ComputedValues;
#[derive(Debug)]
#[derive(Debug, Serialize)]
pub(crate) struct FloatBox {
pub contents: IndependentFormattingContext,
}

View file

@ -7,7 +7,7 @@ use crate::flow::float::FloatBox;
use crate::flow::FlowLayout;
use crate::formatting_contexts::IndependentFormattingContext;
use crate::fragments::CollapsedBlockMargins;
use crate::fragments::{AnonymousFragment, BoxFragment, Fragment, TextFragment};
use crate::fragments::{AnonymousFragment, BoxFragment, DebugId, Fragment, TextFragment};
use crate::geom::flow_relative::{Rect, Sides, Vec2};
use crate::positioned::{relative_adjustement, AbsolutelyPositionedBox, PositioningContext};
use crate::sizing::ContentSizes;
@ -23,12 +23,12 @@ use style::values::specified::text::TextAlignKeyword;
use style::Zero;
use webrender_api::FontInstanceKey;
#[derive(Debug, Default)]
#[derive(Debug, Default, Serialize)]
pub(crate) struct InlineFormattingContext {
pub(super) inline_level_boxes: Vec<Arc<InlineLevelBox>>,
}
#[derive(Debug)]
#[derive(Debug, Serialize)]
pub(crate) enum InlineLevelBox {
InlineBox(InlineBox),
TextRun(TextRun),
@ -37,9 +37,10 @@ pub(crate) enum InlineLevelBox {
Atomic(IndependentFormattingContext),
}
#[derive(Debug)]
#[derive(Debug, Serialize)]
pub(crate) struct InlineBox {
pub tag: OpaqueNode,
#[serde(skip_serializing)]
pub style: Arc<ComputedValues>,
pub first_fragment: bool,
pub last_fragment: bool,
@ -47,9 +48,10 @@ pub(crate) struct InlineBox {
}
/// https://www.w3.org/TR/css-display-3/#css-text-run
#[derive(Debug)]
#[derive(Debug, Serialize)]
pub(crate) struct TextRun {
pub tag: OpaqueNode,
#[serde(skip_serializing)]
pub parent_style: Arc<ComputedValues>,
pub text: String,
}
@ -713,6 +715,7 @@ impl TextRun {
.fragments_so_far
.push(Fragment::Text(TextFragment {
tag: self.tag,
debug_id: DebugId::new(),
parent_style: self.parent_style.clone(),
rect,
ascent: font_ascent.into(),

View file

@ -8,8 +8,8 @@ use crate::context::LayoutContext;
use crate::flow::float::{FloatBox, FloatContext};
use crate::flow::inline::InlineFormattingContext;
use crate::formatting_contexts::{IndependentFormattingContext, IndependentLayout, NonReplacedIFC};
use crate::fragments::{AnonymousFragment, BoxFragment, Fragment};
use crate::fragments::{CollapsedBlockMargins, CollapsedMargin};
use crate::fragments::{AnonymousFragment, BoxFragment};
use crate::fragments::{CollapsedBlockMargins, CollapsedMargin, Fragment};
use crate::geom::flow_relative::{Rect, Sides, Vec2};
use crate::positioned::{AbsolutelyPositionedBox, PositioningContext};
use crate::replaced::ReplacedContent;
@ -30,22 +30,23 @@ mod root;
pub use root::{BoxTreeRoot, FragmentTreeRoot};
#[derive(Debug)]
#[derive(Debug, Serialize)]
pub(crate) struct BlockFormattingContext {
pub contents: BlockContainer,
pub contains_floats: bool,
}
#[derive(Debug)]
#[derive(Debug, Serialize)]
pub(crate) enum BlockContainer {
BlockLevelBoxes(Vec<Arc<BlockLevelBox>>),
InlineFormattingContext(InlineFormattingContext),
}
#[derive(Debug)]
#[derive(Debug, Serialize)]
pub(crate) enum BlockLevelBox {
SameFormattingContextBlock {
tag: OpaqueNode,
#[serde(skip_serializing)]
style: Arc<ComputedValues>,
contents: BlockContainer,
},

View file

@ -28,8 +28,10 @@ use style::properties::ComputedValues;
use style::values::computed::Length;
use style_traits::CSSPixel;
#[derive(Serialize)]
pub struct BoxTreeRoot(BlockFormattingContext);
#[derive(Serialize)]
pub struct FragmentTreeRoot {
/// The children of the root of the fragment tree.
children: Vec<Fragment>,

View file

@ -18,9 +18,10 @@ use style::properties::ComputedValues;
use style::values::computed::Length;
/// https://drafts.csswg.org/css-display/#independent-formatting-context
#[derive(Debug)]
#[derive(Debug, Serialize)]
pub(crate) struct IndependentFormattingContext {
pub tag: OpaqueNode,
#[serde(skip_serializing)]
pub style: Arc<ComputedValues>,
/// If it was requested during construction
@ -38,7 +39,7 @@ pub(crate) struct IndependentLayout {
// Private so that code outside of this module cannot match variants.
// It should got through methods instead.
#[derive(Debug)]
#[derive(Debug, Serialize)]
enum IndependentFormattingContextContents {
Flow(BlockFormattingContext),

View file

@ -4,8 +4,12 @@
use crate::geom::flow_relative::{Rect, Sides, Vec2};
use crate::geom::{PhysicalPoint, PhysicalRect};
#[cfg(debug_assertions)]
use crate::layout_debug;
use gfx::text::glyph::GlyphStore;
use gfx_traits::print_tree::PrintTree;
#[cfg(not(debug_assertions))]
use serde::ser::{Serialize, Serializer};
use servo_arc::Arc as ServoArc;
use std::sync::Arc;
use style::computed_values::overflow_x::T as ComputedOverflow;
@ -16,6 +20,7 @@ use style::values::computed::Length;
use style::Zero;
use webrender_api::{FontInstanceKey, ImageKey};
#[derive(Serialize)]
pub(crate) enum Fragment {
Box(BoxFragment),
Anonymous(AnonymousFragment),
@ -23,8 +28,11 @@ pub(crate) enum Fragment {
Image(ImageFragment),
}
#[derive(Serialize)]
pub(crate) struct BoxFragment {
pub tag: OpaqueNode,
pub debug_id: DebugId,
#[serde(skip_serializing)]
pub style: ServoArc<ComputedValues>,
pub children: Vec<Fragment>,
@ -43,20 +51,23 @@ pub(crate) struct BoxFragment {
pub scrollable_overflow_from_children: PhysicalRect<Length>,
}
#[derive(Serialize)]
pub(crate) struct CollapsedBlockMargins {
pub collapsed_through: bool,
pub start: CollapsedMargin,
pub end: CollapsedMargin,
}
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Serialize)]
pub(crate) struct CollapsedMargin {
max_positive: Length,
min_negative: Length,
}
/// Can contain child fragments with relative coordinates, but does not contribute to painting itself.
#[derive(Serialize)]
pub(crate) struct AnonymousFragment {
pub debug_id: DebugId,
pub rect: Rect<Length>,
pub children: Vec<Fragment>,
pub mode: WritingMode,
@ -65,18 +76,26 @@ pub(crate) struct AnonymousFragment {
pub scrollable_overflow: PhysicalRect<Length>,
}
#[derive(Serialize)]
pub(crate) struct TextFragment {
pub debug_id: DebugId,
pub tag: OpaqueNode,
#[serde(skip_serializing)]
pub parent_style: ServoArc<ComputedValues>,
pub rect: Rect<Length>,
pub ascent: Length,
#[serde(skip_serializing)]
pub font_key: FontInstanceKey,
pub glyphs: Vec<Arc<GlyphStore>>,
}
#[derive(Serialize)]
pub(crate) struct ImageFragment {
pub debug_id: DebugId,
#[serde(skip_serializing)]
pub style: ServoArc<ComputedValues>,
pub rect: Rect<Length>,
#[serde(skip_serializing)]
pub image_key: ImageKey,
}
@ -119,6 +138,7 @@ impl Fragment {
impl AnonymousFragment {
pub fn no_op(mode: WritingMode) -> Self {
Self {
debug_id: DebugId::new(),
children: vec![],
rect: Rect::zero(),
mode,
@ -136,6 +156,7 @@ impl AnonymousFragment {
)
});
AnonymousFragment {
debug_id: DebugId::new(),
rect,
children,
mode,
@ -175,6 +196,7 @@ impl BoxFragment {
});
BoxFragment {
tag,
debug_id: DebugId::new(),
style,
children,
content_rect,
@ -342,3 +364,33 @@ impl CollapsedMargin {
self.max_positive + self.min_negative
}
}
#[cfg(not(debug_assertions))]
#[derive(Clone)]
pub struct DebugId;
#[cfg(debug_assertions)]
#[derive(Clone, Serialize)]
#[serde(transparent)]
pub struct DebugId(u16);
#[cfg(not(debug_assertions))]
impl DebugId {
pub fn new() -> DebugId {
DebugId
}
}
#[cfg(debug_assertions)]
impl DebugId {
pub fn new() -> DebugId {
DebugId(layout_debug::generate_unique_debug_id())
}
}
#[cfg(not(debug_assertions))]
impl Serialize for DebugId {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&format!("{:p}", &self))
}
}

View file

@ -18,19 +18,19 @@ pub type PhysicalRect<U> = euclid::Rect<U, CSSPixel>;
pub type PhysicalSides<U> = euclid::SideOffsets2D<U, CSSPixel>;
pub(crate) mod flow_relative {
#[derive(Clone)]
#[derive(Clone, Serialize)]
pub(crate) struct Vec2<T> {
pub inline: T,
pub block: T,
}
#[derive(Clone)]
#[derive(Clone, Serialize)]
pub(crate) struct Rect<T> {
pub start_corner: Vec2<T>,
pub size: Vec2<T>,
}
#[derive(Clone, Debug)]
#[derive(Clone, Serialize)]
pub(crate) struct Sides<T> {
pub inline_start: T,
pub inline_end: T,

View file

@ -0,0 +1,148 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! Supports writing a trace file created during each layout scope
//! that can be viewed by an external tool to make layout debugging easier.
use crate::flow::{BoxTreeRoot, FragmentTreeRoot};
use serde_json::{to_string, to_value, Value};
use std::cell::RefCell;
use std::fs;
#[cfg(debug_assertions)]
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
thread_local!(static STATE_KEY: RefCell<Option<State>> = RefCell::new(None));
#[cfg(debug_assertions)]
static DEBUG_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
pub struct Scope;
#[macro_export]
macro_rules! layout_debug_scope(
($($arg:tt)*) => (
if cfg!(debug_assertions) {
layout_debug::Scope::new(format!($($arg)*))
} else {
layout_debug::Scope
}
)
);
#[derive(Serialize)]
struct TreeValues {
pub box_tree: Value,
pub fragment_tree: Value,
}
#[derive(Serialize)]
struct ScopeData {
name: String,
pre: TreeValues,
post: TreeValues,
children: Vec<Box<ScopeData>>,
}
impl ScopeData {
fn new(name: String, box_tree: Value, fragment_tree: Value) -> ScopeData {
ScopeData {
name,
pre: TreeValues {
box_tree,
fragment_tree,
},
post: TreeValues {
box_tree: Value::Null,
fragment_tree: Value::Null,
},
children: vec![],
}
}
}
struct State {
fragment_tree: Arc<FragmentTreeRoot>,
box_tree: Arc<BoxTreeRoot>,
scope_stack: Vec<Box<ScopeData>>,
}
/// A layout debugging scope. The entire state of the box and fragment trees
/// will be output at the beginning and end of this scope.
impl Scope {
pub fn new(name: String) -> Scope {
STATE_KEY.with(|ref r| {
if let Some(ref mut state) = *r.borrow_mut() {
let box_tree = to_value(&state.box_tree).unwrap();
let fragment_tree = to_value(&state.fragment_tree).unwrap();
let data = Box::new(ScopeData::new(name.clone(), box_tree, fragment_tree));
state.scope_stack.push(data);
}
});
Scope
}
}
#[cfg(debug_assertions)]
impl Drop for Scope {
fn drop(&mut self) {
STATE_KEY.with(|ref r| {
if let Some(ref mut state) = *r.borrow_mut() {
let mut current_scope = state.scope_stack.pop().unwrap();
current_scope.post = TreeValues {
box_tree: to_value(&state.box_tree).unwrap(),
fragment_tree: to_value(&state.fragment_tree).unwrap(),
};
let previous_scope = state.scope_stack.last_mut().unwrap();
previous_scope.children.push(current_scope);
}
});
}
}
/// Generate a unique ID for Fragments.
#[cfg(debug_assertions)]
pub fn generate_unique_debug_id() -> u16 {
DEBUG_ID_COUNTER.fetch_add(1, Ordering::SeqCst) as u16
}
/// Begin a layout debug trace. If this has not been called,
/// creating debug scopes has no effect.
pub fn begin_trace(box_tree: Arc<BoxTreeRoot>, fragment_tree: Arc<FragmentTreeRoot>) {
assert!(STATE_KEY.with(|ref r| r.borrow().is_none()));
STATE_KEY.with(|ref r| {
let box_tree_value = to_value(&box_tree).unwrap();
let fragment_tree_value = to_value(&fragment_tree).unwrap();
let state = State {
scope_stack: vec![Box::new(ScopeData::new(
"root".to_owned(),
box_tree_value,
fragment_tree_value,
))],
box_tree,
fragment_tree,
};
*r.borrow_mut() = Some(state);
});
}
/// End the debug layout trace. This will write the layout
/// trace to disk in the current directory. The output
/// file can then be viewed with an external tool.
pub fn end_trace(generation: u32) {
let mut thread_state = STATE_KEY.with(|ref r| r.borrow_mut().take().unwrap());
assert_eq!(thread_state.scope_stack.len(), 1);
let mut root_scope = thread_state.scope_stack.pop().unwrap();
root_scope.post = TreeValues {
box_tree: to_value(&thread_state.box_tree).unwrap_or(Value::Null),
fragment_tree: to_value(&thread_state.fragment_tree).unwrap_or(Value::Null),
};
let result = to_string(&root_scope).unwrap();
fs::write(
format!("layout_trace-{}.json", generation),
result.as_bytes(),
)
.unwrap();
}

View file

@ -5,6 +5,9 @@
#![deny(unsafe_code)]
#![feature(exact_size_is_empty)]
#[macro_use]
extern crate serde;
pub mod context;
pub mod data;
pub mod display_list;
@ -14,6 +17,8 @@ mod flow;
mod formatting_contexts;
mod fragments;
mod geom;
#[macro_use]
pub mod layout_debug;
mod opaque_node;
mod positioned;
pub mod query;

View file

@ -18,7 +18,7 @@ use style::properties::ComputedValues;
use style::values::computed::{Length, LengthOrAuto, LengthPercentage, LengthPercentageOrAuto};
use style::Zero;
#[derive(Debug)]
#[derive(Debug, Serialize)]
pub(crate) struct AbsolutelyPositionedBox {
pub contents: IndependentFormattingContext,
}

View file

@ -166,7 +166,7 @@ impl LayoutRPC for LayoutRPCImpl {
pub fn process_content_box_request(
requested_node: OpaqueNode,
fragment_tree_root: Option<&FragmentTreeRoot>,
fragment_tree_root: Option<Arc<FragmentTreeRoot>>,
) -> Option<Rect<Au>> {
let fragment_tree_root = match fragment_tree_root {
Some(fragment_tree_root) => fragment_tree_root,
@ -182,7 +182,7 @@ pub fn process_content_boxes_request(_requested_node: OpaqueNode) -> Vec<Rect<Au
pub fn process_node_geometry_request(
requested_node: OpaqueNode,
fragment_tree_root: Option<&FragmentTreeRoot>,
fragment_tree_root: Option<Arc<FragmentTreeRoot>>,
) -> Rect<i32> {
let fragment_tree_root = match fragment_tree_root {
Some(fragment_tree_root) => fragment_tree_root,

View file

@ -3,7 +3,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use crate::dom_traversal::NodeExt;
use crate::fragments::{Fragment, ImageFragment};
use crate::fragments::{DebugId, Fragment, ImageFragment};
use crate::geom::flow_relative::{Rect, Vec2};
use crate::geom::PhysicalSize;
use crate::sizing::ContentSizes;
@ -17,7 +17,7 @@ use style::values::computed::{Length, LengthOrAuto};
use style::values::CSSFloat;
use style::Zero;
#[derive(Debug)]
#[derive(Debug, Serialize)]
pub(crate) struct ReplacedContent {
pub kind: ReplacedContentKind,
intrinsic: IntrinsicSizes,
@ -34,14 +34,14 @@ pub(crate) struct ReplacedContent {
///
/// * For SVG, see https://svgwg.org/svg2-draft/coords.html#SizingSVGInCSS
/// and again https://github.com/w3c/csswg-drafts/issues/4572.
#[derive(Debug)]
#[derive(Debug, Serialize)]
pub(crate) struct IntrinsicSizes {
pub width: Option<Length>,
pub height: Option<Length>,
pub ratio: Option<CSSFloat>,
}
#[derive(Debug)]
#[derive(Debug, Serialize)]
pub(crate) enum ReplacedContentKind {
Image(Option<Arc<Image>>),
}
@ -113,6 +113,7 @@ impl ReplacedContent {
.and_then(|image| image.id)
.map(|image_key| {
Fragment::Image(ImageFragment {
debug_id: DebugId::new(),
style: style.clone(),
rect: Rect {
start_corner: Vec2::zero(),

View file

@ -48,7 +48,7 @@ impl ContentSizesRequest {
}
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize)]
pub(crate) struct ContentSizes {
pub min_content: Length,
pub max_content: Length,
@ -83,7 +83,7 @@ impl ContentSizes {
}
/// Optional min/max-content for storage in the box tree
#[derive(Debug)]
#[derive(Debug, Serialize)]
pub(crate) enum BoxContentSizes {
NoneWereRequested, // … during box construction
Inline(ContentSizes),

View file

@ -36,6 +36,7 @@ use ipc_channel::ipc::{self, IpcReceiver, IpcSender};
use ipc_channel::router::ROUTER;
use layout::context::LayoutContext;
use layout::display_list::{DisplayListBuilder, WebRenderImageInfo};
use layout::layout_debug;
use layout::query::{
process_content_box_request, process_content_boxes_request, LayoutRPCImpl, LayoutThreadData,
};
@ -172,10 +173,10 @@ pub struct LayoutThread {
outstanding_web_fonts: Arc<AtomicUsize>,
/// The root of the box tree.
box_tree_root: RefCell<Option<BoxTreeRoot>>,
box_tree_root: RefCell<Option<Arc<BoxTreeRoot>>>,
/// The root of the fragment tree.
fragment_tree_root: RefCell<Option<FragmentTreeRoot>>,
fragment_tree_root: RefCell<Option<Arc<FragmentTreeRoot>>>,
/// The document-specific shared lock used for author-origin stylesheets
document_shared_lock: Option<SharedRwLock>,
@ -234,6 +235,10 @@ pub struct LayoutThread {
/// Emits notifications when there is a relayout.
relayout_event: bool,
/// True if each step of layout is traced to an external JSON file
/// for debugging purposes.
trace_layout: bool,
}
impl LayoutThreadFactory for LayoutThread {
@ -266,7 +271,7 @@ impl LayoutThreadFactory for LayoutThread {
dump_rule_tree: bool,
relayout_event: bool,
_nonincremental_layout: bool,
_trace_layout: bool,
trace_layout: bool,
dump_flow_tree: bool,
) {
thread::Builder::new()
@ -315,6 +320,7 @@ impl LayoutThreadFactory for LayoutThread {
dump_style_tree,
dump_rule_tree,
dump_flow_tree,
trace_layout,
);
let reporter_name = format!("layout-reporter-{}", id);
@ -483,6 +489,7 @@ impl LayoutThread {
dump_style_tree: bool,
dump_rule_tree: bool,
dump_flow_tree: bool,
trace_layout: bool,
) -> LayoutThread {
// Let webrender know about this pipeline by sending an empty display list.
webrender_api_sender.send_initial_transaction(webrender_document, id.to_webrender());
@ -568,6 +575,7 @@ impl LayoutThread {
dump_style_tree,
dump_rule_tree,
dump_flow_tree,
trace_layout,
}
}
@ -870,9 +878,9 @@ impl LayoutThread {
self.dump_style_tree,
self.dump_rule_tree,
self.relayout_event,
true, // nonincremental_layout
false, // trace_layout
self.dump_flow_tree,
true, // nonincremental_layout
self.trace_layout, // trace_layout
self.dump_flow_tree, // dump_flow_tree
);
}
@ -1152,7 +1160,8 @@ impl LayoutThread {
} else {
build_box_tree()
};
Some(box_tree)
Some(Arc::new(box_tree))
} else {
None
};
@ -1165,11 +1174,11 @@ impl LayoutThread {
self.viewport_size.height.to_f32_px(),
);
let run_layout = || box_tree.layout(&layout_context, viewport_size);
let fragment_tree = if let Some(pool) = rayon_pool {
let fragment_tree = Arc::new(if let Some(pool) = rayon_pool {
pool.install(run_layout)
} else {
run_layout()
};
});
*self.box_tree_root.borrow_mut() = Some(box_tree);
*self.fragment_tree_root.borrow_mut() = Some(fragment_tree);
}
@ -1201,7 +1210,7 @@ impl LayoutThread {
// Perform post-style recalculation layout passes.
if let Some(root) = &*self.fragment_tree_root.borrow() {
self.perform_post_style_recalc_layout_passes(
root,
root.clone(),
&data.reflow_goal,
Some(&document),
&mut layout_context,
@ -1232,10 +1241,8 @@ impl LayoutThread {
match *reflow_goal {
ReflowGoal::LayoutQuery(ref querymsg, _) => match querymsg {
&QueryMsg::ContentBoxQuery(node) => {
rw_data.content_box_response = process_content_box_request(
node,
(&*self.fragment_tree_root.borrow()).as_ref(),
);
rw_data.content_box_response =
process_content_box_request(node, self.fragment_tree_root.borrow().clone());
},
&QueryMsg::ContentBoxesQuery(node) => {
rw_data.content_boxes_response = process_content_boxes_request(node);
@ -1250,7 +1257,7 @@ impl LayoutThread {
&QueryMsg::ClientRectQuery(node) => {
rw_data.client_rect_response = process_node_geometry_request(
node,
(&*self.fragment_tree_root.borrow()).as_ref(),
self.fragment_tree_root.borrow().clone(),
);
},
&QueryMsg::NodeScrollGeometryQuery(node) => {
@ -1364,7 +1371,7 @@ impl LayoutThread {
let mut layout_context = self.build_layout_context(guards, false, &snapshots, origin);
self.perform_post_style_recalc_layout_passes(
root,
root.clone(),
&ReflowGoal::TickAnimations,
None,
&mut layout_context,
@ -1375,11 +1382,17 @@ impl LayoutThread {
fn perform_post_style_recalc_layout_passes(
&self,
fragment_tree: &FragmentTreeRoot,
fragment_tree: Arc<FragmentTreeRoot>,
reflow_goal: &ReflowGoal,
document: Option<&ServoLayoutDocument>,
context: &mut LayoutContext,
) {
if self.trace_layout {
if let Some(box_tree) = &*self.box_tree_root.borrow() {
layout_debug::begin_trace(box_tree.clone(), fragment_tree.clone());
}
}
if !reflow_goal.needs_display() {
// Defer the paint step until the next ForDisplay.
//
@ -1390,6 +1403,7 @@ impl LayoutThread {
.needs_paint_from_layout();
return;
}
if let Some(document) = document {
document.will_paint();
}
@ -1432,6 +1446,10 @@ impl LayoutThread {
display_list.wr.finalize(),
);
if self.trace_layout {
layout_debug::end_trace(self.generation.get());
}
self.generation.set(self.generation.get() + 1);
}

View file

@ -27,3 +27,7 @@
font-weight: bold;
text-align: right;
}
.tree-collapse {
float: right;
}

View file

@ -0,0 +1,339 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Servo Layout Debugger</title>
<!-- Bootstrap -->
<link href="css/bootstrap.min.css" rel="stylesheet" />
<!-- Treeview -->
<link href="css/bootstrap-treeview.min.css" rel="stylesheet" />
<!-- JSDiffPatch -->
<link href="css/formatters/html.css" rel="stylesheet" />
<!-- Custom -->
<link href="css/main.css" rel="stylesheet" />
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div class="container" role="main">
<div class="row">
<div class="col-sm-12">
<h1>Servo Layout Viewer</h1>
<p>
Check the
<a
href="https://github.com/servo/servo/blob/master/etc/layout_viewer/README.md"
>README</a
>
for instructions.
</p>
</div>
</div>
<div class="row">
<div class="col-sm-4">
<div class="row">
<div class="col-sm-12">
<div class="well">
<input type="file" />
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div id="trace-tree"></div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<ul id="trace-list" class="list-group"></ul>
</div>
</div>
</div>
<div class="col-sm-8">
<div class="row">
<div class="col-sm-12">
<div class="panel panel-default">
<div class="panel-heading">
Box Tree
<a
id="box-tree-collapse"
class="tree-collapse"
data-collapsed="0"
></a>
</div>
<div class="panel-body" id="box-tree"></div>
</div>
</div>
<div class="col-sm-12">
<div id="box-diffs"></div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="panel panel-default">
<div class="panel-heading">
Fragment Tree
<a
id="fragment-tree-collapse"
class="tree-collapse"
data-collapsed="0"
></a>
</div>
<div class="panel-body" id="fragment-tree"></div>
</div>
</div>
<div class="col-sm-12">
<div id="fragment-diffs"></div>
</div>
</div>
</div>
</div>
</div>
<!-- jQuery -->
<script src="js/jquery.min.js"></script>
<!-- Bootstrap -->
<script src="js/bootstrap.min.js"></script>
<!-- Treeview -->
<script src="js/bootstrap-treeview.min.js"></script>
<!-- JSDiffPatch -->
<script src="js/bundle.min.js"></script>
<script src="js/formatters.min.js"></script>
<script>
function get_inner_boxes(box) {
const box_type = Object.keys(box)[0];
switch (box_type) {
case "BlockLevelBoxes":
return box.BlockLevelBoxes;
case "InlineFormattingContext":
return box.InlineFormattingContext.inline_level_boxes;
case "InlineBox":
return box.InlineBox.children;
case "SameFormattingContextBlock":
case "Independent":
case "Flow":
case "OutOfFlowAbsolutelyPositionedBox":
case "OutOfFlowFloatBox":
case "Atomic":
return box[box_type].contents;
}
return null;
}
function box_tree_from_container(container) {
const box_type = Object.keys(container)[0];
let inner_boxes = get_inner_boxes(container);
let nodes = [];
let text = box_type;
if (Array.isArray(inner_boxes)) {
if (!inner_boxes.length) {
nodes = null;
} else {
for (let box in inner_boxes) {
nodes.push(box_tree_from_container(inner_boxes[box]));
}
}
} else if (inner_boxes != null) {
nodes.push(box_tree_from_container(inner_boxes));
} else {
if (box_type == "TextRun") {
text += ` (${container.TextRun.text})`;
}
nodes = null;
}
let info;
if (
box_type != "BlockLevelBoxes" &&
box_type != "InlineFormattingContext"
) {
info = Object.assign({}, Object.values(container)[0]);
delete info.children;
delete info.contents;
delete info.tag;
}
return {
text,
nodes,
info
};
}
function box_tree_from_bfc(bfc) {
const { contains_floats, contents } = bfc;
let block_container = Object.values(contents)[0];
return {
text: "BlockFormattingContext",
info: {
contains_floats
},
nodes: [box_tree_from_container(contents)]
};
}
function create_fragment_tree(root) {
let fragment = Object.values(root)[0];
let node = {
text: Object.keys(root)[0],
id: fragment.debug_id,
href: "#diff-" + fragment.debug_id
};
if (fragment.children) {
let children = [];
for (let i = 0; i < fragment.children.length; ++i) {
children.push(create_fragment_tree(fragment.children[i]));
}
if (children.length > 0) {
node.nodes = children;
}
}
node.info = Object.assign({}, fragment);
delete node.info.children;
delete node.info.debug_id;
return node;
}
function flatten_trace(trace_node) {
const fragment_tree_root = Object.values(
trace_node.fragment_tree.children
)[0];
return {
fragment_tree: create_fragment_tree(fragment_tree_root),
box_tree: box_tree_from_bfc(trace_node.box_tree)
};
}
function create_trace_tree_node(trace_node) {
const trace = flatten_trace(trace_node.pre);
let tree_node = {
text: trace_node.name,
icon: "dummy",
box_tree: trace.box_tree,
fragment_tree: trace.fragment_tree
};
let node = Object.values(trace_node)[0];
if (node.children) {
let children = [];
for (let i = 0; i < node.children.length; ++i) {
children.push(create_trace_tree_node(node.children[i]));
}
if (children.length > 0) {
tree_node.nodes = children;
}
}
return tree_node;
}
function new_data_loaded(data) {
jsondiffpatch.formatters.html.hideUnchanged();
let node_color_hash = {};
let tree = [create_trace_tree_node(data)];
$("#trace-tree").treeview({ data: tree, levels: 3 });
$("#trace-tree").on("nodeSelected", function(event, node) {
$("#fragment-diffs").empty();
$("#trace-tree")
.treeview(true)
.revealNode(node);
const on_tree_node_selected = tree => (event, data) => {
$(`#${tree}-diffs`).empty();
if (!data.info) return;
// XXX(ferjm) no diff for now.
const delta = jsondiffpatch
.create({
objectHash: function(obj) {
return JSON.stringify(obj);
}
})
.diff({}, data.info);
const json = jsondiffpatch.formatters.html.format(delta, data.info);
$(`#${tree}-diffs`).append(
"<div class='panel panel-default'><div class='panel-heading'>" +
data.text +
"</div><div class='panel-body'>" +
json +
"</div></div>"
);
};
const on_fragment_tree_node_selected = on_tree_node_selected(
"fragment"
);
const on_box_tree_node_selected = on_tree_node_selected("box");
$("#fragment-tree").treeview({
data: [node.fragment_tree],
levels: 100,
enableLinks: false,
emptyIcon: "glyphicon glyphicon-unchecked hidden-glyphicon",
onNodeSelected: on_fragment_tree_node_selected
});
$("#box-tree").treeview({
data: [node.box_tree],
levels: 100,
enableLinks: false,
emptyIcon: "glyphicon glyphicon-unchecked hidden-glyphicon",
onNodeSelected: on_box_tree_node_selected
});
["fragment", "box"].forEach(key => {
const collapsable = $(`#${key}-tree-collapse`);
collapsable.html("Collapse all").on("click", () => {
const collapsed = collapsable.data("collapsed");
if (collapsed == 0) {
$(`#${key}-tree`).treeview("collapseAll");
} else {
$(`#${key}-tree`).treeview("expandAll");
}
collapsable.html(collapsed == 0 ? "Expand all" : "Collapse all");
collapsable.data("collapsed", collapsed == 0 ? 1 : 0);
});
});
});
$("#trace-tree")
.treeview(true)
.selectNode(0);
}
$(document).ready(function() {
let upload = document.getElementsByTagName("input")[0];
upload.onchange = function(e) {
e.preventDefault();
let file = upload.files[0],
reader = new FileReader();
reader.onload = function(event) {
new_data_loaded(JSON.parse(event.target.result));
};
reader.readAsText(file);
return false;
};
});
</script>
</body>
</html>