Merge "Rename libtraced_shared to libperfetto"
diff --git a/ui/BUILD.gn b/ui/BUILD.gn
index 8783c9f..7d5b696 100644
--- a/ui/BUILD.gn
+++ b/ui/BUILD.gn
@@ -250,18 +250,20 @@
 # | Build css.                                                                 |
 # +----------------------------------------------------------------------------+
 
+scss_root = "src/assets/perfetto.scss"
+scss_srcs = [
+  "src/assets/sidebar.scss",
+  "src/assets/topbar.scss",
+  "src/assets/record.scss",
+  "src/assets/common.scss",
+]
+
 # Build css.
 node_bin("scss") {
   deps = [
     ":dist_symlink",
   ]
-  main_css = "src/assets/perfetto.scss"
-  inputs = [
-    main_css,
-    "src/assets/sidebar.scss",
-    "src/assets/topbar.scss",
-    "src/assets/record.scss",
-  ]
+  inputs = [ scss_root ] + scss_srcs
   outputs = [
     "$ui_dir/perfetto.css",
   ]
@@ -269,7 +271,7 @@
   node_cmd = "node-sass"
   args = [
     "--quiet",
-    rebase_path(main_css, root_build_dir),
+    rebase_path(scss_root, root_build_dir),
     rebase_path(outputs[0], root_build_dir),
   ]
 }
@@ -288,13 +290,10 @@
 
 copy("assets_dist") {
   sources = [
-    "src/assets/flamegraph.svg",
-    "src/assets/logo-3d.png",
-    "src/assets/logo.png",
-    "src/assets/perfetto.scss",
-    "src/assets/sidebar.scss",
-    "src/assets/topbar.scss",
-  ]
+              "src/assets/flamegraph.svg",
+              "src/assets/logo-3d.png",
+              "src/assets/logo.png",
+            ] + [ scss_root ] + scss_srcs
   outputs = [
     "$ui_dir/assets/{{source_file_part}}",
   ]
