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);
}
});