Merge "COPYBARA_IMPORT=Project import generated by Copybara." into main
diff --git a/docs/contributing/common-tasks.md b/docs/contributing/common-tasks.md
index 43c5421..4a76189 100644
--- a/docs/contributing/common-tasks.md
+++ b/docs/contributing/common-tasks.md
@@ -24,7 +24,7 @@
 
 - Running the file cannot generate any data. There can be only `CREATE PERFETTO {FUNCTION|TABLE|VIEW|MACRO}` statements inside.
 - The name of each standard library object needs to start with `{module_name}_` or be prefixed with an underscore(`_`) for internal objects.
-  The names must only contain lower and upper case letters and underscores. When a module is included (using the `INCLUDE PERFETTO MODULE`) the internal objects  should not be treated as an API. 
+  The names must only contain lower and upper case letters and underscores. When a module is included (using the `INCLUDE PERFETTO MODULE`) the internal objects  should not be treated as an API.
 - Every table or view should have [a schema](/docs/analysis/perfetto-sql-syntax.md#tableview-schema).
 
 ### Documentation
@@ -98,11 +98,11 @@
   arg_set_id INT
 )
 AS
-SELECT 
-  slice_name, 
-  slice_ts, 
-  slice_dur, 
-  thread_name, 
+SELECT
+  slice_name,
+  slice_ts,
+  slice_dur,
+  thread_name,
   arg_set_id
 FROM thread_slices_for_all_launches
 WHERE launch_id = $launch_id AND slice_name GLOB $slice_name;
@@ -156,3 +156,17 @@
 1. Go to `protos/perfetto/trace_processor/trace_processor.proto`
 2. Increment `TRACE_PROCESSOR_CURRENT_API_VERSION`
 3. Add a comment explaining what has changed.
+
+## Update statsd descriptor
+
+Perfetto has limited support for statsd atoms it does not know about.
+
+* Must be referred to using `raw_atom_id` in the config.
+* Show up as `atom_xxx.field_yyy` in trace processor.
+* Only top level messages are parsed.
+
+To update Perfetto's descriptor and handle new atoms from AOSP without these
+limitations:
+
+1. Run `tools/update-statsd-descriptor`.
+2. Upload and land your change as normal.
diff --git a/infra/perfetto.dev/src/assets/script.js b/infra/perfetto.dev/src/assets/script.js
index 44adbf6..6c4d04d 100644
--- a/infra/perfetto.dev/src/assets/script.js
+++ b/infra/perfetto.dev/src/assets/script.js
@@ -69,7 +69,7 @@
     toc.appendChild(li);
     doAfterLoadEvent(() => {
       tocAnchors.push(
-          {top: anchor.offsetTop + anchor.offsetHeight / 2, obj: link});
+        { top: anchor.offsetTop + anchor.offsetHeight / 2, obj: link });
     });
   }
   tocContainer.innerHTML = '';
@@ -81,7 +81,7 @@
     return;
   tocEventHandlersInstalled = true;
   const doc = document.querySelector('.doc');
-  const passive = {passive: true};
+  const passive = { passive: true };
   if (doc) {
     const offY = doc.offsetTop;
     doc.addEventListener('mousemove', (e) => onMouseMove(offY, e), passive);
@@ -91,9 +91,9 @@
   }
   window.addEventListener('scroll', () => onScroll(), passive);
   resizeObserver = new ResizeObserver(() => requestAnimationFrame(() => {
-                                        updateNav();
-                                        updateTOC();
-                                      }));
+    updateNav();
+    updateTOC();
+  }));
   resizeObserver.observe(doc);
 }
 
@@ -235,7 +235,7 @@
 
 function setupSearch() {
   const URL =
-      'https://www.googleapis.com/customsearch/v1?key=AIzaSyBTD2XJkQkkuvDn76LSftsgWOkdBz9Gfwo&cx=007128963598137843411:8suis14kcmy&q='
+    'https://www.googleapis.com/customsearch/v1?key=AIzaSyBTD2XJkQkkuvDn76LSftsgWOkdBz9Gfwo&cx=007128963598137843411:8suis14kcmy&q='
   const searchContainer = document.getElementById('search');
   const searchBox = document.getElementById('search-box');
   const searchRes = document.getElementById('search-res')
diff --git a/infra/perfetto.dev/src/assets/style.scss b/infra/perfetto.dev/src/assets/style.scss
index 63d6fc1..0a0ff2e 100644
--- a/infra/perfetto.dev/src/assets/style.scss
+++ b/infra/perfetto.dev/src/assets/style.scss
@@ -15,234 +15,235 @@
 // Common + CSS reset
 // -----------------------------------------------------------------------------
 :root {
-    --site-header-height: 50px;
-    --home-highlights-height: 128px;
-    --content-max-width: 1100px;
-    --anim-ease: cubic-bezier(0.4, 0.0, 0.2, 1);
+  --site-header-height: 50px;
+  --home-highlights-height: 128px;
+  --content-max-width: 1100px;
+  --anim-ease: cubic-bezier(0.4, 0, 0.2, 1);
 
-    // This is set to 1 by JS after onload. This is to prevent flickering on
-    // page load on the nav bar and other entries while transitioning in their
-    // initial state.
-    --anim-enabled: 0;
+  // This is set to 1 by JS after onload. This is to prevent flickering on
+  // page load on the nav bar and other entries while transitioning in their
+  // initial state.
+  --anim-enabled: 0;
 
-    --anim-time: calc(0.15s * var(--anim-enabled));
+  --anim-time: calc(0.15s * var(--anim-enabled));
 }
 
 $wide: "(max-width: 1100px)";
 $mobile: "(max-width: 768px)";
 
 @mixin minimal-scrollbar {
-    &::-webkit-scrollbar {
-        width: 8px;
-        background-color: transparent;
-    }
-    &::-webkit-scrollbar-thumb {
-        background-color: #ccc;
-        border-radius: 8px;
-    }
+  &::-webkit-scrollbar {
+    width: 8px;
+    background-color: transparent;
+  }
+  &::-webkit-scrollbar-thumb {
+    background-color: #ccc;
+    border-radius: 8px;
+  }
 }
 
 @media (max-aspect-ratio: 1/1) {
-     :root {
-        --home-highlights-height: 256px;
-    }
+  :root {
+    --home-highlights-height: 256px;
+  }
 }
 
 * {
-    box-sizing: border-box;
-    -webkit-tap-highlight-color: none;
+  box-sizing: border-box;
+  -webkit-tap-highlight-color: none;
 }
 
 html {
-    font-family: Roboto, sans-serif;
-    -webkit-font-smoothing: antialiased;
+  font-family: Roboto, sans-serif;
+  -webkit-font-smoothing: antialiased;
 }
 
 html,
 body {
-    padding: 0;
-    margin: 0;
+  padding: 0;
+  margin: 0;
 }
 
 h1,
 h2,
 h3 {
-    font-family: inherit;
-    font-size: inherit;
-    font-weight: inherit;
-    padding: 0;
-    margin: 0;
+  font-family: inherit;
+  font-size: inherit;
+  font-weight: inherit;
+  padding: 0;
+  margin: 0;
 }
 
 // -----------------------------------------------------------------------------
 // Site header
 // -----------------------------------------------------------------------------
 .site-header {
-    background-color: hsl(210, 30%, 16%);
-    color: hsl(210, 17%, 98%);
-    position: sticky; // Sticky so the .docs element below doesn't start @ 0.
+  background-color: hsl(210, 30%, 16%);
+  color: hsl(210, 17%, 98%);
+  position: sticky; // Sticky so the .docs element below doesn't start @ 0.
+  top: 0;
+  width: 100%;
+  --sh-padding-y: 5px;
+  max-height: var(--site-header-height);
+  padding: var(--sh-padding-y) 30px;
+  box-shadow: rgba(0, 0, 0, 0.3) 0 3px 3px 0;
+  overflow: hidden;
+  display: flex;
+  z-index: 10;
+  transition: max-height var(--anim-ease) var(--anim-time);
+  &.expanded {
+    max-height: 100vh;
+  }
+  .brand {
+    img {
+      height: 40px;
+      vertical-align: bottom;
+    }
+    font-weight: 200;
+    font-size: 28px;
+    flex-grow: 1;
+    .brand-docs {
+      text-transform: uppercase;
+      font-size: 14px;
+      color: #ecba2a;
+      vertical-align: bottom;
+      line-height: 30px;
+      font-weight: 400;
+    }
+  }
+  > *:not(:first-child) {
+    line-height: calc(var(--site-header-height) - var(--sh-padding-y) * 2);
+    font-family: "Source Sans Pro", sans-serif;
+    font-weight: 400;
+    font-size: 1.1rem;
+    margin: 0 20px;
+    color: hsl(210, 17%, 85%);
+  }
+  a {
+    text-decoration: none;
+    &:hover {
+      color: hsl(210, 17%, 100%);
+    }
+  }
+  .menu {
+    visibility: hidden;
+    font-family: "Material Icons Round";
+    font-size: 24px;
+    text-align: center;
+    position: absolute;
+    right: 0;
     top: 0;
-    width: 100%;
-    --sh-padding-y: 5px;
-    max-height: var(--site-header-height);
-    padding: var(--sh-padding-y) 30px;
-    box-shadow: rgba(0, 0, 0, 0.3) 0 3px 3px 0;
-    overflow: hidden;
-    display: flex;
-    z-index: 10;
-    transition: max-height var(--anim-ease) var(--anim-time);
-    &.expanded {
-        max-height: 100vh;
-    }
-    .brand {
-        img {
-            height: 40px;
-            vertical-align: bottom;
-        }
-        font-weight: 200;
-        font-size: 28px;
-        flex-grow: 1;
-        .brand-docs {
-            text-transform: uppercase;
-            font-size: 14px;
-            color: #ecba2a;
-            vertical-align: bottom;
-            line-height: 30px;
-            font-weight: 400;
-        }
-    }
-    >*:not(:first-child) {
-        line-height: calc(var(--site-header-height) - var(--sh-padding-y) * 2);
-        font-family: 'Source Sans Pro', sans-serif;
-        font-weight: 400;
-        font-size: 1.1rem;
-        margin: 0 20px;
-        color: hsl(210, 17%, 85%);
-    }
-    a {
-        text-decoration: none;
-        &:hover {
-            color: hsl(210, 17%, 100%);
-        }
+    line-height: var(--site-header-height);
+  }
+
+  @media #{$mobile} {
+    flex-direction: column;
+    > *:not(:first-child) {
+      margin-left: 40px;
     }
     .menu {
-        visibility: hidden;
-        font-family: 'Material Icons Round';
-        font-size: 24px;
-        text-align: center;
-        position: absolute;
-        right: 0;
-        top: 0;
-        line-height: var(--site-header-height);
+      visibility: visible;
     }
-
-    @media #{$mobile} {
-        flex-direction: column;
-        >*:not(:first-child) {
-            margin-left: 40px;
-        }
-        .menu {
-            visibility: visible;
-        }
-    }
+  }
 }
 
