Bug 1605283 - Improve support for invalidation debugging and testing r=gw
☠☠ backed out by 693c1fe229bc ☠ ☠
authorBert Peers <bpeers@mozilla.com>
Thu, 23 Jan 2020 06:22:26 +0000
changeset 511316 7ed018d13880bee244338bf6e1c65ab17d58a870
parent 511315 c0d7cf71074de0d9ccadb96de2d98dec211e6d2f
child 511317 693c1fe229bc143776742206a9c4338aa01ae5db
push id37047
push usermalexandru@mozilla.com
push dateThu, 23 Jan 2020 09:54:33 +0000
treeherdermozilla-central@a1669b599097 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgw
bugs1605283
milestone74.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1605283 - Improve support for invalidation debugging and testing r=gw Second part: trace the updates that are sent to the DataStore, and save at least the Insert/Remove and ItemUID as part of the wr-capture. (We could expand this with more info, eg. the actual Keys, later). TileView then reads them back and generates a color coded report to overlay with the page view. This helps to see the types and amounts of interned primitives that lead to cache invalidations. Differential Revision: https://phabricator.services.mozilla.com/D60619
gfx/wr/tileview/src/main.rs
gfx/wr/tileview/src/tilecache.js
gfx/wr/webrender/src/intern.rs
gfx/wr/webrender/src/lib.rs
gfx/wr/webrender/src/picture.rs
gfx/wr/webrender/src/render_backend.rs
--- a/gfx/wr/tileview/src/main.rs
+++ b/gfx/wr/tileview/src/main.rs
@@ -1,20 +1,22 @@
 /* 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/. */
 
 use webrender::{TileNode, TileNodeKind, InvalidationReason, TileOffset};
-use webrender::{TileSerializer, TileCacheInstanceSerializer};
+use webrender::{TileSerializer, TileCacheInstanceSerializer, TileCacheLoggerUpdateLists};
 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;
 
 #[derive(Deserialize)]
 pub struct Slice {
     pub x: f32,
     pub y: f32,
     pub tile_cache: TileCacheInstanceSerializer
 }
 
@@ -227,17 +229,17 @@ fn slices_to_svg(slices: &[Slice], prev_
                 svg_width,
                 svg_height)
             + "\n"
             + "<rect fill=\"black\" width=\"100%\" height=\"100%\"/>\n"
             + &svg
             + "\n</svg>\n"
 }
 
