Bug 1605283 - Improve support for invalidation debugging and testing r=gw
authorBert Peers <bpeers@mozilla.com>
Mon, 10 Feb 2020 17:35:05 +0000
changeset 513061 813768d074e3accc36dc3313d0bffb658dcc562d
parent 513060 da808bb758190e48123fc4c389ba8f676695918f
child 513062 42d8559f9f4fd842d8e0244b64a12e678f83c0ab
push id37109
push userncsoregi@mozilla.com
push dateMon, 10 Feb 2020 21:33:47 +0000
treeherdermozilla-central@813768d074e3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgw
bugs1605283
milestone75.0a1
first release with
nightly linux32
813768d074e3 / 75.0a1 / 20200210213347 / files
nightly linux64
813768d074e3 / 75.0a1 / 20200210213347 / files
nightly mac
813768d074e3 / 75.0a1 / 20200210213347 / files
nightly win32
813768d074e3 / 75.0a1 / 20200210213347 / files
nightly win64
813768d074e3 / 75.0a1 / 20200210213347 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1605283 - Improve support for invalidation debugging and testing r=gw Fourth iteration: improve the detail in reported tile invalidations. The invalidation enum stores the old and new values for lightweight types. For a change in PrimCount, the old and new list of ItemUids is stored (if logging is enabled); the tool can then diff the two to see what was added and removed. To convert that back into friendly strings, the interning activity is used to build up a map of ItemUid to a string. A similar special-casing of Content Descriptor will print the item that's invalidating the tile, plus the origin and/or rectangle. Also adding zoom and pan command line options both to fix high-DPI issues and also to allow zooming out far enough to see out-of-viewport cache lifetime and prefetching due to scrolling. Also fix a bug where interning updates get lost if more than one update happens without building a frame: switch to a vector of serialized updatelists (one per type) instead of allowing just one string (per type). Differential Revision: https://phabricator.services.mozilla.com/D61656
gfx/wr/tileview/src/main.rs
gfx/wr/tileview/src/tilecache_base.css
gfx/wr/webrender/src/intern.rs
gfx/wr/webrender/src/lib.rs
gfx/wr/webrender/src/picture.rs
gfx/wr/webrender/src/prim_store/mod.rs
--- a/gfx/wr/tileview/src/main.rs
+++ b/gfx/wr/tileview/src/main.rs
@@ -1,22 +1,33 @@
 /* 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 http://mozilla.org/MPL/2.0/. */
 
+/// Command line tool to convert logged tile cache files into a visualization.
+///
+/// Steps to use this:
+/// 1. enable webrender; enable gfx.webrender.debug.tile-cache-logging
+/// 2. take a capture using ctrl-shift-3
+///    if all is well, there will be a .../wr-capture/tilecache folder with *.ron files
+/// 3. run tileview with that folder as the first parameter and some empty output folder as the
+///    2nd:
+///    cargo run --release -- /foo/bar/wr-capture/tilecache /tmp/tilecache
+/// 4. open /tmp/tilecache/index.html
+
 use webrender::{TileNode, TileNodeKind, InvalidationReason, TileOffset};
 use webrender::{TileSerializer, TileCacheInstanceSerializer, TileCacheLoggerUpdateLists};
+use webrender::{PrimitiveCompareResultDetail, CompareHelperResult, UpdateKind, ItemUid};
 use serde::Deserialize;
-//use ron::de::from_reader;
 use std::fs::File;
 use std::io::prelude::*;
 use std::path::Path;
 use std::ffi::OsString;