diff --git a/ui/package.json b/ui/package.json
index 148426d..e5653a9 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -21,7 +21,7 @@
   "devDependencies": {
     "@types/jest": "^22.2.3",
     "@types/puppeteer": "^1.3.4",
-    "dingusjs": "^0.0.2",
+    "dingusjs": "^0.0.3",
     "jest": "^23.1.0",
     "lite-server": "^2.3.0",
     "node-sass": "^4.9.2",
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
new file mode 100644
index 0000000..2ba3830
--- /dev/null
+++ b/ui/src/assets/common.scss
@@ -0,0 +1,386 @@
+// Copyright (C) 2018 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.
+:root {
+    --sidebar-width: 256px;
+    --topbar-height: 48px;
+    --monospace-font: 'Roboto Mono', monospace;
+}
+
+@mixin transition($time:0.1s) {
+    transition: opacity $time ease,
+                background-color $time ease,
+                width $time ease,
+                height $time ease,
+                max-width $time ease,
+                max-height $time ease,
+                margin $time ease,
+                border-radius $time ease;
+}
+
+@mixin material-icon($content) {
+    direction: ltr;
+    display: inline-block;
+    font-family: 'Material Icons';
+    font-size: 24px;
+    font-style: normal;
+    font-weight: normal;
+    letter-spacing: normal;
+    line-height: 1;
+    text-transform: none;
+    white-space: nowrap;
+    word-wrap: normal;
+    -webkit-font-feature-settings: 'liga';
+    -webkit-font-smoothing: antialiased;
+    content: $content;
+}
+
+* {
+    box-sizing: border-box;
+    overflow: hidden;
+    -webkit-tap-highlight-color: none;
+    touch-action: none;
+}
+
+html {
+    font-family: Roboto, verdana, sans-serif;
+    height: 100%;
+    width: 100%;
+}
+
+html,
+body {
+    height: 100%;
+    width: 100%;
+    padding: 0;
+    margin: 0;
+    user-select: none;
+}
+
+h1,
+h2,
+h3 {
+    font-family: initial;
+    font-size: initial;
+    font-weight: initial;
+    padding: 0;
+    margin: 0;
+}
+table {
+    user-select: text;
+}
+
+body {
+    display: grid;
+    grid-template-areas:
+      "sidebar topbar"
+      "sidebar page"
+      "sidebar alerts";
+    grid-template-rows: var(--topbar-height) 1fr auto;
+    grid-template-columns: var(--sidebar-width) auto;
+    color: #121212;
+}
+
+button {
+  background: none;
+  color: inherit;
+  border: none;
+  padding: 0;
+  font: inherit;
+  cursor: pointer;
+  outline: inherit;
+}
+
+button.close {
+  font-family: var(--monospace-font);
+}
+
+.full-page-loading-screen {
+    position: absolute;
+    background: #3e4a5a;
+    width: 100%;
+    height: 100%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    flex-direction: row;
+    background-image: url('assets/logo.png');
+    background-attachment: fixed;
+    background-repeat: no-repeat;
+    background-position: center;
+}
+
+.page {
+    grid-area: page;
+    position: relative;
+}
+
+.alerts {
+  grid-area: alerts;
+  background-color: #262f3c;
+  div {
+    font-family: 'Raleway';
+    font-weight: 400;
+    letter-spacing: 0.25px;
+    color: white;
+    padding: 25px;
+    a {
+      color: white;
+    }
+  }
+}
+
+.home-page {
+    text-align: center;
+    padding-top: 20vh;
+}
+
+.home-page .logo {
+    width: 250px;
+}
+
+.home-page-title {
+    font-size: 60px;
+    margin: 25px;
+    text-align: center;
+    font-family: 'Raleway', sans-serif;
+    font-weight: 100;
+    color: #333;
+}
+
+.query-table {
+    width: 100%;
+    border-collapse: collapse;
+    font-size: 14px;
+    border: 0;
+    thead td {
+        background-color: hsl(214, 22%, 90%);
+        color: #262f3b;
+        text-align: center;
+        padding: 1px 3px;
+        border-style: solid;
+        border-color: #fff;
+        border-right-width: 1px;
+        border-left-width: 1px;
+    }
+    tbody tr {
+        @include transition();
+        background-color: hsl(214, 22%, 100%);
+        font-family: var(--monospace-font);
+        &:nth-child(even) {
+            background-color: hsl(214, 22%, 95%);
+        }
+        td:first-child {
+            padding-left: 5px;
+        }
+        td:last-child {
+            padding-right: 5px;
+        }
+        &:hover {
+            background-color: hsl(214, 22%, 90%);
+        }
+    }
+}
+
+.query-error {
+    padding: 20px 10px;
+    color: hsl(-10, 50%, 50%);
+    font-family: 'Google Sans';
+}
+
+.page header {
+    height: 25px;
+    line-height: 25px;
+    background-color: hsl(213, 22%, 82%);
+    color: hsl(213, 22%, 20%);
+    font-family: 'Google sans';
+    font-size: 15px;
+    font-weight: 400;
+    padding: 0 10px;
+    vertical-align: middle;
+    border-color: hsl(213, 22%, 75%);
+    border-style: solid;
+    border-top-width: 1px;
+    border-bottom-width: 1px;
+    .code {
+        font-family: var(--monospace-font);
+        font-size: 12px;
+        margin-left: 10px;
+        color: hsl(213, 22%, 40%);
+    }
+}
+
+.track {
+    display: grid;
+    grid-template-columns: auto 1fr;
+    grid-template-rows: 1fr;
+    border-top: 1px solid #c7d0db;
+    .track-shell {
+        padding: 0 20px;
+        display: grid;
+        grid-template-areas: "title pin up down";
+        grid-template-columns: 1fr auto auto;
+        align-items: center;
+        width: 300px;
+        background: #fff;
+        border-right: 1px solid #c7d0db;
+        h1 {
+            grid-area: title;
+            margin: 0;
+            font-size: 1em;
+            text-overflow: ellipsis;
+            font-family: 'Google Sans';
+            color: hsl(213, 22%, 30%);
+        }
+        .track-button {
+            margin: 0 5px;
+            color: #495767;
+            cursor: pointer;
+            width: 24px;
+        }
+    }
+}
+
+.scrolling-panel-container {
+  position: relative;
+  overflow-x: hidden;
+  overflow-y: auto;
+  flex: 1 1 auto;
+  will-change: transform;  // Force layer creation.
+}
+
+.pinned-panel-container {
+  position: relative;
+  // Override top level overflow: hidden so height of this flex item can be
+  // its content height.
+  overflow: visible;
+}
+
+// In the scrolling case, since the canvas is overdrawn and continuously
+// repositioned, we need the canvas to be in a div with overflow hidden and
+// height equaling the total height of the content to prevent scrolling
+// height from growing.
+.scroll-limiter {
+  overflow: hidden;
+  position: relative;
+}
+
+canvas.main-canvas {
+  top: 0px;
+  position: absolute;
+}
+
+.panel {
+  position: relative;  // Otherwise canvas covers panel dom.
+}
+
+.pan-and-zoom-content {
+  height: 100%;
+  position: relative;
+  display: flex;
+  flex-flow: column nowrap;
+}
+
+.overview-timeline {
+  height: 100px;
+}
+
+.time-axis-panel {
+  height: 30px;
+}
+
+.flame-graph-panel {
+  height: 500px;
+}
+
+header {
+  height: 25px;
+}
+
+header.overview {
+  display: flex;
+  justify-content: space-between;
+}
+
+.query-error {
+  user-select: text;
+}
+
+span.code {
+  user-select: text;
+}
+
+.debug-panel-border {
+  position: absolute;
+  top: 0px;
+  height: 100%;
+  width: 100%;
+  border: 1px solid rgba(69, 187, 73, 0.5);
+  pointer-events: none;
+}
+
+.perf-stats {
+  --perfetto-orange: hsl(45, 100%, 48%);
+  --perfetto-red: hsl(6, 70%, 53%);
+  --stroke-color: hsl(217, 39%, 94%);
+  position: fixed;
+  bottom: 0;
+  color: var(--stroke-color);
+  font-family: monospace;
+  padding: 2px 0px;
+  z-index: 100;
+  button:hover {
+    color: var(--perfetto-red);
+  }
+  &[expanded=true] {
+    width: 600px;
+    background-color: rgba(27, 28, 29, 0.95);
+    button {
+      color: var(--perfetto-orange);
+      &:hover {
+        color: var(--perfetto-red);
+      }
+    }
+  }
+  &[expanded=false] {
+    width: var(--sidebar-width);
+    background-color: transparent;
+  }
+  i {
+    margin: 0px 24px;
+    font-size: 30px;
+  }
+  .perf-stats-content {
+    margin: 10px 24px;
+    & > section {
+      padding: 5px;
+      border-bottom: 1px solid var(--stroke-color);
+    }
+    button {
+      text-decoration: underline;
+    }
+    div {
+      margin: 2px 0px;
+    }
+    table, td, th {
+      border: 1px solid var(--stroke-color);
+      text-align: center;
+      padding: 4px;
+      margin: 4px 0px;
+    }
+    table {
+      border-collapse: collapse;
+    }
+  }
+}
+
diff --git a/ui/src/assets/perfetto.scss b/ui/src/assets/perfetto.scss
index 8ef1161..cf46528 100644
--- a/ui/src/assets/perfetto.scss
+++ b/ui/src/assets/perfetto.scss
@@ -11,390 +11,8 @@
 // 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.
-:root {
-    --sidebar-width: 256px;
-    --topbar-height: 48px;
-    --monospace-font: 'Roboto Mono', monospace;
-}
 
-@mixin transition($time:0.1s) {
-    transition: opacity $time ease,
-                background-color $time ease,
-                width $time ease,
-                height $time ease,
-                max-width $time ease,
-                max-height $time ease,
-                margin $time ease,
-                border-radius $time ease;
-}
-
-@mixin material-icon($content) {
-    direction: ltr;
-    display: inline-block;
-    font-family: 'Material Icons';
-    font-size: 24px;
-    font-style: normal;
-    font-weight: normal;
-    letter-spacing: normal;
-    line-height: 1;
-    text-transform: none;
-    white-space: nowrap;
-    word-wrap: normal;
-    -webkit-font-feature-settings: 'liga';
-    -webkit-font-smoothing: antialiased;
-    content: $content;
-}
-
-* {
-    box-sizing: border-box;
-    overflow: hidden;
-    -webkit-tap-highlight-color: none;
-    touch-action: none;
-}
-
-html {
-    font-family: Roboto, verdana, sans-serif;
-    height: 100%;
-    width: 100%;
-}
-
-html,
-body {
-    height: 100%;
-    width: 100%;
-    padding: 0;
-    margin: 0;
-    user-select: none;
-}
-
-h1,
-h2,
-h3 {
-    font-family: initial;
-    font-size: initial;
-    font-weight: initial;
-    padding: 0;
-    margin: 0;
-}
-
-table {
-    user-select: text;
-}
-
-body {
-    display: grid;
-    grid-template-areas:
-      "sidebar topbar"
-      "sidebar page"
-      "sidebar alerts";
-    grid-template-rows: var(--topbar-height) 1fr auto;
-    grid-template-columns: var(--sidebar-width) auto;
-    color: #121212;
-}
-
-button {
-  background: none;
-  color: inherit;
-  border: none;
-  padding: 0;
-  font: inherit;
-  cursor: pointer;
-  outline: inherit;
-}
-
-button.close {
-  font-family: var(--monospace-font);
-}
-
-.full-page-loading-screen {
-    position: absolute;
-    background: #3e4a5a;
-    width: 100%;
-    height: 100%;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    flex-direction: row;
-    background-image: url('assets/logo.png');
-    background-attachment: fixed;
-    background-repeat: no-repeat;
-    background-position: center;
-}
-
-.page {
-    grid-area: page;
-    position: relative;
-}
-
-.alerts {
-  grid-area: alerts;
-  background-color: #262f3c;
-  div {
-    font-family: 'Raleway';
-    font-weight: 400;
-    letter-spacing: 0.25px;
-    color: white;
-    padding: 25px;
-    a {
-      color: white;
-    }
-  }
-}
-
+@import 'common';
 @import 'sidebar';
 @import 'topbar';
 @import 'record';
-
-.home-page {
-    text-align: center;
-    padding-top: 20vh;
-}
-
-.home-page .logo {
-    width: 250px;
-}
-
-.home-page-title {
-    font-size: 60px;
-    margin: 25px;
-    text-align: center;
-    font-family: 'Raleway', sans-serif;
-    font-weight: 100;
-    color: #333;
-}
-
-.query-table {
-    width: 100%;
-    border-collapse: collapse;
-    font-size: 14px;
-    border: 0;
-    thead td {
-        background-color: hsl(214, 22%, 90%);
-        color: #262f3b;
-        text-align: center;
-        padding: 1px 3px;
-        border-style: solid;
-        border-color: #fff;
-        border-right-width: 1px;
-        border-left-width: 1px;
-    }
-    tbody tr {
-        @include transition();
-        background-color: hsl(214, 22%, 100%);
-        font-family: var(--monospace-font);
-        &:nth-child(even) {
-            background-color: hsl(214, 22%, 95%);
-        }
-        td:first-child {
-            padding-left: 5px;
-        }
-        td:last-child {
-            padding-right: 5px;
-        }
-        &:hover {
-            background-color: hsl(214, 22%, 90%);
-        }
-    }
-}
-
-.query-error {
-    padding: 20px 10px;
-    color: hsl(-10, 50%, 50%);
-    font-family: 'Google Sans';
-}
-
-.page header {
-    height: 25px;
-    line-height: 25px;
-    background-color: hsl(213, 22%, 82%);
-    color: hsl(213, 22%, 20%);
-    font-family: 'Google sans';
-    font-size: 15px;
-    font-weight: 400;
-    padding: 0 10px;
-    vertical-align: middle;
-    border-color: hsl(213, 22%, 75%);
-    border-style: solid;
-    border-top-width: 1px;
-    border-bottom-width: 1px;
-    .code {
-        font-family: var(--monospace-font);
-        font-size: 12px;
-        margin-left: 10px;
-        color: hsl(213, 22%, 40%);
-    }
-}
-
-.track {
-    display: grid;
-    grid-template-columns: auto 1fr;
-    grid-template-rows: 1fr;
-    border-top: 1px solid #c7d0db;
-    .track-shell {
-        padding: 0 20px;
-        display: grid;
-        grid-template-areas: "title pin up down";
-        grid-template-columns: 1fr auto auto;
-        align-items: center;
-        width: 300px;
-        background: #fff;
-        border-right: 1px solid #c7d0db;
-        h1 {
-            grid-area: title;
-            margin: 0;
-            font-size: 1em;
-            text-overflow: ellipsis;
-            font-family: 'Google Sans';
-            color: hsl(213, 22%, 30%);
-        }
-        .track-button {
-            margin: 0 5px;
-            color: #495767;
-            cursor: pointer;
-            width: 24px;
-        }
-    }
-}
-
-.scrolling-panel-container {
-  position: relative;
-  overflow-x: hidden;
-  overflow-y: auto;
-  flex: 1 1 auto;
-  will-change: transform;  // Force layer creation.
-}
-
-.pinned-panel-container {
-  position: relative;
-  // Override top level overflow: hidden so height of this flex item can be
-  // its content height.
-  overflow: visible;
-}
-
-// In the scrolling case, since the canvas is overdrawn and continuously
-// repositioned, we need the canvas to be in a div with overflow hidden and
-// height equaling the total height of the content to prevent scrolling
-// height from growing.
-.scroll-limiter {
-  overflow: hidden;
-  position: relative;
-}
-
-canvas.main-canvas {
-  top: 0px;
-  position: absolute;
-}
-
-.panel {
-  position: relative;  // Otherwise canvas covers panel dom.
-}
-
-.pan-and-zoom-content {
-  height: 100%;
-  position: relative;
-  display: flex;
-  flex-flow: column nowrap;
-}
-
-.overview-timeline {
-  height: 100px;
-}
-
-.time-axis-panel {
-  height: 30px;
-}
-
-.flame-graph-panel {
-  height: 500px;
-}
-
-header {
-  height: 25px;
-}
-
-header.overview {
-  display: flex;
-  justify-content: space-between;
-}
-
-.query-error {
-  user-select: text;
-}
-
-span.code {
-  user-select: text;
-}
-
-.text-column {
-  font-size: 115%;
-  // 2-3 alphabets per line is comfortable for reading.
-  // https://practicaltypography.com/line-length.html 
-  max-width: calc(26ch*2.34);
-  margin: 3rem auto;
-  user-select: text;
-  word-break: break-word;
-}
-
-.debug-panel-border {
-  position: absolute;
-  top: 0px;
-  height: 100%;
-  width: 100%;
-  border: 1px solid rgba(69, 187, 73, 0.5);
-  pointer-events: none;
-}
-
-.perf-stats {
-  --perfetto-orange: hsl(45, 100%, 48%);
-  --perfetto-red: hsl(6, 70%, 53%);
-  --stroke-color: hsl(217, 39%, 94%);
-  position: fixed;
-  bottom: 0;
-  color: var(--stroke-color);
-  font-family: monospace;
-  padding: 2px 0px;
-  z-index: 100;
-  button:hover {
-    color: var(--perfetto-red);
-  }
-  &[expanded=true] {
-    width: 600px;
-    background-color: rgba(27, 28, 29, 0.95);
-    button {
-      color: var(--perfetto-orange);
-      &:hover {
-        color: var(--perfetto-red);
-      }
-    }
-  }
-  &[expanded=false] {
-    width: var(--sidebar-width);
-    background-color: transparent;
-  }
-  i {
-    margin: 0px 24px;
-    font-size: 30px;
-  }
-  .perf-stats-content {
-    margin: 10px 24px;
-    & > section {
-      padding: 5px;
-      border-bottom: 1px solid var(--stroke-color);
-    }
-    button {
-      text-decoration: underline;
-    }
-    div {
-      margin: 2px 0px;
-    }
-    table, td, th {
-      border: 1px solid var(--stroke-color);
-      text-align: center;
-      padding: 4px;
-      margin: 4px 0px;
-    }
-    table {
-      border-collapse: collapse;
-    }
-  }
-}
diff --git a/ui/src/assets/record.scss b/ui/src/assets/record.scss
index e46456a..cada033 100644
--- a/ui/src/assets/record.scss
+++ b/ui/src/assets/record.scss
@@ -12,39 +12,186 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-.example-code {
-  display: block;
-  padding: 1rem;
-  background-color: black;
-  color: white;
-  margin: 1rem 0;
-  margin-top: calc(20px + 1rem);
-  border-radius: 3px;
-  position: relative;
-  border-top-right-radius: 4px;
-  overflow: initial;
-  user-select: text;
+.record-page {
+  display: grid;
+  grid-template-columns: 1fr 1fr 1fr;
+  grid-template-rows: auto 1fr;
+  grid-column-gap: 2rem;
+  padding: 2rem;
+  overflow-y: scroll;
 
-  ::before {
-    height: 20px;
-    content: "";
+  * {
+    overflow: initial;
+  }
+
+  .control-group {
+    margin-left: 2rem;
+  }
+
+  .text-column {
+    // 2-3 alphabets per line is comfortable for reading.
+    // https://practicaltypography.com/line-length.html
+    max-width: calc(26ch*2.34 + 1rem);
+    user-select: text;
+    word-break: break-word;
+  }
+
+  .example-code {
     display: block;
-    width: 100%;
-    background-color: rgb(87%, 87%, 87%);
-    left: 0;
-    position: absolute;
-    right: 0;
-    top: -18px;
-    border-top-left-radius: 4px;
+    padding: 1rem;
+    background-color: black;
+    color: white;
+    margin: 1rem 0;
+    margin-top: calc(20px + 1rem);
+    border-radius: 3px;
+    position: relative;
     border-top-right-radius: 4px;
+    overflow: initial;
+    user-select: text;
+
+    ::before {
+      height: 20px;
+      content: "";
+      display: block;
+      width: 100%;
+      background-color: rgb(87%, 87%, 87%);
+      left: 0;
+      position: absolute;
+      right: 0;
+      top: -18px;
+      border-top-left-radius: 4px;
+      border-top-right-radius: 4px;
+    }
+
+    button {
+      margin-left: auto;
+      display: block;
+      font-style: italic;
+      font-size: 75%;
+    }
   }
 
-  button {
-    margin-left: auto;
-    display: block;
-    font-style: italic;
-    font-size: 75%;
+
+  label * {
+    overflow: visible;
   }
+
+  input {
+    margin-right: 0.5rem;
+  }
+
+  label {
+    margin: 0.75rem 0;
+    overflow: visible;
+  }
+
+  label.range {
+    display: grid;
+    grid-template-areas: 'title control unit';
+    grid-template-columns: 1fr auto 2rem;
+    align-items: center;
+    .range-control {
+      display: flex;
+      align-items: center;
+
+      button {
+        font-size: smaller;
+        margin-right: 0.5rem;
+        padding: 3px;
+        border-radius: 4px;
+        background-color: #e3f2fd;
+      }
+      button.selected {
+        background-color: #90caf9;
+      }
+      input {
+        text-align: right;
+        font-size: 100%;
+        width: 10ch;
+        padding: 0;
+      }
+    }
+  }
+
+  label.multiselect {
+    display: grid;
+    grid-template-areas: 'label input' 'selected selected';
+    grid-template-columns: 1fr auto;
+    grid-template-rows: auto auto;
+    align-items: center;
+    input {
+      text-align: right;
+      font-size: 100%;
+      padding: 0;
+    }
+    .multiselect-selected {
+      grid-area: selected;
+      button {
+        font-size: smaller;
+        margin-right: 0.5rem;
+        margin-top: 0.5rem;
+        padding: 3px;
+        border-radius: 4px;
+        background-color: #e3f2fd;
+      }
+    }
+  }
+
+  label.checkbox {
+    position: relative;
+    user-select: none;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    input {
+      margin-left: 0px;
+      position: relative;
+      display: block;
+      height: 20px;
+      width: 44px;
+      background: #89898966;
+      border-radius: 100px;
+      cursor: pointer;
+      transition: all 0.3s ease;
+      -moz-appearance: none;
+      -webkit-appearance: none;
+
+      &:focus {
+        outline: none;
+      }
+
+      &::after {
+        position: absolute;
+        left: -2px;
+        top: -3px;
+        display: block;
+        width: 26px;
+        height: 26px;
+        border-radius: 100px;
+        background: #f5f5f5;
+        box-shadow: 0px 3px 3px rgba(0,0,0,0.15);
+        content: '';
+        transition: all 0.3s ease;
+      }
+      &:checked {
+        background: #8398b7;
+      }
+      &:checked::after {
+        left: 20px;
+        background: #27303d;
+      }
+    }
+
+    &.disabled input {
+      opacity: 0;
+    }
+  }
+
+  label.disabled {
+    color: grey;
+    filter: grayscale(1);
+  }
+
 }
 
-
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 5ae4598..70e75a9 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -177,8 +177,35 @@
     // so it appears on the proxy Actions class.
     throw new Error('Called setState on StateActions.');
   },
-};
 
+  // TODO(hjd): Parametrize this to increase type safety. See comments on
+  // aosp/778194
+  setConfigControl(
+      state: StateDraft,
+      args: {name: string; value: string | number | boolean;}): void {
+    const config = state.recordConfig;
+    config[args.name] = args.value;
+  },
+
+  addConfigControl(state: StateDraft, args: {name: string; option: string;}):
+      void {
+        // tslint:disable-next-line no-any
+        const config = state.recordConfig as any;
+        const options = config[args.name];
+        if (options.includes(args.option)) return;
+        options.push(args.option);
+      },
+
+  removeConfigControl(state: StateDraft, args: {name: string; option: string;}):
+      void {
+        // tslint:disable-next-line no-any
+        const config = state.recordConfig as any;
+        const options = config[args.name];
+        const index = options.indexOf(args.option);
+        if (index === -1) return;
+        options.splice(index, 1);
+      },
+};
 
 // When we are on the frontend side, we don't really want to execute the
 // actions above, we just want to serialize them and marshal their
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index 291a865..26af15f 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -51,6 +51,18 @@
   hash?: string;       // Set by the controller when the link has been created.
 }
 