-fn write_html(output_dir: &Path, svg_files: &[String]) {
+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.css\"></link>\n\
                      </head>\n"
                      .to_string();
 
@@ -246,76 +248,120 @@ fn write_html(output_dir: &Path, svg_fil
 
 
     let mut script = "\n<script>\n".to_string();
 
     script = format!("{}var svg_files = [\n", script);
     for svg_file in svg_files {
         script = format!("{}    \"{}\",\n", script, svg_file);
     }
+    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!(
         "{}\n\
-        <object id=\"svg_container0\" type=\"image/svg+xml\" data=\"{}\" class=\"tile_svg\" ></object>\n\
-        <object id=\"svg_container1\" type=\"image/svg+xml\" data=\"{}\" class=\"tile_svg\" ></object>\n\
+        <div class=\"split left\">\n\
+            <div>\n\
+                <object id=\"svg_container0\" type=\"image/svg+xml\" data=\"{}\" class=\"tile_svg\" ></object>\n\
+                <object id=\"svg_container1\" type=\"image/svg+xml\" data=\"{}\" class=\"tile_svg\" ></object>\n\
+            </div>\n\
+        </div>\n\
+        \n\
+        <div class=\"split right\">\n\
+            <iframe width=\"100%\" id=\"intern\" src=\"{}\"></iframe>\n\
+        </div>\n\
+        \n\
         <div id=\"svg_ui_overlay\">\n\
             <div id=\"text_frame_counter\">{}</div>\n\
             <div id=\"text_spacebar\">Spacebar to Play</div>\n\
             <div>Use Left/Right to Step</div>\n\
             <input id=\"frame_slider\" type=\"range\" min=\"0\" max=\"{}\" value=\"0\" class=\"svg_ui_slider\" />
         </div>",
         html_body,
         svg_files[0],
         svg_files[0],
+        intern_files[0],
         svg_files[0],
         svg_files.len() );
 
     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) {
     let mut css = ".tile_svg {\n\
-                   position: fixed;\n\
-                   }\n\n\n\
+                    float: left;\n\
+                   }\n\
+                   \n\
+                   .split {\n\
+                      position: fixed;\n\
+                      z-index: 1;\n\
+                      top: 0;\n\
+                      padding-top: 20px;\n\
+                   }\n\
+                   \n\
+                   .left {\n\
+                      left: 0;\n\
+                   }\n\
+                   \n\
+                   .right {\n\
+                      right: 0;\n\
+                      width: 20%;\n\
+                      height: 100%;\n\
+                      opacity: 90%;\n\
+                   }\n\
+                   \n\
+                   #intern {\n\
+                    position:relative;\n\
+                    top:60px;\n\
+                    width: 100%;\n\
+                    height: 100%;\n\
+                    color: orange;\n\
+                    background-color:white;\n\
+                   }\n\
                    .svg_invalidated {\n\
-                    fill: white;\n\
-                    font-family:monospace;\n\
+                       fill: white;\n\
+                       font-family:monospace;\n\
                    }\n\n\n\
                    #svg_ui_overlay {\n\
-                     position:absolute;\n\
-                     right:0; \n\
-                     top:0; \n\
-                     z-index:70; \n\
-                     color: rgb(255,255,100);\n\
-                     font-family:monospace;\n\
-                     background-color: #404040a0;\n\
+                       position:absolute;\n\
+                       right:0; \n\
+                       top:0; \n\
+                       z-index:70; \n\
+                       color: rgb(255,255,100);\n\
+                       font-family:monospace;\n\
+                       background-color: #404040a0;\n\
                    }\n\n\n\
                    .svg_quadtree {\n\
-                    fill: none;\n\
-                    stroke-width: 1;\n\
-                    stroke: orange;\n\
+                       fill: none;\n\
+                       stroke-width: 1;\n\
+                       stroke: orange;\n\
                    }\n\n\n\
                    .svg_changed_prim {\n\
-                     stroke: red;\n\
-                     stroke-width: 2.0;\n\
+                       stroke: red;\n\
+                       stroke-width: 2.0;\n\
                    }\n\n\n\
                    #svg_ui_slider {\n\
-                     width:90%;\n\
+                       width:90%;\n\
                    }\n\n".to_string();
 
     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 });
