Merge "Fix vtsc for TYPE_MASK."
diff --git a/create-image-angler.sh b/create-image-angler.sh
deleted file mode 100755
index 7b0d71a..0000000
--- a/create-image-angler.sh
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2016 Google Inc. All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-source create-image.sh
-
-vts_multidevice_create_image angler
diff --git a/create-image-angler_treble.sh b/create-image-angler_treble.sh
deleted file mode 100755
index 2e39ac6..0000000
--- a/create-image-angler_treble.sh
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2016 Google Inc. All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-source create-image.sh
-
-vts_multidevice_create_image angler ENABLE_TREBLE=true
diff --git a/create-image-bullhead.sh b/create-image-bullhead.sh
deleted file mode 100755
index a3afce4..0000000
--- a/create-image-bullhead.sh
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2016 Google Inc. All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-source create-image.sh
-
-vts_multidevice_create_image bullhead
diff --git a/create-image.sh b/create-image.sh
index 188a3f6..cc63ecc 100755
--- a/create-image.sh
+++ b/create-image.sh
@@ -18,6 +18,7 @@
   DEVICE=$1
 
   rm ${ANDROID_BUILD_TOP}/test/vts/testcases/hal/nfc/ -rf
+  rm ${ANDROID_BUILD_TOP}/test/vts/testcases/hal/sensors/ -rf
   rm ${ANDROID_BUILD_TOP}/test/vts/testcases/hal/vehicle/ -rf
   rm ${ANDROID_BUILD_TOP}/test/vts/testcases/hal/vibrator/ -rf
   rm ${ANDROID_BUILD_TOP}/test/vts/testcases/hal/vr/ -rf
@@ -26,6 +27,7 @@
   cd ${ANDROID_BUILD_TOP}; lunch ${DEVICE}-userdebug $2
   cd ${ANDROID_BUILD_TOP}/test/vts; mma -j 32 && cd ${ANDROID_BUILD_TOP}; make vts adb -j 32
   cp ${ANDROID_BUILD_TOP}/hardware/interfaces/nfc/1.0/vts/functional/vts/testcases/hal/nfc/ ${ANDROID_BUILD_TOP}/test/vts/testcases/hal/ -rf
+  cp ${ANDROID_BUILD_TOP}/hardware/interfaces/sensors/1.0/vts/functional/vts/testcases/hal/sensors/ ${ANDROID_BUILD_TOP}/test/vts/testcases/hal/ -rf
   cp ${ANDROID_BUILD_TOP}/hardware/interfaces/vehicle/2.0/vts/functional/vts/testcases/hal/vehicle/ ${ANDROID_BUILD_TOP}/test/vts/testcases/hal/ -rf
   cp ${ANDROID_BUILD_TOP}/hardware/interfaces/vibrator/1.0/vts/functional/vts/testcases/hal/vibrator/ ${ANDROID_BUILD_TOP}/test/vts/testcases/hal/ -rf
   cp ${ANDROID_BUILD_TOP}/hardware/interfaces/vr/1.0/vts/functional/vts/testcases/hal/vr/ ${ANDROID_BUILD_TOP}/test/vts/testcases/hal/ -rf
diff --git a/doc/testcase_develop_manual/run_vts_directly.md b/doc/testcase_develop_manual/run_vts_directly.md
index faf2572..997da85 100644
--- a/doc/testcase_develop_manual/run_vts_directly.md
+++ b/doc/testcase_develop_manual/run_vts_directly.md
@@ -6,31 +6,11 @@
 
 `$ cd test/vts`
 
-`$ ./create-image-<your build target>.sh`
-
-For angler_treble and bullhead, please run:
-
-`$ ./create-image-angler_treble.sh`
-
-and
-
-`$ ./create-image-bullhead.sh`
-
-respectively.
+`$ ./create-image.sh <your build target>`
 
 ## Copy Binaries
 