+export interface RecordConfig {
+  [key: string]: number|boolean|string|string[];
+  durationSeconds: number;
+  bufferSizeMb: number;
+  processMetadata: boolean;
+  scanAllProcessesOnStart: boolean;
+  ftrace: boolean;
+  ftraceEvents: string[];
+  atraceCategories: string[];
+  atraceApps: string[];
+}
+
 export interface TraceTime {
   startSec: number;
   endSec: number;
@@ -67,6 +79,11 @@
   nextId: number;
 
   /**
+   * State of the ConfigEditor.
+   */
+  recordConfig: RecordConfig;
+
+  /**
    * Open traces.
    */
   engines: ObjectById<EngineConfig>;
@@ -98,6 +115,20 @@
     scrollingTracks: [],
     queries: {},
     permalink: {},
+    recordConfig: createEmptyRecordConfig(),
     status: {msg: '', timestamp: 0},
   };
 }
+
+export function createEmptyRecordConfig(): RecordConfig {
+  return {
+    durationSeconds: 10.0,
+    bufferSizeMb: 10.0,
+    processMetadata: false,
+    scanAllProcessesOnStart: false,
+    ftrace: false,
+    ftraceEvents: [],
+    atraceApps: [],
+    atraceCategories: [],
+  };
+}
diff --git a/ui/src/controller/app_controller.ts b/ui/src/controller/app_controller.ts
index 3a83f18..cf8e6f0 100644
--- a/ui/src/controller/app_controller.ts
+++ b/ui/src/controller/app_controller.ts
@@ -13,8 +13,10 @@
 // limitations under the License.
 
 import {globals} from '../controller/globals';