@@ -336,16 +382,74 @@ fn write_css(output_dir: &Path, max_slic
                        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 {
+            let mut html = String::new();
+            $(
+                html += &format!("<h4 style=\"margin:5px;\">{}</h4>\n<font color=\"green\">\n", stringify!($name));
+                let mut was_insert = true;
+                for update in &update_lists.$name.1 {
+                    let is_insert = match update.kind {
+                        UpdateKind::Insert => true,
+                        _ => false
+                    };
+                    if was_insert != is_insert {
+                        html += &format!("</font><font color=\"{}\">", if is_insert { "green" } else { "red" });
+                    }
+                    html += &format!("{}, \n", update.index);
+                    was_insert = is_insert;
+                }
+                html += &"</font><hr/>\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>>,
+                                   svg_width: &mut i32, svg_height: &mut i32,
+                                   max_slice_index: &mut usize,
+                                   svg_files: &mut Vec::<String>)
+{
+    let svg = slices_to_svg(&slices, prev_slices, svg_width, svg_height, max_slice_index);
+
+    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();
+}
+
+fn write_update_list_html(entry: &std::fs::DirEntry, output_dir: &Path,
+                          update_lists: &TileCacheLoggerUpdateLists,
+                          html_files: &mut Vec::<String>)
+{
+    let html = updatelist_to_html(update_lists);
+
+    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");
         println!("    where input_dir is a tile_cache folder inside a wr-capture.");
         println!("\nexample: cargo run c:/Users/me/AppData/Local/wr-capture.6/tile_cache/ c:/temp/tilecache/");
         std::process::exit(1);
@@ -361,44 +465,47 @@ fn main() {
 
     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;
 
     for entry in &entries {
         if entry.path().is_dir() {
             continue;
         }
         print!("processing {:?}\t", entry.path());
-        let f = File::open(entry.path()).unwrap();
-        let slices: Vec<Slice> = match ron::de::from_reader(f) {
+        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]) {
             Ok(data) => { data }
             Err(e) => {
-                println!("ERROR: failed to deserialize {:?}\n{:?}", entry.path(), e);
+                println!("ERROR: failed to deserialize slicesg {:?}\n{:?}", entry.path(), e);
                 prev_slices = None;
                 continue;
             }
         };
-
-        let svg = slices_to_svg(&slices, prev_slices, &mut svg_width, &mut svg_height, &mut max_slice_index);
+        let mut update_lists = TileCacheLoggerUpdateLists::new();
+        update_lists.from_ron(&chunks[1]);
 
-        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());
+        write_tile_cache_visualizer_svg(&entry, &output_dir,
+                                        &slices, prev_slices,
+                                        &mut svg_width, &mut svg_height,
+                                        &mut max_slice_index,
+                                        &mut svg_files);
 
-        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();
+        write_update_list_html(&entry, &output_dir, &update_lists,
+                               &mut intern_files);
 
         print!("\r");
         prev_slices = Some(slices);
     }
 
-    write_html(output_dir, &svg_files);
+    write_html(output_dir, &svg_files, &intern_files);
     write_css(output_dir, max_slice_index);
 
     println!("OK. For now, manually copy tilecache.js to the output folder please.                           ");
 }
--- a/gfx/wr/tileview/src/tilecache.js
+++ b/gfx/wr/tileview/src/tilecache.js
@@ -70,16 +70,17 @@ function go_to_svg(index) {
     frontbuffer.style.display = 'none';
 
     var t = frontbuffer;
     frontbuffer = backbuffer;
     backbuffer = t;
     is_loading = false;
   }
   backbuffer.setAttribute('data', svg_files[svg_index]);
+  document.getElementById('intern').src = intern_files[svg_index];
 
   // also see https://stackoverflow.com/a/29915275
 }
 
 function load() {
   window.addEventListener('keypress', handle_keyboard_shortcut);
   window.addEventListener('keydown',  handle_keydown);
 
--- a/gfx/wr/webrender/src/intern.rs
+++ b/gfx/wr/webrender/src/intern.rs
@@ -47,17 +47,17 @@ use crate::util::VecHelper;
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(Debug, Copy, Clone, MallocSizeOf, PartialEq)]
 struct Epoch(u64);
 
 /// A list of updates to be applied to the data store,
 /// provided by the interning structure.
 pub struct UpdateList<S> {
     /// The additions and removals to apply.
-    updates: Vec<Update>,
+    pub updates: Vec<Update>,
     /// Actual new data to insert.
     data: Vec<S>,
 }
 
 lazy_static! {
     static ref NEXT_UID: AtomicUsize = AtomicUsize::new(0);
 }
 
@@ -117,18 +117,18 @@ pub enum UpdateKind {
     Insert,
     Remove,
 }
 
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(MallocSizeOf)]
 pub struct Update {
-    index: usize,
-    kind: UpdateKind,
+    pub index: usize,
+    pub kind: UpdateKind,
 }
 
 pub trait InternDebug {
     fn on_interned(&self, _uid: ItemUid) {}
 }
 
 /// The data store lives in the frame builder thread. It
 /// contains a free-list of items for fast access.
