infra: add perfetto.dev site

Merge the sources of the new website @ perfetto.dev

Change-Id: Ie3bc59fa3f42a41360bf07118d2ad8e12f409660
diff --git a/infra/perfetto.dev/src/assets/README.md b/infra/perfetto.dev/src/assets/README.md
new file mode 100644
index 0000000..9f6190a
--- /dev/null
+++ b/infra/perfetto.dev/src/assets/README.md
@@ -0,0 +1,2 @@
+The vector drawings (.sketch) are stored in the internal teams drive
+under Perfetto -> Graphics -> "Perfetto Site.sketch"
diff --git a/infra/perfetto.dev/src/assets/analysis.png b/infra/perfetto.dev/src/assets/analysis.png
new file mode 100644
index 0000000..f2ccdd6
--- /dev/null
+++ b/infra/perfetto.dev/src/assets/analysis.png
Binary files differ
diff --git a/infra/perfetto.dev/src/assets/app_tracing.png b/infra/perfetto.dev/src/assets/app_tracing.png
new file mode 100644
index 0000000..18fdc10
--- /dev/null
+++ b/infra/perfetto.dev/src/assets/app_tracing.png
Binary files differ
diff --git a/infra/perfetto.dev/src/assets/brand.png b/infra/perfetto.dev/src/assets/brand.png
new file mode 100644
index 0000000..29afdc4
--- /dev/null
+++ b/infra/perfetto.dev/src/assets/brand.png
Binary files differ
diff --git a/infra/perfetto.dev/src/assets/favicon.png b/infra/perfetto.dev/src/assets/favicon.png
new file mode 100644
index 0000000..c405e24
--- /dev/null
+++ b/infra/perfetto.dev/src/assets/favicon.png
Binary files differ
diff --git a/infra/perfetto.dev/src/assets/home.png b/infra/perfetto.dev/src/assets/home.png
new file mode 100644
index 0000000..935e2c3
--- /dev/null
+++ b/infra/perfetto.dev/src/assets/home.png
Binary files differ
diff --git a/infra/perfetto.dev/src/assets/script.js b/infra/perfetto.dev/src/assets/script.js
new file mode 100644
index 0000000..91c57e5
--- /dev/null
+++ b/infra/perfetto.dev/src/assets/script.js
@@ -0,0 +1,287 @@
+// Copyright (C) 2020 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.
+
+'use strict';
+
+let tocAnchors = [];
+let lastMouseOffY = 0;
+let onloadFired = false;
+const postLoadActions = [];
+let tocEventHandlersInstalled = false;
+let resizeObserver = undefined;
+
+// Handles redirects from the old docs.perfetto.dev.
+const legacyRedirectMap = {
+  '#/contributing': '/docs/contributing/getting-started#community',
+  '#/build-instructions': '/docs/contributing/build-instructions',
+  '#/testing': '/docs/contributing/testing',
+  '#/app-instrumentation': '/docs/instrumentation/tracing-sdk',
+  '#/recording-traces': '/docs/instrumentation/tracing-sdk#recording',
+  '#/running': '/docs/quickstart/android-tracing',
+  '#/long-traces': '/docs/concepts/config#long-traces',
+  '#/detached-mode': '/docs/concepts/detached-mode',
+  '#/heapprofd': '/docs/data-sources/native-heap-profiler',
+  '#/java-hprof': '/docs/data-sources/java-heap-profiler',
+  '#/trace-processor': '/docs/analysis/trace-processor',
+  '#/analysis': '/docs/analysis/trace-processor#annotations',
+  '#/metrics': '/docs/analysis/metrics',
+  '#/traceconv': '/docs/quickstart/traceconv',
+  '#/clock-sync': '/docs/concepts/clock-sync',
+  '#/architecture': '/docs/concepts/service-model',
+};
+
+function doAfterLoadEvent(action) {
+  if (onloadFired) {
+    return action();
+  }
+  postLoadActions.push(action);
+}
+
+function setupSandwichMenu() {
+  const header = document.querySelector('.site-header');
+  const docsNav = document.querySelector('.nav');
+  const menu = header.querySelector('.menu');
+  menu.addEventListener('click', (e) => {
+    e.preventDefault();
+
+    // If we are displaying any /docs, toggle the navbar instead (the TOC).
+    if (docsNav) {
+      // |after_first_click| is to avoid spurious transitions on page load.
+      docsNav.classList.add('after_first_click');
+      updateNav();
+      setTimeout(() => docsNav.classList.toggle('expanded'), 0);
+    } else {
+      header.classList.toggle('expanded');
+    }
+  });
+}
+
+// (Re-)Generates the Table Of Contents for docs (the right-hand-side one).
+function updateTOC() {
+  const tocContainer = document.querySelector('.docs .toc');
+  if (!tocContainer)
+    return;
+  const toc = document.createElement('ul');
+  const anchors = document.querySelectorAll('.doc a.anchor');
+  tocAnchors = [];
+  for (const anchor of anchors) {
+    const li = document.createElement('li');
+    const link = document.createElement('a');
+    link.innerText = anchor.parentElement.innerText;
+    link.href = anchor.href;
+    link.onclick = () => {
+      onScroll(link)
+    };
+    li.appendChild(link);
+    if (anchor.parentElement.tagName === 'H3')
+      li.style.paddingLeft = '10px';
+    toc.appendChild(li);
+    doAfterLoadEvent(() => {
+      tocAnchors.push(
+          {top: anchor.offsetTop + anchor.offsetHeight / 2, obj: link});
+    });
+  }
+  tocContainer.innerHTML = '';
+  tocContainer.appendChild(toc);
+
+  // Add event handlers on the first call (can be called more than once to
+  // recompute anchors on resize).
+  if (tocEventHandlersInstalled)
+    return;
+  tocEventHandlersInstalled = true;
+  const doc = document.querySelector('.doc');
+  const passive = {passive: true};
+  if (doc) {
+    const offY = doc.offsetTop;
+    doc.addEventListener('mousemove', (e) => onMouseMove(offY, e), passive);
+    doc.addEventListener('mouseleave', () => {
+      lastMouseOffY = 0;
+    }, passive);
+  }
+  window.addEventListener('scroll', () => onScroll(), passive);
+  resizeObserver = new ResizeObserver(() => requestAnimationFrame(() => {
+                                        updateNav();
+                                        updateTOC();
+                                      }));
+  resizeObserver.observe(doc);
+}
+
+// Highlights the current TOC anchor depending on the scroll offset.
+function onMouseMove(offY, e) {
+  lastMouseOffY = e.clientY - offY;
+  onScroll();
+}
+
+function onScroll(forceHighlight) {
+  const y = document.documentElement.scrollTop + lastMouseOffY;
+  let highEl = undefined;
+  for (const x of tocAnchors) {
+    if (y < x.top)
+      continue;
+    highEl = x.obj;
+  }
+  for (const link of document.querySelectorAll('.docs .toc a')) {
+    if ((!forceHighlight && link === highEl) || (forceHighlight === link)) {
+      link.classList.add('highlighted');
+    } else {
+      link.classList.remove('highlighted');
+    }
+  }
+}
+
+// This function needs to be idempotent as it is called more than once (on every
+// resize).
+function updateNav() {
+  const curDoc = document.querySelector('.doc');
+  let curFileName = '';
+  if (curDoc)
+    curFileName = curDoc.dataset['mdFile'];
+
+  // First identify all the top-level nav entries (Quickstart, Data Sources,
+  // ...) and make them compressible.
+  const toplevelSections = document.querySelectorAll('.docs .nav > ul > li');
+  const toplevelLinks = [];
+  for (const sec of toplevelSections) {
+    const childMenu = sec.querySelector('ul');
+    if (!childMenu) {
+      // Don't make it compressible if it has no children (e.g. the very
+      // first 'Introduction' link).
+      continue;
+    }
+
+    // Don't make it compressible if the entry has an actual link (e.g. the very
+    // first 'Introduction' link), because otherwise it become ambiguous whether
+    // the link should toggle or open the link.
+    const link = sec.querySelector('a');
+    if (!link || !link.href.endsWith('#'))
+      continue;
+
+    sec.classList.add('compressible');
+
+    // Remember the compressed status as long as the page is opened, so clicking
+    // through links keeps the sidebar in a consistent visual state.
+    const memoKey = `docs.nav.compressed[${link.innerHTML}]`;
+
+    if (sessionStorage.getItem(memoKey) === '1') {
+      sec.classList.add('compressed');
+    }
+    doAfterLoadEvent(() => {
+      childMenu.style.maxHeight = `${childMenu.scrollHeight + 40}px`;
+    });
+
+    toplevelLinks.push(link);
+    link.onclick = (evt) => {
+      evt.preventDefault();
+      sec.classList.toggle('compressed');
+      if (sec.classList.contains('compressed')) {
+        sessionStorage.setItem(memoKey, '1');
+      } else {
+        sessionStorage.removeItem(memoKey);
+      }
+    };
+  }
+
+  const exps = document.querySelectorAll('.docs .nav ul a');
+  let found = false;
+  for (const x of exps) {
+    // If the url of the entry matches the url of the page, mark the item as
+    // highlighted and expand all its parents.
+    if (!x.href)
+      continue;
+    const url = new URL(x.href);
+    if (x.href.endsWith('#')) {
+      // This is a non-leaf link to a menu.
+      if (toplevelLinks.indexOf(x) < 0) {
+        x.removeAttribute('href');
+      }
+    } else if (url.pathname === curFileName && !found) {
+      x.classList.add('selected');
+      doAfterLoadEvent(() => x.scrollIntoViewIfNeeded());
+      found = true;  // Highlight only the first occurrence.
+    }
+  }
+}
+
+// If the page contains a ```mermaid ``` block, lazily loads the plugin and
+// renders.
+function initMermaid() {
+  const graphs = document.querySelectorAll('.mermaid');
+
+  // Skip if there are no mermaid graphs to render.
+  if (!graphs.length)
+    return;
+
+  const script = document.createElement('script');
+  script.type = 'text/javascript';
+  script.src = '/assets/mermaid.min.js';
+  const themeCSS = `
+  .cluster rect { fill: #FCFCFC; stroke: #ddd }
+  .node rect { fill: #DCEDC8; stroke: #8BC34A}
+  .edgeLabel:not(:empty) {
+      border-radius: 6px;
+      font-size: 0.9em;
+      padding: 4px;
+      background: #F5F5F5;
+      border: 1px solid #DDDDDD;
+      color: #666;
+  }
+  `;
+  script.addEventListener('load', () => {
+    mermaid.initialize({
+      startOnLoad: false,
+      themeCSS: themeCSS,
+      securityLevel: 'loose',  // To allow #in-page-links
+    });
+    for (const graph of graphs) {
+      requestAnimationFrame(() => {
+        mermaid.init(undefined, graph);
+        graph.classList.add('rendered');
+      });
+    }
+  })
+  document.body.appendChild(script);
+}
+
+window.addEventListener('DOMContentLoaded', () => {
+  updateNav();
+  updateTOC();
+});
+
+window.addEventListener('load', () => {
+  setupSandwichMenu();
+  initMermaid();
+
+  // Don't smooth-scroll on pages that are too long (e.g. reference pages).
+  if (document.body.scrollHeight < 10000) {
+    document.documentElement.style.scrollBehavior = 'smooth';
+  } else {
+    document.documentElement.style.scrollBehavior = 'initial';
+  }
+
+  onloadFired = true;
+  while (postLoadActions.length > 0) {
+    postLoadActions.shift()();
+  }
+
+  updateTOC();
+
+  // Enable animations only after the load event. This is to prevent glitches
+  // when switching pages.
+  document.documentElement.style.setProperty('--anim-enabled', '1')
+});
+
+const fragment = location.hash.split('?')[0].replace('.md', '');
+if (fragment in legacyRedirectMap) {
+  location.replace(legacyRedirectMap[fragment]);
+}
\ No newline at end of file
diff --git a/infra/perfetto.dev/src/assets/sprite.png b/infra/perfetto.dev/src/assets/sprite.png
new file mode 100644
index 0000000..f076c5a
--- /dev/null
+++ b/infra/perfetto.dev/src/assets/sprite.png
Binary files differ
diff --git a/infra/perfetto.dev/src/assets/style.scss b/infra/perfetto.dev/src/assets/style.scss
new file mode 100644
index 0000000..5fc742e
--- /dev/null
+++ b/infra/perfetto.dev/src/assets/style.scss
@@ -0,0 +1,856 @@
+// Copyright (C) 2020 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.
+// -----------------------------------------------------------------------------
+// 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);
+
+    // 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));
+}
+
+$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;
+    }
+}
+
+@media (max-aspect-ratio: 1/1) {
+     :root {
+        --home-highlights-height: 256px;
+    }
+}
+
+* {
+    box-sizing: border-box;
+    -webkit-tap-highlight-color: none;
+}
+
+html {
+    font-family: Roboto, sans-serif;
+    -webkit-font-smoothing: antialiased;
+}
+
+html,
+body {
+    padding: 0;
+    margin: 0;
+}
+
+h1,
+h2,
+h3 {
+    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.
+    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) 0px 3px 3px 0px;
+    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;
+        line-height: var(--site-header-height);
+    }
+
+    @media #{$mobile} {
+        flex-direction: column;
+        >*:not(:first-child) {
+            margin-left: 40px;
+        }
+        .menu {
+            visibility: visible;
+        }
+    }
+}
+
+// -----------------------------------------------------------------------------
+// Site header
+// -----------------------------------------------------------------------------
+
+// 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;
+            }
+        }
+    }
+    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;
+
+    .docs-footer-notice {
+        padding: 0;
+        margin: 0;
+        display: block;
+    }
+
+    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 {
+        display: block;
+        position: relative;
+        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));
+        min-height: 25vw;
+        display: grid;
+        grid-template-columns: 1fr;
+        grid-template-rows: max-content max-content 1fr;
+        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(max(3.5vw, 2rem), 4rem));
+            font-weight: 400;
+            padding-top: 40px;
+        }
+        h2 {
+            font-size: 1.25rem;
+            font-size: calc(min(max(2vw, 1.25rem), 3rem));
+            font-weight: 200;
+            padding-top: 10px;
+        }
+        .home-img {
+            max-height: 100%;
+            max-width: 100%;
+            margin: auto;
+            padding: 20px 0;
+        }
+    }
+    .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);
+                }
+            }
+        }
+    }
+    .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: 100%;
+            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;
+            }
+        }
+    }
+}
+
+// -----------------------------------------------------------------------------
+// Docs
+// -----------------------------------------------------------------------------
+.docs {
+    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));
+
+    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;
+
+        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;
+                }
+            }
+        }
+
+        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; }
+        }
+
+        // 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;
+        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, .1), 0 1px 3px 1px rgba(60, 64, 67, .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;
+            }
+            thead {
+                text-align: left;
+                background-color: #e8eaed;
+                color: #202124;
+            }
+        }
+
+        &[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
+                     * column takes most of the width */
+                    &: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;
+
+        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);
+        }
+    }
+
+    @media #{$wide} {
+        grid-template-columns: var(--nav-width) auto 0;
+        .toc { display: none; }
+    }
+    @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;
+            }
+            &.expanded {
+                visibility: visible;
+                transform: translateX(0);
+                box-shadow: 0 1px 0 100vw rgba(0,0,0,0.4);
+            }
+        }
+    }
+}
diff --git a/infra/perfetto.dev/src/assets/sys_profiling.png b/infra/perfetto.dev/src/assets/sys_profiling.png
new file mode 100644
index 0000000..3135e16
--- /dev/null
+++ b/infra/perfetto.dev/src/assets/sys_profiling.png
Binary files differ
diff --git a/infra/perfetto.dev/src/assets/ui.png b/infra/perfetto.dev/src/assets/ui.png
new file mode 100644
index 0000000..331e0ea
--- /dev/null
+++ b/infra/perfetto.dev/src/assets/ui.png
Binary files differ
diff --git a/infra/perfetto.dev/src/gen_proto_reference.js b/infra/perfetto.dev/src/gen_proto_reference.js
new file mode 100644
index 0000000..044929e
--- /dev/null
+++ b/infra/perfetto.dev/src/gen_proto_reference.js
@@ -0,0 +1,147 @@
+// Copyright (C) 2020 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.
+
+// Generation of reference from protos
+
+'use strict';
+
+const protobufjs = require('protobufjs');
+const fs = require('fs');
+const path = require('path');
+const argv = require('yargs').argv
+
+const PROJECT_ROOT =
+    path.dirname(path.dirname(path.dirname(path.dirname(__filename))));
+
+const visited = {};
+
+
+// Removes \n due to 80col wrapping and preserves only end-of-sentence line
+// breaks.
+function singleLineComment(comment) {
+  comment = comment || '';
+  comment = comment.trim();
+  comment = comment.replace(/\.\n/g, '<br>');
+  comment = comment.replace(/\n/g, ' ');
+  return comment;
+}
+
+function getFullName(pType) {
+  let cur = pType;
+  let name = pType.name;
+  while (cur && cur.parent != cur && cur.parent instanceof protobufjs.Type) {
+    name = `${cur.parent.name}.${name}`;
+    cur = cur.parent;
+  }
+  return name;
+}
+
+function genType(pType, depth) {
+  depth = depth || 0;
+  console.assert(pType instanceof protobufjs.ReflectionObject);
+  const fullName = getFullName(pType);
+  if (fullName in visited)
+    return '';
+  visited[fullName] = true;
+
+  const heading = '#' +
+      '#'.repeat(Math.min(depth, 2));
+  const anchor = depth > 0 ? `{#${fullName}} ` : '';
+  let md = `${heading} ${anchor}${fullName}`;
+  md += '\n';
+  const fileName = path.basename(pType.filename);
+  const relPath = path.relative(PROJECT_ROOT, pType.filename);
+  md += `${(pType.comment || '').replace(/(\n)?^\s*next.*\bid:.*$/img, '')}`;
+  md += `\n\nDefined in [${fileName}](/${relPath})\n\n`;
+
+  const subTypes = [];
+
+  if (pType instanceof protobufjs.Enum) {
+    md += '#### Enum values:\n';
+    md += 'Name | Value | Description\n';
+    md += '---- | ----- | -----------\n';
+    for (const enumName of Object.keys(pType.values)) {
+      const enumVal = pType.values[enumName];
+      const comment = singleLineComment(pType.comments[enumName]);
+      md += `${enumName} | ${enumVal} | ${comment}\n`
+    }
+  } else {
+    md += '#### Fields:\n';
+    md += 'Field | Type | Description\n';
+    md += '----- | ---- | -----------\n';
+
+    for (const fieldName in pType.fields) {
+      const field = pType.fields[fieldName];
+      let type = field.type;
+      if (field.repeated) {
+        type = `${type}[]`;
+      }
+      if (field.resolvedType) {
+        // The TraceConfig proto is linked from the TracePacket reference.
+        // Instead of recursing and generating the TraceConfig types all over
+        // again, just link to the dedicated TraceConfig reference page.
+        if (getFullName(field.resolvedType) === 'TraceConfig') {
+          type = `[${type}](/docs/reference/trace-config-proto.autogen)`;
+        } else {
+          subTypes.push(field.resolvedType);
+          type = `[${type}](#${getFullName(field.resolvedType)})`;
+        }
+      }
+      md += `${fieldName} | ${type} | ${singleLineComment(field.comment)}\n`
+    }
+  }
+  md += '\n\n\n\n';
+
+  for (const subType of subTypes)
+    md += genType(subType, depth + 1);
+
+  return md;
+}
+
+
+function main() {
+  const inProtoFile = argv['i'];
+  const protoName = argv['p'];
+  const outFile = argv['o'];
+  if (!inProtoFile || !protoName) {
+    console.error('Usage: -i input.proto -p protos.RootType [-o out.md]');
+    process.exit(1);
+  }
+
+  const parser = new protobufjs.Root();
+  parser.resolvePath = (_, target) => {
+    if (target == inProtoFile) {
+      // The root proto file passed from the cmdline will be relative to the
+      // root_build_dir (out/xxx) (e.g.: ../../protos/config)
+      return inProtoFile;
+    }
+    // All the other imports, instead, will be relative to the project root
+    // (e.g. protos/config/...)
+    return path.join(PROJECT_ROOT, target);
+  };
+
+  const cfg = parser.loadSync(
+      inProtoFile, {alternateCommentMode: true, keepCase: true});
+  cfg.resolveAll();
+  const traceConfig = cfg.lookup(protoName);
+  const generatedMd = genType(traceConfig);
+  if (outFile) {
+    fs.writeFileSync(outFile, generatedMd);
+  } else {
+    console.log(generatedMd);
+  }
+  process.exit(0);
+}
+
+main();
diff --git a/infra/perfetto.dev/src/gen_sql_tables_reference.js b/infra/perfetto.dev/src/gen_sql_tables_reference.js
new file mode 100644
index 0000000..a605577
--- /dev/null
+++ b/infra/perfetto.dev/src/gen_sql_tables_reference.js
@@ -0,0 +1,305 @@
+// Copyright (C) 2020 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.
+
+// Generation of reference from protos
+
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const argv = require('yargs').argv
+
+// Removes \n due to 80col wrapping and preserves only end-of-sentence line
+// breaks.
+// TODO dedupe, this is copied from the other gen_proto file.
+function singleLineComment(comment) {
+  comment = comment || '';
+  comment = comment.trim();
+  comment = comment.replace(/\.\n/g, '<br>');
+  comment = comment.replace(/\n/g, ' ');
+  return comment;
+}
+
+// Returns an object describing the table as follows:
+// { name: 'HeapGraphObjectTable',
+//   cols: [ {name: 'upid',            type: 'uint32_t', optional: false },
+//           {name: 'graph_sample_ts', type: 'int64_t',  optional: false },
+function parseTableDef(tableDefName, tableDef) {
+  const tableDesc = {
+    name: '',                // The SQL table name, e.g. stack_profile_mapping.
+    cppClassName: '',        // e.g., StackProfileMappingTable.
+    defMacro: tableDefName,  // e.g., PERFETTO_TP_STACK_PROFILE_MAPPING_DEF.
+    comment: '',
+    parent: undefined,   // Will be filled afterwards in the resolution phase.
+    parentDefName: '',   // e.g., PERFETTO_TP_STACK_PROFILE_MAPPING_DEF.
+    tablegroup: 'Misc',  // From @tablegroup in comments.
+    cols: {},
+  };
+  const getOrCreateColumn = (name) => {
+    if (name in tableDesc.cols)
+      return tableDesc.cols[name];
+    tableDesc.cols[name] = {
+      name: name,
+      type: '',
+      comment: '',
+      optional: false,
+      refTableCppName: undefined,
+      joinTable: undefined,
+      joinCol: undefined,
+    };
+    return tableDesc.cols[name];
+  };
+
+  let lastColumn = undefined;
+  for (const line of tableDef.split('\n')) {
+    if (line.startsWith('#define'))
+      continue;  // Skip the first line.
+    let m;
+    if (line.startsWith('//')) {
+      let comm = line.replace(/^\s*\/\/\s*/, '');
+      if (m = comm.match(/@tablegroup (.*)/)) {
+        tableDesc.tablegroup = m[1];
+        continue;
+      }
+      if (m = comm.match(/@name (\w+)/)) {
+        tableDesc.name = m[1];
+        continue;
+      }
+      if (m = comm.match(/@param\s+([^ ]+)\s*({\w+})?\s*(.*)/)) {
+        lastColumn = getOrCreateColumn(/*name=*/ m[1]);
+        lastColumn.type = (m[2] || '').replace(/(^{)|(}$)/g, '');
+        lastColumn.comment = m[3];
+        continue;
+      }
+      if (lastColumn === undefined) {
+        tableDesc.comment += `${comm}\n`;
+      } else {
+        lastColumn.comment = `${lastColumn.comment}${comm}\n`;
+      }
+      continue;
+    }
+    if (m = line.match(/^\s*NAME\((\w+)\s*,\s*"(\w+)"/)) {
+      tableDesc.cppClassName = m[1];
+      if (tableDesc.name === '') {
+        tableDesc.name = m[2];  // Set only if not overridden by @name.
+      }
+      continue;
+    }
+    if (m = line.match(/(PERFETTO_TP_ROOT_TABLE|PARENT)\((\w+)/)) {
+      if (m[1] === 'PARENT') {
+        tableDesc.parentDefName = m[2];
+      }
+      continue;
+    }
+    if (m = line.match(/^\s*C\(([^,]+)\s*,\s*(\w+)/)) {
+      const col = getOrCreateColumn(/*name=*/ m[2]);
+      col.type = m[1];
+      if (m = col.type.match(/Optional<(.*)>/)) {
+        col.type = m[1];
+        col.optional = true;
+      }
+      if (col.type === 'StringPool::Id') {
+        col.type = 'string';
+      }
+      const sep = col.type.indexOf('::');
+      if (sep > 0) {
+        col.refTableCppName = col.type.substr(0, sep);
+      }
+      continue;
+    }
+    throw new Error(`Cannot parse line "${line}" from ${tableDefName}`);
+  }
+
+  // Process {@joinable xxx} annotations.
+  const regex = /\s?\{@joinable\s*(\w+)\.(\w+)\s*\}/;
+  for (const col of Object.values(tableDesc.cols)) {
+    const m = col.comment.match(regex)
+    if (m) {
+      col.joinTable = m[1];
+      col.joinCol = m[2];
+      col.comment = col.comment.replace(regex, '');
+    }
+  }
+  return tableDesc;
+}
+
+
+function parseTablesInCppFile(filePath) {
+  const hdr = fs.readFileSync(filePath, 'UTF8');
+  const regex = /^\s*PERFETTO_TP_TABLE\((\w+)\)/mg;
+  let match = regex.exec(hdr);
+  const tables = [];
+  while (match != null) {
+    const tableDefName = match[1];
+    match = regex.exec(hdr);
+
+    // Now let's extract the table definition, that looks like this:
+    // // Some
+    // // Multiline
+    // // Comment
+    // #define PERFETTO_TP_STACK_PROFILE_FRAME_DEF(NAME, PARENT, C) \
+    // NAME(StackProfileFrameTable, "stack_profile_frame")        \
+    // PERFETTO_TP_ROOT_TABLE(PARENT, C)                          \
+    // C(StringPool::Id, name)                                    \
+    // C(StackProfileMappingTable::Id, mapping)                   \
+    // C(int64_t, rel_pc)                                         \
+    // C(base::Optional<uint32_t>, symbol_set_id)
+    //
+    // Where PERFETTO_TP_STACK_PROFILE_FRAME_DEF is |tableDefName|.
+    let pattern = `(^[ ]*//.*\n)*`;
+    pattern += `^\s*#define\\s+${tableDefName}\\s*\\(`;
+    pattern += `(.*\\\\\\s*\n)+`;
+    pattern += `.+`;
+    const r = new RegExp(pattern, 'mi');
+    const tabMatch = r.exec(hdr);
+    if (!tabMatch) {
+      console.error(`could not find table ${tableDefName}`);
+      continue;
+    }
+    tables.push(parseTableDef(tableDefName, tabMatch[0]));
+  }
+  return tables;
+}
+
+
+function genLink(table) {
+  return `[${table.name}](#${table.name})`;
+}
+
+function tableToMarkdown(table) {
+  let md = `### ${table.name}\n\n`;
+  if (table.parent) {
+    md += `_Extends ${genLink(table.parent)}_\n\n`;
+  }
+  md += table.comment + '\n\n';
+  md += 'Column | Type | Description\n';
+  md += '------ | ---- | -----------\n';
+
+  let curTable = table;
+  while (curTable) {
+    if (curTable != table) {
+      md += `||_Columns inherited from_ ${genLink(curTable)}\n`
+    }
+    for (const col of Object.values(curTable.cols)) {
+      const type = col.type + (col.optional ? '<br>`optional`' : '');
+      let description = col.comment;
+      if (col.joinTable) {
+        description += `\nJoinable with ` +
+            `[${col.joinTable}.${col.joinCol}](#${col.joinTable})`;
+      }
+      md += `${col.name} | ${type} | ${singleLineComment(description)}\n`
+    }
+    curTable = curTable.parent;
+  }
+  md += '\n\n';
+  return md;
+}
+
+function main() {
+  const inFile = argv['i'];
+  const outFile = argv['o'];
+  if (!inFile) {
+    console.error('Usage: -i hdr1.h -i hdr2.h -[-o out.md]');
+    process.exit(1);
+  }
+
+  // Can be either a string (-i single) or an array (-i one -i two).
+  const inFiles = (inFile instanceof Array) ? inFile : [inFile];
+
+  const tables = Array.prototype.concat(...inFiles.map(parseTablesInCppFile));
+
+  // Resolve parents.
+  const tablesIndex = {};    // 'TP_SCHED_SLICE_TABLE_DEF' -> table
+  const tablesByGroup = {};  // 'profilers' => [table1, table2]
+  const tablesCppName = {};  // 'StackProfileMappingTable' => table
+  const tablesByName = {};   // 'profile_mapping' => table
+  for (const table of tables) {
+    tablesIndex[table.defMacro] = table;
+    if (tablesByGroup[table.tablegroup] === undefined) {
+      tablesByGroup[table.tablegroup] = [];
+    }
+    tablesCppName[table.cppClassName] = table;
+    tablesByName[table.name] = table;
+    tablesByGroup[table.tablegroup].push(table);
+  }
+  const tableGroups = Object.keys(tablesByGroup).sort((a, b) => {
+    const keys = {'Tracks': '1', 'Events': '2', 'Misc': 'z'};
+    a = `${keys[a]}_${a}`;
+    b = `${keys[b]}_${b}`;
+    return a.localeCompare(b);
+  });
+
+  for (const table of tables) {
+    if (table.parentDefName) {
+      table.parent = tablesIndex[table.parentDefName];
+    }
+  }
+
+  // Builds a graph of the tables' relationship that can be rendererd with
+  // mermaid.js.
+  let graph = '## Tables diagram\n';
+  const mkLabel = (table) => `${table.defMacro}["${table.name}"]`;
+  for (const tableGroup of tableGroups) {
+    let gaphEdges = '';
+    let gaphLinks = '';
+    graph += `#### ${tableGroup} tables\n`;
+    graph += '```mermaid\ngraph TD\n';
+    graph += `  subgraph ${tableGroup}\n`;
+    for (const table of tablesByGroup[tableGroup]) {
+      graph += `  ${mkLabel(table)}\n`;
+      gaphLinks += `  click ${table.defMacro} "#${table.name}"\n`
+      if (table.parent) {
+        gaphEdges += ` ${mkLabel(table)} --> ${mkLabel(table.parent)}\n`
+      }
+
+      for (const col of Object.values(table.cols)) {
+        let refTable = undefined;
+        if (col.refTableCppName) {
+          refTable = tablesCppName[col.refTableCppName];
+        } else if (col.joinTable) {
+          refTable = tablesByName[col.joinTable];
+          if (!refTable) {
+            throw new Error(`Cannot find @joinable table ${col.joinTable}`);
+          }
+        }
+        if (!refTable)
+          continue;
+        gaphEdges +=
+            `  ${mkLabel(table)} -. ${col.name} .-> ${mkLabel(refTable)}\n`
+        gaphLinks += `  click ${refTable.defMacro} "#${refTable.name}"\n`
+      }
+    }
+    graph += `  end\n`;
+    graph += gaphEdges;
+    graph += gaphLinks;
+    graph += '\n```\n';
+  }
+
+  let md = graph;
+  for (const tableGroup of tableGroups) {
+    md += `## ${tableGroup}\n`
+    for (const table of tablesByGroup[tableGroup]) {
+      md += tableToMarkdown(table);
+    }
+  }
+
+  if (outFile) {
+    fs.writeFileSync(outFile, md);
+  } else {
+    console.log(md);
+  }
+  process.exit(0);
+}
+
+main();
diff --git a/infra/perfetto.dev/src/gen_stats_reference.js b/infra/perfetto.dev/src/gen_stats_reference.js
new file mode 100644
index 0000000..0f7228e
--- /dev/null
+++ b/infra/perfetto.dev/src/gen_stats_reference.js
@@ -0,0 +1,100 @@
+// Copyright (C) 2020 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.
+
+// Generation of reference from protos
+
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const argv = require('yargs').argv
+
+// Removes \n due to 80col wrapping and preserves only end-of-sentence line
+// breaks.
+// TODO dedupe, this is copied from the other gen_proto file.
+function singleLineComment(comment) {
+  comment = comment || '';
+  comment = comment.trim();
+  comment = comment.replace(/\.\n/g, '<br>');
+  comment = comment.replace(/\n/g, ' ');
+  return comment;
+}
+
+function trimQuotes(s) {
+  if (s === undefined) {
+    return s;
+  }
+  const regex = /\"(.*)"/;
+  let m = regex.exec(s);
+  if (m === null) {
+    return null;
+  }
+  return m[1]
+}
+
+function parseTablesInCppFile(filePath) {
+  const hdr = fs.readFileSync(filePath, 'UTF8');
+  const regex = /^\s*F\(([\s\S]*?)\),\s*\\/mg;
+  let match;
+  let table = [];
+  while ((match = regex.exec(hdr)) !== null) {
+    let def = match[1];
+    let s = def.split(',').map(s => s.trim());
+    table.push({
+      name: s[0],
+      cardinality: s[1],
+      type: s[2],
+      scope: s[3],
+      comment: s[4] === undefined ? undefined :
+                                    s[4].split('\n').map(trimQuotes).join(' '),
+    });
+  }
+  return table;
+}
+
+
+function tableToMarkdown(table) {
+  let md = `# Trace Processor Stats\n\n`;
+  md += 'Name | Cardinality | Type | Scope | Description |\n';
+  md += '---- | ----------- | ---- | ----- | ----------- |\n';
+  for (const col of table) {
+    md += `${col.name} | ${col.cardinality} | ${col.type} | ${col.scope} | ${
+        singleLineComment(col.comment)} |\n`
+  }
+  md += '\n\n';
+  return md;
+}
+
+function main() {
+  const inFile = argv['i'];
+  const outFile = argv['o'];
+  if (!inFile) {
+    console.error('Usage: -i hdr1.h -i hdr2.h -[-o out.md]');
+    process.exit(1);
+  }
+
+  // Can be either a string (-i single) or an array (-i one -i two).
+  const inFiles = (inFile instanceof Array) ? inFile : [inFile];
+
+  const table = Array.prototype.concat(...inFiles.map(parseTablesInCppFile));
+  const md = tableToMarkdown(table);
+  if (outFile) {
+    fs.writeFileSync(outFile, md);
+  } else {
+    console.log(md);
+  }
+  process.exit(0);
+}
+
+main();
diff --git a/infra/perfetto.dev/src/markdown_render.js b/infra/perfetto.dev/src/markdown_render.js
new file mode 100644
index 0000000..ebb34d8
--- /dev/null
+++ b/infra/perfetto.dev/src/markdown_render.js
@@ -0,0 +1,220 @@
+// Copyright (C) 2020 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.
+
+const ejs = require('ejs');
+const marked = require('marked');
+const argv = require('yargs').argv
+const fs = require('fs-extra');
+const path = require('path');
+const hljs = require('highlight.js');
+
+const CS_BASE_URL =
+    'https://cs.android.com/android/platform/superproject/+/master:external/perfetto';
+
+const ROOT_DIR = path.dirname(path.dirname(path.dirname(__dirname)));
+const DOCS_DIR = path.join(ROOT_DIR, 'docs');
+
+let outDir = '';
+let curMdFile = '';
+let title = '';
+
+function hrefInDocs(href) {
+  if (href.match(/^(https?:)|^(mailto:)|^#/)) {
+    return undefined;
+  }
+  let pathFromRoot;
+  if (href.startsWith('/')) {
+    pathFromRoot = href;
+  } else {
+    curDocDir = '/' + path.relative(ROOT_DIR, path.dirname(curMdFile));
+    pathFromRoot = path.join(curDocDir, href);
+  }
+  if (pathFromRoot.startsWith('/docs/')) {
+    return pathFromRoot;
+  }
+  return undefined;
+}
+
+function assertNoDeadLink(relPathFromRoot) {
+  relPathFromRoot = relPathFromRoot.replace(/\#.*$/g, '');  // Remove #line.
+
+  // Skip check for build-time generated reference pages.
+  if (relPathFromRoot.endsWith('.autogen'))
+    return;
+
+  const fullPath = path.join(ROOT_DIR, relPathFromRoot);
+  if (!fs.existsSync(fullPath) && !fs.existsSync(fullPath + '.md')) {
+    const msg = `Dead link: ${relPathFromRoot} in ${curMdFile}`;
+    console.error(msg);
+    throw new Error(msg);
+  }
+}
+
+function renderHeading(text, level) {
+  // If the heading has an explicit ${#anchor}, use that. Otherwise infer the
+  // anchor from the text but only for h2 and h3. Note the right-hand-side TOC
+  // is dynamically generated from anchors (explicit or implicit).
+  if (level === 1 && !title) {
+    title = text;
+  }
+  let anchorId = '';
+  const explicitAnchor = /{#([\w-_.]+)}/.exec(text);
+  if (explicitAnchor) {
+    text = text.replace(explicitAnchor[0], '');
+    anchorId = explicitAnchor[1];
+  } else if (level >= 2 && level <= 3) {
+    anchorId = text.toLowerCase().replace(/[^\w]+/g, '-');
+    anchorId = anchorId.replace(/[-]+/g, '-');  // Drop consecutive '-'s.
+  }
+  let anchor = '';
+  if (anchorId) {
+    anchor = `<a name="${anchorId}" class="anchor" href="#${anchorId}"></a>`;
+  }
+  return `<h${level}>${anchor}${text}</h${level}>`;
+}
+
+function renderLink(originalLinkFn, href, title, text) {
+  if (href.startsWith('../')) {
+    throw new Error(
+        `Don\'t use relative paths in docs, always use /docs/xxx ` +
+        `or /src/xxx for both links to docs and code (${href})`)
+  }
+  const docsHref = hrefInDocs(href);
+  let sourceCodeLink = undefined;
+  if (docsHref !== undefined) {
+    // Check that the target doc exists. Skip the check on /reference/ files
+    // that are typically generated at build time.
+    assertNoDeadLink(docsHref);
+    href = docsHref.replace(/[.](md|autogen)\b/, '');
+    href = href.replace(/\/README$/, '/');
+  } else if (href.startsWith('/') && !href.startsWith('//')) {
+    // /tools/xxx -> github/tools/xxx.
+    sourceCodeLink = href;
+  }
+  if (sourceCodeLink !== undefined) {
+    // Fix up line anchors for GitHub link: #42 -> #L42.
+    sourceCodeLink = sourceCodeLink.replace(/#(\d+)$/g, '#L$1')
+    assertNoDeadLink(sourceCodeLink);
+    href = CS_BASE_URL + sourceCodeLink;
+  }
+  return originalLinkFn(href, title, text);
+}
+
+function renderCode(text, lang) {
+  if (lang === 'mermaid') {
+    return `<div class="mermaid">${text}</div>`;
+  }
+
+  let hlHtml = '';
+  if (lang) {
+    hlHtml = hljs.highlight(lang, text).value
+  } else {
+    hlHtml = hljs.highlightAuto(text).value
+  }
+  return `<code class="hljs code-block">${hlHtml}</code>`
+}
+
+function renderImage(originalImgFn, href, title, text) {
+  const docsHref = hrefInDocs(href);
+  if (docsHref !== undefined) {
+    const outFile = outDir + docsHref;
+    const outParDir = path.dirname(outFile);
+    fs.ensureDirSync(outParDir);
+    fs.copyFileSync(ROOT_DIR + docsHref, outFile);
+  }
+  if (href.endsWith('.svg')) {
+    return `<object type="image/svg+xml" data="${href}"></object>`
+  }
+  return originalImgFn(href, title, text);
+}
+
+function renderParagraph(text) {
+  let cssClass = '';
+  if (text.startsWith('NOTE:')) {
+    cssClass = 'note';
+  }
+   else if (text.startsWith('TIP:')) {
+    cssClass = 'tip';
+  }
+   else if (text.startsWith('TODO:') || text.startsWith('FIXME:')) {
+    cssClass = 'todo';
+  }
+   else if (text.startsWith('WARNING:')) {
+    cssClass = 'warning';
+  }
+   else if (text.startsWith('Summary:')) {
+    cssClass = 'summary';
+  }
+  if (cssClass != '') {
+    cssClass = ` class="callout ${cssClass}"`;
+  }
+  return `<p${cssClass}>${text}</p>\n`;
+}
+
+function render(rawMarkdown) {
+  const renderer = new marked.Renderer();
+  const originalLinkFn = renderer.link.bind(renderer);
+  const originalImgFn = renderer.image.bind(renderer);
+  renderer.link = (hr, ti, te) => renderLink(originalLinkFn, hr, ti, te);
+  renderer.image = (hr, ti, te) => renderImage(originalImgFn, hr, ti, te);
+  renderer.code = renderCode;
+  renderer.heading = renderHeading;
+  renderer.paragraph = renderParagraph;
+
+  return marked(rawMarkdown, {renderer: renderer});
+}
+
+function main() {
+  const inFile = argv['i'];
+  const outFile = argv['o'];
+  outDir = argv['odir'];
+  const templateFile = argv['t'];
+  if (!outFile || !outDir) {
+    console.error(
+        'Usage: --odir site -o out.html [-i input.md] [-t templ.html]');
+    process.exit(1);
+  }
+  curMdFile = inFile;
+
+  let markdownHtml = '';
+  if (inFile) {
+    markdownHtml = render(fs.readFileSync(inFile, 'utf8'));
+  }
+
+  if (templateFile) {
+    // TODO rename nav.html to sitemap or something more mainstream.
+    const navFilePath = path.join(outDir, 'docs', '_nav.html');
+    const fallbackTitle =
+        'Perfetto - System profiling, app tracing and trace analysis';
+    const templateData = {
+      markdown: markdownHtml,
+      title: title ? `${title} - Perfetto Tracing Docs` : fallbackTitle,
+      fileName: '/' + outFile.split('/').slice(1).join('/'),
+    };
+    if (fs.existsSync(navFilePath)) {
+      templateData['nav'] = fs.readFileSync(navFilePath, 'utf8');
+    }
+    ejs.renderFile(templateFile, templateData, (err, html) => {
+      if (err)
+        throw err;
+      fs.writeFileSync(outFile, html);
+      process.exit(0);
+    });
+  } else {
+    fs.writeFileSync(outFile, markdownHtml);
+    process.exit(0);
+  }
+}
+
+main();
diff --git a/infra/perfetto.dev/src/template_footer.html b/infra/perfetto.dev/src/template_footer.html
new file mode 100644
index 0000000..5649e26
--- /dev/null
+++ b/infra/perfetto.dev/src/template_footer.html
@@ -0,0 +1,14 @@
+
+<footer class="site-footer">
+  <p class="docs-footer-notice">
+    Except as otherwise noted, the content of this page is licensed under the
+    <a href="https://creativecommons.org/licenses/by/4.0/">Creative Commons
+    Attribution 4.0 License</a>, and code samples are licensed
+    under the <a href="https://www.apache.org/licenses/LICENSE-2.0">Apache 2.0
+    License</a>. Java is a registered trademark of Oracle and/or its affiliates.
+  </p>
+  <ul>
+    <li><a href="//creativecommons.org/licenses/by/4.0/">Site CC BY 4.0</a></li>
+    <li><a href="//www.google.com/intl/en/policies/privacy/">Privacy</a></li>
+  </ul>
+</footer>
diff --git a/infra/perfetto.dev/src/template_header.html b/infra/perfetto.dev/src/template_header.html
new file mode 100644
index 0000000..45960d9
--- /dev/null
+++ b/infra/perfetto.dev/src/template_header.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <title><%= title %></title>
+  <meta property="og:title" content="<%= title %>">
+  <meta property="og:site_name" content="Perfetto">
+  <meta property="og:type" content="website">
+  <meta property="og:locale" content="en">
+  <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
+  <link rel="canonical" href="https://perfetto.dev<%= fileName.replace('/index.html', '/') %>">
+  <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,500,600,800&display=swap" rel="stylesheet">
+  <link href="https://fonts.googleapis.com/icon?family=Roboto+Mono" rel="stylesheet">
+  <link href="https://fonts.googleapis.com/icon?family=Material+Icons+Round" rel="stylesheet">
+  <link rel="stylesheet" href="/assets/style.css">
+  <link rel="stylesheet" href="/assets/tomorrow-night.css">
+  <link rel="shortcut icon" href="/assets/favicon.png" />
+  <script type="text/javascript" src="/assets/script.js"></script>
+</head>
+<body>
+<header class="site-header">
+  <div class="brand">
+    <a href="/">
+      <img src="/assets/brand.png">
+      <% if (fileName.startsWith('/docs/')) { %>
+      <span class="brand-docs">Docs</span>
+      <% } %>
+    </a>
+  </div>
+  <a href="#toggle" class="menu"><i class="material-icons-round">menu</i></a>
+
+  <a href="/docs/">Docs</a>
+  <a href="/docs/contributing/getting-started#community">Community</a>
+  <a href="https://ui.perfetto.dev/">Trace Viewer</a>
+  <a href="https://github.com/google/perfetto">GitHub</a>
+</header>
diff --git a/infra/perfetto.dev/src/template_index.html b/infra/perfetto.dev/src/template_index.html
new file mode 100644
index 0000000..f580f63
--- /dev/null
+++ b/infra/perfetto.dev/src/template_index.html
@@ -0,0 +1,131 @@
+<%- include('template_header.html'); -%>
+<main class="site-content">
+  <div class="section-wrapper">
+  <section class="banner">
+    <h1><span>System profiling,</span> <span>app tracing</span> <span>and trace analysis</span></h1>
+    <h2>Open-Source &centerdot; Portable &centerdot; Efficient</h2>
+    <img src="/assets/home.png" class="home-img" alt="Perfetto">
+  </section>
+  </div>
+
+  <div class="section-wrapper">
+  <section class="home-highlights">
+    <a href="#profiling"><div class="icon"></div>System Profiling</a>
+    <a href="#tracing"><div class="icon"></div>In-App Tracing</a>
+    <a href="#viewer"><div class="icon"></div>Trace Viewer</a>
+    <a href="#analysis"><div class="icon"></div>Trace Analysis</a>
+  </section>
+  </div>
+
+  <div class="section-wrapper">
+  <section class="home-section" id="profiling">
+    <img src="/assets/sys_profiling.png" alt="Profiling illustration">
+    <div>
+      <h2>System-wide profiling for Linux and Android</h2>
+      <div class="home-item">
+        <i class="material-icons-round">sort</i>
+        <h3>Linux kernel tracing</h3>
+        <p>Capture high frequency ftrace data: scheduling activity, task switching latency, CPU frequency and much more</p>
+      </div>
+      <div class="home-item">
+        <i class="material-icons-round">nfc</i>
+        <h3>Userspace profilers and extra probes</h3>
+        <p>Native heap profiling, Java heap profiling, pollers for /proc stat files</p>
+        </div>
+      <div class="home-item">
+        <i class="material-icons-round">android</i>
+        <h3>Built into Android</h3>
+        <p>Part of the platform since Android 9 Pie, runs on Linux as well</p>
+        </div>
+      <a href="/docs/quickstart/android-tracing" class="button">Get Started</a>
+    </div>
+  </section>
+  </div>
+
+  <div class="section-wrapper">
+  <section class="home-section" id="tracing">
+    <img src="/assets/app_tracing.png" alt="Tracing illustration">
+    <div>
+     <h2>App Tracing</h2>
+      <div class="home-item">
+        <i class="material-icons-round">developer_mode</i>
+        <h3>Efficient trace point instrumentation</h3>
+        <p>Log your C++ app’s activity with high throughput, low overhead trace points</p>
+      </div>
+      <div class="home-item">
+        <i class="material-icons-round">ballot</i>
+        <h3>Structured and configurable events</h3>
+        <p>Define custom protobuf messages to represent strongly-typed app-specific information, trace only what you need</p>
+        </div>
+      <div class="home-item">
+        <i class="material-icons-round">view_compact</i>
+        <h3>Integrated with system-wide tracing</h3>
+        <p>Correlate your app’s state with system-wide profiling data on the same timeline</p>
+        </div>
+      <a href="/docs/instrumentation/tracing-sdk" class="button">Get Started</a>
+    </div>
+  </section>
+  </div>
+
+  <div class="section-wrapper">
+  <section class="home-section" id="viewer">
+    <img src="/assets/ui.png" alt="Trace viewer illustration">
+    <div>
+      <h2>Trace Viewer</h2>
+      <div class="home-item">
+        <i class="material-icons-round">timeline</i>
+        <h3>Interactive trace exploration</h3>
+        <p>Record, view and process trace data with the Perfetto UI</p>
+      </div>
+      <div class="home-item">
+        <i class="material-icons-round">file_copy</i>
+        <h3>Supports popular trace format files</h3>
+        <p>TraceEvent JSON, Android systrace, ftrace text output</p>
+        </div>
+      <div class="home-item">
+        <i class="material-icons-round">offline_bolt</i>
+        <h3>Runs fully in your browser</h3>
+        <p>No server interaction involved, works even if you are offline</p>
+        </div>
+      <div>
+        <a href="//ui.perfetto.dev" class="button">Open the UI</a>
+      </div>
+    </div>
+  </section>
+  </div>
+
+  <section class="home-section" id="analysis">
+    <img src="/assets/analysis.png" alt="Trace analysis illustration">
+    <div>
+      <h2>Trace Analysis</h2>
+      <div class="home-item">
+        <i class="material-icons-round">storage</i>
+        <h3>SQL-based trace model</h3>
+        <p>Trace processor ingests traces and exposes a SQLite-based
+           interface to access the contents of the trace, both via shell and UI
+        </p>
+      </div>
+      <div class="home-item">
+        <i class="material-icons-round">speed</i>
+        <h3>Large trace analysis</h3>
+        <p>
+          Supports traces up to tenths of GBs
+          </p>
+        </div>
+      <div class="home-item">
+        <i class="material-icons-round">compare_arrows</i>
+        <h3>Interoperable</h3>
+        <p>Can import and export popular trace formats: Chromium JSON trace format, Android Systrace, ftrace, CSV</p>
+        </div>
+      <div>
+        <a href="/docs/quickstart/trace-analysis" class="button">Learn more</a>
+      </div>
+    </div>
+  </section>
+
+  <%- include('template_footer.html'); -%>
+</main>
+
+</body>
+</html>
+
diff --git a/infra/perfetto.dev/src/template_markdown.html b/infra/perfetto.dev/src/template_markdown.html
new file mode 100644
index 0000000..feeaaa5
--- /dev/null
+++ b/infra/perfetto.dev/src/template_markdown.html
@@ -0,0 +1,17 @@
+<%- include('template_header.html'); -%>
+<div class="docs">
+  <nav class="nav">
+    <%- nav %>
+  </nav>
+
+  <main class="doc" data-md-file="<%= fileName %>">
+    <%- markdown %>
+  </main>
+
+  <section class="toc"></section>
+
+  <%- include('template_footer.html'); -%>
+</div>
+
+</body>
+</html>