+
 import {Child, Controller, ControllerInitializerAny} from './controller';
 import {PermalinkController} from './permalink_controller';
+import {RecordController} from './record_controller';
 import {TraceController} from './trace_controller';
 
 // The root controller for the entire app. It handles the lifetime of all
@@ -33,6 +35,7 @@
   run() {
     const childControllers: ControllerInitializerAny[] = [
       Child('permalink', PermalinkController, {}),
+      Child('record', RecordController, {app: globals}),
     ];
     for (const engineCfg of Object.values(globals.state.engines)) {
       childControllers.push(Child(engineCfg.id, TraceController, engineCfg.id));
diff --git a/ui/src/controller/controller.ts b/ui/src/controller/controller.ts
index 7177e32..7249adb 100644
--- a/ui/src/controller/controller.ts
+++ b/ui/src/controller/controller.ts
@@ -113,4 +113,4 @@
   get state(): StateType {
     return this._state;
   }
-}
\ No newline at end of file
+}
diff --git a/ui/src/controller/globals.ts b/ui/src/controller/globals.ts
index c46ffbf..8201edc 100644
--- a/ui/src/controller/globals.ts
+++ b/ui/src/controller/globals.ts
@@ -27,10 +27,18 @@
   destroyWasmEngine,
 } from './wasm_engine_proxy';
 