-use webrender::api::enumerate_interners;
-use webrender::UpdateKind;
+use std::collections::HashMap;
+use webrender::api::{enumerate_interners, ColorF};
 use euclid::{Rect, Transform3D};
 use webrender_api::units::{PicturePoint, PictureSize, PicturePixel, WorldPixel};
 
 static RES_JAVASCRIPT: &'static str = include_str!("tilecache.js");
 static RES_BASE_CSS: &'static str   = include_str!("tilecache_base.css");
 
 #[derive(Deserialize)]
 pub struct Slice {
@@ -29,58 +40,66 @@ static CSS_FRACTIONAL_OFFSET: &str      
 static CSS_BACKGROUND_COLOR: &str        = "fill:#10c070;fill-opacity:0.1;";
 static CSS_SURFACE_OPACITY_CHANNEL: &str = "fill:#c040c0;fill-opacity:0.1;";
 static CSS_NO_TEXTURE: &str              = "fill:#c04040;fill-opacity:0.1;";
 static CSS_NO_SURFACE: &str              = "fill:#40c040;fill-opacity:0.1;";
 static CSS_PRIM_COUNT: &str              = "fill:#40f0f0;fill-opacity:0.1;";
 static CSS_CONTENT: &str                 = "fill:#f04040;fill-opacity:0.1;";
 static CSS_COMPOSITOR_KIND_CHANGED: &str = "fill:#f0c070;fill-opacity:0.1;";
 
-fn tile_node_to_svg(node: &TileNode, transform: &Transform3D<f32, PicturePixel, WorldPixel>) -> String
+// parameters to tweak the SVG generation
+struct SvgSettings {
+    pub scale: f32,
+    pub x: f32,
+    pub y: f32,
+}
+
+fn tile_node_to_svg(node: &TileNode,
+                    transform: &Transform3D<f32, PicturePixel, WorldPixel>,
+                    svg_settings: &SvgSettings) -> String
 {
     match &node.kind {
         TileNodeKind::Leaf { .. } => {
             let rect_world = transform.transform_rect(&node.rect).unwrap();
             format!("<rect x=\"{:.2}\" y=\"{:.2}\" width=\"{:.2}\" height=\"{:.2}\" />\n",
-                    rect_world.origin.x,
-                    rect_world.origin.y,
-                    rect_world.size.width,
-                    rect_world.size.height)
+                    rect_world.origin.x    * svg_settings.scale + svg_settings.x,
+                    rect_world.origin.y    * svg_settings.scale + svg_settings.y,
+                    rect_world.size.width  * svg_settings.scale,
+                    rect_world.size.height * svg_settings.scale)
         },
         TileNodeKind::Node { children } => {
-            children.iter().fold(String::new(), |acc, child| acc + &tile_node_to_svg(child, transform) )
+            children.iter().fold(String::new(), |acc, child| acc + &tile_node_to_svg(child, transform, svg_settings) )
         }
     }
 }
 
 fn tile_to_svg(key: TileOffset,
                tile: &TileSerializer,
                slice: &Slice,
                prev_tile: Option<&TileSerializer>,
+               itemuid_to_string: &HashMap<ItemUid, String>,
                tile_stroke: &str,
                prim_class: &str,
                invalidation_report: &mut String,
-               svg_width: &mut i32, svg_height: &mut i32 ) -> String
+               svg_width: &mut i32, svg_height: &mut i32,
+               svg_settings: &SvgSettings) -> String
 {
     let mut svg = format!("\n<!-- tile key {},{} ; -->\n", key.x, key.y);
 
 
     let tile_fill =
         match tile.invalidation_reason {
-            Some(InvalidationReason::FractionalOffset) => CSS_FRACTIONAL_OFFSET.to_string(),
-            Some(InvalidationReason::BackgroundColor) => CSS_BACKGROUND_COLOR.to_string(),
-            Some(InvalidationReason::SurfaceOpacityChanged) => CSS_SURFACE_OPACITY_CHANNEL.to_string(),
+            Some(InvalidationReason::FractionalOffset { .. }) => CSS_FRACTIONAL_OFFSET.to_string(),
+            Some(InvalidationReason::BackgroundColor { .. }) => CSS_BACKGROUND_COLOR.to_string(),
+            Some(InvalidationReason::SurfaceOpacityChanged { .. }) => CSS_SURFACE_OPACITY_CHANNEL.to_string(),
             Some(InvalidationReason::NoTexture) => CSS_NO_TEXTURE.to_string(),
             Some(InvalidationReason::NoSurface) => CSS_NO_SURFACE.to_string(),
-            Some(InvalidationReason::PrimCount) => CSS_PRIM_COUNT.to_string(),
+            Some(InvalidationReason::PrimCount { .. }) => CSS_PRIM_COUNT.to_string(),
             Some(InvalidationReason::CompositorKindChanged) => CSS_COMPOSITOR_KIND_CHANGED.to_string(),
-            Some(InvalidationReason::Content { prim_compare_result } ) => {
-                let _foo = prim_compare_result;
-                CSS_CONTENT.to_string() //TODO do something with the compare result
-            }
+            Some(InvalidationReason::Content { .. } ) => CSS_CONTENT.to_string(),
             None => {
                 let mut background = tile.background_color;
                 if background.is_none() {
                     background = slice.tile_cache.background_color
                 }
                 match background {
                    Some(color) => {
                        let rgb = ( (color.r * 255.0) as u8,
@@ -98,33 +117,165 @@ fn tile_to_svg(key: TileOffset,
 
     let title = match tile.invalidation_reason {
         Some(_) => format!("<title>slice {} tile ({},{}) - {:?}</title>",
                             slice.tile_cache.slice, key.x, key.y,
                             tile.invalidation_reason),
         None => String::new()
     };
 
-    if let Some(reason) = tile.invalidation_reason {
+    if let Some(reason) = &tile.invalidation_reason {
         invalidation_report.push_str(
-            &format!("\n<tspan x=\"0\" dy=\"16px\">slice {} tile key ({},{}) invalidated: {:?}</tspan>\n",
-                     slice.tile_cache.slice, key.x, key.y, reason));
+            &format!("<div class=\"subheader\">slice {} key ({},{})</div><div class=\"data\">",
+                     slice.tile_cache.slice,
+                     key.x, key.y));
+
+        // go through most reasons individually so we can print something nicer than
+        // the default debug formatting of old and new:
+        match reason {
+            InvalidationReason::FractionalOffset { old, new } => {
+                invalidation_report.push_str(
+                    &format!("<b>FractionalOffset</b> changed from ({},{}) to ({},{})",
+                             old.x, old.y, new.x, new.y));
+            },
+            InvalidationReason::BackgroundColor { old, new } => {
+                fn to_str(c: &Option<ColorF>) -> String {
+                    if let Some(c) = c {
+                        format!("({},{},{},{})", c.r, c.g, c.b, c.a)
+                    } else {
+                        "none".to_string()
+                    }
+                }
+
+                invalidation_report.push_str(
+                    &format!("<b>BackGroundColor</b> changed from {} to {}",
+                             to_str(old), to_str(new)));
+            },
+            InvalidationReason::SurfaceOpacityChanged { became_opaque } => {
+                invalidation_report.push_str(
+                    &format!("<b>SurfaceOpacityChanged</b> changed from {} to {}",
+                             !became_opaque, became_opaque));
+            },
+            InvalidationReason::PrimCount { old, new } => {
+                // diff the lists to find removed and added ItemUids,
+                // and convert them to strings to pretty-print what changed:
+                let old = old.as_ref().unwrap();
+                let new = new.as_ref().unwrap();
+                let removed = old.iter()
+                                 .filter(|i| !new.contains(i))
+                                 .fold(String::new(),
+                                       |acc, i| acc + "<li>" + &(i.get_uid()).to_string() + "..."
+                                                    + &itemuid_to_string.get(i).unwrap_or(&String::new())
+                                                    + "</li>\n");
+                let added   = new.iter()
+                                 .filter(|i| !old.contains(i))
+                                 .fold(String::new(),
+                                       |acc, i| acc + "<li>" + &(i.get_uid()).to_string() + "..."
+                                                    + &itemuid_to_string.get(i).unwrap_or(&String::new())
+                                                    + "</li>\n");
+                invalidation_report.push_str(
+                    &format!("<b>PrimCount</b> changed from {} to {}:<br/>\
+                              removed:<ul>{}</ul>
+                              added:<ul>{}</ul>",
+                              old.len(), new.len(),
+                              removed, added));
+            },
+            InvalidationReason::Content { prim_compare_result, prim_compare_result_detail } => {
+                let _ = prim_compare_result;
+                match prim_compare_result_detail {
+                    Some(PrimitiveCompareResultDetail::Descriptor { old, new }) => {
+                        if old.prim_uid == new.prim_uid {
+                            // if the prim uid hasn't changed then try to print something useful
+                            invalidation_report.push_str(
+                                &format!("<b>Content: Descriptor</b> changed for uid {}<br/>",
+                                         old.prim_uid.get_uid()));
+                            let mut changes = String::new();
+                            if old.origin != new.origin {
+                                changes += &format!("<li><b>origin</b> changed from ({},{}) to ({},{})</li>",
+                                                    old.origin.x, old.origin.y,
+                                                    new.origin.x, new.origin.y);
+                            }
+                            if old.prim_clip_rect != new.prim_clip_rect {
+                                changes += &format!("<li><b>prim_clip_rect</b> changed from {}x{} at ({},{})",
+                                                    old.prim_clip_rect.w,
+                                                    old.prim_clip_rect.h,
+                                                    old.prim_clip_rect.x,
+                                                    old.prim_clip_rect.y);
+                                changes += &format!(" to {}x{} at ({},{})</li>",
+                                                    new.prim_clip_rect.w,
+                                                    new.prim_clip_rect.h,
+                                                    new.prim_clip_rect.x,
+                                                    new.prim_clip_rect.y);
+                            }
+                            invalidation_report.push_str(
+                                &format!("<ul>{}<li>Item: {}</li></ul>",
+                                             changes,
+                                             &itemuid_to_string.get(&old.prim_uid).unwrap_or(&String::new())));
+                        } else {
+                            // .. if prim UIDs have changed, just dump both items and descriptors.
+                            invalidation_report.push_str(
+                                &format!("<b>Content: Descriptor</b> changed; old uid {}, new uid {}:<br/>",
+                                             old.prim_uid.get_uid(),
+                                             new.prim_uid.get_uid()));
+                            invalidation_report.push_str(
+                                &format!("old:<ul><li>Desc: {:?}</li><li>Item: {}</li></ul>",
+                                             old,
+                                             &itemuid_to_string.get(&old.prim_uid).unwrap_or(&String::new())));
+                            invalidation_report.push_str(
+                                &format!("new:<ul><li>Desc: {:?}</li><li>Item: {}</li></ul>",
+                                             new,
+                                             &itemuid_to_string.get(&new.prim_uid).unwrap_or(&String::new())));
+                        }
+                    },
+                    Some(PrimitiveCompareResultDetail::Clip { detail }) => {
+                        match detail {
+                            CompareHelperResult::Count { prev_count, curr_count } => {
+                                invalidation_report.push_str(
+                                    &format!("<b>Content: Clip</b> count changed from {} to {}<br/>",
+                                             prev_count, curr_count ));
+                            },
+                            CompareHelperResult::NotEqual { prev, curr } => {
+                                invalidation_report.push_str(
+                                    &format!("<b>Content: Clip</b> ItemUids changed from {} to {}:<br/>",
+                                             prev.get_uid(), curr.get_uid() ));
+                                invalidation_report.push_str(
+                                    &format!("old:<ul><li>{}</li></ul>",
+                                             &itemuid_to_string.get(&prev).unwrap_or(&String::new())));
+                                invalidation_report.push_str(
+                                    &format!("new:<ul><li>{}</li></ul>",
+                                             &itemuid_to_string.get(&curr).unwrap_or(&String::new())));
+                            },
+                            reason => {
+                                invalidation_report.push_str(&format!("{:?}", reason));
+                            },
+                        }
+                    },
+                    reason => {
+                        invalidation_report.push_str(&format!("{:?}", reason));
+                    },
+                }
+            },
+            reason => {
+                invalidation_report.push_str(&format!("{:?}", reason));
+            },
+        }
+        invalidation_report.push_str("</div>\n");
     }
 
     svg = format!(r#"{}<rect x="{}" y="{}" width="{}" height="{}" style="{}" ></rect>"#,
             svg,
-            tile.rect.origin.x,
-            tile.rect.origin.y,
-            tile.rect.size.width,
-            tile.rect.size.height,
+            tile.rect.origin.x    * svg_settings.scale + svg_settings.x,
+            tile.rect.origin.y    * svg_settings.scale + svg_settings.y,
+            tile.rect.size.width  * svg_settings.scale,
+            tile.rect.size.height * svg_settings.scale,
             tile_style);
 
     svg = format!("{}\n\n<g class=\"svg_quadtree\">\n{}</g>\n",
                    svg,
-                   tile_node_to_svg(&tile.root, &slice.transform));
+                   tile_node_to_svg(&tile.root, &slice.transform, svg_settings));
 
     let right  = (tile.rect.origin.x + tile.rect.size.width) as i32;
     let bottom = (tile.rect.origin.y + tile.rect.size.height) as i32;
 
     *svg_width  = if right  > *svg_width  { right  } else { *svg_width  };
     *svg_height = if bottom > *svg_height { bottom } else { *svg_height };
 
     svg += "\n<!-- primitives -->\n";
@@ -152,50 +303,52 @@ fn tile_to_svg(key: TileOffset,
                 } else {
                     "class=\"svg_changed_prim\" "
                 }
             } else {
                 "class=\"svg_changed_prim\" "
             };
 
         svg += &format!("<rect x=\"{:.2}\" y=\"{:.2}\" width=\"{:.2}\" height=\"{:.2}\" {}/>",
-                        rect_world.origin.x,
-                        rect_world.origin.y,
-                        rect_world.size.width,
-                        rect_world.size.height,
+                        rect_world.origin.x    * svg_settings.scale + svg_settings.x,
+                        rect_world.origin.y    * svg_settings.scale + svg_settings.y,
+                        rect_world.size.width  * svg_settings.scale,
+                        rect_world.size.height * svg_settings.scale,
                         style);
 
         svg += "\n\t";
     }
 
     svg += "\n</g>\n";
 
     // nearly invisible, all we want is the toolip really
     let style = "style=\"fill-opacity:0.001;";
     svg += &format!("<rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" {}{}\" >{}<\u{2f}rect>",
-                    tile.rect.origin.x,
-                    tile.rect.origin.y,
-                    tile.rect.size.width,
-                    tile.rect.size.height,
+                    tile.rect.origin.x    * svg_settings.scale + svg_settings.x,
+                    tile.rect.origin.y    * svg_settings.scale + svg_settings.y,
+                    tile.rect.size.width  * svg_settings.scale,
+                    tile.rect.size.height * svg_settings.scale,
                     style,
                     tile_stroke,
                     title);
 
     svg
 }
 
 fn slices_to_svg(slices: &[Slice], prev_slices: Option<Vec<Slice>>,
+                 itemuid_to_string: &HashMap<ItemUid, String>,
                  svg_width: &mut i32, svg_height: &mut i32,
-                 max_slice_index: &mut usize) -> String
+                 max_slice_index: &mut usize,
+                 svg_settings: &SvgSettings) -> (String, String)
 {
     let svg_begin = "<?xml\u{2d}stylesheet type\u{3d}\"text/css\" href\u{3d}\"tilecache_base.css\" ?>\n\
                      <?xml\u{2d}stylesheet type\u{3d}\"text/css\" href\u{3d}\"tilecache.css\" ?>\n";
 
     let mut svg = String::new();
-    let mut invalidation_report = String::new();
+    let mut invalidation_report = "<div class=\"header\">Invalidation</div>\n".to_string();
 
     for slice in slices {
         let tile_cache = &slice.tile_cache;
         *max_slice_index = if tile_cache.slice > *max_slice_index { tile_cache.slice } else { *max_slice_index };
 
         let prim_class = format!("tile_slice{}", tile_cache.slice);
 
         //println!("slice {}", tile_cache.slice);
@@ -216,34 +369,36 @@ fn slices_to_svg(slices: &[Slice], prev_
         }
 
         for (key, tile) in &tile_cache.tiles {
             let mut prev_tile = None;
             if let Some(prev) = prev_slice {
                 prev_tile = prev.tile_cache.tiles.get(key);
             }
 
-            //println!("fofs  {:?}", tile.fract_offset);
-            //println!("id    {:?}", tile.id);
-            //println!("invr  {:?}", tile.invalidation_reason);
-            svg.push_str(&tile_to_svg(*key, &tile, &slice, prev_tile, &tile_stroke, &prim_class,
-                                      &mut invalidation_report, svg_width, svg_height));
+            svg.push_str(&tile_to_svg(*key, &tile, &slice, prev_tile,
+                                      itemuid_to_string,
+                                      &tile_stroke, &prim_class,
+                                      &mut invalidation_report,
+                                      svg_width, svg_height, svg_settings));
         }
     }
 
-    svg.push_str(&format!("<text x=\"0\" y=\"-8px\" class=\"svg_invalidated\">{}</text>\n", invalidation_report));
-
-    format!(r#"{}<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg" width="{}" height="{}" >"#,
-                svg_begin,
-                svg_width,
-                svg_height)
+    (
+        format!("{}<svg version=\"1.1\" baseProfile=\"full\" xmlns=\"http://www.w3.org/2000/svg\" \
+                width=\"{}\" height=\"{}\" >",
+                    svg_begin,
+                    svg_width,
+                    svg_height)
             + "\n"
             + "<rect fill=\"black\" width=\"100%\" height=\"100%\"/>\n"
             + &svg
-            + "\n</svg>\n"
+            + "\n</svg>\n",
+        invalidation_report
+    )
 }
 
 fn write_html(output_dir: &Path, svg_files: &[String], intern_files: &[String]) {
     let html_head = "<!DOCTYPE html>\n\
                      <html>\n\
                      <head>\n\
                      <meta charset=\"UTF-8\">\n\
                      <link rel=\"stylesheet\" type=\"text/css\" href=\"tilecache_base.css\"></link>\n\
@@ -264,17 +419,16 @@ fn write_html(output_dir: &Path, svg_fil
     script = format!("{}];\n\n", script);
 
     script = format!("{}var intern_files = [\n", script);
     for intern_file in intern_files {
         script = format!("{}    \"{}\",\n", script, intern_file);
     }
     script = format!("{}];\n</script>\n\n", script);
 
-    //TODO this requires copying the js file from somewhere?
     script = format!("{}<script src=\"tilecache.js\" type=\"text/javascript\"></script>\n\n", script);
 
 
     let html_end   = "</body>\n\
                       </html>\n"
                       .to_string();
 
     let html_body = format!(
@@ -305,142 +459,169 @@ fn write_html(output_dir: &Path, svg_fil
 
     let html = format!("{}{}{}{}", html_head, html_body, script, html_end);
 
     let output_file = output_dir.join("index.html");
     let mut html_output = File::create(output_file).unwrap();
     html_output.write_all(html.as_bytes()).unwrap();
 }
 
-fn write_css(output_dir: &Path, max_slice_index: usize) {
+fn write_css(output_dir: &Path, max_slice_index: usize, svg_settings: &SvgSettings) {
     let mut css = String::new();
 
     for ix in 0..max_slice_index + 1 {
         let color = ( ix % 7 ) + 1;
         let rgb = format!("rgb({},{},{})",
                             if color & 2 != 0 { 205 } else { 90 },
                             if color & 4 != 0 { 205 } else { 90 },
                             if color & 1 != 0 { 225 } else { 90 });
 
         let prim_class = format!("tile_slice{}", ix);
 
         css += &format!("#{} {{\n\
                            fill: {};\n\
                            fill-opacity: 0.03;\n\
-                           stroke-width: 0.8;\n\
+                           stroke-width: {};\n\
                            stroke: {};\n\
                         }}\n\n",
                         prim_class,
                         //rgb,
                         "none",
+                        0.8 * svg_settings.scale,
                         rgb);
     }
 
     let output_file = output_dir.join("tilecache.css");
     let mut css_output = File::create(output_file).unwrap();
     css_output.write_all(css.as_bytes()).unwrap();
 }
 
 macro_rules! updatelist_to_html_macro {
     ( $( $name:ident: $ty:ty, )+ ) => {
-        fn updatelist_to_html(update_lists: &TileCacheLoggerUpdateLists) -> String {
+        fn updatelist_to_html(update_lists: &TileCacheLoggerUpdateLists,
+                              invalidation_report: String) -> String
+        {
             let mut html = "\
                 <!DOCTYPE html>\n\
                 <html> <head> <meta charset=\"UTF-8\">\n\
                 <link rel=\"stylesheet\" type=\"text/css\" href=\"tilecache_base.css\"></link>\n\
                 <link rel=\"stylesheet\" type=\"text/css\" href=\"tilecache.css\"></link>\n\
-                </head> <body>\n".to_string();
+                </head> <body>\n\
+                <div class=\"datasheet\">\n".to_string();
 
+            html += &invalidation_report;
+
+            html += "<div class=\"header\">Interning</div>\n";
             $(
-                html += &format!("<div class=\"intern_header\">{}</div>\n<div class=\"intern_data\">\n",
+                html += &format!("<div class=\"subheader\">{}</div>\n<div class=\"intern data\">\n",
                                  stringify!($name));
-                let mut insert_count = 0;
-                for update in &update_lists.$name.1.updates {
-                    match update.kind {
-                        UpdateKind::Insert => {
-                            html += &format!("<div class=\"insert\">{} {}</div>\n",
-                                             update.index,
-                                             format!("({:?})", update_lists.$name.1.data[insert_count]));
-                            insert_count = insert_count + 1;
-                        }
-                        _ => {
-                            html += &format!("<div class=\"remove\">{}</div>\n",
-                                             update.index);
-                        }
-                    };
+                for list in &update_lists.$name.1 {
+                    let mut insert_count = 0;
+                    for update in &list.updates {
+                        match update.kind {
+                            UpdateKind::Insert => {
+                                html += &format!("<div class=\"insert\"><b>{}</b> {}</div>\n",
+                                                 update.uid.get_uid(),
+                                                 format!("({:?})", list.data[insert_count]));
+                                insert_count = insert_count + 1;
+                            }
+                            _ => {
+                                html += &format!("<div class=\"remove\"><b>{}</b></div>\n",
+                                                 update.uid.get_uid());
+                            }
+                        };
+                    }
                 }
                 html += "</div><br/>\n";
             )+
-            html += "</body> </html>\n";
+            html += "</div> </body> </html>\n";
             html
         }
     }
 }
 enumerate_interners!(updatelist_to_html_macro);
 
 fn write_tile_cache_visualizer_svg(entry: &std::fs::DirEntry, output_dir: &Path,
                                    slices: &[Slice], prev_slices: Option<Vec<Slice>>,
+                                   itemuid_to_string: &HashMap<ItemUid, String>,
                                    svg_width: &mut i32, svg_height: &mut i32,
                                    max_slice_index: &mut usize,
-                                   svg_files: &mut Vec::<String>)
+                                   svg_files: &mut Vec::<String>,
+                                   svg_settings: &SvgSettings) -> String
 {
-    let svg = slices_to_svg(&slices, prev_slices, svg_width, svg_height, max_slice_index);
+    let (svg, invalidation_report) = slices_to_svg(&slices, prev_slices,
+                                                   itemuid_to_string,
+                                                   svg_width, svg_height,
+                                                   max_slice_index,
+                                                   svg_settings);
 
     let mut output_filename = OsString::from(entry.path().file_name().unwrap());
     output_filename.push(".svg");
     svg_files.push(output_filename.to_string_lossy().to_string());
 
     output_filename = output_dir.join(output_filename).into_os_string();
     let mut svg_output = File::create(output_filename).unwrap();
     svg_output.write_all(svg.as_bytes()).unwrap();
+
+    invalidation_report
 }
 
 fn write_update_list_html(entry: &std::fs::DirEntry, output_dir: &Path,
                           update_lists: &TileCacheLoggerUpdateLists,
-                          html_files: &mut Vec::<String>)
+                          html_files: &mut Vec::<String>,
+                          invalidation_report: String)
 {
-    let html = updatelist_to_html(update_lists);
+    let html = updatelist_to_html(update_lists, invalidation_report);
 
     let mut output_filename = OsString::from(entry.path().file_name().unwrap());
     output_filename.push(".html");
     html_files.push(output_filename.to_string_lossy().to_string());
 
     output_filename = output_dir.join(output_filename).into_os_string();
     let mut html_output = File::create(output_filename).unwrap();
     html_output.write_all(html.as_bytes()).unwrap();
 }
 
 fn main() {
     let args: Vec<String> = std::env::args().collect();
 
-    if args.len() != 3 {
-        println!("Usage: tileview input_dir output_dir");
+    if args.len() < 3 {
+        println!("Usage: tileview input_dir output_dir [scale [x y]]");
         println!("    where input_dir is a tile_cache folder inside a wr-capture.");
+        println!("    Scale is an optional scaling factor to compensate for high-DPI.");
+        println!("    X, Y is an optional offset to shift the entire SVG by.");
         println!("\nexample: cargo run c:/Users/me/AppData/Local/wr-capture.6/tile_cache/ c:/temp/tilecache/");
         std::process::exit(1);
     }
 
     let input_dir = Path::new(&args[1]);
     let output_dir = Path::new(&args[2]);
     std::fs::create_dir_all(output_dir).unwrap();
 
+    let scale = if args.len() >= 4 { args[3].parse::<f32>().unwrap() } else { 1.0 };
+    let x     = if args.len() >= 6 { args[4].parse::<f32>().unwrap() } else { 0.0 }; // >= 6, requires X and Y
+    let y     = if args.len() >= 6 { args[5].parse::<f32>().unwrap() } else { 0.0 };
+    let svg_settings = SvgSettings { scale, x, y };
+
     let mut svg_width = 100i32;
     let mut svg_height = 100i32;
     let mut max_slice_index = 0;
 
     let mut entries: Vec<_> = std::fs::read_dir(input_dir).unwrap()
                                                           //.map(|r| r.unwrap())
                                                           .filter_map(|r| r.ok())
                                                           .collect();
     entries.sort_by_key(|dir| dir.path());
 
     let mut svg_files: Vec::<String> = Vec::new();
     let mut intern_files: Vec::<String> = Vec::new();
     let mut prev_slices = None;
 
+    let mut itemuid_to_string = HashMap::default();
+
     for entry in &entries {
         if entry.path().is_dir() {
             continue;
         }
         print!("processing {:?}\t", entry.path());
         let file_data = std::fs::read_to_string(entry.path()).unwrap();
         let chunks: Vec<_> = file_data.split("// @@@ chunk @@@").collect();
         let slices: Vec<Slice> = match ron::de::from_str(&chunks[0]) {
@@ -448,30 +629,34 @@ fn main() {
             Err(e) => {
                 println!("ERROR: failed to deserialize slicesg {:?}\n{:?}", entry.path(), e);
                 prev_slices = None;
                 continue;
             }
         };
         let mut update_lists = TileCacheLoggerUpdateLists::new();
         update_lists.from_ron(&chunks[1]);
+        update_lists.insert_in_lookup(&mut itemuid_to_string);
 
-        write_tile_cache_visualizer_svg(&entry, &output_dir,
-                                        &slices, prev_slices,
-                                        &mut svg_width, &mut svg_height,
-                                        &mut max_slice_index,
-                                        &mut svg_files);
+        let invalidation_report = write_tile_cache_visualizer_svg(
+                                    &entry, &output_dir,
+                                    &slices, prev_slices,
+                                    &itemuid_to_string,
+                                    &mut svg_width, &mut svg_height,
+                                    &mut max_slice_index,
+                                    &mut svg_files,
+                                    &svg_settings);
 
         write_update_list_html(&entry, &output_dir, &update_lists,
-                               &mut intern_files);
+                               &mut intern_files, invalidation_report);
 
         print!("\r");
         prev_slices = Some(slices);
     }
 
     write_html(output_dir, &svg_files, &intern_files);
-    write_css(output_dir, max_slice_index);
+    write_css(output_dir, max_slice_index, &svg_settings);
 
     std::fs::write(output_dir.join("tilecache.js"), RES_JAVASCRIPT).unwrap();
     std::fs::write(output_dir.join("tilecache_base.css"), RES_BASE_CSS).unwrap();
 
     println!("\n");
 }
--- a/gfx/wr/tileview/src/tilecache_base.css
+++ b/gfx/wr/tileview/src/tilecache_base.css
@@ -8,62 +8,77 @@
 	width: 100%;
 	height: 100%;
 	color: orange;
 	border: 0px;
 	overflow: auto;
 	background: white;
 }
 
-.intern_header {
+.datasheet .header {
+	color: white;
+	font-family: Arial;
+	font-weight: bold;
+	font-size: 150%;
+	line-height: 200%;
+	background-color: grey;
+	margin-top: 15px;
+	margin-bottom: 5px;
+	padding-left: 10px;
+}
+
+.datasheet .subheader {
 	color: blue;
 	font-family: Arial;
 	font-weight: bold;
 	line-height: 200%;
 	background-color: lightgrey;
 	margin-top: 5px;
 	margin-bottom: 5px;
 }
 
-.intern_data {
+.datasheet .data {
 	font-family: monospace;
-	font-size: small;
+	margin-bottom: 5px;
 }
 
-.intern_data .insert:nth-child(even) {
+.datasheet .data *:nth-child(even) {
 	background: #FFFFFF;
 }
-.intern_data .insert:nth-child(odd) {
+.datasheet .data *:nth-child(odd) {
 	background: #EFEFEF;
 }
 
-.intern_data .insert {
-	color: #008000;
+.datasheet .data .insert {
+	color: #006000;
 }
 
-.intern_data .remove {
-	color: #800000;
+.datasheet .data .remove {
+	color: #600000;
 }
 
+.datasheet .data .change {
+	color: #000060;
+}
 
 
 .split {
 	position: fixed;
 	z-index: 1;
 	top: 0;
 	padding-top: 14px;
 }
 
 .left {
 	left: 0;
 }
 
 .right {
 	right: 0;
-	width: 25%;
+	width: 30%;
 	height: 90%;
 }
 
 #svg_ui_overlay {
 	position:absolute;
 	right:0; 
 	top:0; 
 	z-index:70; 
--- a/gfx/wr/webrender/src/intern.rs
+++ b/gfx/wr/webrender/src/intern.rs
@@ -120,16 +120,17 @@ pub enum UpdateKind {
     Remove,
 }
 
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(MallocSizeOf)]
 pub struct Update {
     pub index: usize,
+    pub uid: ItemUid,
     pub kind: UpdateKind,
 }
 
 pub trait InternDebug {
     fn on_interned(&self, _uid: ItemUid) {}
 }
 
 /// The data store lives in the frame builder thread. It
@@ -256,28 +257,31 @@ impl<I: Internable> Interner<I> {
         // We need to intern a new data item. First, find out
         // if there is a spare slot in the free-list that we
         // can use. Otherwise, append to the end of the list.
         let index = match self.free_list.pop() {
             Some(index) => index,
             None => self.local_data.len(),
         };
 
+        let uid = ItemUid::next_uid();
+
         // Add a pending update to insert the new data.
         self.updates.push(Update {
             index,
+            uid,
             kind: UpdateKind::Insert,
         });
         self.update_data.alloc().init(data.clone());
 
         // Generate a handle for access via the data store.
         let handle = Handle {
             index: index as u32,
             epoch: self.current_epoch,
-            uid: ItemUid::next_uid(),
+            uid,
             _marker: PhantomData,
         };
 
         #[cfg(debug_assertions)]
         data.on_interned(handle.uid);
 
         // Store this handle so the next time it is
         // interned, it gets re-used.
@@ -311,16 +315,17 @@ impl<I: Internable> Interner<I> {
             if handle.epoch.0 + 10 < current_epoch {
                 // To expire an item:
                 //  - Add index to the free-list for re-use.
                 //  - Add an update to the data store to invalidate this slow.
                 //  - Remove from the hash map.
                 free_list.push(handle.index as usize);
                 updates.push(Update {
                     index: handle.index as usize,
+                    uid: handle.uid,
                     kind: UpdateKind::Remove,
                 });
                 return false;
             }
 
             true
         });
         let updates = UpdateList {
--- a/gfx/wr/webrender/src/lib.rs
+++ b/gfx/wr/webrender/src/lib.rs
@@ -216,11 +216,12 @@ pub use crate::renderer::{
     GraphicsApiInfo, PipelineInfo, Renderer, RendererError, RendererOptions, RenderResults,
     RendererStats, SceneBuilderHooks, ThreadListener, ShaderPrecacheFlags,
     MAX_VERTEX_TEXTURE_WIDTH,
 };
 pub use crate::screen_capture::{AsyncScreenshotHandle, RecordedFrameHandle};
 pub use crate::shade::{Shaders, WrShaders};
 pub use api as webrender_api;
 pub use webrender_build::shader::ProgramSourceDigest;
-pub use crate::picture::{TileDescriptor, TileId, InvalidationReason, PrimitiveCompareResult};
+pub use crate::picture::{TileDescriptor, TileId, InvalidationReason};
+pub use crate::picture::{PrimitiveCompareResult, PrimitiveCompareResultDetail, CompareHelperResult};
 pub use crate::picture::{TileNode, TileNodeKind, TileSerializer, TileCacheInstanceSerializer, TileOffset, TileCacheLoggerUpdateLists};
-pub use crate::intern::{Update,UpdateKind};
+pub use crate::intern::{Update, UpdateKind, ItemUid};
--- a/gfx/wr/webrender/src/picture.rs
+++ b/gfx/wr/webrender/src/picture.rs
@@ -108,16 +108,18 @@ use crate::texture_cache::TextureCacheHa
 use crate::util::{MaxRect, scale_factors, VecHelper, RectHelpers};
 use crate::filterdata::{FilterDataHandle};
 #[cfg(any(feature = "capture", feature = "replay"))]
 use ron;
 #[cfg(feature = "capture")]
 use crate::scene_builder_thread::InternerUpdates;
 #[cfg(any(feature = "capture", feature = "replay"))]
 use crate::intern::{Internable, UpdateList};
+#[cfg(any(feature = "replay"))]
+use crate::intern::{UpdateKind};
 #[cfg(any(feature = "capture", feature = "replay"))]
 use api::{ClipIntern, FilterDataIntern, PrimitiveKeyKind};
 #[cfg(any(feature = "capture", feature = "replay"))]
 use crate::prim_store::backdrop::Backdrop;
 #[cfg(any(feature = "capture", feature = "replay"))]
 use crate::prim_store::borders::{ImageBorder, NormalBorderPrim};
 #[cfg(any(feature = "capture", feature = "replay"))]
 use crate::prim_store::gradient::{LinearGradient, RadialGradient};
@@ -133,16 +135,20 @@ use crate::prim_store::text_run::TextRun
 #[cfg(feature = "capture")]
 use std::fs::File;
 #[cfg(feature = "capture")]
 use std::io::prelude::*;
 #[cfg(feature = "capture")]
 use std::path::PathBuf;
 use crate::scene_building::{SliceFlags};
 
+#[cfg(feature = "replay")]
+// used by tileview so don't use an internal_types FastHashMap
+use std::collections::HashMap;
+
 /// Specify whether a surface allows subpixel AA text rendering.
 #[derive(Debug, Copy, Clone, PartialEq)]
 pub enum SubpixelMode {
     /// This surface allows subpixel AA text
     Allow,
     /// Subpixel AA text cannot be drawn on this surface
     Deny,
 }
@@ -350,29 +356,31 @@ fn clamp(value: i32, low: i32, high: i32
 }
 
 fn clampf(value: f32, low: f32, high: f32) -> f32 {
     value.max(low).min(high)
 }
 
 /// An index into the prims array in a TileDescriptor.
 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub struct PrimitiveDependencyIndex(u32);
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PrimitiveDependencyIndex(pub u32);
 
 /// Information about the state of an opacity binding.
 #[derive(Debug)]
 pub struct OpacityBindingInfo {
     /// The current value retrieved from dynamic scene properties.
     value: f32,
     /// True if it was changed (or is new) since the last frame build.
     changed: bool,
 }
 
 /// Information stored in a tile descriptor for an opacity binding.
-#[derive(Debug, PartialEq, Clone)]
+#[derive(Debug, PartialEq, Clone, Copy)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub enum OpacityBinding {
     Value(f32),
     Binding(PropertyBindingId),
 }
 
 impl From<PropertyBinding<f32>> for OpacityBinding {
@@ -580,16 +588,42 @@ impl TileSurface {
         match *self {
             TileSurface::Color { .. } => "Color",
             TileSurface::Texture { .. } => "Texture",
             TileSurface::Clear => "Clear",
         }
     }
 }
 
+/// Optional extra information returned by is_same when
+/// logging is enabled.
+#[derive(Debug, Copy, Clone, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum CompareHelperResult<T> {
+    /// Primitives match
+    Equal,
+    /// Counts differ
+    Count {
+        prev_count: u8,
+        curr_count: u8,
+    },
+    /// Sentinel
+    Sentinel,
+    /// Two items are not equal
+    NotEqual {
+        prev: T,
+        curr: T,
+    },
+    /// User callback returned true on item
+    PredicateTrue {
+        curr: T
+    },
+}
+
 /// The result of a primitive dependency comparison. Size is a u8
 /// since this is a hot path in the code, and keeping the data small
 /// is a performance win.
 #[derive(Debug, Copy, Clone, PartialEq)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[repr(u8)]
 pub enum PrimitiveCompareResult {
@@ -602,37 +636,80 @@ pub enum PrimitiveCompareResult {
     /// The value of the transform changed
     Transform,
     /// An image dependency was dirty
     Image,
     /// The value of an opacity binding changed
     OpacityBinding,
 }
 
+/// A more detailed version of PrimitiveCompareResult used when
+/// debug logging is enabled.
+#[derive(Debug, Copy, Clone, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum PrimitiveCompareResultDetail {
+    /// Primitives match
+    Equal,
+    /// Something in the PrimitiveDescriptor was different
+    Descriptor {
+        old: PrimitiveDescriptor,
+        new: PrimitiveDescriptor,
+    },
+    /// The clip node content or spatial node changed
+    Clip {
+        detail: CompareHelperResult<ItemUid>,
+    },
+    /// The value of the transform changed
+    Transform {
+        detail: CompareHelperResult<SpatialNodeIndex>,
+    },
+    /// An image dependency was dirty
+    Image {
+        detail: CompareHelperResult<ImageDependency>,
+    },
+    /// The value of an opacity binding changed
+    OpacityBinding {
+        detail: CompareHelperResult<OpacityBinding>,
+    }
+}
+
 /// Debugging information about why a tile was invalidated
-#[derive(Debug,Copy,Clone)]
+#[derive(Debug,Clone)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub enum InvalidationReason {
     /// The fractional offset changed
-    FractionalOffset,
+    FractionalOffset {
+        old: PictureVector2D,
+        new: PictureVector2D,
+    },
     /// The background color changed
-    BackgroundColor,
+    BackgroundColor {
+        old: Option<ColorF>,
+        new: Option<ColorF>,
+    },
     /// The opaque state of the backing native surface changed
-    SurfaceOpacityChanged,
+    SurfaceOpacityChanged{
+        became_opaque: bool
+    },
     /// There was no backing texture (evicted or never rendered)
     NoTexture,
     /// There was no backing native surface (never rendered, or recreated)
     NoSurface,
     /// The primitive count in the dependency list was different
-    PrimCount,
+    PrimCount {
+        old: Option<Vec<ItemUid>>,
+        new: Option<Vec<ItemUid>>,
+    },
     /// The content of one of the primitives was different
     Content {
         /// What changed in the primitive that was different
         prim_compare_result: PrimitiveCompareResult,
+        prim_compare_result_detail: Option<PrimitiveCompareResultDetail>,
     },
     // The compositor type changed
     CompositorKindChanged,
 }
 
 /// A minimal subset of Tile for debug capturing
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
@@ -742,16 +819,17 @@ impl Tile {
     }
 
     /// Check if the content of the previous and current tile descriptors match
     fn update_dirty_rects(
         &mut self,
         ctx: &TilePostUpdateContext,
         state: &mut TilePostUpdateState,
         invalidation_reason: &mut Option<InvalidationReason>,
+        frame_context: &FrameVisibilityContext,
     ) -> PictureRect {
         let mut prim_comparer = PrimitiveComparer::new(
             &self.prev_descriptor,
             &self.current_descriptor,
             state.resource_cache,
             ctx.spatial_nodes,
             ctx.opacity_bindings,
         );
@@ -759,38 +837,41 @@ impl Tile {
         let mut dirty_rect = PictureRect::zero();
         self.root.update_dirty_rects(
             &self.prev_descriptor.prims,
             &self.current_descriptor.prims,
             &mut prim_comparer,
             &mut dirty_rect,
             state.compare_cache,
             invalidation_reason,
+            frame_context,
         );
 
         dirty_rect
     }
 
     /// Invalidate a tile based on change in content. This
     /// must be called even if the tile is not currently
     /// visible on screen. We might be able to improve this
     /// later by changing how ComparableVec is used.
     fn update_content_validity(
         &mut self,
         ctx: &TilePostUpdateContext,
         state: &mut TilePostUpdateState,
+        frame_context: &FrameVisibilityContext,
     ) {
         // Check if the contents of the primitives, clips, and
         // other dependencies are the same.
         state.compare_cache.clear();
         let mut invalidation_reason = None;
         let dirty_rect = self.update_dirty_rects(
             ctx,
             state,
             &mut invalidation_reason,
+            frame_context,
         );
         if !dirty_rect.is_empty() {
             self.invalidate(
                 Some(dirty_rect),
                 invalidation_reason.expect("bug: no invalidation_reason"),
             );
         }
     }
@@ -859,22 +940,26 @@ impl Tile {
             return;
         }
 
         // Determine if the fractional offset of the transform is different this frame
         // from the currently cached tile set.
         let fract_changed = (self.fract_offset.x - ctx.fract_offset.x).abs() > 0.01 ||
                             (self.fract_offset.y - ctx.fract_offset.y).abs() > 0.01;
         if fract_changed {
-            self.invalidate(None, InvalidationReason::FractionalOffset);
+            self.invalidate(None, InvalidationReason::FractionalOffset {
+                                    old: self.fract_offset,
+                                    new: ctx.fract_offset });
             self.fract_offset = ctx.fract_offset;
         }
 
         if ctx.background_color != self.background_color {
-            self.invalidate(None, InvalidationReason::BackgroundColor);
+            self.invalidate(None, InvalidationReason::BackgroundColor {
+                                    old: self.background_color,
+                                    new: ctx.background_color });
             self.background_color = ctx.background_color;
         }
 
         // Clear any dependencies so that when we rebuild them we
         // can compare if the tile has the same content.
         mem::swap(
             &mut self.current_descriptor,
             &mut self.prev_descriptor,
@@ -965,26 +1050,27 @@ impl Tile {
     }
 
     /// Called during tile cache instance post_update. Allows invalidation and dirty
     /// rect calculation after primitive dependencies have been updated.
     fn post_update(
         &mut self,
         ctx: &TilePostUpdateContext,
         state: &mut TilePostUpdateState,
+        frame_context: &FrameVisibilityContext,
     ) -> bool {
         // If tile is not visible, just early out from here - we don't update dependencies
         // so don't want to invalidate, merge, split etc. The tile won't need to be drawn
         // (and thus updated / invalidated) until it is on screen again.
         if !self.is_visible {
             return false;
         }
 
         // Invalidate the tile based on the content changing.
-        self.update_content_validity(ctx, state);
+        self.update_content_validity(ctx, state, frame_context);
 
         // If there are no primitives there is no need to draw or cache it.
         if self.current_descriptor.prims.is_empty() {
             // If there is a native compositor surface allocated for this (now empty) tile
             // it must be freed here, otherwise the stale tile with previous contents will
             // be composited. If the tile subsequently gets new primitives added to it, the
             // surface will be re-allocated when it's added to the composite draw list.
             if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { mut id, .. }, .. }) = self.surface.take() {
@@ -1121,24 +1207,24 @@ impl Tile {
         // Store the current surface backing info for use during batching.
         self.surface = Some(surface);
 
         true
     }
 }
 
 /// Defines a key that uniquely identifies a primitive instance.
-#[derive(Debug, Clone)]
+#[derive(Debug, Copy, Clone)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct PrimitiveDescriptor {
     /// Uniquely identifies the content of the primitive template.
-    prim_uid: ItemUid,
+    pub prim_uid: ItemUid,
     /// The origin in world space of this primitive.
-    origin: PointKey,
+    pub origin: PointKey,
     /// The clip rect for this primitive. Included here in
     /// dependencies since there is no entry in the clip chain
     /// dependencies for the local clip rect.
     pub prim_clip_rect: RectangleKey,
     /// The number of extra dependencies that this primitive has.
     transform_dep_count: u8,
     image_dep_count: u8,
     opacity_binding_dep_count: u8,
@@ -1173,24 +1259,24 @@ impl PartialEq for PrimitiveDescriptor {
             return false;
         }
 
         true
     }
 }
 
 /// A small helper to compare two arrays of primitive dependencies.
-struct CompareHelper<'a, T> {
+struct CompareHelper<'a, T> where T: Copy {
     offset_curr: usize,
     offset_prev: usize,
     curr_items: &'a [T],
     prev_items: &'a [T],
 }
 
-impl<'a, T> CompareHelper<'a, T> where T: PartialEq {
+impl<'a, T> CompareHelper<'a, T> where T: Copy + PartialEq {
     /// Construct a new compare helper for a current / previous set of dependency information.
     fn new(
         prev_items: &'a [T],
         curr_items: &'a [T],
     ) -> Self {
         CompareHelper {
             offset_curr: 0,
             offset_prev: 0,
@@ -1207,47 +1293,56 @@ impl<'a, T> CompareHelper<'a, T> where T
 
     /// Test if two sections of the dependency arrays are the same, by checking both
     /// item equality, and a user closure to see if the content of the item changed.
     fn is_same<F>(
         &self,
         prev_count: u8,
         curr_count: u8,
         f: F,
+        opt_detail: Option<&mut CompareHelperResult<T>>,
     ) -> bool where F: Fn(&T) -> bool {
         // If the number of items is different, trivial reject.
         if prev_count != curr_count {
+            if let Some(detail) = opt_detail { *detail = CompareHelperResult::Count{ prev_count, curr_count }; }
             return false;
         }
         // If both counts are 0, then no need to check these dependencies.
         if curr_count == 0 {
+            if let Some(detail) = opt_detail { *detail = CompareHelperResult::Equal; }
             return true;
         }
         // If both counts are u8::MAX, this is a sentinel that we can't compare these
         // deps, so just trivial reject.
         if curr_count as usize == MAX_PRIM_SUB_DEPS {
+            if let Some(detail) = opt_detail { *detail = CompareHelperResult::Sentinel; }
             return false;
         }
 
         let end_prev = self.offset_prev + prev_count as usize;
         let end_curr = self.offset_curr + curr_count as usize;
 
         let curr_items = &self.curr_items[self.offset_curr .. end_curr];
         let prev_items = &self.prev_items[self.offset_prev .. end_prev];
 
         for (curr, prev) in curr_items.iter().zip(prev_items.iter()) {
             if prev != curr {
+                if let Some(detail) = opt_detail {
+                    *detail = CompareHelperResult::NotEqual{ prev: *prev, curr: *curr };
+                }
                 return false;
             }
 
             if f(curr) {
+                if let Some(detail) = opt_detail { *detail = CompareHelperResult::PredicateTrue{ curr: *curr }; }
                 return false;
             }
         }
 
+        if let Some(detail) = opt_detail { *detail = CompareHelperResult::Equal; }
         true
     }
 
     // Advance the prev dependency array by a given amount
     fn advance_prev(&mut self, count: u8) {
         self.offset_prev += count as usize;
     }
 
@@ -1547,80 +1642,110 @@ macro_rules! declare_tile_cache_logger_u
             $(
                 /// Generate storage, one per interner.
                 /// the tuple is a workaround to avoid the need for multiple
                 /// fields that start with $name (macro concatenation).
                 /// the string is .ron serialized updatelist at capture time;
                 /// the updates is the list of DataStore updates (avoid UpdateList
                 /// due to Default() requirements on the Keys) reconstructed at
                 /// load time.
-                pub $name: (String, UpdateList<<$ty as Internable>::Key>),
+                pub $name: (Vec<String>, Vec<UpdateList<<$ty as Internable>::Key>>),
             )+
         }
 
         impl TileCacheLoggerUpdateLists {
             pub fn new() -> Self {
                 TileCacheLoggerUpdateLists {
                     $(
-                        $name : ( String::new(), UpdateList{ updates: Vec::new(), data: Vec::new()} ),
+                        $name : ( Vec::new(), Vec::new() ),
                     )+
                 }
             }
 
             /// serialize all interners in updates to .ron
             #[cfg(feature = "capture")]
             fn serialize_updates(
                 &mut self,
                 updates: &InternerUpdates
             ) {
                 $(
-                    self.$name.0 = ron::ser::to_string_pretty(&updates.$name, Default::default()).unwrap();
+                    self.$name.0.push(ron::ser::to_string_pretty(&updates.$name, Default::default()).unwrap());
                 )+
             }
 
             fn is_empty(&self) -> bool {
                 $(
                     if !self.$name.0.is_empty() { return false; }
                 )+
                 true
             }
 
             #[cfg(feature = "capture")]
             fn to_ron(&self) -> String {
                 let mut serializer =
                     TileCacheLoggerUpdateListsSerializer { ron_string: Vec::new() };
                 $(
                     serializer.ron_string.push(
-                        if self.$name.0.is_empty() {
-                            "( updates: [], data: [] )".to_string()
-                        } else {
-                            self.$name.0.clone()
-                        });
+                        ron::ser::to_string_pretty(&self.$name.0, Default::default()).unwrap());
                 )+
                 ron::ser::to_string_pretty(&serializer, Default::default()).unwrap()
             }
 
             #[cfg(feature = "replay")]
             pub fn from_ron(&mut self, text: &str) {
                 let serializer : TileCacheLoggerUpdateListsSerializer =
                     match ron::de::from_str(&text) {
                         Ok(data) => { data }
                         Err(e) => {
                             println!("ERROR: failed to deserialize updatelist: {:?}\n{:?}", &text, e);
                             return;
                         }
                     };
                 let mut index = 0;
                 $(
-                    self.$name.1 = ron::de::from_str(&serializer.ron_string[index]).unwrap();
+                    let ron_lists : Vec<String> = ron::de::from_str(&serializer.ron_string[index]).unwrap();
+                    self.$name.1 = ron_lists.iter()
+                                            .map( |list| ron::de::from_str(&list).unwrap() )
+                                            .collect();
                     index = index + 1;
                 )+
                 // error: value assigned to `index` is never read
                 let _ = index;
             }
+
+            /// helper method to add a stringified version of all interned keys into
+            /// a lookup table based on ItemUid.  Use strings as a form of type erasure
+            /// so all UpdateLists can go into a single map.
+            /// Then during analysis, when we see an invalidation reason due to
+            /// "ItemUid such and such was added to the tile primitive list", the lookup
+            /// allows mapping that back into something readable.
+            #[cfg(feature = "replay")]
+            pub fn insert_in_lookup(
+                        &mut self,
+                        itemuid_to_string: &mut HashMap<ItemUid, String>)
+            {
+                $(
+                    {
+                        for list in &self.$name.1 {
+                            let mut insert_count = 0;
+                            for update in &list.updates {
+                                match update.kind {
+                                    UpdateKind::Insert => {
+                                        itemuid_to_string.insert(
+                                            update.uid,
+                                            format!("{:?}", list.data[insert_count]));
+                                        insert_count = insert_count + 1;
+                                    },
+                                    _ => {}
+                                }
+                            }
+                        }
+                    }
+                )+
+            }
         }
     }
 }
 
 #[cfg(any(feature = "capture", feature = "replay"))]
 enumerate_interners!(declare_tile_cache_logger_updatelists);
 
 #[cfg(not(any(feature = "capture", feature = "replay")))]
@@ -2757,17 +2882,17 @@ impl TileCacheInstance {
             composite_state: frame_state.composite_state,
             compare_cache: &mut self.compare_cache,
         };
 
         // Step through each tile and invalidate if the dependencies have changed. Determine
         // the current opacity setting and whether it's changed.
         let mut tile_cache_is_opaque = true;
         for (key, tile) in self.tiles.iter_mut() {
-            if tile.post_update(&ctx, &mut state) {
+            if tile.post_update(&ctx, &mut state, frame_context) {
                 self.tiles_to_draw.push(*key);
                 tile_cache_is_opaque &= tile.is_opaque;
             }
         }
 
         // If opacity changed, the native compositor surface and all tiles get invalidated.
         // (this does nothing if not using native compositor mode).
         // TODO(gw): This property probably changes very rarely, so it is OK to invalidate
@@ -2780,17 +2905,18 @@ impl TileCacheInstance {
                 // Since the native surface will be destroyed, need to clear the compositor tile
                 // handle for all tiles. This means the tiles will be reallocated on demand
                 // when the tiles are added to render tasks.
                 for tile in self.tiles.values_mut() {
                     if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { ref mut id, .. }, .. }) = tile.surface {
                         *id = None;
                     }
                     // Invalidate the entire tile to force a redraw.
-                    tile.invalidate(None, InvalidationReason::SurfaceOpacityChanged);
+                    tile.invalidate(None, InvalidationReason::SurfaceOpacityChanged {
+                                            became_opaque: tile_cache_is_opaque });
                 }
                 // Destroy the compositor surface. It will be reallocated with the correct
                 // opacity flag when render tasks are generated for tiles.
                 frame_state.resource_cache.destroy_compositor_surface(native_surface_id);
             }
             self.is_opaque = tile_cache_is_opaque;
         }
 
@@ -5123,17 +5249,17 @@ struct PrimitiveComparisonKey {
     prev_index: PrimitiveDependencyIndex,
     curr_index: PrimitiveDependencyIndex,
 }
 
 /// Information stored an image dependency
 #[derive(Debug, Copy, Clone, PartialEq)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
-struct ImageDependency {
+pub struct ImageDependency {
     key: ImageKey,
     generation: ImageGeneration,
 }
 
 /// A helper struct to compare a primitive and all its sub-dependencies.
 struct PrimitiveComparer<'a> {
     clip_comparer: CompareHelper<'a, ItemUid>,
     transform_comparer: CompareHelper<'a, SpatialNodeIndex>,
@@ -5204,75 +5330,97 @@ impl<'a> PrimitiveComparer<'a> {
         self.opacity_comparer.advance_curr(prim.opacity_binding_dep_count);
     }
 
     /// Check if two primitive descriptors are the same.
     fn compare_prim(
         &mut self,
         prev: &PrimitiveDescriptor,
         curr: &PrimitiveDescriptor,
+        opt_detail: Option<&mut PrimitiveCompareResultDetail>,
     ) -> PrimitiveCompareResult {
         let resource_cache = self.resource_cache;
         let spatial_nodes = self.spatial_nodes;
         let opacity_bindings = self.opacity_bindings;
 
         // Check equality of the PrimitiveDescriptor
         if prev != curr {
+            if let Some(detail) = opt_detail {
+                *detail = PrimitiveCompareResultDetail::Descriptor{ old: *prev, new: *curr };
+            }
             return PrimitiveCompareResult::Descriptor;
         }
 
         // Check if any of the clips  this prim has are different.
+        let mut clip_result = CompareHelperResult::Equal;
         if !self.clip_comparer.is_same(
             prev.clip_dep_count,
             curr.clip_dep_count,
             |_| {
                 false
-            }
+            },
+            if opt_detail.is_some() { Some(&mut clip_result) } else { None }
         ) {
+            if let Some(detail) = opt_detail { *detail = PrimitiveCompareResultDetail::Clip{ detail: clip_result }; }
             return PrimitiveCompareResult::Clip;
         }
 
         // Check if any of the transforms  this prim has are different.
+        let mut transform_result = CompareHelperResult::Equal;
         if !self.transform_comparer.is_same(
             prev.transform_dep_count,
             curr.transform_dep_count,
             |curr| {
                 spatial_nodes[curr].changed
+            },
+            if opt_detail.is_some() { Some(&mut transform_result) } else { None },
+        ) {
+            if let Some(detail) = opt_detail {
+                *detail = PrimitiveCompareResultDetail::Transform{ detail: transform_result };
             }
-        ) {
             return PrimitiveCompareResult::Transform;
         }
 
         // Check if any of the images this prim has are different.
+        let mut image_result = CompareHelperResult::Equal;
         if !self.image_comparer.is_same(
             prev.image_dep_count,
             curr.image_dep_count,
             |curr| {
                 resource_cache.get_image_generation(curr.key) != curr.generation
+            },
+            if opt_detail.is_some() { Some(&mut image_result) } else { None },
+        ) {
+            if let Some(detail) = opt_detail {
+                *detail = PrimitiveCompareResultDetail::Image{ detail: image_result };
             }
-        ) {
             return PrimitiveCompareResult::Image;
         }
 
         // Check if any of the opacity bindings this prim has are different.
+        let mut bind_result = CompareHelperResult::Equal;
         if !self.opacity_comparer.is_same(
             prev.opacity_binding_dep_count,
             curr.opacity_binding_dep_count,
             |curr| {
                 if let OpacityBinding::Binding(id) = curr {
                     if opacity_bindings
                         .get(id)
                         .map_or(true, |info| info.changed) {
                         return true;
                     }
                 }
 
                 false
+            },
+            if opt_detail.is_some() { Some(&mut bind_result) } else { None },
+        ) {
+            if let Some(detail) = opt_detail {
+                *detail = PrimitiveCompareResultDetail::OpacityBinding{ detail: bind_result };
             }
-        ) {
             return PrimitiveCompareResult::OpacityBinding;
         }
 
         PrimitiveCompareResult::Equal
     }
 }
 
 /// Details for a node in a quadtree that tracks dirty rects for a tile.
@@ -5621,27 +5769,29 @@ impl TileNode {
     fn update_dirty_rects(
         &mut self,
         prev_prims: &[PrimitiveDescriptor],
         curr_prims: &[PrimitiveDescriptor],
         prim_comparer: &mut PrimitiveComparer,
         dirty_rect: &mut PictureRect,
         compare_cache: &mut FastHashMap<PrimitiveComparisonKey, PrimitiveCompareResult>,
         invalidation_reason: &mut Option<InvalidationReason>,
+        frame_context: &FrameVisibilityContext,
     ) {
         match self.kind {
             TileNodeKind::Node { ref mut children, .. } => {
                 for child in children.iter_mut() {
                     child.update_dirty_rects(
                         prev_prims,
                         curr_prims,
                         prim_comparer,
                         dirty_rect,
                         compare_cache,
                         invalidation_reason,
+                        frame_context,
                     );
                 }
             }
             TileNodeKind::Leaf { ref prev_indices, ref curr_indices, ref mut dirty_tracker, .. } => {
                 // If the index buffers are of different length, they must be different
                 if prev_indices.len() == curr_indices.len() {
                     let mut prev_i0 = 0;
                     let mut prev_i1 = 0;
@@ -5663,42 +5813,80 @@ impl TileNode {
 
                         // Compare the primitives, caching the result in a hash map
                         // to save comparisons in other tree nodes.
                         let key = PrimitiveComparisonKey {
                             prev_index: *prev_index,
                             curr_index: *curr_index,
                         };
 
+                        #[cfg(any(feature = "capture", feature = "replay"))]
+                        let mut compare_detail = PrimitiveCompareResultDetail::Equal;
+                        #[cfg(any(feature = "capture", feature = "replay"))]
+                        let prim_compare_result_detail =
+                            if frame_context.debug_flags.contains(DebugFlags::TILE_CACHE_LOGGING_DBG) {
+                                Some(&mut compare_detail)
+                            } else {
+                                None
+                            };
+
+                        #[cfg(not(any(feature = "capture", feature = "replay")))]
+                        let compare_detail = PrimitiveCompareResultDetail::Equal;
+                        #[cfg(not(any(feature = "capture", feature = "replay")))]
+                        let prim_compare_result_detail = None;
+
                         let prim_compare_result = *compare_cache
                             .entry(key)
                             .or_insert_with(|| {
                                 let prev = &prev_prims[i0];
                                 let curr = &curr_prims[i1];
-                                prim_comparer.compare_prim(prev, curr)
+                                prim_comparer.compare_prim(prev, curr, prim_compare_result_detail)
                             });
 
                         // If not the same, mark this node as dirty and update the dirty rect
                         if prim_compare_result != PrimitiveCompareResult::Equal {
                             if invalidation_reason.is_none() {
                                 *invalidation_reason = Some(InvalidationReason::Content {
                                     prim_compare_result,
+                                    prim_compare_result_detail: Some(compare_detail)
                                 });
                             }
                             *dirty_rect = self.rect.union(dirty_rect);
                             *dirty_tracker = *dirty_tracker | 1;
                             break;
                         }
 
                         prev_i0 = i0;
                         prev_i1 = i1;
                     }
                 } else {
                     if invalidation_reason.is_none() {
-                        *invalidation_reason = Some(InvalidationReason::PrimCount);
+                        // if and only if tile logging is enabled, do the expensive step of
+                        // converting indices back to ItemUids and allocating old and new vectors
+                        // to store them in.
+                        #[cfg(any(feature = "capture", feature = "replay"))]
+                        {
+                            if frame_context.debug_flags.contains(DebugFlags::TILE_CACHE_LOGGING_DBG) {
+                                let old = prev_indices.iter().map( |i| prev_prims[i.0 as usize].prim_uid ).collect();
+                                let new = curr_indices.iter().map( |i| curr_prims[i.0 as usize].prim_uid ).collect();
+                                *invalidation_reason = Some(InvalidationReason::PrimCount {
+                                                                old: Some(old),
+                                                                new: Some(new) });
+                            } else {
+                                *invalidation_reason = Some(InvalidationReason::PrimCount {
+                                                                old: None,
+                                                                new: None });
+                            }
+                        }
+                        #[cfg(not(any(feature = "capture", feature = "replay")))]
+                        {
+                            *invalidation_reason = Some(InvalidationReason::PrimCount {
+                                                                old: None,
+                                                                new: None });
+                        }
                     }
                     *dirty_rect = self.rect.union(dirty_rect);
                     *dirty_tracker = *dirty_tracker | 1;
                 }
             }
         }
     }
 }
--- a/gfx/wr/webrender/src/prim_store/mod.rs
+++ b/gfx/wr/webrender/src/prim_store/mod.rs
@@ -607,17 +607,17 @@ impl From<WorldVector2D> for VectorKey {
             y: vec.y,
         }
     }
 }
 
 /// A hashable point for using as a key during primitive interning.
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
-#[derive(Debug, Clone, MallocSizeOf, PartialEq)]
+#[derive(Debug, Copy, Clone, MallocSizeOf, PartialEq)]
 pub struct PointKey {
     pub x: f32,
     pub y: f32,
 }
 
 impl Eq for PointKey {}
 
 impl hash::Hash for PointKey {