-
 #search {
-    position: relative;
-    flex-grow: 0;
-    transition: flex-grow cubic-bezier(1, 0.01, 1, 1) var(--anim-time), background-color ease var(--anim-time);
-    padding: 0;
+  position: relative;
+  flex-grow: 0;
+  transition: flex-grow cubic-bezier(1, 0.01, 1, 1) var(--anim-time),
+    background-color ease var(--anim-time);
+  padding: 0;
+  &::before {
+    visibility: hidden;
+    user-select: none;
+    content: "";
+    position: fixed;
+    left: 0;
+    right: 0;
+    top: var(--site-header-height);
+    bottom: 0;
+    z-index: -100;
+    background-color: rgba(255, 255, 255, 0.8);
+    backdrop-filter: blur(3px);
+    opacity: 0;
+    transition: opacity ease var(--anim-time), visibility 0s;
+  }
+  &:focus-within {
+    flex-grow: 1000;
     &::before {
-        visibility: hidden;
-        user-select: none;
-        content: '';
-        position: fixed;
-        left: 0;
-        right: 0;
-        top: var(--site-header-height);
-        bottom: 0;
-        z-index: -100;
-        background-color: rgba(255, 255, 255, 0.8);
-        backdrop-filter: blur(3px);
-        opacity: 0;
-        transition: opacity ease var(--anim-time), visibility 0s;
-
+      display: block;
+      opacity: 1;
+      visibility: visible;
     }
-    &:focus-within {
-        flex-grow: 1000;
-        &::before {
-            display: block;
-            opacity: 1;
-            visibility: visible;
-        }
-        #search-res {
-            display: block;
-        }
-
-    }
-
-    @media #{$mobile} {
-        display: none;
-    }
-
-    #search-box {
-        width: 100%;
-        height: 32px;
-        font-size: 1rem;;
-        color: #333;
-        background-color: rgba(255, 255, 255, 0.9);
-        border: 1px solid #eee;
-        border-radius: 2px;
-        background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M39.8 41.95 26.65 28.8q-1.5 1.3-3.5 2.025-2 .725-4.25.725-5.4 0-9.15-3.75T6 18.75q0-5.3 3.75-9.05 3.75-3.75 9.1-3.75 5.3 0 9.025 3.75 3.725 3.75 3.725 9.05 0 2.15-.7 4.15-.7 2-2.1 3.75L42 39.75Zm-20.95-13.4q4.05 0 6.9-2.875Q28.6 22.8 28.6 18.75t-2.85-6.925Q22.9 8.95 18.85 8.95q-4.1 0-6.975 2.875T9 18.75q0 4.05 2.875 6.925t6.975 2.875Z"/></svg>');
-        background-repeat: no-repeat;
-        background-size: contain;
-        padding-left: 40px;
-        outline: none;
-        &:hover, &:focus {
-            background-color: rgba(255, 255, 255, 0.95);
-        }
-    }
-
     #search-res {
-        display: none;
-        background-color: rgba(255, 255, 255, 1.0);
-        border: 1px solid #eee;
-        box-shadow: #aaa 0px 1px 5px;
-        color: #333;
-        line-height: initial;
-        margin-top: -4px;
-        overflow-x: auto;
-        position: fixed;
-        top: var(--site-header-height);
-        max-height: calc(100vh - var(--site-header-height));
-        z-index: 10;
-        >div {
-            padding: 10px;
-            margin: 0;
-            &:hover {
-                background-color: #f0f0f0;
-            }
-        }
-        .sr-title {
-            color: #333;
-            font-weight: bold;
-        }
-        .sr-snippet {
-            color: #444;
-            font-size: 0.9rem;
-         }
+      display: block;
+    }
+  }
 
-        a { text-decoration: none; }
-        a:hover { color: initial };
+  @media #{$mobile} {
+    display: none;
+  }
 
-        &:empty {
-            visibility: hidden;
-        }
+  #search-box {
+    width: 100%;
+    height: 32px;
+    font-size: 1rem;
+    color: #333;
+    background-color: rgba(255, 255, 255, 0.9);
+    border: 1px solid #eee;
+    border-radius: 2px;
+    background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M39.8 41.95 26.65 28.8q-1.5 1.3-3.5 2.025-2 .725-4.25.725-5.4 0-9.15-3.75T6 18.75q0-5.3 3.75-9.05 3.75-3.75 9.1-3.75 5.3 0 9.025 3.75 3.725 3.75 3.725 9.05 0 2.15-.7 4.15-.7 2-2.1 3.75L42 39.75Zm-20.95-13.4q4.05 0 6.9-2.875Q28.6 22.8 28.6 18.75t-2.85-6.925Q22.9 8.95 18.85 8.95q-4.1 0-6.975 2.875T9 18.75q0 4.05 2.875 6.925t6.975 2.875Z"/></svg>');
+    background-repeat: no-repeat;
+    background-size: contain;
+    padding-left: 40px;
+    outline: none;
+    &:hover,
+    &:focus {
+      background-color: rgba(255, 255, 255, 0.95);
+    }
+  }
+
+  #search-res {
+    display: none;
+    background-color: rgba(255, 255, 255, 1);
+    border: 1px solid #eee;
+    box-shadow: #aaa 0px 1px 5px;
+    color: #333;
+    line-height: initial;
+    margin-top: -4px;
+    overflow-x: auto;
+    position: fixed;
+    top: var(--site-header-height);
+    max-height: calc(100vh - var(--site-header-height));
+    z-index: 10;
+    > div {
+      padding: 10px;
+      margin: 0;
+      &:hover {
+        background-color: #f0f0f0;
+      }
+    }
+    .sr-title {
+      color: #333;
+      font-weight: bold;
+    }
+    .sr-snippet {
+      color: #444;
+      font-size: 0.9rem;
     }
 
-}
+    a {
+      text-decoration: none;
+    }
+    a:hover {
+      color: initial;
+    }
 
+    &:empty {
+      visibility: hidden;
+    }
+  }
+}
 
 // -----------------------------------------------------------------------------
 // Site footer
