servo: Merge #17256 - Implement basic Time To First Paint and First Contentful Paint PWMs (from ferjm:ttfp); r=jdm
authorFernando Jiménez Moreno <ferjmoreno@gmail.com>
Thu, 20 Jul 2017 11:34:35 -0700
changeset 418741 3a683a5acc3ff0946f373f1ecfdf8dd290cf97fe
parent 418740 0e80f3606780c2d6ba84c70443ed9099099d6d8a
child 418742 b08d3ba207d5be19bed364f181ee23f3c71b644b
push id7566
push usermtabara@mozilla.com
push dateWed, 02 Aug 2017 08:25:16 +0000
treeherdermozilla-beta@86913f512c3c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjdm
milestone56.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
servo: Merge #17256 - Implement basic Time To First Paint and First Contentful Paint PWMs (from ferjm:ttfp); r=jdm - [X] `./mach build -d` does not report any errors - [X] `./mach test-tidy` does not report any errors Source-Repo: https://github.com/servo/servo Source-Revision: eba573d774dd2ac07ec8d62f1ad8deffca4667a4
servo/Cargo.lock
servo/components/config/opts.rs
servo/components/constellation/Cargo.toml
servo/components/constellation/lib.rs
servo/components/constellation/pipeline.rs
servo/components/layout_thread/Cargo.toml
servo/components/layout_thread/lib.rs
servo/components/layout_traits/Cargo.toml
servo/components/layout_traits/lib.rs
servo/components/metrics/Cargo.toml
servo/components/metrics/lib.rs
servo/components/profile/time.rs
servo/components/profile_traits/time.rs
servo/components/script/Cargo.toml
servo/components/script/lib.rs
servo/components/script/script_thread.rs
servo/components/script_layout_interface/Cargo.toml
servo/components/script_layout_interface/lib.rs
servo/components/script_layout_interface/message.rs
servo/components/script_traits/Cargo.toml
--- a/servo/Cargo.lock
+++ b/servo/Cargo.lock
@@ -517,16 +517,17 @@ dependencies = [
  "gaol 0.0.1 (git+https://github.com/servo/gaol)",
  "gfx 0.0.1",
  "gfx_traits 0.0.1",
  "hyper 0.10.10 (registry+https://github.com/rust-lang/crates.io-index)",
  "ipc-channel 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "itertools 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)",
  "layout_traits 0.0.1",
  "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "metrics 0.0.1",
  "msg 0.0.1",
  "net 0.0.1",
  "net_traits 0.0.1",
  "offscreen_gl_context 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "profile_traits 0.0.1",
  "script_traits 0.0.1",
  "serde 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "servo_config 0.0.1",