-`$ ./setup-<your build target>.sh`
-
-For angler_treble and bullhead, please run:
-
-`$ ./setup-angler_treble.sh`
-
-and
-
-`$ ./setup-bullhead.sh`
-
-respectively.
+`$ ./setup.sh <your build target>
 
 ## Run a test direclty
 
@@ -55,4 +35,4 @@
 
 Optionally, the command used to add a new test can be also added to:
 
-`test/vts/run-angler.sh`
+`test/vts/run-local.sh`
diff --git a/run-angler.sh b/run-local.sh
similarity index 100%
rename from run-angler.sh
rename to run-local.sh
diff --git a/run-unittest.sh b/run-unittest.sh
old mode 100644
new mode 100755
diff --git a/runners/host/base_test_with_webdb.py b/runners/host/base_test_with_webdb.py
index e13bf59..22d6648 100644
--- a/runners/host/base_test_with_webdb.py
+++ b/runners/host/base_test_with_webdb.py
@@ -248,13 +248,16 @@
             if self._current_test_report_msg:
                 self._current_test_report_msg.end_timestamp = test_end_time
                 if self._systrace_controller and self._systrace_controller.is_valid:
-                    if self.getUserParam(
-                            keys.ConfigKeys.IKEY_SYSTRACE_UPLAD_TO_DASHBOARD,
-                            default_value=False):
-                        try:
-                            systrace_msg = self._current_test_report_msg.systrace.add(
-                            )
-                            systrace_msg.process_name = self._systrace_controller.process_name
+                    systrace_msg = None
+                    try:
+                        systrace_msg = self._current_test_report_msg.systrace.add(
+                        )
+                        systrace_msg.process_name = self._systrace_controller.process_name
+
+                        if self.getUserParam(
+                                keys.ConfigKeys.
+                                IKEY_SYSTRACE_UPLAD_TO_DASHBOARD,
+                                default_value=False):
                             html = self._systrace_controller.ReadLastOutput()
                             if html is None:
                                 logging.error(
@@ -264,30 +267,47 @@
                                 logging.info(
                                     'Systrace html data added to report message. Length: %s',
                                     len(html))
-                            suc = self._systrace_controller.ClearLastOutput()
-                            if not suc:
-                                logging.error(
-                                    'failed to clear last systrace output.')
-                        except Exception as e:  # TODO(yuexima): more specific exceptions catch
-                            logging.error(
-                                'Failed to add systrace to resport message %s',
-                                e)
+                    except Exception as e:  # TODO(yuexima): more specific exceptions catch
+                        logging.exception(
+                            'Failed to add systrace to resport message %s', e)
 
                     report_path = self.getUserParam(
                         keys.ConfigKeys.IKEY_SYSTRACE_REPORT_PATH,
                         default_value=None)
                     if report_path:
-                        report_destination_file = os.path.join(
-                            report_path,
-                            '{module}_{test}_{process}_{time}.html'.format(
-                                module=self.test_module_name,
-                                test=test_name,
-                                process=self._systrace_controller.process_name,
-                                time=test_end_time))
+                        report_destination_file_name = '{module}_{test}_{process}_{time}.html'.format(
+                            module=self.test_module_name,
+                            test=test_name,
+                            process=self._systrace_controller.process_name,
+                            time=test_end_time)
+                        report_destination_file_path = os.path.join(
+                            report_path, report_destination_file_name)
                         self._systrace_controller.SaveLastOutput(
-                            report_destination_file)
+                            report_destination_file_path)
                         logging.info('Systrace output saved to %s',
-                                     report_destination_file)
+                                     report_destination_file_path)
+
+                        report_url_prefix = self.getUserParam(
+                            keys.ConfigKeys.IKEY_SYSTRACE_REPORT_URL_PREFIX,
+                            default_value=None)
+                        if report_url_prefix and systrace_msg:
+                            report_url_prefix = str(report_url_prefix)
+                            report_destination_file_url = '%s%s' % (
+                                report_url_prefix,
+                                report_destination_file_name)
+
+                            try:
+                                systrace_msg.url.append(
+                                    report_destination_file_url)
+                                logging.info(
+                                    'systrace result url %s added to protobuf message.',
+                                    report_destination_file_url)
+                            except Exception as e:  # TODO(yuexima): more specific exceptions catch
+                                logging.exception(
+                                    'failed to append systrace result url "%s" to proto message: %s',
+                                    (report_destination_file_url, e))
+                    if not self._systrace_controller.ClearLastOutput():
+                        logging.error('failed to clear last systrace output.')
             else:
                 logging.info(
                     "test result of '%s' is empty and will not be uploaded.",
@@ -478,10 +498,9 @@
             regression_mode: specifies the direction of change which indicates
                              performance regression.
         """
-        self.AddProfilingDataVector(name, labels, values,
-                                    ReportMsg.VTS_PROFILING_TYPE_LABELED_VECTOR,
-                                    options, x_axis_label, y_axis_label,
-                                    regression_mode)
+        self.AddProfilingDataVector(
+            name, labels, values, ReportMsg.VTS_PROFILING_TYPE_LABELED_VECTOR,
+            options, x_axis_label, y_axis_label, regression_mode)
 
     def AddProfilingDataUnlabeledVector(
             self,
@@ -502,10 +521,9 @@
             regression_mode: specifies the direction of change which indicates
                              performance regression.
         """
-        self.AddProfilingDataVector(name, None, values,
-                                    ReportMsg.VTS_PROFILING_TYPE_UNLABELED_VECTOR,
-                                    options, x_axis_label, y_axis_label,
-                                    regression_mode)
+        self.AddProfilingDataVector(
+            name, None, values, ReportMsg.VTS_PROFILING_TYPE_UNLABELED_VECTOR,
+            options, x_axis_label, y_axis_label, regression_mode)
 
     def AddProfilingDataTimestamp(
             self,
@@ -595,7 +613,8 @@
         try:
             build_client = artifact_fetcher.AndroidBuildClient(
                 service_json_path)
-        except Exception:
+        except Exception as e:
+            logging.exception('Failed to instantiate build client: %s', e)
             logging.error("Invalid service JSON file %s", service_json_path)
             return False
 
@@ -603,7 +622,8 @@
         try:
             revision_dict = build_client.GetRepoDictionary(
                 self.BRANCH, build_flavor, device_build_id)
-        except:
+        except Exception as e:
+            logging.exception('Failed to fetch repo dictionary: %s', e)
             logging.error("Could not read build info for branch: %s, " +
                           "target: %s, id: %s", self.BRANCH, build_flavor,
                           device_build_id)
@@ -615,7 +635,8 @@
                 build_client.GetCoverage("master", build_flavor,
                                          device_build_id, product))
             cov_zip = zipfile.ZipFile(cov_zip)
-        except:
+        except Exception as e:
+            logging.exception('Failed to fetch coverage zip: %s', e)
             logging.error("Could not read coverage zip for branch: %s, " +
                           "target: %s, id: %s, product: %s", self.BRANCH,
                           build_flavor, device_build_id, product)
@@ -672,7 +693,7 @@
             cov_zip = getattr(self, self.COVERAGE_ZIP)
             revision_dict = getattr(self, self.REVISION_DICT)
         except AttributeError as e:
-            logging.error("attributes not found %s", str(e))
+            logging.exception("attributes not found %s", e)
             return False
 
         if not self.IsCoverageConfigSpecified():
diff --git a/runners/host/keys.py b/runners/host/keys.py
index 43af264..300b2b3 100644
--- a/runners/host/keys.py
+++ b/runners/host/keys.py
@@ -69,6 +69,7 @@
     # Keys for systrace (for hal tests)
     IKEY_SYSTRACE_PROCESS_NAME = "systrace_process_name"
     IKEY_SYSTRACE_REPORT_PATH = "systrace_report_path"
+    IKEY_SYSTRACE_REPORT_URL_PREFIX = "systrace_report_url_prefix"
     IKEY_SYSTRACE_UPLAD_TO_DASHBOARD = "systrace_upload_to_dashboard"
 
     # Keys for coverage
diff --git a/setup-angler.sh b/setup-angler.sh
deleted file mode 100755
index e662263..0000000
--- a/setup-angler.sh
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2016 Google Inc. All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-source setup.sh
-
-vts_multidevice_target_setup angler
diff --git a/setup-angler_treble.sh b/setup-angler_treble.sh
deleted file mode 100755
index 98762d8..0000000
--- a/setup-angler_treble.sh
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2016 Google Inc. All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-source setup.sh
-
-vts_multidevice_target_setup angler_treble
diff --git a/setup-bullhead.sh b/setup-bullhead.sh
deleted file mode 100755
index c8512dd..0000000
--- a/setup-bullhead.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2016 Google Inc. All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-source setup.sh
-
-vts_multidevice_target_setup bullhead
-
diff --git a/setup.sh b/setup.sh
index 4be362f..959b119 100755
--- a/setup.sh
+++ b/setup.sh
@@ -48,8 +48,8 @@
   adb push ${ANDROID_BUILD_TOP}/out/host/linux-x86/vts/android-vts/testcases/DATA/lib64/libprotobuf-cpp-full.so /data/local/tmp/64/
 
   echo "install vts drivers for hidl"
-  adb push ${ANDROID_BUILD_TOP}/out/target/product/${DEVICE}/system/lib/android.hardware.*.vts.driver@1.0.so /data/local/tmp/32/
-  adb push ${ANDROID_BUILD_TOP}/out/target/product/${DEVICE}/system/lib64/android.hardware.*.vts.driver@1.0.so /data/local/tmp/64/
+  adb push ${ANDROID_BUILD_TOP}/out/target/product/${DEVICE}/system/lib/android.hardware.*.vts.driver@*.so /data/local/tmp/32/
+  adb push ${ANDROID_BUILD_TOP}/out/target/product/${DEVICE}/system/lib64/android.hardware.*.vts.driver@*.so /data/local/tmp/64/
 
   echo "install hal packages"
   adb shell mkdir -p /data/local/tmp/32/hw
@@ -80,12 +80,11 @@
   adb push ${ANDROID_BUILD_TOP}/test/vts/specification/hal_conventional/WifiHalV1.vts /data/local/tmp/spec/WifiHalV1.vts
   adb push ${ANDROID_BUILD_TOP}/test/vts/specification/hal_conventional/BluetoothHalV1.vts /data/local/tmp/spec/BluetoothHalV1.vts
   adb push ${ANDROID_BUILD_TOP}/test/vts/specification/hal_conventional/BluetoothHalV1bt_interface_t.vts /data/local/tmp/spec/BluetoothHalV1bt_interface_t.vts
-  adb push ${ANDROID_BUILD_TOP}/hardware/interfaces/nfc/1.0/vts/*.vts /data/local/tmp/spec/
-  adb push ${ANDROID_BUILD_TOP}/hardware/interfaces/vr/1.0/vts/Vr.vts /data/local/tmp/spec/Vr.vts
-  adb push ${ANDROID_BUILD_TOP}/hardware/interfaces/vehicle/2.0/vts/*.vts /data/local/tmp/spec/
-  # uncomment to test TV CEC HAL
+  # adb push ${ANDROID_BUILD_TOP}/hardware/interfaces/nfc/1.0/vts/*.vts /data/local/tmp/spec/
+  # adb push ${ANDROID_BUILD_TOP}/hardware/interfaces/vr/1.0/vts/Vr.vts /data/local/tmp/spec/Vr.vts
+  # adb push ${ANDROID_BUILD_TOP}/hardware/interfaces/vehicle/2.0/vts/*.vts /data/local/tmp/spec/
+  # adb push ${ANDROID_BUILD_TOP}/hardware/interfaces/sensors/1.0/vts/*.vts /data/local/tmp/spec/
   # adb push ${ANDROID_BUILD_TOP}/hardware/interfaces/tv/cec/1.0/vts/*.vts /data/local/tmp/spec/
-  # uncomment to test vibrator HAL
   # adb push ${ANDROID_BUILD_TOP}/hardware/interfaces/vibrator/1.0/vts/Vibrator.vts /data/local/tmp/spec/
   # adb push ${ANDROID_BUILD_TOP}/hardware/interfaces/vibrator/1.0/vts/types.vts /data/local/tmp/spec/
   adb push ${ANDROID_BUILD_TOP}/test/vts/specification/lib_bionic/libmV1.vts /data/local/tmp/spec/libmV1.vts
diff --git a/tools/build/tasks/list/vts_bin_package_list.mk b/tools/build/tasks/list/vts_bin_package_list.mk
index 06ed482..04ee73a 100644
--- a/tools/build/tasks/list/vts_bin_package_list.mk
+++ b/tools/build/tasks/list/vts_bin_package_list.mk
@@ -17,3 +17,4 @@
   vts_hal_agent \
   vtssysfuzzer \
   vts_shell_driver \
+  vts_profiling_configure \
diff --git a/tools/vts-tradefed/res/config/vts-serving-staging-hal-hidl-profiling-tv.xml b/tools/vts-tradefed/res/config/vts-serving-staging-hal-hidl-profiling-tv.xml
new file mode 100644
index 0000000..4ff26e8
--- /dev/null
+++ b/tools/vts-tradefed/res/config/vts-serving-staging-hal-hidl-profiling-tv.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 Google Inc.
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Run New VTS Tests for HIDL HALs in the Staging environment">
+    <include name="everything" />
+    <option name="plan" value="vts" />
+    <option name="compatibility:include-filter" value="TvCecHidlProfilingTest" />
+    <option name="compatibility:include-filter" value="TvInputHidlProfilingTest" />
+    <template-include name="reporters" default="basic-reporters" />
+</configuration>
diff --git a/tools/vts-tradefed/res/config/vts-serving-staging-hal-hidl-tv.xml b/tools/vts-tradefed/res/config/vts-serving-staging-hal-hidl-tv.xml
index 147792d..c341a9a 100644
--- a/tools/vts-tradefed/res/config/vts-serving-staging-hal-hidl-tv.xml
+++ b/tools/vts-tradefed/res/config/vts-serving-staging-hal-hidl-tv.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 Google Inc.
+<!-- Copyright (C) 2017 Google Inc.
 
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
diff --git a/utils/python/mirror/mirror_object.py b/utils/python/mirror/mirror_object.py
index e766b6f..51d0b12 100644
--- a/utils/python/mirror/mirror_object.py
+++ b/utils/python/mirror/mirror_object.py
@@ -619,5 +619,17 @@
         raise MirrorObjectError("unknown api name %s" % api_name)
 
     def GetRawCodeCoverage(self):
-      """Returns any measured raw code coverage data."""
-      return self._last_raw_code_coverage_data
+        """Returns any measured raw code coverage data."""
+        return self._last_raw_code_coverage_data
+
+    def __str__(self):
+        """Prints all the attributes and methods."""
+        result = ""
+        if self._if_spec_msg:
+            if self._if_spec_msg.attribute:
+                for attribute in self._if_spec_msg.attribute:
+                    result += "attribute %s\n" % attribute.name
+            if self._if_spec_msg.api:
+                for api in self._if_spec_msg.api:
+                    result += "api %s\n" % api.name
+        return result
diff --git a/utils/python/mirror/mirror_object_for_types.py b/utils/python/mirror/mirror_object_for_types.py
index 6932c4e..a4f2b54 100644
--- a/utils/python/mirror/mirror_object_for_types.py
+++ b/utils/python/mirror/mirror_object_for_types.py
@@ -143,7 +143,6 @@
                             if enumerator == type_name:
                                 return copy.copy(attribute)
             return None
-            return None
         except AttributeError as e:
             # TODO: check in advance whether self._if_spec_msg Interface
             # SpecificationMessage.
@@ -261,3 +260,16 @@
             return ConstGenerator()
 
         raise MirrorObjectError("unknown api name %s" % api_name)
+
+    def __str__(self):
+        """Prints all the attributes and methods."""
+        result = ""
+        if self._if_spec_msg:
+            if self._if_spec_msg.attribute:
+                for attribute in self._if_spec_msg.attribute:
+                    result += "global attribute %s\n" % attribute.name
+            if self._if_spec_msg.interface.attribute:
+                for attribute in self._if_spec_msg.interface.attribute:
+                    result += "interface attribute %s\n" % attribute.name
+        return result
+
diff --git a/utils/python/mirror/py2pb.py b/utils/python/mirror/py2pb.py
index 4eb99da..6a2e72b 100644
--- a/utils/python/mirror/py2pb.py
+++ b/utils/python/mirror/py2pb.py
@@ -194,13 +194,13 @@
 
     if pb_spec.type == CompSpecMsg.TYPE_STRUCT:
         PyDict2PbStruct(message, pb_spec, py_value)
-    elif attr.type == CompSpecMsg.TYPE_ENUM:
+    elif pb_spec.type == CompSpecMsg.TYPE_ENUM:
         PyValue2PbEnum(message, pb_spec, py_value)
-    elif attr.type == CompSpecMsg.TYPE_SCALAR:
+    elif pb_spec.type == CompSpecMsg.TYPE_SCALAR:
         PyValue2PbScalar(message, pb_spec, py_value)
-    elif attr.type == CompSpecMsg.TYPE_STRING:
+    elif pb_spec.type == CompSpecMsg.TYPE_STRING:
         PyStringPbString(attr_msg, attr, curr_value)
-    elif attr.type == CompSpecMsg.TYPE_VECTOR:
+    elif pb_spec.type == CompSpecMsg.TYPE_VECTOR:
         PyList2PbVector(message, pb_spec, py_value)
     else:
         logging.error("py2pb.Convert: unsupported type %s",
diff --git a/utils/python/systrace/systrace_controller.py b/utils/python/systrace/systrace_controller.py
index 4f5d6dc..1c8a54e 100644
--- a/utils/python/systrace/systrace_controller.py
+++ b/utils/python/systrace/systrace_controller.py
@@ -197,7 +197,7 @@
 
         if self._path_output:
             try:
-                shutil.rmtree(self._path_output)
+                shutil.rmtree(os.path.basename(self._path_output))
             except Exception as e:
                 logging.error('failed to remove systrace output file. %s', e)
                 return False
diff --git a/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/BaseServlet.java b/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/BaseServlet.java
index cfd2466..c62bd55 100644
--- a/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/BaseServlet.java
+++ b/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/BaseServlet.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright (c) 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you
+ * may not use this file except in compliance with the License. You may
+ * obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
 package com.android.vts.servlet;
 
 import com.google.appengine.api.users.User;
@@ -8,6 +24,7 @@
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import java.util.List;
 import java.util.logging.Logger;
 
 public abstract class BaseServlet extends HttpServlet {
@@ -21,7 +38,41 @@
 
     // Common constants
     protected static final long ONE_DAY = 86400000000L;  // units microseconds
+    protected static final long MILLI_TO_MICRO = 1000;  // conversion factor from milli to micro units
     protected static final String TABLE_PREFIX = "result_";
+    protected static final String CURRENT_PAGE = "#";
+
+    public enum Page {
+        HOME ("VTS Dashboard Home", "/"),
+        PREFERENCES ("Preferences", "/show_preferences"),
+        TABLE ("", "/show_table"),
+        GRAPH ("Profiling", "/show_graph"),
+        COVERAGE ("Coverage", "/show_coverage"),
+        PERFORMANCE ("Performance Digest", "/show_performance_digest");
+
+        private final String name;
+        private final String url;
+
+        Page(String name, String url) {
+            this.name = name;
+            this.url = url;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public String getUrl() {
+            return url;
+        }
+    }
+
+    /**
+     * Get a list of URL/Display name pairs for the navbar heirarchy.
+     * @param request The HttpServletRequest object for the page request.
+     * @return a list of 2-entried String arrays in the order [page url, page name]
+     */
+    public abstract List<String[]> getNavbarLinks(HttpServletRequest request);
 
     @Override
     public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
@@ -29,7 +80,9 @@
         // Set the logout URL to direct back to a login page that directs to the current request.
         UserService userService = UserServiceFactory.getUserService();
         User currentUser = userService.getCurrentUser();
-        String loginURI = userService.createLoginURL(request.getRequestURI());
+        String requestUri = request.getRequestURI();
+        String requestArgs = request.getQueryString();
+        String loginURI = userService.createLoginURL(requestUri + '?' + requestArgs);
         String logoutURI = userService.createLogoutURL(loginURI);
         if (currentUser == null || currentUser.getEmail() == null) {
             response.sendRedirect(loginURI);
@@ -37,7 +90,9 @@
         }
         request.setAttribute("logoutURL", logoutURI);
         request.setAttribute("email", currentUser.getEmail());
-        request.setAttribute("analytics_id", new Gson().toJson(ANALYTICS_ID));
+        request.setAttribute("analyticsID", new Gson().toJson(ANALYTICS_ID));
+        request.setAttribute("navbarLinksJson", new Gson().toJson(getNavbarLinks(request)));
+        request.setAttribute("navbarLinks", getNavbarLinks(request));
         response.setContentType("text/html");
         doGetHandler(request, response);
     }
diff --git a/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/DashboardMainServlet.java b/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/DashboardMainServlet.java
index 40ec300..4d7b82c 100644
--- a/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/DashboardMainServlet.java
+++ b/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/DashboardMainServlet.java
@@ -46,7 +46,7 @@
  */
 public class DashboardMainServlet extends BaseServlet {
 
-    private static final String DASHBOARD_MAIN_JSP = "/dashboard_main.jsp";
+    private static final String DASHBOARD_MAIN_JSP = "WEB-INF/jsp/dashboard_main.jsp";
     private static final String DASHBOARD_ALL_LINK = "/?showAll=true";
     private static final String DASHBOARD_FAVORITES_LINK = "/";
     private static final byte[] EMAIL_FAMILY = Bytes.toBytes("email_to_test");
@@ -60,6 +60,14 @@
     private static final String UP_ARROW = "keyboard_arrow_up";
     private static final String DOWN_ARROW = "keyboard_arrow_down";
 
+    @Override
+    public List<String[]> getNavbarLinks(HttpServletRequest request) {
+        List<String[]> links = new ArrayList<>();
+        Page root = Page.HOME;
+        String[] rootEntry = new String[]{CURRENT_PAGE, root.getName()};
+        links.add(rootEntry);
+        return links;
+    }
 
     @Override
     public void doGetHandler(HttpServletRequest request, HttpServletResponse response)
diff --git a/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/ShowCoverageServlet.java b/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/ShowCoverageServlet.java
index 8c98422..c6097ec 100644
--- a/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/ShowCoverageServlet.java
+++ b/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/ShowCoverageServlet.java
@@ -45,11 +45,34 @@
  */
 public class ShowCoverageServlet extends BaseServlet {
 
+    private static final String COVERAGE_JSP = "WEB-INF/jsp/show_coverage.jsp";
     private static final byte[] FAMILY = Bytes.toBytes("test");
     private static final byte[] QUALIFIER = Bytes.toBytes("data");
     private static final String ALL_TESTCASES_LABEL = "All";
 
     @Override
+    public List<String[]> getNavbarLinks(HttpServletRequest request) {
+        List<String[]> links = new ArrayList<>();
+        Page root = Page.HOME;
+        String[] rootEntry = new String[]{root.getUrl(), root.getName()};
+        links.add(rootEntry);
+
+        Page table = Page.TABLE;
+        String testName = request.getParameter("testName");
+        String name = table.getName() + testName;
+        String url = table.getUrl() + "?testName=" + testName;
+        String[] tableEntry = new String[]{url, name};
+        links.add(tableEntry);
+
+        Page coverage = Page.COVERAGE;
+        String startTime = request.getParameter("startTime");
+        url = coverage.getUrl() + "?testName=" + testName + "&startTime=" + startTime;
+        String[] coverageEntry = new String[]{url, coverage.getName()};
+        links.add(coverageEntry);
+        return links;
+    }
+
+    @Override
     public void doGetHandler(HttpServletRequest request, HttpServletResponse response)
             throws IOException {
         RequestDispatcher dispatcher = null;
@@ -57,15 +80,13 @@
         TableName tableName = null;
         tableName = TableName.valueOf(TABLE_PREFIX + request.getParameter("testName"));
 
-        // key is a unique combination of build Id and timestamp that helps identify the
-        // corresponding build id.
-        String key = request.getParameter("key");
+        String timeString = request.getParameter("startTime");
         Scan scan = new Scan();
         long time = -1;
         try {
-            time = Long.parseLong(key);
-            scan.setStartRow(key.getBytes());
-            scan.setStopRow(Long.toString((time + 1)).getBytes());
+            time = Long.parseLong(timeString);
+            scan.setStartRow(Long.toString(time - 1).getBytes());
+            scan.setStopRow(Long.toString(time).getBytes());
         } catch (NumberFormatException e) { }  // Use unbounded scan
 
         TestReportMessage testReportMessage = null;
@@ -80,7 +101,7 @@
 
             // filter empty build IDs and add only numbers
             if (buildId.length() > 0) {
-                if (time == currentTestReportMessage.getStartTimestamp()) {
+                if (time == currentTestReportMessage.getStartTimestamp() * MILLI_TO_MICRO) {
                     testReportMessage = currentTestReportMessage;
                     break;
                 }
@@ -166,8 +187,7 @@
         request.setAttribute("indicators", new Gson().toJson(indicators));
         request.setAttribute("sectionMap", new Gson().toJson(sectionMap));
         request.setAttribute("startTime", request.getParameter("startTime"));
-        request.setAttribute("endTime", request.getParameter("endTime"));
-        dispatcher = request.getRequestDispatcher("/show_coverage.jsp");
+        dispatcher = request.getRequestDispatcher(COVERAGE_JSP);
 
         try {
             dispatcher.forward(request, response);
diff --git a/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/ShowGraphServlet.java b/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/ShowGraphServlet.java
index c26c753..0034189 100644
--- a/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/ShowGraphServlet.java
+++ b/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/ShowGraphServlet.java
@@ -54,6 +54,7 @@
  */
 public class ShowGraphServlet extends BaseServlet {
 
+    private static final String GRAPH_JSP = "WEB-INF/jsp/show_graph.jsp";
     private static final byte[] FAMILY = Bytes.toBytes("test");
     private static final byte[] QUALIFIER = Bytes.toBytes("data");
 
@@ -63,8 +64,27 @@
             new HashSet<String>(Arrays.asList(splitKeysArray));
     private static final String PROFILING_DATA_ALERT = "No profiling data was found.";
 
+    @Override
+    public List<String[]> getNavbarLinks(HttpServletRequest request) {
+        List<String[]> links = new ArrayList<>();
+        Page root = Page.HOME;
+        String[] rootEntry = new String[]{root.getUrl(), root.getName()};
+        links.add(rootEntry);
 
-    private static final long MILLI_TO_MICRO = 1000;  // conversion factor from milli to micro units
+        Page table = Page.TABLE;
+        String testName = request.getParameter("testName");
+        String name = table.getName() + testName;
+        String url = table.getUrl() + "?testName=" + testName;
+        String[] tableEntry = new String[]{url, name};
+        links.add(tableEntry);
+
+        Page graph = Page.GRAPH;
+        String profilingPointName = request.getParameter("profilingPoint");
+        url = graph.getUrl() + "?testName=" + testName + "&profilingPoint=" + profilingPointName;
+        String[] graphEntry = new String[]{url, graph.getName()};
+        links.add(graphEntry);
+        return links;
+    }
 
     /**
      * Process a profiling report message and determine which line graph to insert the point into.
@@ -195,7 +215,7 @@
         request.setAttribute("graphs", gson.toJson(graphList));
 
         request.setAttribute("profilingPointName", profilingPointName);
-        dispatcher = request.getRequestDispatcher("/show_graph.jsp");
+        dispatcher = request.getRequestDispatcher(GRAPH_JSP);
         try {
             dispatcher.forward(request, response);
         } catch (ServletException e) {
diff --git a/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/ShowPerformanceDigestServlet.java b/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/ShowPerformanceDigestServlet.java
index cb184e4..6437e21 100644
--- a/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/ShowPerformanceDigestServlet.java
+++ b/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/ShowPerformanceDigestServlet.java
@@ -40,8 +40,8 @@
  */
 public class ShowPerformanceDigestServlet extends BaseServlet {
 
+    private static final String PERF_DIGEST_JSP = "WEB-INF/jsp/show_performance_digest.jsp";
     private static final int N_DIGITS = 2;
-    private static final long MILLI_TO_MICRO = 1000;  // conversion factor from milli to micro units
     private static final String HIDL_HAL_OPTION = "hidl_hal_mode";
     private static final String[] splitKeysArray = new String[]{HIDL_HAL_OPTION};
     private static final Set<String> splitKeySet = new HashSet<String>(Arrays.asList(splitKeysArray));
@@ -54,6 +54,27 @@
     private static final String LOWER_IS_BETTER = "Note: Lower values are better. Minimum is the best-case performance.";
     private static final String STD = "Std";
 
+    @Override
+    public List<String[]> getNavbarLinks(HttpServletRequest request) {
+        List<String[]> links = new ArrayList<>();
+        Page root = Page.HOME;
+        String[] rootEntry = new String[]{root.getUrl(), root.getName()};
+        links.add(rootEntry);
+
+        Page table = Page.TABLE;
+        String testName = request.getParameter("testName");
+        String name = table.getName() + testName;
+        String url = table.getUrl() + "?testName=" + testName;
+        String[] tableEntry = new String[]{url, name};
+        links.add(tableEntry);
+
+        Page perf = Page.PERFORMANCE;
+        url = perf.getUrl() + "?testName=" + testName;
+        String[] perfEntry = new String[]{url, perf.getName()};
+        links.add(perfEntry);
+        return links;
+    }
+
     /**
      * Generates an HTML summary of the performance changes for the profiling results in the
      * specified table.
@@ -225,7 +246,7 @@
         request.setAttribute("selectedDevice", selectedDevice);
         request.setAttribute("devices", devices);
 
-        dispatcher = request.getRequestDispatcher("/show_performance_digest.jsp");
+        dispatcher = request.getRequestDispatcher(PERF_DIGEST_JSP);
         try {
             dispatcher.forward(request, response);
         } catch (ServletException e) {
diff --git a/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/ShowPreferencesServlet.java b/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/ShowPreferencesServlet.java
index 31f3716..db3b3f6 100644
--- a/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/ShowPreferencesServlet.java
+++ b/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/ShowPreferencesServlet.java
@@ -48,12 +48,24 @@
  */
 public class ShowPreferencesServlet extends BaseServlet {
 
-    private static final String PREFERENCES_JSP = "/show_preferences.jsp";
+    private static final String PREFERENCES_JSP = "WEB-INF/jsp/show_preferences.jsp";
     private static final String DASHBOARD_MAIN_LINK = "/";
     private static final byte[] EMAIL_FAMILY = Bytes.toBytes("email_to_test");
     private static final byte[] TEST_FAMILY = Bytes.toBytes("test_to_email");
     private static final String STATUS_TABLE = "vts_status_table";
 
+    @Override
+    public List<String[]> getNavbarLinks(HttpServletRequest request) {
+        List<String[]> links = new ArrayList<>();
+        Page root = Page.HOME;
+        String[] rootEntry = new String[]{root.getUrl(), root.getName()};
+        links.add(rootEntry);
+
+        Page prefs = Page.PREFERENCES;
+        String[] prefsEntry = new String[]{CURRENT_PAGE, prefs.getName()};
+        links.add(prefsEntry);
+        return links;
+    }
 
     @Override
     public void doGetHandler(HttpServletRequest request, HttpServletResponse response)
diff --git a/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/ShowTableServlet.java b/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/ShowTableServlet.java
index 136778c..0aa5a2b 100644
--- a/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/ShowTableServlet.java
+++ b/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/ShowTableServlet.java
@@ -58,6 +58,7 @@
  */
 public class ShowTableServlet extends BaseServlet {
 
+    private static final String TABLE_JSP = "WEB-INF/jsp/show_table.jsp";
     // Error message displayed on the webpage is tableName passed is null.
     private static final String TABLE_NAME_ERROR = "Error : Table name must be passed!";
     private static final String PROFILING_DATA_ALERT = "No profiling data was found.";
@@ -78,6 +79,22 @@
     private static final byte[] FAMILY = Bytes.toBytes("test");
     private static final byte[] QUALIFIER = Bytes.toBytes("data");
 
+    @Override
+    public List<String[]> getNavbarLinks(HttpServletRequest request) {
+        List<String[]> links = new ArrayList<>();
+        Page root = Page.HOME;
+        String[] rootEntry = new String[]{root.getUrl(), root.getName()};
+        links.add(rootEntry);
+
+        Page table = Page.TABLE;
+        String testName = request.getParameter("testName");
+        String name = table.getName() + testName;
+        String url = table.getUrl() + "?testName=" + testName;
+        String[] tableEntry = new String[]{url, name};
+        links.add(tableEntry);
+        return links;
+    }
+
     /**
      * Parse the search string to populate the searchPairs map and the generalTerms set.
      * General terms apply to any field, while pairs in searchPairs are for a particular field.
@@ -508,10 +525,8 @@
                 double coveragePct = Math.round((100 * coveredLineCount /
                                                  totalLineCount) * 100f) / 100f;
                 coveragePctInfo = Double.toString(coveragePct) + "%" +
-                        "<a href=\"/show_coverage?key=" + report.getStartTimestamp() +
-                        "&testName=" + request.getParameter("testName") +
-                        "&startTime=" + startTime +
-                        "&endTime=" + endTime +
+                        "<a href=\"/show_coverage?testName=" + request.getParameter("testName") +
+                        "&startTime=" + (report.getStartTimestamp() * MILLI_TO_MICRO) +
                         "\" class=\"waves-effect waves-light btn red right coverage-btn\">" +
                         "<i class=\"material-icons coverage-icon\">menu</i></a>";
                 coverageInfo = coveredLineCount + "/" + totalLineCount;
@@ -579,7 +594,7 @@
         request.setAttribute("showPresubmit", showPresubmit);
         request.setAttribute("showPostsubmit", showPostsubmit);
 
-        dispatcher = request.getRequestDispatcher("/show_table.jsp");
+        dispatcher = request.getRequestDispatcher(TABLE_JSP);
         try {
             dispatcher.forward(request, response);
         } catch (ServletException e) {
diff --git a/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/VtsAlertJobServlet.java b/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/VtsAlertJobServlet.java
index 5735df8..05edd67 100644
--- a/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/VtsAlertJobServlet.java
+++ b/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/VtsAlertJobServlet.java
@@ -63,9 +63,13 @@
     private static final byte[] DATA_QUALIFIER = Bytes.toBytes("data");
     private static final byte[] TIME_QUALIFIER = Bytes.toBytes("upload_timestamp");
     private static final String STATUS_TABLE = "vts_status_table";
-    private static final long MILLI_TO_MICRO = 1000;  // conversion factor from milli to micro units
     private static final long THREE_MINUTES = 180000000L;  // units microseconds
 
+    @Override
+    public List<String[]> getNavbarLinks(HttpServletRequest request) {
+        return null;
+    }
+
     /**
      * Checks whether any new failures have occurred beginning since (and including) startTime.
      * @param tableName The name of the table that stores the results for a test, string.
diff --git a/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/VtsPerformanceJobServlet.java b/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/VtsPerformanceJobServlet.java
index 111f99a..26985e6 100644
--- a/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/VtsPerformanceJobServlet.java
+++ b/web/dashboard/appengine/servlet/src/main/java/com/android/vts/servlet/VtsPerformanceJobServlet.java
@@ -45,7 +45,6 @@
 
     private static final String STATUS_TABLE = "vts_status_table";
     private static final int N_DIGITS = 2;
-    private static final long MILLI_TO_MICRO = 1000;  // conversion factor from milli to micro units
 
     private static final String MEAN = "Mean";
     private static final String MAX = "Max";
@@ -66,6 +65,11 @@
     private static final String INNER_CELL_STYLE = "border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;";
     private static final String OUTER_CELL_STYLE = "border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;";
 
+    @Override
+    public List<String[]> getNavbarLinks(HttpServletRequest request) {
+        return null;
+    }
+
     /**
      * Generates an HTML summary of the performance changes for the profiling results in the
      * specified table.
diff --git a/web/dashboard/appengine/servlet/src/main/webapp/WEB-INF/jsp/dashboard_main.jsp b/web/dashboard/appengine/servlet/src/main/webapp/WEB-INF/jsp/dashboard_main.jsp
new file mode 100644
index 0000000..52eddbe
--- /dev/null
+++ b/web/dashboard/appengine/servlet/src/main/webapp/WEB-INF/jsp/dashboard_main.jsp
@@ -0,0 +1,63 @@
+<%--
+  ~ Copyright (c) 2016 Google Inc. All Rights Reserved.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License"); you
+  ~ may not use this file except in compliance with the License. You may
+  ~ obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+  ~ implied. See the License for the specific language governing
+  ~ permissions and limitations under the License.
+  --%>
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
+
+<html>
+  <link rel='stylesheet' href='/css/dashboard_main.css'>
+  <%@ include file="header.jsp" %>
+  <body>
+    <div class='container'>
+      <div class='row' id='options'>
+        <c:choose>
+          <c:when test="${not empty error}">
+            <div id="error-container" class="row card">
+              <div class="col s12 center-align">
+                <h5>${error}</h5>
+              </div>
+            </div>
+          </c:when>
+          <c:otherwise>
+            <div class='col s12'>
+              <h4 id='section-header'>${headerLabel}</h4>
+            </div>
+            <c:forEach items='${testNames}' var='test'>
+              <a href='${pageContext.request.contextPath}/show_table?testName=${test}'>
+                <div class='col s12 card hoverable option valign-wrapper waves-effect'>
+                  <span class='entry valign'>${test}</span>
+                </div>
+              </a>
+            </c:forEach>
+          </c:otherwise>
+        </c:choose>
+      </div>
+      <div class='row center-align'>
+        <a href='${buttonLink}' id='show-button' class='btn waves-effect red'>${buttonLabel}
+          <i id='show-button-arrow' class='material-icons right'>${buttonIcon}</i>
+        </a>
+      </div>
+    </div>
+    <c:if test='${not showAll}'>
+      <div id='edit-button-wrapper' class='fixed-action-btn'>
+        <a href='/show_preferences' id='edit-button' class='btn-floating btn-large red waves-effect'>
+          <i class='large material-icons'>mode_edit</i>
+        </a>
+      </div>
+    </c:if>
+    <%@ include file="footer.jsp" %>
+  </body>
+</html>
diff --git a/web/dashboard/appengine/servlet/src/main/webapp/WEB-INF/jsp/footer.jsp b/web/dashboard/appengine/servlet/src/main/webapp/WEB-INF/jsp/footer.jsp
new file mode 100644
index 0000000..ed2d950
--- /dev/null
+++ b/web/dashboard/appengine/servlet/src/main/webapp/WEB-INF/jsp/footer.jsp
@@ -0,0 +1,25 @@
+<%--
+  ~ Copyright (c) 2017 Google Inc. All Rights Reserved.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License"); you
+  ~ may not use this file except in compliance with the License. You may
+  ~ obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+  ~ implied. See the License for the specific language governing
+  ~ permissions and limitations under the License.
+  --%>
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+
+<footer class='page-footer'>
+  <div class='footer-copyright'>
+    <div class='container'>
+      © 2017 - The Android Open Source Project
+    </div>
+  </div>
+</footer>
diff --git a/web/dashboard/appengine/servlet/src/main/webapp/WEB-INF/jsp/header.jsp b/web/dashboard/appengine/servlet/src/main/webapp/WEB-INF/jsp/header.jsp
new file mode 100644
index 0000000..1978511
--- /dev/null
+++ b/web/dashboard/appengine/servlet/src/main/webapp/WEB-INF/jsp/header.jsp
@@ -0,0 +1,61 @@
+<%--
+  ~ Copyright (c) 2017 Google Inc. All Rights Reserved.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License"); you
+  ~ may not use this file except in compliance with the License. You may
+  ~ obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+  ~ implied. See the License for the specific language governing
+  ~ permissions and limitations under the License.
+  --%>
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+
+<head>
+  <link rel='stylesheet' href='/css/navbar.css'>
+  <link rel='stylesheet' href='/css/common.css'>
+  <link rel='stylesheet' href='https://www.gstatic.com/external_hosted/materialize/all_styles-bundle.css'>
+  <link rel='icon' href='https://www.gstatic.com/images/branding/googleg/1x/googleg_standard_color_32dp.png' sizes='32x32'>
+  <link rel='stylesheet' href='https://fonts.googleapis.com/icon?family=Material+Icons'>
+  <link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700'>
+  <script src='https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js'></script>
+  <script src='https://www.gstatic.com/external_hosted/materialize/materialize.min.js'></script>
+  <script type='text/javascript'>
+    if (${analyticsID}) {
+        // Autogenerated from Google Analytics
+        (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+            (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
+            m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
+        })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
+        ga('create', ${analyticsID}, 'auto');
+        ga('send', 'pageview');
+    }
+    links = ${navbarLinksJson};
+  </script>
+  <title>VTS Dashboard</title>
+</head>
+<body>
+  <nav id='navbar'>
+    <div class='nav-wrapper'>
+      <span>
+        <c:forEach items='${navbarLinks}' var='link'>
+          <a href='${link[0]}' class='breadcrumb'>${link[1]}</a>
+        </c:forEach>
+      </span>
+      <ul class='right'><li>
+        <a id='dropdown-button' class='dropdown-button btn red lighten-3' href='#' data-activates='dropdown'>
+          ${email}
+        </a>
+      </li></ul>
+      <ul id='dropdown' class='dropdown-content'>
+        <li><a href='${logoutURL}'>Log out</a></li>
+      </ul>
+      </div>
+    </div>
+  </nav>
+</body>
diff --git a/web/dashboard/appengine/servlet/src/main/webapp/show_coverage.jsp b/web/dashboard/appengine/servlet/src/main/webapp/WEB-INF/jsp/show_coverage.jsp
similarity index 76%
rename from web/dashboard/appengine/servlet/src/main/webapp/show_coverage.jsp
rename to web/dashboard/appengine/servlet/src/main/webapp/WEB-INF/jsp/show_coverage.jsp
index ea878d0..e3388f1 100644
--- a/web/dashboard/appengine/servlet/src/main/webapp/show_coverage.jsp
+++ b/web/dashboard/appengine/servlet/src/main/webapp/WEB-INF/jsp/show_coverage.jsp
@@ -13,26 +13,16 @@
   ~ implied. See the License for the specific language governing
   ~ permissions and limitations under the License.
   --%>
-<%@ page contentType="text/html;charset=UTF-8" language="java" %>
-<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
-<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
-
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
 
 <html>
-  <head>
-    <title>Coverage Information</title>
-    <link rel="icon" href="https://www.gstatic.com/images/branding/googleg/1x/googleg_standard_color_32dp.png" sizes="32x32">
-    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
-    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700">
-    <link rel="stylesheet" href="https://www.gstatic.com/external_hosted/materialize/all_styles-bundle.css">
-    <link rel="stylesheet" href="/css/navbar.css">
-    <link rel="stylesheet" href="/css/show_coverage.css">
-    <script src='/js/analytics.js' type='text/javascript'></script>
-    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
-    <script src="https://www.gstatic.com/external_hosted/materialize/materialize.min.js"></script>
-    <script src="https://apis.google.com/js/api.js" type="text/javascript"></script>
+  <%@ include file="header.jsp" %>
+  <link rel="stylesheet" href="/css/show_coverage.css">
+  <script src="https://apis.google.com/js/api.js" type="text/javascript"></script>
+  <body>
     <script type="text/javascript">
-        if (${analytics_id}) analytics_init(${analytics_id});
         var coverageVectors = ${coverageVectors};
         $(document).ready(function() {
             // Initialize AJAX for CORS
@@ -171,32 +161,8 @@
             }).find('.collapsible-header').click(onClick);
         }
     </script>
-    <nav id="navbar">
-      <div class="nav-wrapper">
-        <span>
-          <a href="${pageContext.request.contextPath}/" class="breadcrumb">VTS Dashboard Home</a>
-          <a href="${pageContext.request.contextPath}/show_table?testName=${testName}&startTime=${startTime}&endTime=${endTime}" class="breadcrumb">${testName}</a>
-          <a href="#!" class="breadcrumb">Coverage</a>
-        </span>
-        <ul class='right'><li>
-          <a id='dropdown-button' class='dropdown-button btn red lighten-3' href='#' data-activates='dropdown'>
-            ${email}
-          </a>
-        </li></ul>
-        <ul id='dropdown' class='dropdown-content'>
-          <li><a href='${logoutURL}'>Log out</a></li>
-        </ul>
-      </div>
-    </nav>
-  </head>
-  <body>
-    <div id="coverage-container" class="container">
+    <div id='coverage-container' class='wide container'>
     </div>
-    <footer class="page-footer">
-      <div class="footer-copyright">
-        <div class="container">© 2016 - The Android Open Source Project
-        </div>
-      </div>
-    </footer>
+    <%@ include file="footer.jsp" %>
   </body>
 </html>
diff --git a/web/dashboard/appengine/servlet/src/main/webapp/WEB-INF/jsp/show_graph.jsp b/web/dashboard/appengine/servlet/src/main/webapp/WEB-INF/jsp/show_graph.jsp
new file mode 100644
index 0000000..8b8aef4
--- /dev/null
+++ b/web/dashboard/appengine/servlet/src/main/webapp/WEB-INF/jsp/show_graph.jsp
@@ -0,0 +1,257 @@
+<%--
+  ~ Copyright (c) 2016 Google Inc. All Rights Reserved.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License"); you
+  ~ may not use this file except in compliance with the License. You may
+  ~ obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+  ~ implied. See the License for the specific language governing
+  ~ permissions and limitations under the License.
+  --%>
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
+
+<html>
+  <%@ include file="header.jsp" %>
+  <link type='text/css' href='/css/datepicker.css' rel='stylesheet'>
+  <link type='text/css' href='/css/show_graph.css' rel='stylesheet'>
+  <link rel='stylesheet' href='https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.0/jquery-ui.css'>
+  <script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script>
+  <script type='text/javascript' src='https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js'></script>
+  <body>
+    <script type='text/javascript'>
+        google.charts.load('current', {packages:['corechart', 'table', 'line']});
+        google.charts.setOnLoadCallback(drawAllGraphs);
+
+        ONE_DAY = 86400000000;
+        MICRO_PER_MILLI = 1000;
+
+        $(function() {
+            $('select').material_select();
+            var date = $('#date').datepicker({
+                showAnim: 'slideDown',
+                maxDate: new Date()
+            });
+            date.datepicker('setDate', new Date(${startTime} / MICRO_PER_MILLI));
+            $('#load').click(load);
+        });
+
+        // Draw all graphs.
+        function drawAllGraphs() {
+            var graphs = ${graphs};
+            graphs.forEach(function(graph) {
+                if (graph.type == 'LINE_GRAPH') drawLineGraph(graph);
+                else if (graph.type == 'HISTOGRAM') drawHistogram(graph);
+            });
+        }
+
+       /**
+        * Draw a line graph.
+        *
+        * Args:
+        *     lineGraph: a JSON object containing the following fields:
+        *                - name: the name of the graph
+        *                - values: an array of numbers
+        *                - ticks: an array of strings to use as x-axis labels
+        *                - ids: an array of string labels for each point (e.g. the
+        *                       build info for the run that produced the point)
+        *                - x_label: the string label for the x axis
+        *                - y_label: the string label for the y axis
+        */
+        function drawLineGraph(lineGraph) {
+            var title = 'Performance';
+            if (lineGraph.name) title += ' (' + lineGraph.name + ')';
+            if (lineGraph.ticks.length < 1) {
+                return;
+            }
+            lineGraph.ticks.forEach(function (label, i) {
+                lineGraph.values[i].unshift(label);
+            });
+            var data = new google.visualization.DataTable();
+            data.addColumn('string', lineGraph.x_label);
+            lineGraph.ids.forEach(function(id) {
+                data.addColumn('number', id);
+            });
+            data.addRows(lineGraph.values);
+            var options = {
+              chart: {
+                  title: title,
+                  subtitle: lineGraph.y_label
+              },
+              legend: { position: 'none' }
+            };
+            var container = $('<div class="row card center-align col s12 graph-wrapper"></div>');
+            container.appendTo('#profiling-container');
+            var chartDiv = $('<div class="col s12 graph"></div>');
+            chartDiv.appendTo(container);
+            var chart = new google.charts.Line(chartDiv[0]);
+            chart.draw(data, options);
+        }
+
+       /**
+        * Draw a histogram.
+        *
+        * Args:
+        *     hist: a JSON object containing the following fields:
+        *           - name: the name of the graph
+        *           - values: an array of numbers
+        *           - ids: an array of string labels for each point (e.g. the
+        *                  build info for the run that produced the point)
+        *           - x_label: the string label for the x axis
+        *           - y_label: the string label for the y axis
+        */
+        function drawHistogram(hist) {
+            test = hist;
+            var title = 'Performance';
+            if (hist.name) title += ' (' + hist.name + ')';
+            var values = hist.values;
+            var histogramData = values.map(function(d, i) {
+                return [hist.ids[i], d];
+            });
+            var min = Math.min.apply(null, values),
+                max = Math.max.apply(null, values);
+
+            var histogramTicks = new Array(10);
+            var delta = (max - min) / 10;
+            for (var i = 0; i <= 10; i++) {
+                histogramTicks[i] = Math.round(min + delta * i);
+            }
+
+            var data = google.visualization.arrayToDataTable(histogramData, true);
+
+            var options = {
+                title: title,
+                titleTextStyle: {
+                    color: '#757575',
+                    fontSize: 16,
+                    bold: false
+                },
+                legend: { position: 'none' },
+                colors: ['#4285F4'],
+                fontName: 'Roboto',
+                vAxis:{
+                    title: hist.y_label,
+                    titleTextStyle: {
+                        color: '#424242',
+                        fontSize: 12,
+                        italic: false
+                    },
+                    textStyle: {
+                        fontSize: 12,
+                        color: '#757575'
+                    },
+                },
+                hAxis: {
+                    ticks: histogramTicks,
+                    title: hist.x_label,
+                    textStyle: {
+                        fontSize: 12,
+                        color: '#757575'
+                    },
+                    titleTextStyle: {
+                        color: '#424242',
+                        fontSize: 12,
+                        italic: false
+                    }
+                },
+                bar: { gap: 0 },
+                histogram: {
+                    maxNumBuckets: 200,
+                    minValue: min * 0.95,
+                    maxValue: max * 1.05
+                },
+                chartArea: {
+                    width: '100%',
+                    top: 40,
+                    left: 50,
+                    height: '80%'
+                }
+            };
+            var container = $('<div class="row card col s12 graph-wrapper"></div>');
+            container.appendTo('#profiling-container');
+
+            var chartDiv = $('<div class="col s12 graph"></div>');
+            chartDiv.appendTo(container);
+            var chart = new google.visualization.Histogram(chartDiv[0]);
+            chart.draw(data, options);
+
+            var tableDiv = $('<div class="col s12"></div>');
+            tableDiv.appendTo(container);
+
+            var tableHtml = '<table class="percentile-table"><thead><tr>';
+            hist.percentiles.forEach(function(p) {
+                tableHtml += '<th data-field="id">' + p + '%</th>';
+            });
+            tableHtml += '</tr></thead><tbody><tr>';
+            hist.percentile_values.forEach(function(v) {
+                tableHtml += '<td>' + v + '</td>';
+            });
+            tableHtml += '</tbody></table>';
+            $(tableHtml).appendTo(tableDiv);
+        }
+
+        // Reload the page.
+        function load() {
+            var startTime = $('#date').datepicker('getDate').getTime();
+            startTime = startTime + (ONE_DAY / MICRO_PER_MILLI) - 1;
+            var ctx = '${pageContext.request.contextPath}';
+            var link = ctx + '/show_graph?profilingPoint=${profilingPointName}' +
+                '&testName=${testName}' +
+                '&startTime=' + (startTime * MICRO_PER_MILLI);
+            if ($('#device-select').prop('selectedIndex') > 1) {
+                link += '&device=' + $('#device-select').val();
+            }
+            window.open(link,'_self');
+        }
+    </script>
+    <div id='download' class='fixed-action-btn'>
+      <a id='b' class='btn-floating btn-large red waves-effect waves-light'>
+        <i class='large material-icons'>file_download</i>
+      </a>
+    </div>
+    <div class='container'>
+      <div class='row card'>
+        <div id='header-container' class='valign-wrapper col s12'>
+          <div class='col s3 valign'>
+            <h5>Profiling Point:</h5>
+          </div>
+          <div class='col s9 right-align valign'>
+            <h5 class='profiling-name truncate'>${profilingPointName}</h5>
+          </div>
+        </div>
+        <div id='date-container' class='col s12'>
+          <div id='device-select-wrapper' class='input-field col s6 m3 offset-m6'>
+            <select id='device-select'>
+              <option value='' disabled>Select device</option>
+              <option value='0' ${empty selectedDevice ? 'selected' : ''}>All Devices</option>
+              <c:forEach items='${devices}' var='device' varStatus='loop'>
+                <option value=${device} ${selectedDevice eq device ? 'selected' : ''}>${device}</option>
+              </c:forEach>
+            </select>
+          </div>
+          <input type='text' id='date' name='date' class='col s5 m2'>
+          <a id='load' class='btn-floating btn-medium red right waves-effect waves-light'>
+            <i class='medium material-icons'>cached</i>
+          </a>
+        </div>
+      </div>
+      <div id='profiling-container'>
+      </div>
+      <c:if test='${not empty error}'>
+        <div id='error-container' class='row card'>
+          <div class='col s10 offset-s1 center-align'>
+            <!-- Error in case of profiling data is missing -->
+            <h5>${error}</h5>
+          </div>
+        </div>
+      </c:if>
+    </div>
+    <%@ include file="footer.jsp" %>
+  </body>
+</html>
diff --git a/web/dashboard/appengine/servlet/src/main/webapp/WEB-INF/jsp/show_performance_digest.jsp b/web/dashboard/appengine/servlet/src/main/webapp/WEB-INF/jsp/show_performance_digest.jsp
new file mode 100644
index 0000000..224d847
--- /dev/null
+++ b/web/dashboard/appengine/servlet/src/main/webapp/WEB-INF/jsp/show_performance_digest.jsp
@@ -0,0 +1,100 @@
+<%--
+  ~ Copyright (c) 2016 Google Inc. All Rights Reserved.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License"); you
+  ~ may not use this file except in compliance with the License. You may
+  ~ obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+  ~ implied. See the License for the specific language governing
+  ~ permissions and limitations under the License.
+  --%>
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
+
+<html>
+  <%@ include file="header.jsp" %>
+  <link type='text/css' href='/css/datepicker.css' rel='stylesheet'>
+  <link type='text/css' href='/css/show_performance_digest.css' rel='stylesheet'>
+  <link rel='stylesheet' href='https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.0/jquery-ui.css'>
+  <script src='https://www.gstatic.com/external_hosted/moment/min/moment-with-locales.min.js'></script>
+  <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
+  <body>
+    <script type='text/javascript'>
+        ONE_DAY = 86400000000;
+        MICRO_PER_MILLI = 1000;
+
+        function load() {
+            var time = $('#date').datepicker('getDate').getTime() - 1;
+            time = time * MICRO_PER_MILLI + ONE_DAY;  // end of day
+            var ctx = '${pageContext.request.contextPath}';
+            var link = ctx + '/show_performance_digest?profilingPoint=${profilingPointName}' +
+                '&testName=${testName}' +
+                '&startTime=' + time;
+            if ($('#device-select').prop('selectedIndex') > 1) {
+                link += '&device=' + $('#device-select').val();
+            }
+            window.open(link,'_self');
+        }
+
+        $(function() {
+            var date = $('#date').datepicker({
+                showAnim: "slideDown",
+                maxDate: new Date()
+            });
+            date.datepicker('setDate', new Date(${startTime} / MICRO_PER_MILLI));
+            $('#load').click(load);
+
+            $('.date-label').each(function(i) {
+                var label = $(this);
+                label.html(moment(parseInt(label.html())).format('M/D/YY'));
+            });
+            $('select').material_select();
+        });
+    </script>
+    <div class='wide container'>
+      <div class='row card'>
+        <div id='header-container' class='col s12'>
+          <div class='col s12'>
+            <h4>Daily Performance Digest</h4>
+          </div>
+          <div id='device-select-wrapper' class='input-field col s6 m3 offset-m6'>
+            <select id='device-select'>
+              <option value='' disabled>Select device</option>
+              <option value='0' ${empty selectedDevice ? 'selected' : ''}>All Devices</option>
+              <c:forEach items='${devices}' var='device' varStatus='loop'>
+                <option value=${device} ${selectedDevice eq device ? 'selected' : ''}>${device}</option>
+              </c:forEach>
+            </select>
+          </div>
+          <input type='text' id='date' name='date' class='col s5 m2'>
+          <a id='load' class='btn-floating btn-medium red right waves-effect waves-light'>
+            <i class='medium material-icons'>cached</i>
+          </a>
+        </div>
+      </div>
+      <div class='row'>
+        <c:forEach items='${tables}' var='table' varStatus='loop'>
+          <div class='col s12 card summary'>
+            <div class='col s3 valign'>
+              <h5>Profiling Point:</h5>
+            </div>
+            <div class='col s9 right-align valign'>
+              <h5 class="profiling-name truncate">${tableTitles[loop.index]}</h5>
+            </div>
+            ${table}
+            <span class='profiling-subtitle'>
+              ${tableSubtitles[loop.index]}
+            </span>
+          </div>
+        </c:forEach>
+      </div>
+    </div>
+    <%@ include file="footer.jsp" %>
+  </body>
+</html>
diff --git a/web/dashboard/appengine/servlet/src/main/webapp/show_preferences.jsp b/web/dashboard/appengine/servlet/src/main/webapp/WEB-INF/jsp/show_preferences.jsp
similarity index 78%
rename from web/dashboard/appengine/servlet/src/main/webapp/show_preferences.jsp
rename to web/dashboard/appengine/servlet/src/main/webapp/WEB-INF/jsp/show_preferences.jsp
index ced594e..b41ea06 100644
--- a/web/dashboard/appengine/servlet/src/main/webapp/show_preferences.jsp
+++ b/web/dashboard/appengine/servlet/src/main/webapp/WEB-INF/jsp/show_preferences.jsp
@@ -1,4 +1,3 @@
-<%-- //[START all]--%>
 <%--
   ~ Copyright (c) 2016 Google Inc. All Rights Reserved.
   ~
@@ -18,40 +17,12 @@
 <%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
 <%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
 
-
 <html>
-  <link rel='icon' href='https://www.gstatic.com/images/branding/googleg/1x/googleg_standard_color_32dp.png' sizes='32x32'>
-  <link rel='stylesheet' href='https://fonts.googleapis.com/icon?family=Material+Icons'>
-  <link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700'>
-  <link rel='stylesheet' href='https://www.gstatic.com/external_hosted/materialize/all_styles-bundle.css'>
-  <link rel='stylesheet' href='/css/navbar.css'>
+  <%@ include file="header.jsp" %>
   <link rel='stylesheet' href='/css/show_preferences.css'>
-  <script src='/js/analytics.js' type='text/javascript'></script>
-  <script src='https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js'></script>
-  <script src='https://www.gstatic.com/external_hosted/materialize/materialize.min.js'></script>
   <script type='text/javascript' src='https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.0/jquery-ui.min.js'></script>
-  <head>
-    <title>VTS Dashboard</title>
-
-    <nav id='navbar'>
-      <div class='nav-wrapper'>
-        <span>
-          <a href='/' class='breadcrumb'>VTS Dashboard Home</a>
-          <a href='#!' class='breadcrumb'>Preferences</a>
-        </span>
-        <ul class='right'><li>
-          <a id='dropdown-button' class='dropdown-button btn red lighten-3' href='#' data-activates='dropdown'>
-            ${email}
-          </a>
-        </li></ul>
-        <ul id='dropdown' class='dropdown-content'>
-          <li><a href='${logoutURL}'>Log out</a></li>
-        </ul>
-        </div>
-      </div>
-    </nav>
+  <body>
     <script>
-        if (${analytics_id}) analytics_init(${analytics_id});
         var subscribedSet = new Set(${subscribedTestsJson});
         var displayedSet = new Set(${subscribedTestsJson});
         var allTests = ${allTestsJson};
@@ -141,9 +112,6 @@
             $('#save-button-wrapper').hide();
         });
     </script>
-  </head>
-
-  <body>
     <div class='container'>
       <div class='row'>
         <h3 class='col s12 header'>Favorites</h3>
@@ -181,13 +149,6 @@
         <i class='large material-icons'>done</i>
       </a>
     </div>
-    <footer class='page-footer'>
-      <div class='footer-copyright'>
-        <div class='container'>
-          © 2016 - The Android Open Source Project
-        </div>
-      </div>
-    </footer>
+    <%@ include file="footer.jsp" %>
   </body>
 </html>
-<%-- //[END all]--%>
diff --git a/web/dashboard/appengine/servlet/src/main/webapp/show_table.jsp b/web/dashboard/appengine/servlet/src/main/webapp/WEB-INF/jsp/show_table.jsp
similarity index 86%
rename from web/dashboard/appengine/servlet/src/main/webapp/show_table.jsp
rename to web/dashboard/appengine/servlet/src/main/webapp/WEB-INF/jsp/show_table.jsp
index d888a9b..a527de0 100644
--- a/web/dashboard/appengine/servlet/src/main/webapp/show_table.jsp
+++ b/web/dashboard/appengine/servlet/src/main/webapp/WEB-INF/jsp/show_table.jsp
@@ -18,21 +18,11 @@
 <%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
 
 <html>
-  <head>
-    <title>VTS Table</title>
-    <link rel='icon' href='//www.gstatic.com/images/branding/googleg/1x/googleg_standard_color_32dp.png' sizes='32x32'>
-    <link rel='stylesheet' href='https://fonts.googleapis.com/icon?family=Material+Icons'>
-    <link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700'>
-    <link rel='stylesheet' href='https://www.gstatic.com/external_hosted/materialize/all_styles-bundle.css'>
-    <link type='text/css' href='/css/navbar.css' rel='stylesheet'>
-    <link type='text/css' href='/css/show_table.css' rel='stylesheet'>
-    <script src='/js/analytics.js' type='text/javascript'></script>
-    <script type='text/javascript' src='https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js'></script>
-    <script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script>
-    <script src='https://www.gstatic.com/external_hosted/materialize/materialize.min.js'></script>
-    <script src='https://www.gstatic.com/external_hosted/moment/min/moment-with-locales.min.js'></script>
-    <script type='text/javascript'>
-      if (${analytics_id}) analytics_init(${analytics_id});
+  <%@ include file="header.jsp" %>
+  <link type='text/css' href='/css/show_table.css' rel='stylesheet'>
+  <script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script>
+  <script src='https://www.gstatic.com/external_hosted/moment/min/moment-with-locales.min.js'></script>
+  <script type='text/javascript'>
       google.charts.load('current', {'packages':['table', 'corechart']});
       google.charts.setOnLoadCallback(drawGridTable);
       google.charts.setOnLoadCallback(drawProfilingTable);
@@ -244,28 +234,10 @@
           };
           table.draw(data, options);
       }
-    </script>
-
-    <nav id='navbar'>
-      <div class='nav-wrapper'>
-        <span>
-          <a href='${pageContext.request.contextPath}/' class='breadcrumb'>VTS Dashboard Home</a>
-          <a href='#!' class='breadcrumb'>${testName}</a>
-        </span>
-        <ul class='right'><li>
-          <a id='dropdown-button' class='dropdown-button btn red lighten-3' href='#' data-activates='dropdown'>
-            ${email}
-          </a>
-        </li></ul>
-        <ul id='dropdown' class='dropdown-content'>
-          <li><a href='${logoutURL}'>Log out</a></li>
-        </ul>
-      </div>
-    </nav>
-  </head>
+  </script>
 
   <body>
-    <div class='container'>
+    <div class='wide container'>
       <div class='row'>
         <div class='col s12'>
           <div class='card' id='filter-wrapper'>
@@ -369,11 +341,6 @@
         <a href="#!" class="modal-action modal-close waves-effect btn-flat">Close</a>
       </div>
     </div>
-    <footer class='page-footer'>
-      <div class='footer-copyright'>
-          <div class='container'>© 2016 - The Android Open Source Project
-          </div>
-      </div>
-    </footer>
+    <%@ include file="footer.jsp" %>
   </body>
 </html>
diff --git a/web/dashboard/appengine/servlet/src/main/webapp/css/common.css b/web/dashboard/appengine/servlet/src/main/webapp/css/common.css
new file mode 100644
index 0000000..974f129
--- /dev/null
+++ b/web/dashboard/appengine/servlet/src/main/webapp/css/common.css
@@ -0,0 +1,25 @@
+/* Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+.container {
+    min-height: 80%;
+}
+
+@media only screen and (min-width: 993px) {
+    .wide.container {
+        width: 80%;
+        max-width: 1600px;
+    }
+}
diff --git a/web/dashboard/appengine/servlet/src/main/webapp/css/dashboard_main.css b/web/dashboard/appengine/servlet/src/main/webapp/css/dashboard_main.css
index 0e5fa02..aa18c5f 100644
--- a/web/dashboard/appengine/servlet/src/main/webapp/css/dashboard_main.css
+++ b/web/dashboard/appengine/servlet/src/main/webapp/css/dashboard_main.css
@@ -12,9 +12,6 @@
  See the License for the specific language governing permissions and
  limitations under the License.
 */
-.container {
-    min-height: 85%;
-}
 
 #edit-button-wrapper {
     bottom: 25px;
@@ -32,7 +29,7 @@
     text-align: center;
 }
 
-.card.option {
+.row .col.s12.card.option {
     padding: 10px 30px;
     margin: 8px;
 }
diff --git a/web/dashboard/appengine/servlet/src/main/webapp/css/show_coverage.css b/web/dashboard/appengine/servlet/src/main/webapp/css/show_coverage.css
index 6c530bf..c187fd3 100644
--- a/web/dashboard/appengine/servlet/src/main/webapp/css/show_coverage.css
+++ b/web/dashboard/appengine/servlet/src/main/webapp/css/show_coverage.css
@@ -13,17 +13,6 @@
  limitations under the License.
 */
 
-@media only screen and (min-width: 993px) {
-    .container {
-        width: 80%;
-        max-width: 1600px;
-    }
-}
-
-.container {
-    min-height: 80%;
-}
-
 .collapsible.popout {
     margin-bottom: 50px;
 }
diff --git a/web/dashboard/appengine/servlet/src/main/webapp/css/show_performance_digest.css b/web/dashboard/appengine/servlet/src/main/webapp/css/show_performance_digest.css
index 8884392..5d0c0e6 100644
--- a/web/dashboard/appengine/servlet/src/main/webapp/css/show_performance_digest.css
+++ b/web/dashboard/appengine/servlet/src/main/webapp/css/show_performance_digest.css
@@ -13,17 +13,6 @@
  limitations under the License.
 */
 
-@media only screen and (min-width: 993px) {
-    .container {
-        width: 80%;
-        max-width: 1600px;
-    }
-}
-
-.container {
-    min-height: 85%;
-}
-
 #header-container {
     padding: 25px;
 }
diff --git a/web/dashboard/appengine/servlet/src/main/webapp/css/show_preferences.css b/web/dashboard/appengine/servlet/src/main/webapp/css/show_preferences.css
index 55920df..9731037 100644
--- a/web/dashboard/appengine/servlet/src/main/webapp/css/show_preferences.css
+++ b/web/dashboard/appengine/servlet/src/main/webapp/css/show_preferences.css
@@ -12,9 +12,6 @@
  See the License for the specific language governing permissions and
  limitations under the License.
 */
-.container {
-    min-height: 85%;
-}
 
 .card.option {
     padding: 10px 30px;
diff --git a/web/dashboard/appengine/servlet/src/main/webapp/css/show_table.css b/web/dashboard/appengine/servlet/src/main/webapp/css/show_table.css
index 5d8f749..7a82ae9 100644
--- a/web/dashboard/appengine/servlet/src/main/webapp/css/show_table.css
+++ b/web/dashboard/appengine/servlet/src/main/webapp/css/show_table.css
@@ -12,12 +12,6 @@
  See the License for the specific language governing permissions and
  limitations under the License.
 */
-@media only screen and (min-width: 993px) {
-    .container {
-        width: 80%;
-        max-width: 1600px;
-    }
-}
 
 table {
     font-family: Roboto !important;
diff --git a/web/dashboard/appengine/servlet/src/main/webapp/dashboard_main.jsp b/web/dashboard/appengine/servlet/src/main/webapp/dashboard_main.jsp
deleted file mode 100644
index 5b799d6..0000000
--- a/web/dashboard/appengine/servlet/src/main/webapp/dashboard_main.jsp
+++ /dev/null
@@ -1,103 +0,0 @@
-<%-- //[START all]--%>
-<%--
-  ~ Copyright (c) 2016 Google Inc. All Rights Reserved.
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License"); you
-  ~ may not use this file except in compliance with the License. You may
-  ~ obtain a copy of the License at
-  ~
-  ~     http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-  ~ implied. See the License for the specific language governing
-  ~ permissions and limitations under the License.
-  --%>
-<%@ page contentType='text/html;charset=UTF-8' language='java' %>
-<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
-<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
-
-
-<html>
-  <link rel='icon' href='https://www.gstatic.com/images/branding/googleg/1x/googleg_standard_color_32dp.png' sizes='32x32'>
-  <link rel='stylesheet' href='https://fonts.googleapis.com/icon?family=Material+Icons'>
-  <link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700'>
-  <link rel='stylesheet' href='https://www.gstatic.com/external_hosted/materialize/all_styles-bundle.css'>
-  <link rel='stylesheet' href='/css/navbar.css'>
-  <link rel='stylesheet' href='/css/dashboard_main.css'>
-  <script src='/js/analytics.js' type='text/javascript'></script>
-  <script src='https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js'></script>
-  <script src='https://www.gstatic.com/external_hosted/materialize/materialize.min.js'></script>
-  <script>
-      if (${analytics_id}) analytics_init(${analytics_id});
-  </script>
-  <head>
-    <title>VTS Dashboard</title>
-
-    <nav id='navbar'>
-      <div class='nav-wrapper'>
-        <span>
-          <a href='#!' class='breadcrumb'>VTS Dashboard Home</a>
-        </span>
-        <ul class='right'><li>
-          <a id='dropdown-button' class='dropdown-button btn red lighten-3' href='#' data-activates='dropdown'>
-            ${email}
-          </a>
-        </li></ul>
-        <ul id='dropdown' class='dropdown-content'>
-          <li><a href='${logoutURL}'>Log out</a></li>
-        </ul>
-        </div>
-      </div>
-    </nav>
-  </head>
-
-  <body>
-    <div class='container'>
-      <div class='row' id='options'>
-        <c:choose>
-          <c:when test="${not empty error}">
-            <div id="error-container" class="row card">
-              <div class="col s12 center-align">
-                <h5>${error}</h5>
-              </div>
-            </div>
-          </c:when>
-          <c:otherwise>
-            <div class='col s12'>
-              <h4 id='section-header'>${headerLabel}</h4>
-            </div>
-            <c:forEach items='${testNames}' var='test'>
-              <a href='${pageContext.request.contextPath}/show_table?testName=${test}'>
-                <div class='col s12 card hoverable option valign-wrapper waves-effect'>
-                  <span class='entry valign'>${test}</span>
-                </div>
-              </a>
-            </c:forEach>
-          </c:otherwise>
-        </c:choose>
-      </div>
-      <div class='row center-align'>
-        <a href='${buttonLink}' id='show-button' class='btn waves-effect red'>${buttonLabel}
-          <i id='show-button-arrow' class='material-icons right'>${buttonIcon}</i>
-        </a>
-      </div>
-    </div>
-    <c:if test='${not showAll}'>
-      <div id='edit-button-wrapper' class='fixed-action-btn'>
-        <a href='/show_preferences' id='edit-button' class='btn-floating btn-large red waves-effect'>
-          <i class='large material-icons'>mode_edit</i>
-        </a>
-      </div>
-    </c:if>
-    <footer class='page-footer'>
-      <div class='footer-copyright'>
-        <div class='container'>
-          © 2016 - The Android Open Source Project
-        </div>
-      </div>
-    </footer>
-  </body>
-</html>
-<%-- //[END all]--%>
diff --git a/web/dashboard/appengine/servlet/src/main/webapp/js/analytics.js b/web/dashboard/appengine/servlet/src/main/webapp/js/analytics.js
deleted file mode 100644
index 94a5350..0000000
--- a/web/dashboard/appengine/servlet/src/main/webapp/js/analytics.js
+++ /dev/null
@@ -1,10 +0,0 @@
-function analytics_init (id) {
-  // Autogenerated from Google Analytics
-  (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
-    (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
-    m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
-    })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
-
-    ga('create', id, 'auto');
-    ga('send', 'pageview');
-}
\ No newline at end of file
diff --git a/web/dashboard/appengine/servlet/src/main/webapp/show_graph.jsp b/web/dashboard/appengine/servlet/src/main/webapp/show_graph.jsp
deleted file mode 100644
index e9b8301..0000000
--- a/web/dashboard/appengine/servlet/src/main/webapp/show_graph.jsp
+++ /dev/null
@@ -1,286 +0,0 @@
-<%--
-  ~ Copyright (c) 2016 Google Inc. All Rights Reserved.
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License"); you
-  ~ may not use this file except in compliance with the License. You may
-  ~ obtain a copy of the License at
-  ~
-  ~     http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-  ~ implied. See the License for the specific language governing
-  ~ permissions and limitations under the License.
-  --%>
-<%@ page contentType="text/html;charset=UTF-8" language="java" %>
-<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-
-<html>
-  <head>
-    <link rel='icon' href='https://www.gstatic.com/images/branding/googleg/1x/googleg_standard_color_32dp.png' sizes='32x32'>
-    <link rel='stylesheet' href='https://fonts.googleapis.com/icon?family=Material+Icons'>
-    <link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700'>
-    <link rel='stylesheet' href='https://www.gstatic.com/external_hosted/materialize/all_styles-bundle.css'>
-    <link type='text/css' href='/css/navbar.css' rel='stylesheet'>
-    <link type='text/css' href='/css/datepicker.css' rel='stylesheet'>
-    <link type='text/css' href='/css/show_graph.css' rel='stylesheet'>
-    <link rel='stylesheet' href='https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.0/jquery-ui.css'>
-    <script src='/js/analytics.js' type='text/javascript'></script>
-    <script type='text/javascript' src='https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js'></script>
-    <script src='https://www.gstatic.com/external_hosted/materialize/materialize.min.js'></script>
-    <script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script>
-    <script type='text/javascript' src='https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js'></script>
-    <script src='/js/analytics.js' type='text/javascript'></script>
-    <title>Graph</title>
-    <script type='text/javascript'>
-      if (${analytics_id}) analytics_init(${analytics_id});
-      google.charts.load('current', {packages:['corechart', 'table', 'line']});
-      google.charts.setOnLoadCallback(drawAllGraphs);
-
-      ONE_DAY = 86400000000;
-      MICRO_PER_MILLI = 1000;
-
-      $(function() {
-          $('select').material_select();
-          var date = $('#date').datepicker({
-              showAnim: 'slideDown',
-              maxDate: new Date()
-          });
-          date.datepicker('setDate', new Date(${startTime} / MICRO_PER_MILLI));
-          $('#load').click(load);
-      });
-
-      // Draw all graphs.
-      function drawAllGraphs() {
-          var graphs = ${graphs};
-          graphs.forEach(function(graph) {
-              if (graph.type == 'LINE_GRAPH') drawLineGraph(graph);
-              else if (graph.type == 'HISTOGRAM') drawHistogram(graph);
-          });
-      }
-
-     /**
-      * Draw a line graph.
-      *
-      * Args:
-      *     lineGraph: a JSON object containing the following fields:
-      *                - name: the name of the graph
-      *                - values: an array of numbers
-      *                - ticks: an array of strings to use as x-axis labels
-      *                - ids: an array of string labels for each point (e.g. the
-      *                       build info for the run that produced the point)
-      *                - x_label: the string label for the x axis
-      *                - y_label: the string label for the y axis
-      */
-      function drawLineGraph(lineGraph) {
-          var title = 'Performance';
-          if (lineGraph.name) title += ' (' + lineGraph.name + ')';
-          if (lineGraph.ticks.length < 1) {
-              return;
-          }
-          lineGraph.ticks.forEach(function (label, i) {
-              lineGraph.values[i].unshift(label);
-          });
-          var data = new google.visualization.DataTable();
-          data.addColumn('string', lineGraph.x_label);
-          lineGraph.ids.forEach(function(id) {
-              data.addColumn('number', id);
-          });
-          data.addRows(lineGraph.values);
-          var options = {
-            chart: {
-                title: title,
-                subtitle: lineGraph.y_label
-            },
-            legend: { position: 'none' }
-          };
-          var container = $('<div class="row card center-align col s12 graph-wrapper"></div>');
-          container.appendTo('#profiling-container');
-          var chartDiv = $('<div class="col s12 graph"></div>');
-          chartDiv.appendTo(container);
-          var chart = new google.charts.Line(chartDiv[0]);
-          chart.draw(data, options);
-      }
-
-     /**
-      * Draw a histogram.
-      *
-      * Args:
-      *     hist: a JSON object containing the following fields:
-      *           - name: the name of the graph
-      *           - values: an array of numbers
-      *           - ids: an array of string labels for each point (e.g. the
-      *                  build info for the run that produced the point)
-      *           - x_label: the string label for the x axis
-      *           - y_label: the string label for the y axis
-      */
-      function drawHistogram(hist) {
-          test = hist;
-          var title = 'Performance';
-          if (hist.name) title += ' (' + hist.name + ')';
-          var values = hist.values;
-          var histogramData = values.map(function(d, i) {
-              return [hist.ids[i], d];
-          });
-          var min = Math.min.apply(null, values),
-              max = Math.max.apply(null, values);
-
-          var histogramTicks = new Array(10);
-          var delta = (max - min) / 10;
-          for (var i = 0; i <= 10; i++) {
-              histogramTicks[i] = Math.round(min + delta * i);
-          }
-
-          var data = google.visualization.arrayToDataTable(histogramData, true);
-
-          var options = {
-              title: title,
-              titleTextStyle: {
-                  color: '#757575',
-                  fontSize: 16,
-                  bold: false
-              },
-              legend: { position: 'none' },
-              colors: ['#4285F4'],
-              fontName: 'Roboto',
-              vAxis:{
-                  title: hist.y_label,
-                  titleTextStyle: {
-                      color: '#424242',
-                      fontSize: 12,
-                      italic: false
-                  },
-                  textStyle: {
-                      fontSize: 12,
-                      color: '#757575'
-                  },
-              },
-              hAxis: {
-                  ticks: histogramTicks,
-                  title: hist.x_label,
-                  textStyle: {
-                      fontSize: 12,
-                      color: '#757575'
-                  },
-                  titleTextStyle: {
-                      color: '#424242',
-                      fontSize: 12,
-                      italic: false
-                  }
-              },
-              bar: { gap: 0 },
-              histogram: {
-                  maxNumBuckets: 200,
-                  minValue: min * 0.95,
-                  maxValue: max * 1.05
-              },
-              chartArea: {
-                  width: '100%',
-                  top: 40,
-                  left: 50,
-                  height: '80%'
-              }
-          };
-          var container = $('<div class="row card col s12 graph-wrapper"></div>');
-          container.appendTo('#profiling-container');
-
-          var chartDiv = $('<div class="col s12 graph"></div>');
-          chartDiv.appendTo(container);
-          var chart = new google.visualization.Histogram(chartDiv[0]);
-          chart.draw(data, options);
-
-          var tableDiv = $('<div class="col s12"></div>');
-          tableDiv.appendTo(container);
-
-          var tableHtml = '<table class="percentile-table"><thead><tr>';
-          hist.percentiles.forEach(function(p) {
-              tableHtml += '<th data-field="id">' + p + '%</th>';
-          });
-          tableHtml += '</tr></thead><tbody><tr>';
-          hist.percentile_values.forEach(function(v) {
-              tableHtml += '<td>' + v + '</td>';
-          });
-          tableHtml += '</tbody></table>';
-          $(tableHtml).appendTo(tableDiv);
-      }
-
-      // Reload the page.
-      function load() {
-          var startTime = $('#date').datepicker('getDate').getTime();
-          startTime = startTime + (ONE_DAY / MICRO_PER_MILLI) - 1;
-          var ctx = '${pageContext.request.contextPath}';
-          var link = ctx + '/show_graph?profilingPoint=${profilingPointName}' +
-              '&testName=${testName}' +
-              '&startTime=' + (startTime * MICRO_PER_MILLI);
-          if ($('#device-select').prop('selectedIndex') > 1) {
-              link += '&device=' + $('#device-select').val();
-          }
-          window.open(link,'_self');
-      }
-
-    </script>
-    <nav id='navbar'>
-      <div class='nav-wrapper'>
-        <span>
-          <a href='${pageContext.request.contextPath}/' class='breadcrumb'>VTS Dashboard Home</a>
-          <a href='${pageContext.request.contextPath}/show_table?testName=${testName}&startTime=${startTime}' class='breadcrumb'>${testName}</a>
-          <a href='#!' class='breadcrumb'>Profiling</a>
-        </span>
-        <ul class='right'><li>
-          <a id='dropdown-button' class='dropdown-button btn red lighten-3' href='#' data-activates='dropdown'>
-            ${email}
-          </a>
-        </li></ul>
-        <ul id='dropdown' class='dropdown-content'>
-          <li><a href='${logoutURL}'>Log out</a></li>
-        </ul>
-      </div>
-    </nav>
-  </head>
-
-  <body>
-    <div id='download' class='fixed-action-btn'>
-      <a id='b' class='btn-floating btn-large red waves-effect waves-light'>
-        <i class='large material-icons'>file_download</i>
-      </a>
-    </div>
-    <div class='container'>
-      <div class='row card'>
-        <div id='header-container' class='valign-wrapper col s12'>
-          <div class='col s3 valign'>
-            <h5>Profiling Point:</h5>
-          </div>
-          <div class='col s9 right-align valign'>
-            <h5 class='profiling-name truncate'>${profilingPointName}</h5>
-          </div>
-        </div>
-        <div id='date-container' class='col s12'>
-          <div id='device-select-wrapper' class='input-field col s6 m3 offset-m6'>
-            <select id='device-select'>
-              <option value='' disabled>Select device</option>
-              <option value='0' ${empty selectedDevice ? 'selected' : ''}>All Devices</option>
-              <c:forEach items='${devices}' var='device' varStatus='loop'>
-                <option value=${device} ${selectedDevice eq device ? 'selected' : ''}>${device}</option>
-              </c:forEach>
-            </select>
-          </div>
-          <input type='text' id='date' name='date' class='col s5 m2'>
-          <a id='load' class='btn-floating btn-medium red right waves-effect waves-light'>
-            <i class='medium material-icons'>cached</i>
-          </a>
-        </div>
-      </div>
-      <div id='profiling-container'>
-      </div>
-      <c:if test='${not empty error}'>
-        <div id='error-container' class='row card'>
-          <div class='col s10 offset-s1 center-align'>
-            <!-- Error in case of profiling data is missing -->
-            <h5>${error}</h5>
-          </div>
-        </div>
-      </c:if>
-    </div>
-  </body>
-</html>
diff --git a/web/dashboard/appengine/servlet/src/main/webapp/show_performance_digest.jsp b/web/dashboard/appengine/servlet/src/main/webapp/show_performance_digest.jsp
deleted file mode 100644
index affe4c3..0000000
--- a/web/dashboard/appengine/servlet/src/main/webapp/show_performance_digest.jsp
+++ /dev/null
@@ -1,138 +0,0 @@
-<%--
-  ~ Copyright (c) 2016 Google Inc. All Rights Reserved.
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License"); you
-  ~ may not use this file except in compliance with the License. You may
-  ~ obtain a copy of the License at
-  ~
-  ~     http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-  ~ implied. See the License for the specific language governing
-  ~ permissions and limitations under the License.
-  --%>
-<%@ page contentType='text/html;charset=UTF-8' language='java' %>
-<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
-<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
-
-<html>
-  <head>
-    <title>VTS Table</title>
-    <link rel='icon' href='//www.gstatic.com/images/branding/googleg/1x/googleg_standard_color_32dp.png' sizes='32x32'>
-    <link rel='stylesheet' href='https://fonts.googleapis.com/icon?family=Material+Icons'>
-    <link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700'>
-    <link rel='stylesheet' href='https://www.gstatic.com/external_hosted/materialize/all_styles-bundle.css'>
-    <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.0/jquery-ui.css">
-    <link type='text/css' href='/css/navbar.css' rel='stylesheet'>
-    <link type="text/css" href="/css/datepicker.css" rel="stylesheet">
-    <link type='text/css' href='/css/show_performance_digest.css' rel='stylesheet'>
-    <script src='/js/analytics.js' type='text/javascript'></script>
-    <script type='text/javascript' src='https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js'></script>
-    <script src='https://www.gstatic.com/external_hosted/materialize/materialize.min.js'></script>
-    <script src='https://www.gstatic.com/external_hosted/moment/min/moment-with-locales.min.js'></script>
-    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
-    <script type='text/javascript'>
-      if (${analytics_id}) analytics_init(${analytics_id});
-
-      ONE_DAY = 86400000000;
-      MICRO_PER_MILLI = 1000;
-
-      function load() {
-          var time = $('#date').datepicker('getDate').getTime() - 1;
-          time = time * MICRO_PER_MILLI + ONE_DAY;  // end of day
-          var ctx = '${pageContext.request.contextPath}';
-          var link = ctx + '/show_performance_digest?profilingPoint=${profilingPointName}' +
-              '&testName=${testName}' +
-              '&startTime=' + time;
-          if ($('#device-select').prop('selectedIndex') > 1) {
-              link += '&device=' + $('#device-select').val();
-          }
-          window.open(link,'_self');
-      }
-
-      $(function() {
-          var date = $('#date').datepicker({
-              showAnim: "slideDown",
-              maxDate: new Date()
-          });
-          date.datepicker('setDate', new Date(${startTime} / MICRO_PER_MILLI));
-          $('#load').click(load);
-
-          $('.date-label').each(function(i) {
-              var label = $(this);
-              label.html(moment(parseInt(label.html())).format('M/D/YY'));
-          });
-          $('select').material_select();
-      });
-
-    </script>
-
-    <nav id='navbar'>
-      <div class='nav-wrapper'>
-        <span>
-          <a href='${pageContext.request.contextPath}/' class='breadcrumb'>VTS Dashboard Home</a>
-          <a href='${pageContext.request.contextPath}/show_table?testName=${testName}' class='breadcrumb'>${testName}</a>
-          <a href="#!" class="breadcrumb">Performance Digest</a>
-        </span>
-        <ul class='right'><li>
-          <a id='dropdown-button' class='dropdown-button btn red lighten-3' href='#' data-activates='dropdown'>
-            ${email}
-          </a>
-        </li></ul>
-        <ul id='dropdown' class='dropdown-content'>
-          <li><a href='${logoutURL}'>Log out</a></li>
-        </ul>
-      </div>
-    </nav>
-  </head>
-
-  <body>
-    <div class='container'>
-      <div class='row card'>
-        <div id='header-container' class='col s12'>
-          <div class='col s12'>
-            <h4>Daily Performance Digest</h4>
-          </div>
-          <div id='device-select-wrapper' class='input-field col s6 m3 offset-m6'>
-            <select id='device-select'>
-              <option value='' disabled>Select device</option>
-              <option value='0' ${empty selectedDevice ? 'selected' : ''}>All Devices</option>
-              <c:forEach items='${devices}' var='device' varStatus='loop'>
-                <option value=${device} ${selectedDevice eq device ? 'selected' : ''}>${device}</option>
-              </c:forEach>
-            </select>
-          </div>
-          <input type='text' id='date' name='date' class='col s5 m2'>
-          <a id='load' class='btn-floating btn-medium red right waves-effect waves-light'>
-            <i class='medium material-icons'>cached</i>
-          </a>
-        </div>
-      </div>
-      <div class='row'>
-        <c:forEach items='${tables}' var='table' varStatus='loop'>
-          <div class='col s12 card summary'>
-            <div class='col s3 valign'>
-              <h5>Profiling Point:</h5>
-            </div>
-            <div class='col s9 right-align valign'>
-              <h5 class="profiling-name truncate">${tableTitles[loop.index]}</h5>
-            </div>
-            ${table}
-            <span class='profiling-subtitle'>
-              ${tableSubtitles[loop.index]}
-            </span>
-          </div>
-        </c:forEach>
-      </div>
-    </div>
-
-    <footer class='page-footer'>
-      <div class='footer-copyright'>
-          <div class='container'>© 2016 - The Android Open Source Project
-          </div>
-      </div>
-    </footer>
-  </body>
-</html>
diff --git a/web/dashboard/appengine/servlet/src/test/java/com/android/vts/servlet/VtsPerformanceJobServletTest.java b/web/dashboard/appengine/servlet/src/test/java/com/android/vts/servlet/VtsPerformanceJobServletTest.java
index e23d30f..24db08a 100644
--- a/web/dashboard/appengine/servlet/src/test/java/com/android/vts/servlet/VtsPerformanceJobServletTest.java
+++ b/web/dashboard/appengine/servlet/src/test/java/com/android/vts/servlet/VtsPerformanceJobServletTest.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright (c) 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you
+ * may not use this file except in compliance with the License. You may
+ * obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
 package com.android.vts.servlet;
 
 import static org.junit.Assert.*;
diff --git a/web/dashboard/appengine/servlet/src/test/java/com/android/vts/util/MathUtilTest.java b/web/dashboard/appengine/servlet/src/test/java/com/android/vts/util/MathUtilTest.java
index e8532b9..766ba0b 100644
--- a/web/dashboard/appengine/servlet/src/test/java/com/android/vts/util/MathUtilTest.java
+++ b/web/dashboard/appengine/servlet/src/test/java/com/android/vts/util/MathUtilTest.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright (c) 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you
+ * may not use this file except in compliance with the License. You may
+ * obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
 package com.android.vts.util;
 
 import static org.junit.Assert.*;
diff --git a/web/dashboard/appengine/servlet/src/test/java/com/android/vts/util/ProfilingPointSummaryTest.java b/web/dashboard/appengine/servlet/src/test/java/com/android/vts/util/ProfilingPointSummaryTest.java
index 2bdcd47..a83b2c5 100644
--- a/web/dashboard/appengine/servlet/src/test/java/com/android/vts/util/ProfilingPointSummaryTest.java
+++ b/web/dashboard/appengine/servlet/src/test/java/com/android/vts/util/ProfilingPointSummaryTest.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright (c) 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you
+ * may not use this file except in compliance with the License. You may
+ * obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
 package com.android.vts.util;
 
 import static org.junit.Assert.*;
diff --git a/web/dashboard/appengine/servlet/src/test/java/com/android/vts/util/StatSummaryTest.java b/web/dashboard/appengine/servlet/src/test/java/com/android/vts/util/StatSummaryTest.java
index b262c0f..846c354 100644
--- a/web/dashboard/appengine/servlet/src/test/java/com/android/vts/util/StatSummaryTest.java
+++ b/web/dashboard/appengine/servlet/src/test/java/com/android/vts/util/StatSummaryTest.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright (c) 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you
+ * may not use this file except in compliance with the License. You may
+ * obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
 package com.android.vts.util;
 
 import static org.junit.Assert.*;