@@ -250,722 +251,759 @@
 
 // Footer in the index page.
 .site-footer {
-    background-color: hsl(210, 30%, 16%);
-    padding: 1em 0;
-    font-size: 14px;
-    color: #fff;
-    text-align: center;
-    ul {
-        list-style: none;
-        margin: 0;
-        padding: 0;
-        li {
-            display: inline;
-            padding: 0 10px;
-            &:not(:last-child) {
-                border-right: solid 1px #fff;
-            }
-        }
+  background-color: hsl(210, 30%, 16%);
+  padding: 1em 0;
+  font-size: 14px;
+  color: #fff;
+  text-align: center;
+  ul {
+    list-style: none;
+    margin: 0;
+    padding: 0;
+    li {
+      display: inline;
+      padding: 0 10px;
+      &:not(:last-child) {
+        border-right: solid 1px #fff;
+      }
     }
-    a,
-    a:visited {
-        text-decoration: none;
-        color: inherit;
-    }
-    .docs-footer-notice { display: none; }
+  }
+  a,
+  a:visited {
+    text-decoration: none;
+    color: inherit;
+  }
+  .docs-footer-notice {
+    display: none;
+  }
 }
 
 // Footer overrides for the /docs/ page.
 .docs .site-footer {
-    grid-area: footer;
-    background: transparent;
-    color: #666;
-    text-align: left;
-    margin: 0 20px;
-    padding: 12px 0;
+  grid-area: footer;
+  background: transparent;
+  color: #666;
+  text-align: left;
+  margin: 0 20px;
+  padding: 12px 0;
 
-    .docs-footer-notice {
-        padding: 0;
-        margin: 0;
-        display: block;
-    }
+  .docs-footer-notice {
+    padding: 0;
+    margin: 0;
+    display: block;
+  }
 
-    ul { display: none; }
+  ul {
+    display: none;
+  }
 }
 
 // -----------------------------------------------------------------------------
 // Site content
 // -----------------------------------------------------------------------------
 .site-content {
-    .section-wrapper {
-        border-bottom: solid 1px #eee;
-        &:nth-child(2n+1) {
-          background-color: hsl(210, 17%, 98%);
-        }
+  .section-wrapper {
+    border-bottom: solid 1px #eee;
+    &:nth-child(2n + 1) {
+      background-color: hsl(210, 17%, 98%);
     }
-    section {
+  }
+  section {
+    display: block;
+    position: relative;
+    overflow: hidden;
+    padding: 0 20px;
+    margin: 0 auto;
+    max-width: calc(var(--content-max-width) + 2 * 20px);
+  }
+
+  .banner {
+    height: calc(
+      100vh - var(--home-highlights-height) - var(--site-header-height)
+    );
+    @media (max-height: 639px) {
+      // If the screen is too short (e.g. smartphone in landscape mode)
+      // move the highlights sections (the four tiles) out of the visible
+      // viewport.
+      height: calc(100vh - var(--site-header-height));
+    }
+    min-height: 25vw;
+    display: grid;
+    grid-template-columns: 1fr;
+    grid-template-rows: 1fr 1fr 5fr;
+    h1,
+    h2 {
+      margin: auto;
+      font-family: "Source Sans Pro", sans-serif;
+      text-align: center;
+      color: hsl(0, 0, 35%);
+      span {
+        white-space: nowrap;
+      }
+    }
+    h1 {
+      font-size: 2.5rem;
+      font-size: calc(min(4rem, 8vw, 6vh));
+      font-weight: 400;
+      padding-top: calc(max(1rem, 2vh));
+    }
+    h2 {
+      font-size: 1.25rem;
+      font-size: calc(min(2rem, 6vw, 4vh));
+      font-weight: 200;
+      padding-top: 10px;
+    }
+    .home-img {
+      padding: 1rem 0;
+      overflow: hidden;
+      position: relative;
+      display: flex;
+      img {
+        max-height: 100%;
+        max-width: 100%;
+        margin: auto;
         display: block;
-        position: relative;
-        overflow: hidden;
-        padding: 0 20px;
-        margin: 0 auto;
-        max-width: calc(var(--content-max-width) + 2 * 20px);
+      }
     }
+  }
 
-    .banner {
-        height: calc(100vh - var(--home-highlights-height) - var(--site-header-height));
-        @media (max-height: 639px) {
-            // If the screen is too short (e.g. smartphone in landscape mode)
-            // move the highlights sections (the four tiles) out of the visible
-            // viewport.
-            height: calc(100vh - var(--site-header-height));
+  .home-highlights {
+    &:before {
+      border-top: 1px solid hsl(210, 17%, 90%);
+    }
+    height: var(--home-highlights-height);
+    display: grid;
+    grid-template-columns: repeat(4, 1fr);
+    grid-template-rows: 1fr;
+    background-color: #fff;
+    z-index: 2;
+    @media (max-aspect-ratio: 1/1) {
+      grid-template-columns: repeat(2, 1fr);
+    }
+    > a {
+      color: hsl(0, 0, 20%);
+      font-size: 22px;
+      font-weight: 400;
+      text-align: center;
+      padding: 20px 0;
+      font-family: "Source Sans Pro", sans-serif;
+      text-decoration: none;
+      .icon {
+        background-image: url("/assets/sprite.png");
+        background-repeat: no-repeat;
+        width: 64px;
+        height: 64px;
+        margin: auto;
+        background-size: 256px 128px;
+        filter: grayscale(1);
+        transition: filter ease var(--anim-time);
+      }
+      &:nth-child(1) .icon {
+        background-position: 0 -64px;
+      }
+      &:nth-child(2) .icon {
+        background-position: -64px -64px;
+      }
+      &:nth-child(3) .icon {
+        background-position: -128px -64px;
+      }
+      &:nth-child(4) .icon {
+        background-position: -192px -64px;
+      }
+      &:hover {
+        background-color: hsl(210, 17%, 90%);
+        .icon {
+          filter: grayscale(0);
         }
-        min-height: 25vw;
-        display: grid;
+      }
+    }
+  }
+  .home-section {
+    min-height: calc(min(100vh - var(--site-header-height), 800px));
+    padding: 5% 20px;
+    display: grid;
+    grid-template-rows: 1fr;
+    grid-column-gap: 4vw;
+    > img {
+      grid-area: img;
+      max-width: 100%;
+      max-height: 55vh;
+      margin: auto;
+      margin-top: 40px;
+    }
+    h2,
+    > div {
+      grid-area: content;
+    }
+    h2 {
+      font-family: "Source Sans Pro", sans-serif;
+      font-weight: 600;
+      font-size: 2.5rem;
+      color: #333;
+      text-align: center;
+    }
+    &:nth-child(2n) {
+      grid-template-columns: 5fr 4fr;
+      grid-template-areas: "content img";
+      h2 {
+        padding: 0 0 0 50px;
+        text-align: left;
+      }
+    }
+    &:nth-child(2n + 1) {
+      grid-template-columns: 4fr 5fr;
+      grid-template-areas: "img content";
+      h2 {
+        padding: 0 50px 0 0;
+        text-align: left;
+      }
+    }
+    @media (max-aspect-ratio: 1/1) {
+      padding: 5vh 20px;
+      &:nth-child(n) {
+        grid-template-rows: auto auto;
         grid-template-columns: 1fr;
-        grid-template-rows: 1fr 1fr 5fr;
-        h1,
+        grid-template-areas: "img" "content";
+        grid-row-gap: 30px;
         h2 {
-            margin: auto;
-            font-family: 'Source Sans Pro', sans-serif;
-            text-align: center;
-            color: hsl(0, 0, 35%);
-            span {
-                white-space: nowrap;
-            }
+          padding: 0;
+          text-align: center;
         }
-        h1 {
-            font-size: 2.5rem;
-            font-size: calc(min(4rem, 8vw, 6vh));
-            font-weight: 400;
-            padding-top: calc(max(1rem, 2vh));
-        }
-        h2 {
-            font-size: 1.25rem;
-            font-size: calc(min(2rem, 6vw, 4vh));
-            font-weight: 200;
-            padding-top: 10px;
-        }
-        .home-img {
-            padding: 1rem 0;
-            overflow: hidden;
-            position: relative;
-            display: flex;
-            img {
-                max-height: 100%;
-                max-width: 100%;
-                margin: auto;
-                display: block;
-            }
-        }
+      }
+      > img {
+        padding: 0 10vw;
+      }
     }
-
-
-    .home-highlights {
-        &:before {
-            border-top: 1px solid hsl(210, 17%, 90%);
+    div {
+      grid-area: content;
+      .button {
+        display: inline-block;
+        background: #337ab7;
+        font-weight: 500;
+        color: #fff;
+        border-radius: 6px;
+        font-size: 18px;
+        padding: 10px 16px;
+        transition: background-color ease var(--anim-time);
+        text-decoration: none;
+        &:hover {
+          background: #286090;
         }
-        height: var(--home-highlights-height);
-        display: grid;
-        grid-template-columns: repeat(4, 1fr);
-        grid-template-rows: 1fr;
-        background-color: #fff;
-        z-index: 2;
-        @media (max-aspect-ratio: 1/1) {
-            grid-template-columns: repeat(2, 1fr);
-        }
-        >a {
-            color: hsl(0, 0, 20%);
-            font-size: 22px;
-            font-weight: 400;
-            text-align: center;
-            padding: 20px 0;
-            font-family: 'Source Sans Pro', sans-serif;
-            text-decoration: none;
-            .icon {
-                background-image: url('/assets/sprite.png');
-                background-repeat: no-repeat;
-                width: 64px;
-                height: 64px;
-                margin: auto;
-                background-size: 256px 128px;
-                filter: grayscale(1);
-                transition: filter ease var(--anim-time);
-            }
-            &:nth-child(1) .icon {
-                background-position: 0 -64px;
-            }
-            &:nth-child(2) .icon {
-                background-position: -64px -64px;
-            }
-            &:nth-child(3) .icon {
-                background-position: -128px -64px;
-            }
-            &:nth-child(4) .icon {
-                background-position: -192px -64px;
-            }
-            &:hover {
-                background-color: hsl(210, 17%, 90%);
-                .icon {
-                    filter: grayscale(0);
-                }
-            }
-        }
+      }
     }
-    .home-section {
-        min-height: calc(min(100vh - var(--site-header-height), 800px));
-        padding: 5% 20px;
-        display: grid;
-        grid-template-rows: 1fr;
-        grid-column-gap: 4vw;
-        >img {
-            grid-area: img;
-            max-width: 100%;
-            max-height: 55vh;
-            margin: auto;
-            margin-top: 40px;
-        }
-        h2,
-        >div {
-            grid-area: content;
-        }
-        h2 {
-            font-family: 'Source Sans Pro', sans-serif;
-            font-weight: 600;
-            font-size: 2.5rem;
-            color: #333;
-            text-align: center;
-        }
-        &:nth-child(2n) {
-            grid-template-columns: 5fr 4fr;
-            grid-template-areas: "content img";
-            h2 {
-                padding: 0 0 0 50px;
-                text-align: left;
-            }
-        }
-        &:nth-child(2n+1) {
-            grid-template-columns: 4fr 5fr;
-            grid-template-areas: "img content";
-            h2 {
-                padding: 0 50px 0 0;
-                text-align: left;
-            }
-        }
-        @media (max-aspect-ratio: 1/1) {
-            padding: 5vh 20px;
-            &:nth-child(n) {
-                grid-template-rows: auto auto;
-                grid-template-columns: 1fr;
-                grid-template-areas: "img" "content";
-                grid-row-gap: 30px;
-                h2 {
-                    padding: 0;
-                    text-align: center;
-                }
-            }
-            >img {
-                padding: 0 10vw;
-            }
-        }
-        div {
-            grid-area: content;
-            .button {
-                display: inline-block;
-                background: #337ab7;
-                font-weight: 500;
-                color: #fff;
-                border-radius: 6px;
-                font-size: 18px;
-                padding: 10px 16px;
-                transition: background-color ease var(--anim-time);
-                text-decoration: none;
-                &:hover {
-                    background: #286090;
-                }
-            }
-        }
-        .home-item {
-            display: grid;
-            grid-template-rows: auto auto;
-            grid-template-columns: 50px auto;
-            grid-template-areas: "img title" "img label";
-            grid-column-gap: 20px;
-            padding: 20px 30px;
-            margin: 10px 0;
-            // border: 1px solid #eee;
-            font-family: 'Source Sans Pro', sans-serif;
-            color: #111111;
-            transition: filter var(--anim-ease) var(--anim-time), background-color var(--anim-ease) var(--anim-time), transform var(--anim-ease) var(--anim-time), box-shadow linear var(--anim-time);
-            border-radius: 6px;
-            filter: opacity(0.6);
-            &:hover {
-                background-color: hsla(0, 0, 0, 0.02);
-                filter: opacity(1);
-                transform: scale(1.01);
-            }
-            >img,
-            >i {
-                grid-area: img;
-                margin: auto;
-                font-size: 50px;
-            }
-            >h3 {
-                grid-area: title;
-                font-size: 1.25rem;
-                line-height: 20px;
-                font-weight: 600;
-            }
-            >p {
-                grid-area: label;
-                font-size: 1rem;
-                font-weight: 400;
-                margin: 1em 0;
-            }
-        }
+    .home-item {
+      display: grid;
+      grid-template-rows: auto auto;
+      grid-template-columns: 50px auto;
+      grid-template-areas: "img title" "img label";
+      grid-column-gap: 20px;
+      padding: 20px 30px;
+      margin: 10px 0;
+      // border: 1px solid #eee;
+      font-family: "Source Sans Pro", sans-serif;
+      color: #111111;
+      transition: filter var(--anim-ease) var(--anim-time),
+        background-color var(--anim-ease) var(--anim-time),
+        transform var(--anim-ease) var(--anim-time),
+        box-shadow linear var(--anim-time);
+      border-radius: 6px;
+      filter: opacity(0.6);
+      &:hover {
+        background-color: hsla(0, 0, 0, 0.02);
+        filter: opacity(1);
+        transform: scale(1.01);
+      }
+      > img,
+      > i {
+        grid-area: img;
+        margin: auto;
+        font-size: 50px;
+      }
+      > h3 {
+        grid-area: title;
+        font-size: 1.25rem;
+        line-height: 20px;
+        font-weight: 600;
+      }
+      > p {
+        grid-area: label;
+        font-size: 1rem;
+        font-weight: 400;
+        margin: 1em 0;
+      }
     }
+  }
 }
 
 // -----------------------------------------------------------------------------
 // Docs
 // -----------------------------------------------------------------------------
 .docs {
-    min-height: 100vh;
-    display: grid;
-    --nav-width: 240px;
-    --toc-width: 180px;
+  min-height: 100vh;
+  display: grid;
+  --nav-width: 240px;
+  --toc-width: 180px;
 
-    // 1665px is the clientWidth on a macbook pro. Adjust the layout so that
-    // the max-width of the central .doc fits precisely when the browser is
-    // full-screen on a macbook.
-    --max-doc-width: calc(1665px - var(--toc-width) - var(--nav-width));
+  // 1665px is the clientWidth on a macbook pro. Adjust the layout so that
+  // the max-width of the central .doc fits precisely when the browser is
+  // full-screen on a macbook.
+  --max-doc-width: calc(1665px - var(--toc-width) - var(--nav-width));
 
-    grid-template-columns: var(--nav-width) minmax(auto, var(--max-doc-width)) var(--toc-width);
-    grid-template-rows: 1fr max-content;
-    grid-template-areas: "nav doc toc" "nav footer toc";
+  grid-template-columns: var(--nav-width) minmax(auto, var(--max-doc-width)) var(
+      --toc-width
+    );
+  grid-template-rows: 1fr max-content;
+  grid-template-areas: "nav doc toc" "nav footer toc";
 
-    background-color: hsl(210, 10%, 97%);
-    .nav {
-        grid-area: nav;
-        border-right: 1px solid hsl(210, 30%, 90%);
-        background-color: #fefefe;
-        padding: 20px 0;
-        padding-right: 16px;
+  background-color: hsl(210, 10%, 97%);
+  .nav {
+    grid-area: nav;
+    border-right: 1px solid hsl(210, 30%, 90%);
+    background-color: #fefefe;
+    padding: 20px 0;
+    padding-right: 16px;
 
-        position: sticky;
-        top: var(--site-header-height);
-        height: calc(100vh -  var(--site-header-height));
-        overflow-y: auto;
-        @include minimal-scrollbar;
+    position: sticky;
+    top: var(--site-header-height);
+    height: calc(100vh - var(--site-header-height));
+    overflow-y: auto;
+    @include minimal-scrollbar;
 
-        a {
-            color: inherit;
-            text-decoration: none;
-            line-height: 24px;
-            display: flex;
-            transition: background-color var(--anim-ease) var(--anim-time),
-                        visibility linear var(--anim-time);
-            border-radius: 0 10px 10px 0;
-            -webkit-tap-highlight-color: transparent;
-            &[href] {
-                &:hover {
-                    color: #000;
-                    background-color: #f1f3f4;
-                }
-                &.selected {
-                    background-color: #ecba2a;
-                }
-            }
+    a {
+      color: inherit;
+      text-decoration: none;
+      line-height: 24px;
+      display: flex;
+      transition: background-color var(--anim-ease) var(--anim-time),
+        visibility linear var(--anim-time);
+      border-radius: 0 10px 10px 0;
+      -webkit-tap-highlight-color: transparent;
+      &[href] {
+        &:hover {
+          color: #000;
+          background-color: #f1f3f4;
         }
-
-        ul {
-            list-style: none;
-            margin: 0;
-            padding: 0;
-            overflow: hidden;
-            li {
-                font-size: 1rem;
-                font-weight: 400;
-                font-family: 'Source Sans Pro', sans-serif;
-                color: #4a4a4a;
-                max-width: 100%;
-                margin: 3px 0;
-            }
-            p { margin: 0; }
+        &.selected {
+          background-color: #ecba2a;
         }
-
-        // Applies only to outer-level submenus.
-        >ul {
-            position: static; // Otherwise gets v-centered in the middle.
-            > li {
-                padding-bottom: 10px;
-                margin-bottom: 10px;
-                font-weight: 600;
-                color: #111;
-
-                &:not(:last-child) {
-                    border-bottom: 1px solid #eee;
-                }
-
-                &.compressible {
-                    > p > a::after {
-                        content: 'keyboard_arrow_up';
-                        font-family: 'Material Icons Round';
-                        font-size: 24px;
-                        width: 24px;
-                        transition: transform var(--anim-ease) var(--anim-time);
-                        margin: 0 0 0 auto;
-                        font-weight: 200;
-                        color: #666;
-                    }
-                    > ul {
-                        transition: max-height var(--anim-ease) var(--anim-time),
-                                    opacity var(--anim-ease) var(--anim-time);
-                        opacity: 1;
-                    }
-                    &.compressed {
-                        // The JS will compute and set the maxHeight on each
-                        // element depending on the size of their children.
-                        // !important is needed to override the element-inline
-                        // max-height property set by JS, which is prioritary.
-                        > ul {
-                            max-height: 0 !important;
-                            visibility: hidden;
-                            opacity: 0;
-                        }
-                        > p > a::after {
-                            transform: scaleY(-1);
-                        }
-                    }
-                }  // .compressible
-
-            }
-        }
-
-        li a {
-            padding-left: 16px;
-        }
-        li li a {
-            padding-left: 30px;
-        }
-        li li li a {
-            padding-left: 44px;
-        }
-        .expanded a::after {
-            transform: rotate(180deg);
-        }
+      }
     }
-    .doc {
-        grid-area: doc;
-        background-color: #fff;
-        margin: 20px;
-        padding: 30px 40px;
-        font-family: Roboto, sans-serif;
+
+    ul {
+      list-style: none;
+      margin: 0;
+      padding: 0;
+      overflow: hidden;
+      li {
         font-size: 1rem;
         font-weight: 400;
-        line-height: 24px;
-        -webkit-font-smoothing: antialiased;
+        font-family: "Source Sans Pro", sans-serif;
         color: #4a4a4a;
-        position: relative;
-        box-shadow: 0 1px 2px 0 rgba(60, 64, 67, .1), 0 1px 3px 1px rgba(60, 64, 67, .15);
-        overflow: hidden;
+        max-width: 100%;
+        margin: 3px 0;
+      }
+      p {
+        margin: 0;
+      }
+    }
 
-        a {
-            text-decoration: none;
-            &:link { color: #007b83; }
-            &:visited { color: #8e3317; }
-            &:hover { color: #009da8; }
-            &[href^="http"] {
-                // External link.
-                &:after {
-                    content: 'open_in_new';
-                    font-family: 'Material Icons Round';
-                    color: #666;
-                    text-decoration: none;
-                    margin-left: 2px;
-                    margin-right: 4px;
-                    vertical-align: bottom;
-                }
-            }
+    // Applies only to outer-level submenus.
+    > ul {
+      position: static; // Otherwise gets v-centered in the middle.
+      > li {
+        padding-bottom: 10px;
+        margin-bottom: 10px;
+        font-weight: 600;
+        color: #111;
+
+        &:not(:last-child) {
+          border-bottom: 1px solid #eee;
         }
 
-        h1,
-        h2,
-        h3 {
-            margin: 10px 0;
-            padding: 0;
-            padding-top: 30px;
-        }
-        h1 {
-            font-size: 2.25rem;
-            line-height: 2.25rem;
-            margin: 0;
-            padding: 0;
-            margin-bottom: 1.5rem;
-            font-family: 'Source Sans Pro', sans-serif;
-        }
-        h2 {
-            font-size: 1.5rem;
-            border-bottom: 1px solid #e8eaed;
-            padding-bottom: 6px;
-        }
-        h3 {
-            font-size: 1.25rem;
-        }
-        * {
-            max-width: 100%;
-        }
-
-        img[alt$="screenshot"] {
-            box-shadow: 0 0 10px 2px #eee;
-        }
-
-        code:not(.code-block) {
-            background: hsla(210, 17%, 90%, 0.2);
-            border: 1px solid #E8EAED;
-            border-radius: 6px;
-            padding: 1px 4px;
-        }
-        .code-block {
-            overflow-x: auto;
-            white-space: pre;
-            border-radius: 6px;
-            box-shadow: 1px 1px 6px #999;
-            border-top: 5px solid #8BC34A;
-        }
-        // Hide mermaid graphs until they are rendered, this is to avoid showing
-        // the mermaid source while the renderer generates the SVG.
-        .mermaid {
-            transition: opacity var(--anim-ease) var(--anim-time);
-            &:not(.rendered) {
-                opacity: 0;
-            }
-        }
-        .anchor {
-            margin-left: -29px;
-            padding-right: 5px;
-            text-decoration: none;
-            position: absolute;
-            padding-top: var(--site-header-height);
-            margin-top: calc(-1 * var(--site-header-height));
-            outline: none;
-            opacity: 0;
-            transition: opacity var(--anim-ease) var(--anim-time);
-            &::before {
-                content: 'insert_link';
-                font-family: 'Material Icons Round';
-                color: #333;
-                font-size: 24px;
-            }
-        }
-        *:hover .anchor {
+        &.compressible {
+          > p > a::after {
+            content: "keyboard_arrow_up";
+            font-family: "Material Icons Round";
+            font-size: 24px;
+            width: 24px;
+            transition: transform var(--anim-ease) var(--anim-time);
+            margin: 0 0 0 auto;
+            font-weight: 200;
+            color: #666;
+          }
+          > ul {
+            transition: max-height var(--anim-ease) var(--anim-time),
+              opacity var(--anim-ease) var(--anim-time);
             opacity: 1;
+          }
+          &.compressed {
+            // The JS will compute and set the maxHeight on each
+            // element depending on the size of their children.
+            // !important is needed to override the element-inline
+            // max-height property set by JS, which is prioritary.
+            > ul {
+              max-height: 0 !important;
+              visibility: hidden;
+              opacity: 0;
+            }
+            > p > a::after {
+              transform: scaleY(-1);
+            }
+          }
+        } // .compressible
+      }
+    }
+
+    li a {
+      padding-left: 16px;
+    }
+    li li a {
+      padding-left: 30px;
+    }
+    li li li a {
+      padding-left: 44px;
+    }
+    .expanded a::after {
+      transform: rotate(180deg);
+    }
+  }
+  .doc {
+    grid-area: doc;
+    background-color: #fff;
+    margin: 20px;
+    padding: 30px 40px;
+    font-family: Roboto, sans-serif;
+    font-size: 1rem;
+    font-weight: 400;
+    line-height: 24px;
+    -webkit-font-smoothing: antialiased;
+    color: #4a4a4a;
+    position: relative;
+    box-shadow: 0 1px 2px 0 rgba(60, 64, 67, 0.1),
+      0 1px 3px 1px rgba(60, 64, 67, 0.15);
+    overflow: hidden;
+
+    a {
+      text-decoration: none;
+      &:link {
+        color: #007b83;
+      }
+      &:visited {
+        color: #8e3317;
+      }
+      &:hover {
+        color: #009da8;
+      }
+      &[href^="http"] {
+        // External link.
+        &:after {
+          content: "open_in_new";
+          font-family: "Material Icons Round";
+          color: #666;
+          text-decoration: none;
+          margin-left: 2px;
+          margin-right: 4px;
+          vertical-align: bottom;
         }
+      }
+    }
+
+    h1,
+    h2,
+    h3 {
+      margin: 10px 0;
+      padding: 0;
+      padding-top: 30px;
+    }
+    h1 {
+      font-size: 2.25rem;
+      line-height: 2.25rem;
+      margin: 0;
+      padding: 0;
+      margin-bottom: 1.5rem;
+      font-family: "Source Sans Pro", sans-serif;
+    }
+    h2 {
+      font-size: 1.5rem;
+      border-bottom: 1px solid #e8eaed;
+      padding-bottom: 6px;
+    }
+    h3 {
+      font-size: 1.25rem;
+    }
+    * {
+      max-width: 100%;
+    }
+
+    img[alt$="screenshot"] {
+      box-shadow: 0 0 10px 2px #eee;
+    }
+
+    code:not(.code-block) {
+      background: hsla(210, 17%, 90%, 0.2);
+      border: 1px solid #e8eaed;
+      border-radius: 6px;
+      padding: 1px 4px;
+    }
+    .code-block {
+      overflow-x: auto;
+      white-space: pre;
+      border-radius: 6px;
+      box-shadow: 1px 1px 6px #999;
+      border-top: 5px solid #8bc34a;
+    }
+    // Hide mermaid graphs until they are rendered, this is to avoid showing
+    // the mermaid source while the renderer generates the SVG.
+    .mermaid {
+      transition: opacity var(--anim-ease) var(--anim-time);
+      &:not(.rendered) {
+        opacity: 0;
+      }
+    }
+    .anchor {
+      margin-left: -29px;
+      padding-right: 5px;
+      text-decoration: none;
+      position: absolute;
+      padding-top: var(--site-header-height);
+      margin-top: calc(-1 * var(--site-header-height));
+      outline: none;
+      opacity: 0;
+      transition: opacity var(--anim-ease) var(--anim-time);
+      &::before {
+        content: "insert_link";
+        font-family: "Material Icons Round";
+        color: #333;
+        font-size: 24px;
+      }
+    }
+    *:hover .anchor {
+      opacity: 1;
+    }
+    code {
+      font-family: "Roboto Mono", monospace;
+      font-size: 14px;
+    }
+    table {
+      width: 100%;
+      font-size: 14px;
+      border-spacing: 0;
+      border-collapse: collapse;
+      th,
+      td {
+        padding: 8px;
+        border: 0 solid #dadce0;
+        border-top-width: 1px;
+        border-bottom-width: 1px;
+      }
+      tr {
+        height: 20px;
+      }
+      tr:target {
+        background-color: #ecba2a;
+      }
+      thead {
+        text-align: left;
+        background-color: #e8eaed;
+        color: #202124;
+      }
+    }
+
+    &[data-md-file^="/docs/reference/"] {
+      h1,
+      h2,
+      h3 {
         code {
-            font-family: 'Roboto Mono', monospace;
-            font-size: 14px;
+          margin-left: 20px;
+          color: #666;
         }
-        table {
-            width: 100%;
-            font-size: 14px;
-            border-spacing: 0;
-            border-collapse: collapse;
-            th, td {
-                padding: 8px;
-                border: 0 solid #dadce0;
-                border-top-width: 1px;
-                border-bottom-width: 1px;
-
-            }
-            tr {
-                height: 20px;
-            }
-            tr:target {
-                background-color: #ecba2a;
-            }
-            thead {
-                text-align: left;
-                background-color: #e8eaed;
-                color: #202124;
-            }
+      }
+      table {
+        width: 100%;
+        font-size: 14px;
+        border-spacing: 0;
+        border-collapse: collapse;
+        th,
+        td {
+          padding: 8px;
+          border: 0 solid #dadce0;
+          border-top-width: 1px;
+          border-bottom-width: 1px;
         }
+        tr {
+          height: 20px;
+        }
+        thead {
+          text-align: left;
+          background-color: #e8eaed;
+          color: #202124;
+        }
+        td {
+          &:first-child {
+            background: #f7f7f7;
+          }
 
-        &[data-md-file^="/docs/reference/"] {
-            h1, h2, h3 {
-                code {
-                    margin-left: 20px;
-                    color: #666;
-                }
-            }
-            table {
-                width: 100%;
-                font-size: 14px;
-                border-spacing: 0;
-                border-collapse: collapse;
-                th, td {
-                    padding: 8px;
-                    border: 0 solid #dadce0;
-                    border-top-width: 1px;
-                    border-bottom-width: 1px;
-
-                }
-                tr {
-                    height: 20px;
-                }
-                thead {
-                    text-align: left;
-                    background-color: #e8eaed;
-                    color: #202124;
-                }
-                td {
-                    &:first-child { background: #f7f7f7; }
-
-                    /* Not really 100% but makes sure that the description
+          /* Not really 100% but makes sure that the description
                      * column takes most of the width */
-                    &:last-child { width: 80%; }
-                }
-            }
+          &:last-child {
+            width: 80%;
+          }
         }
-
-        .callout {
-            padding: .5rem .5rem .5rem 2rem;
-            border: none;
-            border-radius: 2px;
-            margin-left: auto;
-            margin-right: auto;
-            width: 90%;
-            border-left: 3px solid transparent;
-            box-shadow: 0 0.2rem 0.5rem rgba(0,0,0,.05), 0 0 0.05rem rgba(0,0,0,.1);
-
-            &:before {
-                font-family: 'Material Icons Round';
-                position: absolute;
-                font-size: 1.5rem;
-                margin-left: -1.75rem;
-                margin-top: -2px;
-            }
-
-            &.note {
-                background-color: #E8F0FE;
-                border-color: #1967D2;
-                color: #1967D2;
-                &:before { content: 'bookmark'; }
-            }
-
-            &.summary {
-                background-color: #E4F7FB;
-                border-color:  #129EAF;
-                color: #129EAF;
-                &:before { content: 'sms'; }
-            }
-
-            &.tip {
-                background-color: #E6F4EA;
-                border-color: #188038;
-                color: #188038;
-                &:before { content: 'star'; }
-            }
-
-            &.todo {
-                background-color: #F1F3F4;
-                border-color: #5F6368;
-                color: #5F6368;
-                &:before { content: 'error'; }
-            }
-
-            &.warning {
-                background-color: #FCE8E6;
-                border-color: #C5221F;
-                color: #C5221F;
-                &:before { content: 'warning'; }
-            }
-        }
+      }
     }
-    .toc {
-        grid-area: toc;
-        padding: 20px 16px 20px 0;
 
-        position: sticky;
-        top: var(--site-header-height);
-        height: calc(100vh - var(--site-header-height));
-        overflow-y: auto;
-        @include minimal-scrollbar;
+    .callout {
+      padding: 0.5rem 0.5rem 0.5rem 2rem;
+      border: none;
+      border-radius: 2px;
+      margin-left: auto;
+      margin-right: auto;
+      width: 90%;
+      border-left: 3px solid transparent;
+      box-shadow: 0 0.2rem 0.5rem rgba(0, 0, 0, 0.05),
+        0 0 0.05rem rgba(0, 0, 0, 0.1);
 
-        font-family: 'Source Sans Pro', sans-serif;
-        word-break: break-word;
-        a {
-            text-decoration: none;
+      &:before {
+        font-family: "Material Icons Round";
+        position: absolute;
+        font-size: 1.5rem;
+        margin-left: -1.75rem;
+        margin-top: -2px;
+      }
+
+      &.note {
+        background-color: #e8f0fe;
+        border-color: #1967d2;
+        color: #1967d2;
+        &:before {
+          content: "bookmark";
         }
-        a,
-        a:visited {
-            color: #333;
+      }
+
+      &.summary {
+        background-color: #e4f7fb;
+        border-color: #129eaf;
+        color: #129eaf;
+        &:before {
+          content: "sms";
         }
-        a.highlighted {
-            font-weight: 500;
-            color: hsl(45, 100%, 40%);
+      }
+
+      &.tip {
+        background-color: #e6f4ea;
+        border-color: #188038;
+        color: #188038;
+        &:before {
+          content: "star";
         }
-        font-size: 0.875rem;
-        ul {
-            list-style: none;
-            margin: 0;
-            padding: 0;
-            li {
-                margin: 5px 0;
-                /* This make it so that a single word gets elided but if there
+      }
+
+      &.todo {
+        background-color: #f1f3f4;
+        border-color: #5f6368;
+        color: #5f6368;
+        &:before {
+          content: "error";
+        }
+      }
+
+      &.warning {
+        background-color: #fce8e6;
+        border-color: #c5221f;
+        color: #c5221f;
+        &:before {
+          content: "warning";
+        }
+      }
+    }
+  }
+  .toc {
+    grid-area: toc;
+    padding: 20px 16px 20px 0;
+
+    position: sticky;
+    top: var(--site-header-height);
+    height: calc(100vh - var(--site-header-height));
+    overflow-y: auto;
+    @include minimal-scrollbar;
+
+    font-family: "Source Sans Pro", sans-serif;
+    word-break: break-word;
+    a {
+      text-decoration: none;
+    }
+    a,
+    a:visited {
+      color: #333;
+    }
+    a.highlighted {
+      font-weight: 500;
+      color: hsl(45, 100%, 40%);
+    }
+    font-size: 0.875rem;
+    ul {
+      list-style: none;
+      margin: 0;
+      padding: 0;
+      li {
+        margin: 5px 0;
+        /* This make it so that a single word gets elided but if there
                  * are multiple words they span across lines.  */
-                overflow: hidden;
-                text-overflow: ellipsis;
-                white-space: break-spaces;
-                word-break: normal;
-            }
-        }
-        >ul {
-            border-left: 4px solid #ecba2a;
-            padding-left: 10px;
-            position: static;  // Otherwise gets v-centered in the middle.
-            top: calc(var(--site-header-height) + 25px);
-        }
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: break-spaces;
+        word-break: normal;
+      }
     }
+    > ul {
+      border-left: 4px solid #ecba2a;
+      padding-left: 10px;
+      position: static; // Otherwise gets v-centered in the middle.
+      top: calc(var(--site-header-height) + 25px);
+    }
+  }
 
-    @media #{$wide} {
-        grid-template-columns: var(--nav-width) auto 0;
-        .toc { display: none; }
+  @media #{$wide} {
+    grid-template-columns: var(--nav-width) auto 0;
+    .toc {
+      display: none;
     }
-    @media #{$mobile} {
+  }
+  @media #{$mobile} {
+    display: block;
+    .doc {
+      margin: 0;
+      padding: 20px;
+    }
+    .nav {
+      // JS will persistently toggle to .after_first_click. This is to
+      // avoid spurious transitions on page load.
+      display: none;
+
+      --nav-width-mobile: calc(min(90vw, 360px));
+      width: var(--nav-width-mobile);
+      position: fixed;
+      z-index: 2;
+      height: 100vh;
+      overflow-y: auto;
+      top: var(--site-header-height);
+      transition: transform var(--anim-ease) var(--anim-time),
+        box-shadow var(--anim-ease) var(--anim-time),
+        visibility ease var(--anim-time);
+      transform: translateX(calc(-1 * var(--nav-width-mobile)));
+      visibility: hidden;
+      > ul {
+        position: static;
+        top: 0;
+      }
+      &.after_first_click {
         display: block;
-        .doc {
-            margin: 0;
-            padding: 20px;
-        }
-        .nav {
-            // JS will persistently toggle to .after_first_click. This is to
-            // avoid spurious transitions on page load.
-            display: none;
-
-            --nav-width-mobile: calc(min(90vw, 360px));
-            width: var(--nav-width-mobile);
-            position: fixed;
-            z-index: 2;
-            height: 100vh;
-            overflow-y: auto;
-            top: var(--site-header-height);
-            transition: transform var(--anim-ease) var(--anim-time),
-                        box-shadow var(--anim-ease) var(--anim-time),
-                        visibility ease var(--anim-time);
-            transform: translateX(calc(-1 * var(--nav-width-mobile)));
-            visibility: hidden;
-            >ul {
-                position: static;
-                top: 0;
-            }
-            &.after_first_click {
-                display: block;
-            }
-            &.expanded {
-                visibility: visible;
-                transform: translateX(0);
-                box-shadow: 0 1px 0 100vw rgba(0,0,0,0.4);
-            }
-        }
+      }
+      &.expanded {
+        visibility: visible;
+        transform: translateX(0);
+        box-shadow: 0 1px 0 100vw rgba(0, 0, 0, 0.4);
+      }
     }
+  }
 }
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index 0ca6c01..0c18970 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -35,6 +35,7 @@
 import {TrackMouseEvent, TrackRenderContext} from '../public/track';
 import {Point2D, VerticalBounds} from '../base/geom';
 import {Trace} from '../public/trace';
+import {SourceDataset, Dataset} from '../trace_processor/dataset';
 
 // The common class that underpins all tracks drawing slices.
 
@@ -972,6 +973,17 @@
     });
     return {ts: Time.fromRaw(row.ts), dur: Duration.fromRaw(row.dur)};
   }
+
+  getDataset(): Dataset | undefined {
+    return new SourceDataset({
+      src: this.getSqlSource(),
+      schema: {
+        id: NUM,
+        ts: LONG,
+        dur: LONG,
+      },
+    });
+  }
 }
 
 // This is the argument passed to onSliceOver(args).
diff --git a/ui/src/frontend/named_slice_track.ts b/ui/src/frontend/named_slice_track.ts
index ed9b5f0..7a23285 100644
--- a/ui/src/frontend/named_slice_track.ts
+++ b/ui/src/frontend/named_slice_track.ts
@@ -16,7 +16,7 @@
 import {TrackEventDetailsPanel} from '../public/details_panel';
 import {TrackEventSelection} from '../public/selection';
 import {Slice} from '../public/track';
-import {STR_NULL} from '../trace_processor/query_result';
+import {LONG, NUM, STR, STR_NULL} from '../trace_processor/query_result';
 import {
   BASE_ROW,
   BaseSliceTrack,
@@ -30,6 +30,7 @@
 import {renderDuration} from './widgets/duration';
 import {TraceImpl} from '../core/trace_impl';
 import {assertIsInstance} from '../base/logging';
+import {SourceDataset, Dataset} from '../trace_processor/dataset';
 
 export const NAMED_ROW = {
   // Base columns (tsq, ts, dur, id, depth).
@@ -80,4 +81,16 @@
     // because this class is exposed to plugins (which see only Trace).
     return new ThreadSliceDetailsPanel(assertIsInstance(this.trace, TraceImpl));
   }
+
+  override getDataset(): Dataset | undefined {
+    return new SourceDataset({
+      src: this.getSqlSource(),
+      schema: {
+        id: NUM,
+        name: STR,
+        ts: LONG,
+        dur: LONG,
+      },
+    });
+  }
 }
diff --git a/ui/src/plugins/dev.perfetto.AsyncSlices/async_slice_track.ts b/ui/src/plugins/dev.perfetto.AsyncSlices/async_slice_track.ts
index 1bb31a5..0486e6e 100644
--- a/ui/src/plugins/dev.perfetto.AsyncSlices/async_slice_track.ts
+++ b/ui/src/plugins/dev.perfetto.AsyncSlices/async_slice_track.ts
@@ -19,7 +19,14 @@
 import {NewTrackArgs} from '../../frontend/track';
 import {TrackEventDetails} from '../../public/selection';
 import {Slice} from '../../public/track';
-import {LONG_NULL} from '../../trace_processor/query_result';
+import {SourceDataset, Dataset} from '../../trace_processor/dataset';
+import {
+  LONG,
+  LONG_NULL,
+  NUM,
+  NUM_NULL,
+  STR,
+} from '../../trace_processor/query_result';
 
 export const THREAD_SLICE_ROW = {
   // Base columns (tsq, ts, dur, id, depth).
@@ -104,4 +111,21 @@
       tableName: 'slice',
     };
   }
+
+  override getDataset(): Dataset {
+    return new SourceDataset({
+      src: `slice`,
+      filter: {
+        col: 'track_id',
+        in: this.trackIds,
+      },
+      schema: {
+        id: NUM,
+        name: STR,
+        ts: LONG,
+        dur: LONG,
+        parent_id: NUM_NULL,
+      },
+    });
+  }
 }
diff --git a/ui/src/plugins/dev.perfetto.AsyncSlices/slice_selection_aggregator.ts b/ui/src/plugins/dev.perfetto.AsyncSlices/slice_selection_aggregator.ts
index 55f7c95..23226bc 100644
--- a/ui/src/plugins/dev.perfetto.AsyncSlices/slice_selection_aggregator.ts
+++ b/ui/src/plugins/dev.perfetto.AsyncSlices/slice_selection_aggregator.ts
@@ -16,16 +16,27 @@
 import {AreaSelection} from '../../public/selection';
 import {Engine} from '../../trace_processor/engine';
 import {AreaSelectionAggregator} from '../../public/selection';
-import {SLICE_TRACK_KIND} from '../../public/track_kinds';
+import {UnionDataset} from '../../trace_processor/dataset';
+import {LONG, NUM, STR} from '../../trace_processor/query_result';
 
 export class SliceSelectionAggregator implements AreaSelectionAggregator {
   readonly id = 'slice_aggregation';
 
   async createAggregateView(engine: Engine, area: AreaSelection) {
-    const selectedTrackKeys = getSelectedTrackSqlIds(area);
-
-    if (selectedTrackKeys.length === 0) return false;
-
+    const desiredSchema = {
+      id: NUM,
+      name: STR,
+      ts: LONG,
+      dur: LONG,
+    };
+    const validDatasets = area.tracks
+      .map((track) => track.track.getDataset?.())
+      .filter((ds) => ds !== undefined)
+      .filter((ds) => ds.implements(desiredSchema));
+    if (validDatasets.length === 0) {
+      return false;
+    }
+    const unionDataset = new UnionDataset(validDatasets);
     await engine.query(`
       create or replace perfetto table ${this.id} as
       select
@@ -33,12 +44,13 @@
         sum(dur) AS total_dur,
         sum(dur)/count() as avg_dur,
         count() as occurrences
-        from slices
-      where track_id in (${selectedTrackKeys})
-        and ts + dur > ${area.start}
+        from (${unionDataset.optimize().query()})
+      where
+        ts + dur > ${area.start}
         and ts < ${area.end}
       group by name
     `);
+
     return true;
   }
 
@@ -83,14 +95,3 @@
     ];
   }
 }
-
-function getSelectedTrackSqlIds(area: AreaSelection): number[] {
-  const selectedTrackKeys: number[] = [];
-  for (const trackInfo of area.tracks) {
-    if (trackInfo?.tags?.kind === SLICE_TRACK_KIND) {
-      trackInfo.tags.trackIds &&
-        selectedTrackKeys.push(...trackInfo.tags.trackIds);
-    }
-  }
-  return selectedTrackKeys;
-}
diff --git a/ui/src/plugins/dev.perfetto.Frames/actual_frames_track.ts b/ui/src/plugins/dev.perfetto.Frames/actual_frames_track.ts
index d75dd77..2c19110 100644
--- a/ui/src/plugins/dev.perfetto.Frames/actual_frames_track.ts
+++ b/ui/src/plugins/dev.perfetto.Frames/actual_frames_track.ts
@@ -102,6 +102,15 @@
       tableName: 'slice',
     };
   }
+
+  // Override dataset from base class NamedSliceTrack as we don't want these
+  // tracks to participate in generic area selection aggregation (frames tracks
+  // have their own dedicated aggregation panel).
+  // TODO(stevegolton): In future CLs this will be handled with aggregation keys
+  // instead, as this track will have to expose a dataset anyway.
+  override getDataset() {
+    return undefined;
+  }
 }
 
 function getColorSchemeForJank(
diff --git a/ui/src/plugins/dev.perfetto.Ftrace/ftrace_track.ts b/ui/src/plugins/dev.perfetto.Ftrace/ftrace_track.ts
index 78c59c8..4ef7793 100644
--- a/ui/src/plugins/dev.perfetto.Ftrace/ftrace_track.ts
+++ b/ui/src/plugins/dev.perfetto.Ftrace/ftrace_track.ts
@@ -20,10 +20,11 @@
 import {TrackData} from '../../common/track_data';
 import {Engine} from '../../trace_processor/engine';
 import {Track} from '../../public/track';
-import {LONG, STR} from '../../trace_processor/query_result';
+import {LONG, NUM, STR} from '../../trace_processor/query_result';
 import {FtraceFilter} from './common';
 import {Monitor} from '../../base/monitor';
 import {TrackRenderContext} from '../../public/track';
+import {SourceDataset, Dataset} from '../../trace_processor/dataset';
 
 const MARGIN = 2;
 const RECT_HEIGHT = 18;
@@ -56,6 +57,25 @@
     this.monitor = new Monitor([() => store.state]);
   }
 
+  getDataset(): Dataset {
+    return new SourceDataset({
+      // 'ftrace_event' doesn't have a dur column, but injecting dur=0 (all
+      // ftrace events are effectively 'instant') allows us to participate in
+      // generic slice aggregations
+      src: 'select id, ts, 0 as dur, name from ftrace_event',
+      schema: {
+        id: NUM,
+        name: STR,
+        ts: LONG,
+        dur: LONG,
+      },
+      filter: {
+        col: 'cpu',
+        eq: this.cpu,
+      },
+    });
+  }
+
   async onUpdate({
     visibleWindow,
     resolution,
diff --git a/ui/src/public/track.ts b/ui/src/public/track.ts
index 94ac9e7..6d1b1dc 100644
--- a/ui/src/public/track.ts
+++ b/ui/src/public/track.ts
@@ -20,6 +20,7 @@
 import {ColorScheme} from './color_scheme';
 import {TrackEventDetailsPanel} from './details_panel';
 import {TrackEventDetails, TrackEventSelection} from './selection';
+import {Dataset} from '../trace_processor/dataset';
 
 export interface TrackManager {
   /**
@@ -175,6 +176,12 @@
   onMouseOut?(): void;
 
   /**
+   * Optional: Returns a dataset that represents the events displayed on this
+   * track.
+   */
+  getDataset?(): Dataset | undefined;
+
+  /**
    * Optional: Get details of a track event given by eventId on this track.
    */
   getSelectionDetails?(eventId: number): Promise<TrackEventDetails | undefined>;
diff --git a/ui/src/trace_processor/dataset.ts b/ui/src/trace_processor/dataset.ts
new file mode 100644
index 0000000..25c64cb
--- /dev/null
+++ b/ui/src/trace_processor/dataset.ts
@@ -0,0 +1,290 @@
+// Copyright (C) 2024 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.
+
+import {assertUnreachable} from '../base/logging';
+import {getOrCreate} from '../base/utils';
+import {ColumnType, SqlValue} from './query_result';
+
+/**
+ * A dataset defines a set of rows in TraceProcessor and a schema of the
+ * resultant columns. Dataset implementations describe how to get the data in
+ * different ways - e.g. 'source' datasets define a dataset as a table name (or
+ * select statement) + filters, whereas a 'union' dataset defines a dataset as
+ * the union of other datasets.
+ *
+ * The idea is that users can build arbitrarily complex trees of datasets, then
+ * at any point call `optimize()` to create the smallest possible tree that
+ * represents the same dataset, and `query()` which produces a select statement
+ * for the resultant dataset.
+ *
+ * Users can also use the `schema` property and `implements()` to get and test
+ * the schema of a given dataset.
+ */
+export interface Dataset {
+  /**
+   * Get or calculate the resultant schema of this dataset.
+   */
+  readonly schema: DatasetSchema;
+
+  /**
+   * Produce a query for this dataset.
+   *
+   * @param schema - The schema to use for extracting columns - if undefined,
+   * the most specific possible schema is evaluated from the dataset first and
+   * used instead.
+   */
+  query(schema?: DatasetSchema): string;
+
+  /**
+   * Optimizes a dataset into the smallest possible expression.
+   *
+   * For example by combining elements of union data sets that have the same src
+   * and similar filters into a single set.
+   *
+   * For example, the following 'union' dataset...
+   *
+   * ```
+   * {
+   *   union: [
+   *     {
+   *       src: 'foo',
+   *       schema: {
+   *         'a': NUM,
+   *         'b': NUM,
+   *       },
+   *       filter: {col: 'a', eq: 1},
+   *     },
+   *     {
+   *       src: 'foo',
+   *       schema: {
+   *         'a': NUM,
+   *         'b': NUM,
+   *       },
+   *       filter: {col: 'a', eq: 2},
+   *     },
+   *   ]
+   * }
+   * ```
+   *
+   * ...will be combined into a single 'source' dataset...
+   *
+   * ```
+   * {
+   *   src: 'foo',
+   *   schema: {
+   *     'a': NUM,
+   *     'b': NUM,
+   *   },
+   *   filter: {col: 'a', in: [1, 2]},
+   * },
+   * ```
+   */
+  optimize(): Dataset;
+
+  /**
+   * Returns true if this dataset implements a given schema.
+   *
+   * @param schema - The schema to test against.
+   */
+  implements(schema: DatasetSchema): boolean;
+}
+
+/**
+ * Defines a list of columns and types that define the shape of the data
+ * represented by a dataset.
+ */
+export type DatasetSchema = Record<string, ColumnType>;
+
+/**
+ * A filter used to express that a column must equal a value.
+ */
+interface EqFilter {
+  readonly col: string;
+  readonly eq: SqlValue;
+}
+
+/**
+ * A filter used to express that column must be one of a set of values.
+ */
+interface InFilter {
+  readonly col: string;
+  readonly in: ReadonlyArray<SqlValue>;
+}
+
+/**
+ * Union of all filter types.
+ */
+type Filter = EqFilter | InFilter;
+
+/**
+ * Named arguments for a SourceDataset.
+ */
+interface SourceDatasetConfig {
+  readonly src: string;
+  readonly schema: DatasetSchema;
+  readonly filter?: Filter;
+}
+
+/**
+ * Defines a dataset with a source SQL select statement of table name, a
+ * schema describing the columns, and an optional filter.
+ */
+export class SourceDataset implements Dataset {
+  readonly src: string;
+  readonly schema: DatasetSchema;
+  readonly filter?: Filter;
+
+  constructor(config: SourceDatasetConfig) {
+    this.src = config.src;
+    this.schema = config.schema;
+    this.filter = config.filter;
+  }
+
+  query(schema?: DatasetSchema) {
+    schema = schema ?? this.schema;
+    const cols = Object.keys(schema);
+    const whereClause = this.filterToQuery();
+    return `select ${cols.join(', ')} from (${this.src}) ${whereClause}`.trim();
+  }
+
+  optimize() {
+    // Cannot optimize SourceDataset
+    return this;
+  }
+
+  implements(schema: DatasetSchema) {
+    return Object.entries(schema).every(([name, kind]) => {
+      return name in this.schema && this.schema[name] === kind;
+    });
+  }
+
+  private filterToQuery() {
+    const filter = this.filter;
+    if (filter === undefined) {
+      return '';
+    }
+    if ('eq' in filter) {
+      return `where ${filter.col} = ${filter.eq}`;
+    } else if ('in' in filter) {
+      return `where ${filter.col} in (${filter.in.join(',')})`;
+    } else {
+      assertUnreachable(filter);
+    }
+  }
+}
+
+/**
+ * A dataset that represents the union of multiple datasets.
+ */
+export class UnionDataset implements Dataset {
+  constructor(readonly union: ReadonlyArray<Dataset>) {}
+
+  get schema(): DatasetSchema {
+    // Find the minimal set of columns that are supported by all datasets of
+    // the union
+    let sch: Record<string, ColumnType> | undefined = undefined;
+    this.union.forEach((ds) => {
+      const dsSchema = ds.schema;
+      if (sch === undefined) {
+        // First time just use this one
+        sch = dsSchema;
+      } else {
+        const newSch: Record<string, ColumnType> = {};
+        for (const [key, kind] of Object.entries(sch)) {
+          if (key in dsSchema && dsSchema[key] === kind) {
+            newSch[key] = kind;
+          }
+        }
+        sch = newSch;
+      }
+    });
+    return sch ?? {};
+  }
+
+  query(schema?: DatasetSchema): string {
+    schema = schema ?? this.schema;
+    return this.union
+      .map((dataset) => dataset.query(schema))
+      .join(' union all ');
+  }
+
+  optimize(): Dataset {
+    // Recursively optimize each dataset of this union
+    const optimizedUnion = this.union.map((ds) => ds.optimize());
+
+    // Find all source datasets and combine then based on src
+    const combinedSrcSets = new Map<string, SourceDataset[]>();
+    const otherDatasets: Dataset[] = [];
+    for (const e of optimizedUnion) {
+      if (e instanceof SourceDataset) {
+        const set = getOrCreate(combinedSrcSets, e.src, () => []);
+        set.push(e);
+      } else {
+        otherDatasets.push(e);
+      }
+    }
+
+    const mergedSrcSets = Array.from(combinedSrcSets.values()).map(
+      (srcGroup) => {
+        if (srcGroup.length === 1) return srcGroup[0];
+
+        // Combine schema across all members in the union
+        const combinedSchema = srcGroup.reduce((acc, e) => {
+          Object.assign(acc, e.schema);
+          return acc;
+        }, {} as DatasetSchema);
+
+        // Merge filters for the same src
+        const inFilters: InFilter[] = [];
+        for (const {filter} of srcGroup) {
+          if (filter) {
+            if ('eq' in filter) {
+              inFilters.push({col: filter.col, in: [filter.eq]});
+            } else {
+              inFilters.push(filter);
+            }
+          }
+        }
+
+        const mergedFilter = mergeFilters(inFilters);
+        return new SourceDataset({
+          src: srcGroup[0].src,
+          schema: combinedSchema,
+          filter: mergedFilter,
+        });
+      },
+    );
+
+    const finalUnion = [...mergedSrcSets, ...otherDatasets];
+
+    if (finalUnion.length === 1) {
+      return finalUnion[0];
+    } else {
+      return new UnionDataset(finalUnion);
+    }
+  }
+
+  implements(schema: DatasetSchema) {
+    return Object.entries(schema).every(([name, kind]) => {
+      return name in this.schema && this.schema[name] === kind;
+    });
+  }
+}
+
+function mergeFilters(filters: InFilter[]): InFilter | undefined {
+  if (filters.length === 0) return undefined;
+  const col = filters[0].col;
+  const values = new Set(filters.flatMap((filter) => filter.in));
+  return {col, in: Array.from(values)};
+}
diff --git a/ui/src/trace_processor/dataset_unittest.ts b/ui/src/trace_processor/dataset_unittest.ts
new file mode 100644
index 0000000..2bd4e53
--- /dev/null
+++ b/ui/src/trace_processor/dataset_unittest.ts
@@ -0,0 +1,228 @@
+// Copyright (C) 2024 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.
+
+import {SourceDataset, UnionDataset} from './dataset';
+import {LONG, NUM, STR} from './query_result';
+
+test('get query for simple dataset', () => {
+  const dataset = new SourceDataset({
+    src: 'slice',
+    schema: {id: NUM},
+  });
+
+  expect(dataset.query()).toEqual('select id from (slice)');
+});
+
+test("get query for simple dataset with 'eq' filter", () => {
+  const dataset = new SourceDataset({
+    src: 'slice',
+    schema: {id: NUM},
+    filter: {
+      col: 'id',
+      eq: 123,
+    },
+  });
+
+  expect(dataset.query()).toEqual('select id from (slice) where id = 123');
+});
+
+test("get query for simple dataset with an 'in' filter", () => {
+  const dataset = new SourceDataset({
+    src: 'slice',
+    schema: {id: NUM},
+    filter: {
+      col: 'id',
+      in: [123, 456],
+    },
+  });
+
+  expect(dataset.query()).toEqual(
+    'select id from (slice) where id in (123,456)',
+  );
+});
+
+test('get query for union dataset', () => {
+  const dataset = new UnionDataset([
+    new SourceDataset({
+      src: 'slice',
+      schema: {id: NUM},
+      filter: {
+        col: 'id',
+        eq: 123,
+      },
+    }),
+    new SourceDataset({
+      src: 'slice',
+      schema: {id: NUM},
+      filter: {
+        col: 'id',
+        eq: 456,
+      },
+    }),
+  ]);
+
+  expect(dataset.query()).toEqual(
+    'select id from (slice) where id = 123 union all select id from (slice) where id = 456',
+  );
+});
+
+test('doesImplement', () => {
+  const dataset = new SourceDataset({
+    src: 'slice',
+    schema: {id: NUM, ts: LONG},
+  });
+
+  expect(dataset.implements({id: NUM})).toBe(true);
+  expect(dataset.implements({id: NUM, ts: LONG})).toBe(true);
+  expect(dataset.implements({id: NUM, ts: LONG, name: STR})).toBe(false);
+  expect(dataset.implements({id: LONG})).toBe(false);
+});
+
+test('find the schema of a simple dataset', () => {
+  const dataset = new SourceDataset({
+    src: 'slice',
+    schema: {id: NUM, ts: LONG},
+  });
+
+  expect(dataset.schema).toMatchObject({id: NUM, ts: LONG});
+});
+
+test('find the schema of a union where source sets differ in their names', () => {
+  const dataset = new UnionDataset([
+    new SourceDataset({
+      src: 'slice',
+      schema: {foo: NUM},
+    }),
+    new SourceDataset({
+      src: 'slice',
+      schema: {bar: NUM},
+    }),
+  ]);
+
+  expect(dataset.schema).toMatchObject({});
+});
+
+test('find the schema of a union with differing source sets', () => {
+  const dataset = new UnionDataset([
+    new SourceDataset({
+      src: 'slice',
+      schema: {foo: NUM},
+    }),
+    new SourceDataset({
+      src: 'slice',
+      schema: {foo: LONG},
+    }),
+  ]);
+
+  expect(dataset.schema).toMatchObject({});
+});
+
+test('find the schema of a union with one column in common', () => {
+  const dataset = new UnionDataset([
+    new SourceDataset({
+      src: 'slice',
+      schema: {foo: NUM, bar: NUM},
+    }),
+    new SourceDataset({
+      src: 'slice',
+      schema: {foo: NUM, baz: NUM},
+    }),
+  ]);
+
+  expect(dataset.schema).toMatchObject({foo: NUM});
+});
+
+test('optimize a union dataset', () => {
+  const dataset = new UnionDataset([
+    new SourceDataset({
+      src: 'slice',
+      schema: {},
+      filter: {
+        col: 'track_id',
+        eq: 123,
+      },
+    }),
+    new SourceDataset({
+      src: 'slice',
+      schema: {},
+      filter: {
+        col: 'track_id',
+        eq: 456,
+      },
+    }),
+  ]);
+
+  expect(dataset.optimize()).toEqual({
+    src: 'slice',
+    schema: {},
+    filter: {
+      col: 'track_id',
+      in: [123, 456],
+    },
+  });
+});
+
+test('optimize a union dataset with different types of filters', () => {
+  const dataset = new UnionDataset([
+    new SourceDataset({
+      src: 'slice',
+      schema: {},
+      filter: {
+        col: 'track_id',
+        eq: 123,
+      },
+    }),
+    new SourceDataset({
+      src: 'slice',
+      schema: {},
+      filter: {
+        col: 'track_id',
+        in: [456, 789],
+      },
+    }),
+  ]);
+
+  expect(dataset.optimize()).toEqual({
+    src: 'slice',
+    schema: {},
+    filter: {
+      col: 'track_id',
+      in: [123, 456, 789],
+    },
+  });
+});
+
+test('optimize a union dataset with different schemas', () => {
+  const dataset = new UnionDataset([
+    new SourceDataset({
+      src: 'slice',
+      schema: {foo: NUM},
+    }),
+    new SourceDataset({
+      src: 'slice',
+      schema: {bar: NUM},
+    }),
+  ]);
+
+  expect(dataset.optimize()).toEqual({
+    src: 'slice',
+    // The resultant schema is the combination of the union's member's schemas,
+    // as we know the source is the same as we know we can get all of the 'seen'
+    // columns from the source.
+    schema: {
+      foo: NUM,
+      bar: NUM,
+    },
+  });
+});