@@ -1496,16 +1497,17 @@ dependencies = [
  "gfx_traits 0.0.1",
  "heapsize 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "html5ever 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "ipc-channel 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "layout 0.0.1",
  "layout_traits 0.0.1",
  "lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "metrics 0.0.1",
  "msg 0.0.1",
  "net_traits 0.0.1",
  "parking_lot 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "profile_traits 0.0.1",
  "range 0.0.1",
  "rayon 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "script 0.0.1",
  "script_layout_interface 0.0.1",
@@ -1522,16 +1524,17 @@ dependencies = [
 ]
 
 [[package]]
 name = "layout_traits"
 version = "0.0.1"
 dependencies = [
  "gfx 0.0.1",
  "ipc-channel 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "metrics 0.0.1",
  "msg 0.0.1",
  "net_traits 0.0.1",
  "profile_traits 0.0.1",
  "script_traits 0.0.1",
  "servo_url 0.0.1",
  "webrender_api 0.48.0 (git+https://github.com/servo/webrender)",
 ]
 
@@ -1690,16 +1693,26 @@ version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "error-chain 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
  "toml 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
+name = "metrics"
+version = "0.0.1"
+dependencies = [
+ "gfx 0.0.1",
+ "profile_traits 0.0.1",
+ "servo_config 0.0.1",
+ "time 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
 name = "mime"
 version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -2448,16 +2461,17 @@ dependencies = [
  "hyper_serde 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "image 0.12.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "ipc-channel 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "js 0.1.6 (git+https://github.com/servo/rust-mozjs)",
  "jstraceable_derive 0.0.1",
  "lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "metrics 0.0.1",
  "mime 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "mime_guess 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "msg 0.0.1",
  "net_traits 0.0.1",
  "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
  "offscreen_gl_context 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "open 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "parking_lot 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -2507,16 +2521,17 @@ dependencies = [
  "euclid 0.15.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "gfx_traits 0.0.1",
  "heapsize 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "heapsize_derive 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "html5ever 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "ipc-channel 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "metrics 0.0.1",
  "msg 0.0.1",
  "net_traits 0.0.1",
  "profile_traits 0.0.1",
  "range 0.0.1",
  "script_traits 0.0.1",
  "selectors 0.19.0",
  "servo_arc 0.0.1",
  "servo_atoms 0.0.1",
@@ -2551,16 +2566,17 @@ dependencies = [
  "euclid 0.15.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "gfx_traits 0.0.1",
  "heapsize 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "heapsize_derive 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "hyper 0.10.10 (registry+https://github.com/rust-lang/crates.io-index)",
  "hyper_serde 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "ipc-channel 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
+ "metrics 0.0.1",
  "msg 0.0.1",
  "net_traits 0.0.1",
  "offscreen_gl_context 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "profile_traits 0.0.1",
  "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "servo_atoms 0.0.1",
  "servo_url 0.0.1",
--- a/servo/components/config/opts.rs
+++ b/servo/components/config/opts.rs
@@ -219,16 +219,19 @@ pub struct Opts {
     /// Print the version and exit.
     pub is_printing_version: bool,
 
     /// Path to SSL certificates.
     pub certificate_path: Option<String>,
 
     /// Unminify Javascript.
     pub unminify_js: bool,
+
+    /// Print Progressive Web Metrics to console.
+    pub print_pwm: bool,
 }
 
 fn print_usage(app: &str, opts: &Options) {
     let message = format!("Usage: {} [ options ... ] [URL]\n\twhere options include", app);
     println!("{}", opts.usage(&message));
 }
 
 
@@ -539,16 +542,17 @@ pub fn default_opts() -> Opts {
         is_printing_version: false,
         webrender_debug: false,
         webrender_record: false,
         webrender_batch: true,
         precache_shaders: false,
         signpost: false,
         certificate_path: None,
         unminify_js: false,
+        print_pwm: false,
     }
 }
 
 pub fn from_cmdline_args(args: &[String]) -> ArgumentParsingResult {
     let (app_name, args) = args.split_first().unwrap();
 
     let mut opts = Options::new();
     opts.optflag("c", "cpu", "CPU painting");
@@ -603,16 +607,17 @@ pub fn from_cmdline_args(args: &[String]
     opts.optopt("G", "graphics", "Select graphics backend (gl or es2)", "gl");
     opts.optopt("", "config-dir",
                     "config directory following xdg spec on linux platform", "");
     opts.optflag("v", "version", "Display servo version information");
     opts.optflag("", "unminify-js", "Unminify Javascript");
     opts.optopt("", "profiler-db-user", "Profiler database user", "");
     opts.optopt("", "profiler-db-pass", "Profiler database password", "");
     opts.optopt("", "profiler-db-name", "Profiler database name", "");
+    opts.optflag("", "print-pwm", "Print Progressive Web Metrics");
 
     let opt_match = match opts.parse(args) {
         Ok(m) => m,
         Err(f) => args_fail(&f.to_string()),
     };
 
     set_resources_path(opt_match.opt_str("resources-path"));
 
@@ -838,16 +843,17 @@ pub fn from_cmdline_args(args: &[String]
         is_printing_version: is_printing_version,
         webrender_debug: debug_options.webrender_debug,
         webrender_record: debug_options.webrender_record,
         webrender_batch: !debug_options.webrender_disable_batch,
         precache_shaders: debug_options.precache_shaders,
         signpost: debug_options.signpost,
         certificate_path: opt_match.opt_str("certificate-path"),
         unminify_js: opt_match.opt_present("unminify-js"),
+        print_pwm: opt_match.opt_present("print-pwm"),
     };
 
     set_defaults(opts);
 
     // These must happen after setting the default options, since the prefs rely on
     // on the resource path.
     // Note that command line preferences have the highest precedence
 
--- a/servo/components/constellation/Cargo.toml
+++ b/servo/components/constellation/Cargo.toml
@@ -21,16 +21,17 @@ devtools_traits = {path = "../devtools_t
 euclid = "0.15"
 gfx = {path = "../gfx"}
 gfx_traits = {path = "../gfx_traits"}
 hyper = "0.10"
 ipc-channel = "0.8"
 itertools = "0.5"
 layout_traits = {path = "../layout_traits"}
 log = "0.3.5"
+metrics = {path = "../metrics"}
 msg = {path = "../msg"}
 net = {path = "../net"}
 net_traits = {path = "../net_traits"}
 offscreen_gl_context = { version = "0.11", features = ["serde"] }
 profile_traits = {path = "../profile_traits"}
 script_traits = {path = "../script_traits"}
 serde = "1.0"
 style_traits = {path = "../style_traits"}
--- a/servo/components/constellation/lib.rs
+++ b/servo/components/constellation/lib.rs
@@ -21,16 +21,17 @@ extern crate gaol;
 extern crate gfx;
 extern crate gfx_traits;
 extern crate hyper;
 extern crate ipc_channel;
 extern crate itertools;
 extern crate layout_traits;
 #[macro_use]
 extern crate log;
+extern crate metrics;
 extern crate msg;
 extern crate net;
 extern crate net_traits;
 extern crate offscreen_gl_context;
 extern crate profile_traits;
 extern crate script_traits;
 #[macro_use] extern crate serde;
 extern crate servo_config;
--- a/servo/components/constellation/pipeline.rs
+++ b/servo/components/constellation/pipeline.rs
@@ -9,16 +9,17 @@ use compositing::compositor_thread::Msg 
 use devtools_traits::{DevtoolsControlMsg, ScriptToDevtoolsControlMsg};
 use euclid::{TypedSize2D, ScaleFactor};
 use event_loop::EventLoop;
 use gfx::font_cache_thread::FontCacheThread;
 use ipc_channel::Error;
 use ipc_channel::ipc::{self, IpcReceiver, IpcSender};
 use ipc_channel::router::ROUTER;
 use layout_traits::LayoutThreadFactory;
+use metrics::PaintTimeMetrics;
 use msg::constellation_msg::{BrowsingContextId, TopLevelBrowsingContextId, FrameType, PipelineId, PipelineNamespaceId};
 use net::image_cache::ImageCacheImpl;
 use net_traits::{IpcSend, ResourceThreads};
 use net_traits::image_cache::ImageCache;
 use profile_traits::mem as profile_mem;
 use profile_traits::time;
 use script_traits::{ConstellationControlMsg, DevicePixel, DiscardBrowsingContext};
 use script_traits::{DocumentActivity, InitialScriptState};
@@ -466,16 +467,17 @@ pub struct UnprivilegedPipelineContent {
 }
 
 impl UnprivilegedPipelineContent {
     pub fn start_all<Message, LTF, STF>(self, wait_for_completion: bool)
         where LTF: LayoutThreadFactory<Message=Message>,
               STF: ScriptThreadFactory<Message=Message>
     {
         let image_cache = Arc::new(ImageCacheImpl::new(self.webrender_api_sender.create_api()));
+        let paint_time_metrics = PaintTimeMetrics::new(self.time_profiler_chan.clone());
         let layout_pair = STF::create(InitialScriptState {
             id: self.id,
             browsing_context_id: self.browsing_context_id,
             top_level_browsing_context_id: self.top_level_browsing_context_id,
             parent_info: self.parent_info,
             control_chan: self.script_chan.clone(),
             control_port: self.script_port,
             constellation_chan: self.constellation_chan,
@@ -485,17 +487,17 @@ impl UnprivilegedPipelineContent {
             resource_threads: self.resource_threads,
             image_cache: image_cache.clone(),
             time_profiler_chan: self.time_profiler_chan.clone(),
             mem_profiler_chan: self.mem_profiler_chan.clone(),
             devtools_chan: self.devtools_chan,
             window_size: self.window_size,
             pipeline_namespace_id: self.pipeline_namespace_id,
             content_process_shutdown_chan: self.script_content_process_shutdown_chan,
-            webvr_thread: self.webvr_thread
+            webvr_thread: self.webvr_thread,
         }, self.load_data.clone());
 
         LTF::create(self.id,
                     self.top_level_browsing_context_id,
                     self.load_data.url,
                     self.parent_info.is_some(),
                     layout_pair,
                     self.pipeline_port,
@@ -503,17 +505,18 @@ impl UnprivilegedPipelineContent {
                     self.script_chan,
                     image_cache.clone(),
                     self.font_cache_thread,
                     self.time_profiler_chan,
                     self.mem_profiler_chan,
                     Some(self.layout_content_process_shutdown_chan),
                     self.webrender_api_sender,
                     self.prefs.get("layout.threads").expect("exists").value()
-                        .as_u64().expect("count") as usize);
+                        .as_u64().expect("count") as usize,
+                    paint_time_metrics);
 
         if wait_for_completion {
             let _ = self.script_content_process_shutdown_port.recv();
             let _ = self.layout_content_process_shutdown_port.recv();
         }
     }
 
     #[cfg(not(target_os = "windows"))]
--- a/servo/components/layout_thread/Cargo.toml
+++ b/servo/components/layout_thread/Cargo.toml
@@ -18,16 +18,17 @@ gfx = {path = "../gfx"}
 gfx_traits = {path = "../gfx_traits"}
 heapsize = "0.4"
 html5ever = "0.18"
 ipc-channel = "0.8"
 layout = {path = "../layout"}
 layout_traits = {path = "../layout_traits"}
 lazy_static = "0.2"
 log = "0.3.5"
+metrics = {path = "../metrics"}
 msg = {path = "../msg"}
 net_traits = {path = "../net_traits"}
 parking_lot = {version = "0.4", features = ["nightly"]}
 profile_traits = {path = "../profile_traits"}
 range = {path = "../range"}
 rayon = "0.8"
 script = {path = "../script"}
 script_layout_interface = {path = "../script_layout_interface"}
--- a/servo/components/layout_thread/lib.rs
+++ b/servo/components/layout_thread/lib.rs
@@ -22,16 +22,17 @@ extern crate html5ever;
 extern crate ipc_channel;
 #[macro_use]
 extern crate layout;
 extern crate layout_traits;
 #[macro_use]
 extern crate lazy_static;
 #[macro_use]
 extern crate log;
+extern crate metrics;
 extern crate msg;
 extern crate net_traits;
 extern crate parking_lot;
 #[macro_use]
 extern crate profile_traits;
 extern crate range;
 extern crate rayon;
 extern crate script;
@@ -78,16 +79,17 @@ use layout::query::{LayoutRPCImpl, Layou
 use layout::query::{process_margin_style_query, process_node_overflow_request, process_resolved_style_request};
 use layout::query::{process_node_geometry_request, process_node_scroll_area_request};
 use layout::query::{process_node_scroll_root_id_request, process_offset_parent_query};
 use layout::sequential;
 use layout::traversal::{ComputeAbsolutePositions, RecalcStyleAndConstructFlows};
 use layout::webrender_helpers::WebRenderDisplayListConverter;
 use layout::wrapper::LayoutNodeLayoutData;
 use layout_traits::LayoutThreadFactory;
+use metrics::{PaintTimeMetrics, ProfilerMetadataFactory};
 use msg::constellation_msg::PipelineId;
 use msg::constellation_msg::TopLevelBrowsingContextId;
 use net_traits::image_cache::{ImageCache, UsePlaceholder};
 use parking_lot::RwLock;
 use profile_traits::mem::{self, Report, ReportKind, ReportsChan};
 use profile_traits::time::{self, TimerMetadata, profile};
 use profile_traits::time::{TimerMetadataFrameType, TimerMetadataReflowType};
 use script_layout_interface::message::{Msg, NewLayoutThreadInfo, Reflow, ReflowQueryType};
@@ -243,17 +245,20 @@ pub struct LayoutThread {
     /// only be a test-mode timer during testing for animations.
     timer: Timer,
 
     // Number of layout threads. This is copied from `servo_config::opts`, but we'd
     // rather limit the dependency on that module here.
     layout_threads: usize,
 
     /// Which quirks mode are we rendering the document in?
-    quirks_mode: Option<QuirksMode>
+    quirks_mode: Option<QuirksMode>,
+
+    /// Paint time metrics.
+    paint_time_metrics: PaintTimeMetrics,
 }
 
 impl LayoutThreadFactory for LayoutThread {
     type Message = Msg;
 
     /// Spawns a new layout thread.
     fn create(id: PipelineId,
               top_level_browsing_context_id: TopLevelBrowsingContextId,
@@ -264,17 +269,18 @@ impl LayoutThreadFactory for LayoutThrea
               constellation_chan: IpcSender<ConstellationMsg>,
               script_chan: IpcSender<ConstellationControlMsg>,
               image_cache: Arc<ImageCache>,
               font_cache_thread: FontCacheThread,
               time_profiler_chan: time::ProfilerChan,
               mem_profiler_chan: mem::ProfilerChan,
               content_process_shutdown_chan: Option<IpcSender<()>>,
               webrender_api_sender: webrender_api::RenderApiSender,
-              layout_threads: usize) {
+              layout_threads: usize,
+              paint_time_metrics: PaintTimeMetrics) {
         thread::Builder::new().name(format!("LayoutThread {:?}", id)).spawn(move || {
             thread_state::initialize(thread_state::LAYOUT);
 
             // In order to get accurate crash reports, we install the top-level bc id.
             TopLevelBrowsingContextId::install(top_level_browsing_context_id);
 
             { // Ensures layout thread is destroyed before we send shutdown message
                 let sender = chan.0;
@@ -286,17 +292,18 @@ impl LayoutThreadFactory for LayoutThrea
                                                pipeline_port,
                                                constellation_chan,
                                                script_chan,
                                                image_cache.clone(),
                                                font_cache_thread,
                                                time_profiler_chan,
                                                mem_profiler_chan.clone(),
                                                webrender_api_sender,
-                                               layout_threads);
+                                               layout_threads,
+                                               paint_time_metrics);
 
                 let reporter_name = format!("layout-reporter-{}", id);
                 mem_profiler_chan.run_with_memory_reporting(|| {
                     layout.start();
                 }, reporter_name, sender, Msg::CollectReports);
             }
             if let Some(content_process_shutdown_chan) = content_process_shutdown_chan {
                 let _ = content_process_shutdown_chan.send(());
@@ -447,17 +454,18 @@ impl LayoutThread {
            pipeline_port: IpcReceiver<LayoutControlMsg>,
            constellation_chan: IpcSender<ConstellationMsg>,
            script_chan: IpcSender<ConstellationControlMsg>,
            image_cache: Arc<ImageCache>,
            font_cache_thread: FontCacheThread,
            time_profiler_chan: time::ProfilerChan,
            mem_profiler_chan: mem::ProfilerChan,
            webrender_api_sender: webrender_api::RenderApiSender,
-           layout_threads: usize)
+           layout_threads: usize,
+           paint_time_metrics: PaintTimeMetrics)
            -> LayoutThread {
         let device = Device::new(
             MediaType::Screen,
             opts::get().initial_window_size.to_f32() * ScaleFactor::new(1.0));
 
         let configuration =
             rayon::Configuration::new().num_threads(layout_threads);
         let parallel_traversal = if layout_threads > 1 {
@@ -546,16 +554,17 @@ impl LayoutThread {
                 if PREFS.get("layout.animations.test.enabled")
                            .as_boolean().unwrap_or(false) {
                    Timer::test_mode()
                 } else {
                     Timer::new()
                 },
             layout_threads: layout_threads,
             quirks_mode: None,
+            paint_time_metrics: paint_time_metrics,
         }
     }
 
     /// Starts listening on the port.
     fn start(mut self) {
         let rw_data = self.rw_data.clone();
         let mut possibly_locked_rw_data = Some(rw_data.lock().unwrap());
         let mut rw_data = RwData {
@@ -728,17 +737,20 @@ impl LayoutThread {
             Msg::PrepareToExit(response_chan) => {
                 self.prepare_to_exit(response_chan);
                 return false
             },
             Msg::ExitNow => {
                 debug!("layout: ExitNow received");
                 self.exit_now();
                 return false
-            }
+            },
+            Msg::SetNavigationStart(time) => {
+                self.paint_time_metrics.set_navigation_start(time);
+            },
         }
 
         true
     }
 
     fn collect_reports<'a, 'b>(&self,
                                reports_chan: ReportsChan,
                                possibly_locked_rw_data: &mut RwData<'a, 'b>) {
@@ -780,17 +792,18 @@ impl LayoutThread {
                              info.constellation_chan,
                              info.script_chan.clone(),
                              info.image_cache.clone(),
                              self.font_cache_thread.clone(),
                              self.time_profiler_chan.clone(),
                              self.mem_profiler_chan.clone(),
                              info.content_process_shutdown_chan,
                              self.webrender_api.clone_sender(),
-                             info.layout_threads);
+                             info.layout_threads,
+                             info.paint_time_metrics);
     }
 
     /// Enters a quiescent state in which no new messages will be processed until an `ExitNow` is
     /// received. A pong is immediately sent on the given response channel.
     fn prepare_to_exit(&mut self, response_chan: Sender<()>) {
         response_chan.send(()).unwrap();
         loop {
             match self.port.recv().unwrap() {
@@ -1015,16 +1028,22 @@ impl LayoutThread {
             let viewport_size = Size2D::new(self.viewport_size.width.to_f32_px(),
                                             self.viewport_size.height.to_f32_px());
 
             let mut epoch = self.epoch.get();
             epoch.next();
             self.epoch.set(epoch);
 
             let viewport_size = webrender_api::LayoutSize::from_untyped(&viewport_size);
+
+            // Set paint metrics if needed right before sending the display list to WebRender.
+            // XXX At some point, we may want to set this metric from WebRender itself.
+            self.paint_time_metrics.maybe_set_first_paint(self);
+            self.paint_time_metrics.maybe_set_first_contentful_paint(self, &display_list);
+
             self.webrender_api.set_display_list(
                 Some(get_root_flow_background_color(layout_root)),
                 webrender_api::Epoch(epoch.0),
                 viewport_size,
                 builder.finalize(),
                 true);
             self.webrender_api.generate_frame(None);
         });
@@ -1650,16 +1669,21 @@ impl LayoutThread {
                 TimerMetadataReflowType::FirstReflow
             } else {
                 TimerMetadataReflowType::Incremental
             },
         })
     }
 }
 
+impl ProfilerMetadataFactory for LayoutThread {
+    fn new_metadata(&self) -> Option<TimerMetadata> {
+        self.profiler_metadata()
+    }
+}
 
 // The default computed value for background-color is transparent (see
 // http://dev.w3.org/csswg/css-backgrounds/#background-color). However, we
 // need to propagate the background color from the root HTML/Body
 // element (http://dev.w3.org/csswg/css-backgrounds/#special-backgrounds) if
 // it is non-transparent. The phrase in the spec "If the canvas background
 // is not opaque, what shows through is UA-dependent." is handled by rust-layers
 // clearing the frame buffer to white. This ensures that setting a background
--- a/servo/components/layout_traits/Cargo.toml
+++ b/servo/components/layout_traits/Cargo.toml
@@ -7,14 +7,15 @@ publish = false
 
 [lib]
 name = "layout_traits"
 path = "lib.rs"
 
 [dependencies]
 gfx = {path = "../gfx"}
 ipc-channel = "0.8"
+metrics = {path = "../metrics"}
 msg = {path = "../msg"}
 net_traits = {path = "../net_traits"}
 profile_traits = {path = "../profile_traits"}
 script_traits = {path = "../script_traits"}
 servo_url = {path = "../url"}
 webrender_api = {git = "https://github.com/servo/webrender", features = ["ipc"]}
--- a/servo/components/layout_traits/lib.rs
+++ b/servo/components/layout_traits/lib.rs
@@ -1,30 +1,32 @@
 /* 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/. */
 
 #![deny(unsafe_code)]
 
 extern crate gfx;
 extern crate ipc_channel;
+extern crate metrics;
 extern crate msg;
 extern crate net_traits;
 extern crate profile_traits;
 extern crate script_traits;
 extern crate servo_url;
 extern crate webrender_api;
 
 // This module contains traits in layout used generically
 //   in the rest of Servo.
 // The traits are here instead of in layout so
 //   that these modules won't have to depend on layout.
 
 use gfx::font_cache_thread::FontCacheThread;
 use ipc_channel::ipc::{IpcReceiver, IpcSender};
+use metrics::PaintTimeMetrics;
 use msg::constellation_msg::PipelineId;
 use msg::constellation_msg::TopLevelBrowsingContextId;
 use net_traits::image_cache::ImageCache;
 use profile_traits::{mem, time};
 use script_traits::{ConstellationControlMsg, LayoutControlMsg};
 use script_traits::LayoutMsg as ConstellationMsg;
 use servo_url::ServoUrl;
 use std::sync::Arc;
@@ -43,10 +45,11 @@ pub trait LayoutThreadFactory {
               constellation_chan: IpcSender<ConstellationMsg>,
               script_chan: IpcSender<ConstellationControlMsg>,
               image_cache: Arc<ImageCache>,
               font_cache_thread: FontCacheThread,
               time_profiler_chan: time::ProfilerChan,
               mem_profiler_chan: mem::ProfilerChan,
               content_process_shutdown_chan: Option<IpcSender<()>>,
               webrender_api_sender: webrender_api::RenderApiSender,
-              layout_threads: usize);
+              layout_threads: usize,
+              paint_time_metrics: PaintTimeMetrics);
 }
new file mode 100644
--- /dev/null
+++ b/servo/components/metrics/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "metrics"
+version = "0.0.1"
+authors = ["The Servo Project Developers"]
+license = "MPL-2.0"
+publish = false
+
+[lib]
+name = "metrics"
+path = "lib.rs"
+
+[dependencies]
+gfx = {path = "../gfx"}
+profile_traits = {path = "../profile_traits"}
+servo_config = {path = "../config"}
+time = "0.1.12"
new file mode 100644
--- /dev/null
+++ b/servo/components/metrics/lib.rs
@@ -0,0 +1,113 @@
+/* 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/. */
+
+extern crate gfx;
+extern crate profile_traits;
+extern crate servo_config;
+extern crate time;
+
+use gfx::display_list::{DisplayItem, DisplayList};
+use profile_traits::time::{ProfilerChan, ProfilerCategory, send_profile_data};
+use profile_traits::time::TimerMetadata;
+use servo_config::opts;
+use std::cell::Cell;
+
+pub trait ProfilerMetadataFactory {
+    fn new_metadata(&self) -> Option<TimerMetadata>;
+}
+
+macro_rules! make_time_setter(
+    ( $attr:ident, $func:ident, $category:ident, $label:expr ) => (
+        fn $func<T>(&self, profiler_metadata_factory: &T)
+            where T: ProfilerMetadataFactory {
+            let navigation_start = match self.navigation_start {
+                Some(time) => time,
+                None => {
+                    println!("Trying to set metric before navigation start");
+                    return;
+                }
+            };
+
+            let now = time::precise_time_ns() as f64;
+            let time = now - navigation_start;
+            self.$attr.set(Some(time));
+
+            // Send the metric to the time profiler.
+            send_profile_data(ProfilerCategory::$category,
+                              profiler_metadata_factory.new_metadata(),
+                              &self.time_profiler_chan,
+                              time as u64, time as u64, 0, 0);
+
+            // Print the metric to console if the print-pwm option was given.
+            if opts::get().print_pwm {
+                println!("{:?} {:?}", $label, time);
+            }
+        }
+    );
+);
+
+pub struct PaintTimeMetrics {
+    navigation_start: Option<f64>,
+    first_paint: Cell<Option<f64>>,
+    first_contentful_paint: Cell<Option<f64>>,
+    time_profiler_chan: ProfilerChan,
+}
+
+impl PaintTimeMetrics {
+    pub fn new(time_profiler_chan: ProfilerChan)
+        -> PaintTimeMetrics {
+        PaintTimeMetrics {
+            navigation_start: None,
+            first_paint: Cell::new(None),
+            first_contentful_paint: Cell::new(None),
+            time_profiler_chan: time_profiler_chan,
+        }
+    }
+
+    pub fn set_navigation_start(&mut self, time: f64) {
+        self.navigation_start = Some(time);
+    }
+
+    make_time_setter!(first_paint, set_first_paint,
+                      TimeToFirstPaint,
+                      "first-paint");
+    make_time_setter!(first_contentful_paint, set_first_contentful_paint,
+                      TimeToFirstContentfulPaint,
+                      "first-contentful-paint");
+
+    pub fn maybe_set_first_paint<T>(&self, profiler_metadata_factory: &T)
+        where T: ProfilerMetadataFactory {
+        {
+            if self.first_paint.get().is_some() {
+                return;
+            }
+        }
+
+        self.set_first_paint(profiler_metadata_factory);
+    }
+
+    pub fn maybe_set_first_contentful_paint<T>(&self, profiler_metadata_factory: &T,
+                                               display_list: &DisplayList)
+        where T: ProfilerMetadataFactory {
+        {
+            if self.first_contentful_paint.get().is_some() {
+                return;
+            }
+        }
+
+        // Analyze display list to figure out if this is the first contentful
+        // paint (i.e. the display list contains items of type text, image,
+        // non-white canvas or SVG)
+        for item in &display_list.list {
+            match item {
+                &DisplayItem::Text(_) |
+                &DisplayItem::Image(_) => {
+                    self.set_first_contentful_paint(profiler_metadata_factory);
+                    return;
+                },
+                _ => (),
+            }
+        }
+    }
+}
--- a/servo/components/profile/time.rs
+++ b/servo/components/profile/time.rs
@@ -149,16 +149,18 @@ impl Formattable for ProfilerCategory {
             ProfilerCategory::ScriptStylesheetLoad => "Script Stylesheet Load",
             ProfilerCategory::ScriptWebSocketEvent => "Script Web Socket Event",
             ProfilerCategory::ScriptWorkerEvent => "Script Worker Event",
             ProfilerCategory::ScriptServiceWorkerEvent => "Script Service Worker Event",
             ProfilerCategory::ScriptEnterFullscreen => "Script Enter Fullscreen",
             ProfilerCategory::ScriptExitFullscreen => "Script Exit Fullscreen",
             ProfilerCategory::ScriptWebVREvent => "Script WebVR Event",
             ProfilerCategory::ScriptWorkletEvent => "Script Worklet Event",
+            ProfilerCategory::TimeToFirstPaint => "Time To First Paint",
+            ProfilerCategory::TimeToFirstContentfulPaint => "Time To First Contentful Paint",
             ProfilerCategory::ApplicationHeartbeat => "Application Heartbeat",
         };
         format!("{}{}", padding, name)
     }
 }
 
 type ProfilerBuckets = BTreeMap<(ProfilerCategory, Option<TimerMetadata>), Vec<f64>>;
 
--- a/servo/components/profile_traits/time.rs
+++ b/servo/components/profile_traits/time.rs
@@ -85,16 +85,18 @@ pub enum ProfilerCategory {
     ScriptWebSocketEvent = 0x73,
     ScriptWorkerEvent = 0x74,
     ScriptServiceWorkerEvent = 0x75,
     ScriptParseXML = 0x76,
     ScriptEnterFullscreen = 0x77,
     ScriptExitFullscreen = 0x78,
     ScriptWebVREvent = 0x79,
     ScriptWorkletEvent = 0x7a,
+    TimeToFirstPaint = 0x80,
+    TimeToFirstContentfulPaint = 0x81,
     ApplicationHeartbeat = 0x90,
 }
 
 #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)]
 pub enum TimerMetadataFrameType {
     RootWindow,
     IFrame,
 }
--- a/servo/components/script/Cargo.toml
+++ b/servo/components/script/Cargo.toml
@@ -51,16 +51,17 @@ hyper = "0.10"
 hyper_serde = "0.7"
 image = "0.12"
 ipc-channel = "0.8"
 js = {git = "https://github.com/servo/rust-mozjs", features = ["promises"]}
 jstraceable_derive = {path = "../jstraceable_derive"}
 lazy_static = "0.2"
 libc = "0.2"
 log = "0.3.5"
+metrics = {path = "../metrics"}
 mime = "0.2.1"
 mime_guess = "1.8.0"
 msg = {path = "../msg"}
 net_traits = {path = "../net_traits"}
 num-traits = "0.1.32"
 offscreen_gl_context = { version = "0.11", features = ["serde"] }
 open = "1.1.1"
 parking_lot = "0.4"
--- a/servo/components/script/lib.rs
+++ b/servo/components/script/lib.rs
@@ -59,16 +59,17 @@ extern crate ipc_channel;
 extern crate js;
 #[macro_use]
 extern crate jstraceable_derive;
 #[macro_use]
 extern crate lazy_static;
 extern crate libc;
 #[macro_use]
 extern crate log;
+extern crate metrics;
 #[macro_use]
 extern crate mime;
 extern crate mime_guess;
 extern crate msg;
 extern crate net_traits;
 extern crate num_traits;
 extern crate offscreen_gl_context;
 extern crate open;
--- a/servo/components/script/script_thread.rs
+++ b/servo/components/script/script_thread.rs
@@ -66,16 +66,17 @@ use hyper_serde::Serde;
 use ipc_channel::ipc::{self, IpcSender};
 use ipc_channel::router::ROUTER;
 use js::glue::GetWindowProxyClass;
 use js::jsapi::{JSAutoCompartment, JSContext, JS_SetWrapObjectCallbacks};
 use js::jsapi::{JSTracer, SetWindowProxyClass};
 use js::jsval::UndefinedValue;
 use js::rust::Runtime;
 use mem::heap_size_of_self_and_children;
+use metrics::PaintTimeMetrics;
 use microtask::{MicrotaskQueue, Microtask};
 use msg::constellation_msg::{BrowsingContextId, FrameType, PipelineId, PipelineNamespace, TopLevelBrowsingContextId};
 use net_traits::{FetchMetadata, FetchResponseListener, FetchResponseMsg};
 use net_traits::{Metadata, NetworkError, ReferrerPolicy, ResourceThreads};
 use net_traits::image_cache::{ImageCache, PendingImageResponse};
 use net_traits::request::{CredentialsMode, Destination, RedirectMode, RequestInit};
 use net_traits::storage_thread::StorageType;
 use profile_traits::mem::{self, OpaqueSender, Report, ReportKind, ReportsChan};
@@ -171,29 +172,31 @@ impl InProgressLoad {
            browsing_context_id: BrowsingContextId,
            top_level_browsing_context_id: TopLevelBrowsingContextId,
            parent_info: Option<(PipelineId, FrameType)>,
            layout_chan: Sender<message::Msg>,
            window_size: Option<WindowSizeData>,
            url: ServoUrl,
            origin: MutableOrigin) -> InProgressLoad {
         let current_time = get_time();
+        let navigation_start_precise = precise_time_ns() as f64;
+        layout_chan.send(message::Msg::SetNavigationStart(navigation_start_precise)).unwrap();
         InProgressLoad {
             pipeline_id: id,
             browsing_context_id: browsing_context_id,
             top_level_browsing_context_id: top_level_browsing_context_id,
             parent_info: parent_info,
             layout_chan: layout_chan,
             window_size: window_size,
             activity: DocumentActivity::FullyActive,
             is_visible: true,
             url: url,
             origin: origin,
             navigation_start: (current_time.sec * 1000 + current_time.nsec as i64 / 1000000) as u64,
-            navigation_start_precise: precise_time_ns() as f64,
+            navigation_start_precise: navigation_start_precise,
         }
     }
 }
 
 /// Encapsulated state required to create cancellable runnables from non-script threads.
 pub struct RunnableWrapper {
     pub cancelled: Option<Arc<AtomicBool>>,
 }
@@ -1448,16 +1451,17 @@ impl ScriptThread {
             is_parent: false,
             layout_pair: layout_pair,
             pipeline_port: pipeline_port,
             constellation_chan: self.layout_to_constellation_chan.clone(),
             script_chan: self.control_chan.clone(),
             image_cache: self.image_cache.clone(),
             content_process_shutdown_chan: content_process_shutdown_chan,
             layout_threads: layout_threads,
+            paint_time_metrics: PaintTimeMetrics::new(self.time_profiler_chan.clone()),
         });
 
         // Pick a layout thread, any layout thread
         let current_layout_chan = self.documents.borrow().iter().next()
             .map(|(_, document)| document.window().layout_chan().clone())
             .or_else(|| self.incomplete_loads.borrow().first().map(|load| load.layout_chan.clone()));
 
         match current_layout_chan {
--- a/servo/components/script_layout_interface/Cargo.toml
+++ b/servo/components/script_layout_interface/Cargo.toml
@@ -17,16 +17,17 @@ cssparser = "0.17.0"
 euclid = "0.15"
 gfx_traits = {path = "../gfx_traits"}
 heapsize = "0.4"
 heapsize_derive = "0.1"
 html5ever = "0.18"
 ipc-channel = "0.8"
 libc = "0.2"
 log = "0.3.5"
+metrics = {path = "../metrics"}
 msg = {path = "../msg"}
 net_traits = {path = "../net_traits"}
 profile_traits = {path = "../profile_traits"}
 range = {path = "../range"}
 script_traits = {path = "../script_traits"}
 selectors = { path = "../selectors" }
 servo_arc = {path = "../servo_arc"}
 servo_atoms = {path = "../atoms"}
--- a/servo/components/script_layout_interface/lib.rs
+++ b/servo/components/script_layout_interface/lib.rs
@@ -19,16 +19,17 @@ extern crate euclid;
 extern crate gfx_traits;
 extern crate heapsize;
 #[macro_use] extern crate heapsize_derive;
 #[macro_use] extern crate html5ever;
 extern crate ipc_channel;
 extern crate libc;
 #[macro_use]
 extern crate log;
+extern crate metrics;
 extern crate msg;
 extern crate net_traits;
 extern crate profile_traits;
 extern crate range;
 extern crate script_traits;
 extern crate selectors;
 extern crate servo_arc;
 extern crate servo_atoms;
--- a/servo/components/script_layout_interface/message.rs
+++ b/servo/components/script_layout_interface/message.rs
@@ -2,16 +2,17 @@
  * 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 {OpaqueStyleAndLayoutData, TrustedNodeAddress, PendingImage};
 use app_units::Au;
 use euclid::{Point2D, Rect};
 use gfx_traits::Epoch;
 use ipc_channel::ipc::{IpcReceiver, IpcSender};
+use metrics::PaintTimeMetrics;
 use msg::constellation_msg::PipelineId;
 use net_traits::image_cache::ImageCache;
 use profile_traits::mem::ReportsChan;
 use rpc::LayoutRPC;
 use script_traits::{ConstellationControlMsg, LayoutControlMsg, LayoutMsg as ConstellationMsg};
 use script_traits::{ScrollState, UntrustedNodeAddress, WindowSizeData};
 use script_traits::Painter;
 use servo_arc::Arc as ServoArc;
@@ -84,16 +85,19 @@ pub enum Msg {
     SetScrollStates(Vec<ScrollState>),
 
     /// Tells layout about a single new scrolling offset from the script. The rest will
     /// remain untouched and layout won't forward this back to script.
     UpdateScrollStateFromScript(ScrollState),
 
     /// Tells layout that script has added some paint worklet modules.
     RegisterPaint(Atom, Vec<Atom>, Arc<Painter>),
+
+    /// Send to layout the precise time when the navigation started.
+    SetNavigationStart(f64),
 }
 
 
 /// Any query to perform with this reflow.
 #[derive(Debug, PartialEq)]
 pub enum ReflowQueryType {
     NoQuery,
     ContentBoxQuery(TrustedNodeAddress),
@@ -153,9 +157,10 @@ pub struct NewLayoutThreadInfo {
     pub is_parent: bool,
     pub layout_pair: (Sender<Msg>, Receiver<Msg>),
     pub pipeline_port: IpcReceiver<LayoutControlMsg>,
     pub constellation_chan: IpcSender<ConstellationMsg>,
     pub script_chan: IpcSender<ConstellationControlMsg>,
     pub image_cache: Arc<ImageCache>,
     pub content_process_shutdown_chan: Option<IpcSender<()>>,
     pub layout_threads: usize,
+    pub paint_time_metrics: PaintTimeMetrics,
 }
--- a/servo/components/script_traits/Cargo.toml
+++ b/servo/components/script_traits/Cargo.toml
@@ -18,16 +18,17 @@ devtools_traits = {path = "../devtools_t
 euclid = "0.15"
 gfx_traits = {path = "../gfx_traits"}
 heapsize = "0.4"
 heapsize_derive = "0.1"
 hyper = "0.10"
 hyper_serde = "0.7"
 ipc-channel = "0.8"
 libc = "0.2"
+metrics = {path = "../metrics"}
 msg = {path = "../msg"}
 net_traits = {path = "../net_traits"}
 offscreen_gl_context = { version = "0.11", features = ["serde"] }
 profile_traits = {path = "../profile_traits"}
 rustc-serialize = "0.3.4"
 serde = "1.0"
 servo_atoms = {path = "../atoms"}
 servo_url = {path = "../url"}