+
+export interface App {
+  state: State;
+  dispatch(action: DeferredAction): void;
+  publish(what: 'OverviewData'|'TrackData'|'Threads'|'QueryResult', data: {}):
+      void;
+}
+
 /**
  * Global accessors for state/dispatch in the controller.
  */
-class Globals {
+class Globals implements App {
   private _state?: State;
   private _rootController?: ControllerAny;
   private _frontend?: Remote;
diff --git a/ui/src/controller/record_controller.ts b/ui/src/controller/record_controller.ts
new file mode 100644
index 0000000..616ab37
--- /dev/null
+++ b/ui/src/controller/record_controller.ts
@@ -0,0 +1,127 @@
+// Copyright (C) 2018 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 {TraceConfig} from '../common/protos';
+import {RecordConfig} from '../common/state';
+import {Controller} from './controller';
+import {App} from './globals';
+
+export function uint8ArrayToBase64(buffer: Uint8Array): string {
+  return btoa(String.fromCharCode.apply(null, buffer));
+}
+
+export function encodeConfig(config: RecordConfig): Uint8Array {
+  const sizeKb = config.bufferSizeMb * 1024;
+  const durationMs = config.durationSeconds * 1000;
+
+  const dataSources = [];
+  if (config.ftrace) {
+    dataSources.push({
+      config: {
+        name: 'linux.ftrace',
+        targetBuffer: 0,
+        ftraceConfig: {
+          ftraceEvents: config.ftraceEvents,
+          atraceApps: config.atraceApps,
+          atraceCategories: config.atraceCategories,
+        },
+      },
+    });
+  }
+
+  if (config.processMetadata) {
+    dataSources.push({
+      config: {
+        name: 'linux.process_stats',
+        processStatsConfig: {
+          scanAllProcessesOnStart: config.scanAllProcessesOnStart,
+        },
+        targetBuffer: 0,
+      },
+    });
+  }
+
+  const buffer = TraceConfig
+                     .encode({
+                       buffers: [
+                         {
+                           sizeKb,
+                         },
+                       ],
+                       dataSources,
+                       durationMs,
+                     })
+                     .finish();
+  return buffer;
+}
+
+export function toPbtxt(configBuffer: Uint8Array): string {
+  const json = TraceConfig.decode(configBuffer).toJSON();
+  function snakeCase(s: string): string {
+    return s.replace(/[A-Z]/g, c => '_' + c.toLowerCase());
+  }
+  function* message(msg: {}, indent: number): IterableIterator<string> {
+    for (const [key, value] of Object.entries(msg)) {
+      const isRepeated = Array.isArray(value);
+      const isNested = typeof value === 'object' && !isRepeated;
+      for (const entry of (isRepeated ? value as Array<{}>: [value])) {
+        yield ' '.repeat(indent) + `${snakeCase(key)}${isNested ? '' : ':'} `;
+        if (typeof entry === 'string') {
+          yield`"${entry}"`;
+        } else if (typeof entry === 'number') {
+          yield entry.toString();
+        } else if (typeof entry === 'boolean') {
+          yield entry.toString();
+        } else {
+          yield '{\n';
+          yield* message(entry, indent + 4);
+          yield ' '.repeat(indent) + '}';
+        }
+        yield '\n';
+      }
+    }
+  }
+  return [...message(json, 0)].join('');
+}
+
+export class RecordController extends Controller<'main'> {
+  private app: App;
+  private config: RecordConfig|null = null;
+
+  constructor(args: {app: App}) {
+    super('main');
+    this.app = args.app;
+  }
+
+  run() {
+    if (this.app.state.recordConfig === this.config) return;
+    this.config = this.app.state.recordConfig;
+    const configProto = encodeConfig(this.config);
+    const configProtoText = toPbtxt(configProto);
+    const commandline = `
+      echo '${uint8ArrayToBase64(configProto)}' |
+      base64 --decode |
+      adb shell "perfetto -c - -o /data/misc/perfetto-traces/trace" &&
+      adb pull /data/misc/perfetto-traces/trace /tmp/trace
+    `;
+    // TODO(hjd): This should not be TrackData after we unify the stores.
+    this.app.publish('TrackData', {
+      id: 'config',
+      data: {
+        commandline,
+        pbtxt: configProtoText,
+      }
+    });
+  }
+}
diff --git a/ui/src/controller/record_controller_jsdomtest.ts b/ui/src/controller/record_controller_jsdomtest.ts
new file mode 100644
index 0000000..cd2a4f6
--- /dev/null
+++ b/ui/src/controller/record_controller_jsdomtest.ts
@@ -0,0 +1,99 @@
+// Copyright (C) 2018 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 {dingus} from 'dingusjs';
+
+import {TraceConfig} from '../common/protos';
+import {createEmptyRecordConfig} from '../common/state';
+
+import {App} from './globals';
+import {
+  encodeConfig,
+  RecordController,
+  toPbtxt,
+  uint8ArrayToBase64
+} from './record_controller';
+
+test('uint8ArrayToBase64', () => {
+  const bytes = [...'Hello, world'].map(c => c.charCodeAt(0));
+  const buffer = new Uint8Array(bytes);
+  expect(uint8ArrayToBase64(buffer)).toEqual('SGVsbG8sIHdvcmxk');
+});
+
+test('encodeConfig', () => {
+  const config = createEmptyRecordConfig();
+  config.durationSeconds = 10;
+  const result = TraceConfig.decode(encodeConfig(config));
+  expect(result.durationMs).toBe(10000);
+});
+
+test('toPbtxt', () => {
+  const config = {
+    durationMs: 1000,
+    buffers: [
+      {
+        sizeKb: 42,
+      },
+    ],
+    dataSources: [{
+      config: {
+        name: 'linux.ftrace',
+        targetBuffer: 1,
+        ftraceConfig: {
+          ftraceEvents: ['sched_switch', 'print'],
+        },
+      },
+    }],
+    producers: [
+      {
+        producerName: 'perfetto.traced_probes',
+      },
+    ],
+  };
+
+  const text = toPbtxt(TraceConfig.encode(config).finish());
+
+  expect(text).toEqual(`buffers: {
+    size_kb: 42
+}
+data_sources: {
+    config {
+        name: "linux.ftrace"
+        target_buffer: 1
+        ftrace_config {
+            ftrace_events: "sched_switch"
+            ftrace_events: "print"
+        }
+    }
+}
+duration_ms: 1000
+producers: {
+    producer_name: "perfetto.traced_probes"
+}
+`);
+});
+
+test('RecordController', () => {
+  const app = dingus<App>('globals');
+  // app.state.recordConfig.durationSeconds = 1000;
+  const controller = new RecordController({app});
+  controller.run();
+  controller.run();
+  controller.run();
+  // tslint:disable-next-line no-any
+  const calls = app.calls.filter((call: any) => call[0] === 'publish()');
+  expect(calls.length).toBe(1);
+  // TODO(hjd): Fix up dingus to have a more sensible API.
+  expect(calls[0][1][0]).toEqual('TrackData');
+});
diff --git a/ui/src/frontend/record_page.ts b/ui/src/frontend/record_page.ts
index 21e295e..7462921 100644
--- a/ui/src/frontend/record_page.ts
+++ b/ui/src/frontend/record_page.ts
@@ -14,37 +14,600 @@
 
 import * as m from 'mithril';
 
+import {Actions} from '../common/actions';
+
 import {copyToClipboard} from './clipboard';
+import {globals} from './globals';
 import {createPage} from './pages';
 
-const RECORD_COMMAND_LINE =
-    'echo CgYIgKAGIAESIwohCgxsaW51eC5mdHJhY2UQAKIGDhIFc2NoZWQSBWlucHV0GJBOMh0KFnBlcmZldHRvLnRyYWNlZF9wcm9iZXMQgCAYBEAASAA= | base64 --decode | adb shell "perfetto -c - -o /data/misc/perfetto-traces/trace" && adb pull /data/misc/perfetto-traces/trace /tmp/trace';
+const CONFIG_PROTO_URL =
+    `https://android.googlesource.com/platform/external/perfetto/+/master/protos/perfetto/config/perfetto_config.proto`;
+
+const FTRACE_EVENTS = [
+  'print',
+  'sched_switch',
+  'cpufreq_interactive_already',
+  'cpufreq_interactive_boost',
+  'cpufreq_interactive_notyet',
+  'cpufreq_interactive_setspeed',
+  'cpufreq_interactive_target',
+  'cpufreq_interactive_unboost',
+  'cpu_frequency',
+  'cpu_frequency_limits',
+  'cpu_idle',
+  'clock_enable',
+  'clock_disable',
+  'clock_set_rate',
+  'sched_wakeup',
+  'sched_blocked_reason',
+  'sched_cpu_hotplug',
+  'sched_waking',
+  'ipi_entry',
+  'ipi_exit',
+  'ipi_raise',
+  'softirq_entry',
+  'softirq_exit',
+  'softirq_raise',
+  'i2c_read',
+  'i2c_write',
+  'i2c_result',
+  'i2c_reply',
+  'smbus_read',
+  'smbus_write',
+  'smbus_result',
+  'smbus_reply',
+  'lowmemory_kill',
+  'irq_handler_entry',
+  'irq_handler_exit',
+  'sync_pt',
+  'sync_timeline',
+  'sync_wait',
+  'ext4_da_write_begin',
+  'ext4_da_write_end',
+  'ext4_sync_file_enter',
+  'ext4_sync_file_exit',
+  'block_rq_issue',
+  'mm_vmscan_direct_reclaim_begin',
+  'mm_vmscan_direct_reclaim_end',
+  'mm_vmscan_kswapd_wake',
+  'mm_vmscan_kswapd_sleep',
+  'binder_transaction',
+  'binder_transaction_received',
+  'binder_set_priority',
+  'binder_lock',
+  'binder_locked',
+  'binder_unlock',
+  'workqueue_activate_work',
+  'workqueue_execute_end',
+  'workqueue_execute_start',
+  'workqueue_queue_work',
+  'regulator_disable',
+  'regulator_disable_complete',
+  'regulator_enable',
+  'regulator_enable_complete',
+  'regulator_enable_delay',
+  'regulator_set_voltage',
+  'regulator_set_voltage_complete',
+  'cgroup_attach_task',
+  'cgroup_mkdir',
+  'cgroup_remount',
+  'cgroup_rmdir',
+  'cgroup_transfer_tasks',
+  'cgroup_destroy_root',
+  'cgroup_release',
+  'cgroup_rename',
+  'cgroup_setup_root',
+  'mdp_cmd_kickoff',
+  'mdp_commit',
+  'mdp_perf_set_ot',
+  'mdp_sspp_change',
+  'tracing_mark_write',
+  'mdp_cmd_pingpong_done',
+  'mdp_compare_bw',
+  'mdp_perf_set_panic_luts',
+  'mdp_sspp_set',
+  'mdp_cmd_readptr_done',
+  'mdp_misr_crc',
+  'mdp_perf_set_qos_luts',
+  'mdp_trace_counter',
+  'mdp_cmd_release_bw',
+  'mdp_mixer_update',
+  'mdp_perf_set_wm_levels',
+  'mdp_video_underrun_done',
+  'mdp_cmd_wait_pingpong',
+  'mdp_perf_prefill_calc',
+  'mdp_perf_update_bus',
+  'rotator_bw_ao_as_context',
+  'mm_filemap_add_to_page_cache',
+  'mm_filemap_delete_from_page_cache',
+  'mm_compaction_begin',
+  'mm_compaction_defer_compaction',
+  'mm_compaction_deferred',
+  'mm_compaction_defer_reset',
+  'mm_compaction_end',
+  'mm_compaction_finished',
+  'mm_compaction_isolate_freepages',
+  'mm_compaction_isolate_migratepages',
+  'mm_compaction_kcompactd_sleep',
+  'mm_compaction_kcompactd_wake',
+  'mm_compaction_migratepages',
+  'mm_compaction_suitable',
+  'mm_compaction_try_to_compact_pages',
+  'mm_compaction_wakeup_kcompactd',
+  'suspend_resume',
+  'sched_wakeup_new',
+  'block_bio_backmerge',
+  'block_bio_bounce',
+  'block_bio_complete',
+  'block_bio_frontmerge',
+  'block_bio_queue',
+  'block_bio_remap',
+  'block_dirty_buffer',
+  'block_getrq',
+  'block_plug',
+  'block_rq_abort',
+  'block_rq_complete',
+  'block_rq_insert',
+  '  removed',
+  'block_rq_remap',
+  'block_rq_requeue',
+  'block_sleeprq',
+  'block_split',
+  'block_touch_buffer',
+  'block_unplug',
+  'ext4_alloc_da_blocks',
+  'ext4_allocate_blocks',
+  'ext4_allocate_inode',
+  'ext4_begin_ordered_truncate',
+  'ext4_collapse_range',
+  'ext4_da_release_space',
+  'ext4_da_reserve_space',
+  'ext4_da_update_reserve_space',
+  'ext4_da_write_pages',
+  'ext4_da_write_pages_extent',
+  'ext4_direct_IO_enter',
+  'ext4_direct_IO_exit',
+  'ext4_discard_blocks',
+  'ext4_discard_preallocations',
+  'ext4_drop_inode',
+  'ext4_es_cache_extent',
+  'ext4_es_find_delayed_extent_range_enter',
+  'ext4_es_find_delayed_extent_range_exit',
+  'ext4_es_insert_extent',
+  'ext4_es_lookup_extent_enter',
+  'ext4_es_lookup_extent_exit',
+  'ext4_es_remove_extent',
+  'ext4_es_shrink',
+  'ext4_es_shrink_count',
+  'ext4_es_shrink_scan_enter',
+  'ext4_es_shrink_scan_exit',
+  'ext4_evict_inode',
+  'ext4_ext_convert_to_initialized_enter',
+  'ext4_ext_convert_to_initialized_fastpath',
+  'ext4_ext_handle_unwritten_extents',
+  'ext4_ext_in_cache',
+  'ext4_ext_load_extent',
+  'ext4_ext_map_blocks_enter',
+  'ext4_ext_map_blocks_exit',
+  'ext4_ext_put_in_cache',
+  'ext4_ext_remove_space',
+  'ext4_ext_remove_space_done',
+  'ext4_ext_rm_idx',
+  'ext4_ext_rm_leaf',
+  'ext4_ext_show_extent',
+  'ext4_fallocate_enter',
+  'ext4_fallocate_exit',
+  'ext4_find_delalloc_range',
+  'ext4_forget',
+  'ext4_free_blocks',
+  'ext4_free_inode',
+  'ext4_get_implied_cluster_alloc_exit',
+  'ext4_get_reserved_cluster_alloc',
+  'ext4_ind_map_blocks_enter',
+  'ext4_ind_map_blocks_exit',
+  'ext4_insert_range',
+  'ext4_invalidatepage',
+  'ext4_journal_start',
+  'ext4_journal_start_reserved',
+  'ext4_journalled_invalidatepage',
+  'ext4_journalled_write_end',
+  'ext4_load_inode',
+  'ext4_load_inode_bitmap',
+  'ext4_mark_inode_dirty',
+  'ext4_mb_bitmap_load',
+  'ext4_mb_buddy_bitmap_load',
+  'ext4_mb_discard_preallocations',
+  'ext4_mb_new_group_pa',
+  'ext4_mb_new_inode_pa',
+  'ext4_mb_release_group_pa',
+  'ext4_mb_release_inode_pa',
+  'ext4_mballoc_alloc',
+  'ext4_mballoc_discard',
+  'ext4_mballoc_free',
+  'ext4_mballoc_prealloc',
+  'ext4_other_inode_update_time',
+  'ext4_punch_hole',
+  'ext4_read_block_bitmap_load',
+  'ext4_readpage',
+  'ext4_releasepage',
+  'ext4_remove_blocks',
+  'ext4_request_blocks',
+  'ext4_request_inode',
+  'ext4_sync_fs',
+  'ext4_trim_all_free',
+  'ext4_trim_extent',
+  'ext4_truncate_enter',
+  'ext4_truncate_exit',
+  'ext4_unlink_enter',
+  'ext4_unlink_exit',
+  'ext4_write_begin',
+  'ext4_write_end',
+  'ext4_writepage',
+  'ext4_writepages',
+  'ext4_writepages_result',
+  'ext4_zero_range',
+  'task_newtask',
+  'task_rename',
+  'sched_process_exec',
+  'sched_process_exit',
+  'sched_process_fork',
+  'sched_process_free',
+  'sched_process_hang',
+  'sched_process_wait',
+  'f2fs_do_submit_bio',
+  'f2fs_evict_inode',
+  'f2fs_fallocate',
+  'f2fs_get_data_block',
+  'f2fs_get_victim',
+  'f2fs_iget',
+  'f2fs_iget_exit',
+  'f2fs_new_inode',
+  'f2fs_readpage',
+  'f2fs_reserve_new_block',
+  'f2fs_set_page_dirty',
+  'f2fs_submit_write_page',
+  'f2fs_sync_file_enter',
+  'f2fs_sync_file_exit',
+  'f2fs_sync_fs',
+  'f2fs_truncate',
+  'f2fs_truncate_blocks_enter',
+  'f2fs_truncate_blocks_exit',
+  'f2fs_truncate_data_blocks_range',
+  'f2fs_truncate_inode_blocks_enter',
+  'f2fs_truncate_inode_blocks_exit',
+  'f2fs_truncate_node',
+  'f2fs_truncate_nodes_enter',
+  'f2fs_truncate_nodes_exit',
+  'f2fs_truncate_partial_nodes',
+  'f2fs_unlink_enter',
+  'f2fs_unlink_exit',
+  'f2fs_vm_page_mkwrite',
+  'f2fs_write_begin',
+  'f2fs_write_checkpoint',
+  'f2fs_write_end',
+];
+
+const ATRACE_CATERGORIES = [
+  'gfx',         'input',     'view',       'webview',    'wm',
+  'am',          'sm',        'audio',      'video',      'camera',
+  'hal',         'res',       'dalvik',     'rs',         'bionic',
+  'power',       'pm',        'ss',         'database',   'network',
+  'adb',         'vibrator',  'aidl',       'nnapi',      'sched',
+  'irq',         'irqoff',    'preemptoff', 'i2c',        'freq',
+  'membus',      'idle',      'disk',       'mmc',        'load',
+  'sync',        'workq',     'memreclaim', 'regulators', 'binder_driver',
+  'binder_lock', 'pagecache',
+];
+
+const ATRACE_APPS = [
+  'com.android.chrome',
+  'com.android.bluetooth',
+  'com.android.chrome',
+  'com.android.nfc',
+  'com.android.phone',
+  'com.android.settings',
+  'com.android.systemui',
+  'com.android.vending',
+  'com.google.android.apps.messaging',
+  'com.google.android.apps.nexuslauncher',
+  'com.google.android.connectivitymonitor',
+  'com.google.android.contacts',
+  'com.google.android.gms',
+  'com.google.android.gms.learning',
+  'com.google.android.gms.persistent',
+  'com.google.android.gms.unstable',
+  'com.google.android.googlequicksearchbox',
+  'com.google.android.setupwizard',
+  'com.google.android.volta',
+];
+
+const DURATION_HELP = `Duration to trace for.`;
+const BUFFER_SIZE_HELP = `Size of the ring buffer which stores the trace.`;
+const PROCESS_METADATA_HELP =
+    `Record process names and parent child relationships.`;
+const SCAN_ALL_PROCESSES_ON_START_HELP =
+    `When tracing begins read metadata for all processes.`;
+
+function toId(label: string): string {
+  return label.toLowerCase().replace(' ', '-');
+}
 
 interface CodeSampleAttrs {
   text: string;
+  hardWhitespace?: boolean;
 }
 
 class CodeSample implements m.ClassComponent<CodeSampleAttrs> {
   view({attrs}: m.CVnode<CodeSampleAttrs>) {
     return m(
         '.example-code',
-        m('code', attrs.text),
+        m('code',
+          {
+            style: {
+              'white-space': attrs.hardWhitespace ? 'pre' : null,
+            },
+          },
+          attrs.text),
         m('button',
           {
             onclick: () => copyToClipboard(attrs.text),
           },
-          'Copy to clipboard'), );
+          'Copy to clipboard'));
+  }
+}
+
+interface ToggleAttrs {
+  label: string;
+  value: boolean;
+  help: string;
+  enabled: boolean;
+  onchange: (v: boolean) => void;
+}
+
+class Toggle implements m.ClassComponent<ToggleAttrs> {
+  view({attrs}: m.CVnode<ToggleAttrs>) {
+    return m(
+        'label.checkbox',
+        {
+          title: attrs.help,
+          class: attrs.enabled ? '' : 'disabled',
+
+        },
+        attrs.label,
+        m('input[type="checkbox"]', {
+          onchange: m.withAttr('checked', attrs.onchange),
+          disabled: !attrs.enabled,
+          checked: attrs.value,
+        }));
+  }
+}
+
+interface MultiSelectAttrs {
+  enabled: boolean;
+  label: string;
+  selected: string[];
+  options: string[];
+  onadd: (value: string) => void;
+  onsubtract: (value: string) => void;
+}
+
+class MultiSelect implements m.ClassComponent<MultiSelectAttrs> {
+  view({attrs}: m.CVnode<MultiSelectAttrs>) {
+    return m(
+        'label.multiselect',
+        {class: attrs.enabled ? '' : 'disabled'},
+        attrs.label,
+        m('input', {
+          list: toId(attrs.label),
+          disabled: !attrs.enabled,
+          onchange: (e: Event) => {
+            const elem = e.target as HTMLInputElement;
+            attrs.onadd(elem.value);
+            elem.value = '';
+          },
+        }),
+        m('datalist',
+          {
+            id: toId(attrs.label),
+          },
+          attrs.options.filter(option => !attrs.selected.includes(option))
+              .map(value => m('option', {value}))),
+        m('.multiselect-selected',
+          attrs.selected.map(
+              selected =>
+                  m('button.multiselect-selected',
+                    {
+                      onclick: (_: Event) => attrs.onsubtract(selected),
+                    },
+                    selected))), );
+  }
+}
+
+interface Preset {
+  label: string;
+  value: number;
+}
+
+interface NumericAttrs {
+  label: string;
+  sublabel: string;
+  help: string;
+  value: number;
+  onchange: (value: number) => void;
+  presets: Preset[];
+}
+
+class Numeric implements m.ClassComponent<NumericAttrs> {
+  view({attrs}: m.CVnode<NumericAttrs>) {
+    return m(
+        'label.range',
+        {
+          'for': `range-${attrs.label}`,
+          'title': attrs.help,
+        },
+        attrs.label,
+        m('.range-control',
+          attrs.presets.map(
+              p =>
+                  m('button',
+                    {
+                      class: attrs.value === p.value ? 'selected' : '',
+                      onclick: () => attrs.onchange(p.value),
+                    },
+                    p.label)),
+          m('input[type=number][min=0]', {
+            id: `range-${attrs.label}`,
+            value: attrs.value,
+            onchange: m.withAttr('value', attrs.onchange),
+          })),
+        m('small', attrs.sublabel), );
   }
 }
 
 export const RecordPage = createPage({
   view() {
+    const state = globals.state.recordConfig;
+    const data = globals.trackDataStore.get('config') as {
+      commandline: string,
+      pbtxt: string,
+    } | null;
     return m(
-        '.text-column',
-        'To collect a 10 second Perfetto trace from an Android phone run this',
-        ' command:',
-        m(CodeSample, {text: RECORD_COMMAND_LINE}),
-        'Then click "Open trace file" in the menu to the left and select',
-        ' "/tmp/trace".');
+        '.record-page',
+
+        m('.text-column', ),
+        m('.text-column', `To collect a ${state.durationSeconds}
+          second Perfetto trace from an Android phone run this command:`),
+        m('.text-column',
+          `A Perfetto config controls what and how much information is
+        collected. It is encoded as a `,
+          m('a',
+            {
+              href: CONFIG_PROTO_URL,
+            },
+            'proto'),
+          '.'),
+
+        m('.text-column',
+          m(Numeric, {
+            label: 'Duration',
+            sublabel: 's',
+            value: state.durationSeconds,
+            help: DURATION_HELP,
+            onchange: (value: number) => {
+              globals.dispatch(
+                  Actions.setConfigControl({name: 'durationSeconds', value}));
+            },
+            presets: [
+              {label: '10s', value: 10},
+              {label: '1m', value: 60},
+            ]
+          }),
+
+          m(Numeric, {
+            label: 'Buffer size',
+            sublabel: 'mb',
+            help: BUFFER_SIZE_HELP,
+            value: state.bufferSizeMb,
+            onchange: (value: number) => {
+              globals.dispatch(
+                  Actions.setConfigControl({name: 'bufferSizeMb', value}));
+            },
+            presets: [
+              {label: '1mb', value: 1},
+              {label: '10mb', value: 10},
+              {label: '20mb', value: 20},
+            ]
+          }),
+
+          m(Toggle, {
+            label: 'Process Metadata',
+            help: PROCESS_METADATA_HELP,
+            value: state.processMetadata,
+            enabled: true,
+            onchange: (value: boolean) => {
+              globals.dispatch(
+                  Actions.setConfigControl({name: 'processMetadata', value}));
+            },
+          }),
+          m('.control-group', m(Toggle, {
+              label: 'Scan all processes on start',
+              value: state.scanAllProcessesOnStart,
+              help: SCAN_ALL_PROCESSES_ON_START_HELP,
+              enabled: state.processMetadata,
+              onchange: (value: boolean) => {
+                globals.dispatch(Actions.setConfigControl(
+                    {name: 'scanAllProcessesOnStart', value}));
+              },
+            }), ),
+
+          m(Toggle, {
+            label: 'Ftrace & Atrace',
+            value: state.ftrace,
+            enabled: true,
+            help: SCAN_ALL_PROCESSES_ON_START_HELP,
+            onchange: (value: boolean) => {
+              globals.dispatch(
+                  Actions.setConfigControl({name: 'ftrace', value}));
+            },
+          }),
+
+          m('.control-group',
+            m(MultiSelect, {
+              label: 'Ftrace Events',
+              enabled: state.ftrace,
+              selected: state.ftraceEvents,
+              options: FTRACE_EVENTS,
+              onadd: (option: string) => {
+                globals.dispatch(
+                    Actions.addConfigControl({name: 'ftraceEvents', option}));
+              },
+              onsubtract: (option: string) => {
+                globals.dispatch(Actions.removeConfigControl(
+                    {name: 'ftraceEvents', option}));
+              },
+            }),
+
+            m(MultiSelect, {
+              label: 'Atrace Categories',
+              enabled: state.ftrace,
+              selected: state.atraceCategories,
+              options: ATRACE_CATERGORIES,
+              onadd: (option: string) => {
+                globals.dispatch(Actions.addConfigControl(
+                    {name: 'atraceCategories', option}));
+              },
+              onsubtract: (option: string) => {
+                globals.dispatch(Actions.removeConfigControl(
+                    {name: 'atraceCategories', option}));
+              },
+            }),
+
+            m(MultiSelect, {
+              label: 'Atrace Apps',
+              enabled: state.ftrace,
+              selected: state.atraceApps,
+              options: ATRACE_APPS,
+              onadd: (option: string) => {
+                globals.dispatch(
+                    Actions.addConfigControl({name: 'atraceApps', option}));
+              },
+              onsubtract: (option: string) => {
+                globals.dispatch(
+                    Actions.removeConfigControl({name: 'atraceApps', option}));
+              },
+            }), ),
+
+          ),
+
+        data ?
+            [
+              m('.text-column',
+                m(CodeSample, {text: data.commandline}),
+                'Then click "Open trace file" in the menu to the left and select',
+                ' "/tmp/trace".', ),
+              m('.text-column',
+                m(CodeSample, {text: data.pbtxt, hardWhitespace: true}), ),
+            ] :
+            null);
   }
 });