--- a/gfx/wr/webrender/src/lib.rs
+++ b/gfx/wr/webrender/src/lib.rs
@@ -217,9 +217,10 @@ pub use crate::renderer::{
     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::{TileNode, TileNodeKind, TileSerializer, TileCacheInstanceSerializer, TileOffset};
+pub use crate::picture::{TileNode, TileNodeKind, TileSerializer, TileCacheInstanceSerializer, TileOffset, TileCacheLoggerUpdateLists};
+pub use crate::intern::{Update,UpdateKind};
--- a/gfx/wr/webrender/src/picture.rs
+++ b/gfx/wr/webrender/src/picture.rs
@@ -104,16 +104,21 @@ use crate::scene::SceneProperties;
 use smallvec::SmallVec;
 use std::{mem, u8, marker, u32};
 use std::sync::atomic::{AtomicUsize, Ordering};
 use crate::texture_cache::TextureCacheHandle;
 use crate::util::{TransformedRectKind, MatrixHelpers, MaxRect, scale_factors, VecHelper, RectHelpers};
 use crate::filterdata::{FilterDataHandle};
 #[cfg(feature = "capture")]
 use ron;
+#[cfg(feature = "capture")]
+use crate::scene_builder_thread::InternerUpdates;
+#[cfg(feature = "capture")]
+use crate::intern::Update;
+
 
 #[cfg(feature = "capture")]
 use std::fs::File;
 #[cfg(feature = "capture")]
 use std::io::prelude::*;
 #[cfg(feature = "capture")]
 use std::path::PathBuf;
 
@@ -1488,65 +1493,196 @@ impl BackdropInfo {
 }
 
 #[derive(Clone)]
 pub struct TileCacheLoggerSlice {
     pub serialized_slice: String,
     pub local_to_world_transform: DeviceRect
 }
 
+#[cfg(any(feature = "capture", feature = "replay"))]
+macro_rules! declare_tile_cache_logger_updatelists {
+    ( $( $name:ident : $ty:ty, )+ ) => {
+        #[cfg_attr(feature = "capture", derive(Serialize))]
+        #[cfg_attr(feature = "replay", derive(Deserialize))]
+        struct TileCacheLoggerUpdateListsSerializer {
+            pub ron_string: Vec<String>,
+        }
+
+        pub struct TileCacheLoggerUpdateLists {
+            $(
+                /// 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, Vec<Update>),
+            )+
+        }
+
+        impl TileCacheLoggerUpdateLists {
+            pub fn new() -> Self {
+                TileCacheLoggerUpdateLists {
+                    $(
+                        $name : ( String::new(), Vec::<Update>::new() ),
+                    )+
+                }
+            }
+
+            /// serialize all interners in updates to .ron
+            fn serialize_updates(
+                &mut self,
+                updates: &InternerUpdates
+            ) {
+                $(
+                    self.$name.0 = ron::ser::to_string_pretty(&updates.$name.updates, 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() {
+                            "[]".to_string()
+                        } else {
+                            self.$name.0.clone()
+                        });
+                )+
+                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();
+                    index = index + 1;
+                )+
+                // error: value assigned to `index` is never read
+                let _ = index;
+            }
+        }
+    }
+}
+
+#[cfg(any(feature = "capture", feature = "replay"))]
+enumerate_interners!(declare_tile_cache_logger_updatelists);
+
+#[cfg(not(feature = "capture"))]
+#[derive(Clone)]
+pub struct TileCacheLoggerUpdateLists {
+}
+
+#[cfg(not(feature = "capture"))]
+impl TileCacheLoggerUpdateLists {
+    pub fn new() -> Self { TileCacheLoggerUpdateLists {} }
+    fn is_empty(&self) -> bool { true }
+}
+
+/// Log tile cache activity for one single frame.
+/// Also stores the commands sent to the interning data_stores
+/// so we can see which items were created or destroyed this frame,
+/// and correlate that with tile invalidation activity.
+pub struct TileCacheLoggerFrame {
+    /// slices in the frame, one per take_context call
+    pub slices: Vec<TileCacheLoggerSlice>,
+    /// interning activity
+    pub update_lists: TileCacheLoggerUpdateLists
+}
+
+impl TileCacheLoggerFrame {
+    pub fn new() -> Self {
+        TileCacheLoggerFrame {
+            slices: Vec::new(),
+            update_lists: TileCacheLoggerUpdateLists::new()
+        }
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.slices.is_empty() && self.update_lists.is_empty()
+    }
+}
+
 /// Log tile cache activity whenever anything happens in take_context.
 pub struct TileCacheLogger {
     /// next write pointer
     pub write_index : usize,
     /// ron serialization of tile caches;
-    /// each frame consists of slices, one per take_context call
-    pub frames: Vec<Vec<TileCacheLoggerSlice>>
+    pub frames: Vec<TileCacheLoggerFrame>
 }
 
 impl TileCacheLogger {
     pub fn new(
         num_frames: usize
     ) -> Self {
         let mut frames = Vec::with_capacity(num_frames);
-        let empty_element = Vec::new();
-        frames.resize(num_frames, empty_element);
+        for _i in 0..num_frames { // no Clone so no resize
+            frames.push(TileCacheLoggerFrame::new());
+        }
         TileCacheLogger {
             write_index: 0,
             frames
         }
     }
 
     pub fn is_enabled(&self) -> bool {
         !self.frames.is_empty()
     }
 
     #[cfg(feature = "capture")]
     pub fn add(&mut self, serialized_slice: String, local_to_world_transform: DeviceRect) {
         if !self.is_enabled() {
             return;
         }
-        self.frames[self.write_index].push(
+        self.frames[self.write_index].slices.push(
             TileCacheLoggerSlice {
                 serialized_slice,
                 local_to_world_transform });
     }
 
+    #[cfg(feature = "capture")]
+    pub fn serialize_updates(&mut self, updates: &InternerUpdates) {
+        if !self.is_enabled() {
+            return;
+        }
+        self.frames[self.write_index].update_lists.serialize_updates(updates);
+    }
+
     /// see if anything was written in this frame, and if so,
     /// advance the write index in a circular way and clear the
     /// recorded string.
     pub fn advance(&mut self) {
         if !self.is_enabled() || self.frames[self.write_index].is_empty() {
             return;
         }
         self.write_index = self.write_index + 1;
         if self.write_index >= self.frames.len() {
             self.write_index = 0;
         }
-        self.frames[self.write_index] = Vec::new();
+        self.frames[self.write_index] = TileCacheLoggerFrame::new();
     }
 
     #[cfg(feature = "capture")]
     pub fn save_capture(
         &self, root: &PathBuf
     ) {
         if !self.is_enabled() {
             return;
@@ -1566,29 +1702,36 @@ impl TileCacheLogger {
             // save_capture, we've add()ed entries but haven't advance()d yet,
             // so the actual oldest entry is write_index + 1
             let index = (self.write_index + 1 + ix) % self.frames.len();
             if self.frames[index].is_empty() {
                 continue;
             }
 
             let filename = path_tile_cache.join(format!("frame{:05}.ron", files_written));
-            files_written = files_written + 1;
             let mut output = File::create(filename).unwrap();
+            output.write_all(b"// slice data\n").unwrap();
             output.write_all(b"[\n").unwrap();
-            for item in &self.frames[index] {
+            for item in &self.frames[index].slices {
                 output.write_all(format!("( x: {}, y: {},\n",
                                          item.local_to_world_transform.origin.x,
                                          item.local_to_world_transform.origin.y)
                                  .as_bytes()).unwrap();
                 output.write_all(b"tile_cache:\n").unwrap();
                 output.write_all(item.serialized_slice.as_bytes()).unwrap();
                 output.write_all(b"\n),\n").unwrap();
             }
-            output.write_all(b"]\n").unwrap();
+            output.write_all(b"]\n\n").unwrap();
+
+            output.write_all(b"// @@@ chunk @@@\n\n").unwrap();
+
+            output.write_all(b"// interning data\n").unwrap();
+            output.write_all(self.frames[index].update_lists.to_ron().as_bytes()).unwrap();
+
+            files_written = files_written + 1;
         }
     }
 }
 
 /// Represents a cache of tiles that make up a picture primitives.
 pub struct TileCacheInstance {
     /// Index of the tile cache / slice for this frame builder. It's determined
     /// by the setup_picture_caching method during flattening, which splits the
--- a/gfx/wr/webrender/src/render_backend.rs
+++ b/gfx/wr/webrender/src/render_backend.rs
@@ -1461,16 +1461,22 @@ impl RenderBackend {
 
         let requires_frame_build = self.requires_frame_build();
         let doc = self.documents.get_mut(&document_id).unwrap();
         doc.has_built_scene |= has_built_scene;
 
         // If there are any additions or removals of clip modes
         // during the scene build, apply them to the data store now.
         if let Some(updates) = interner_updates {
+            #[cfg(feature = "capture")]
+            {
+                if self.debug_flags.contains(DebugFlags::TILE_CACHE_LOGGING_DBG) {
+                    self.tile_cache_logger.serialize_updates(&updates);
+                }
+            }
             doc.data_stores.apply_updates(updates, profile_counters);
         }
 
         // TODO: this scroll variable doesn't necessarily mean we scrolled. It is only used
         // for something wrench specific and we should remove it.
         let mut scroll = false;
         for frame_msg in frame_ops {
             let _timer = profile_counters.total_time.timer();