1// Copyright (C) 2018 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import * as m from 'mithril'; 16 17import {Actions} from '../common/actions'; 18 19import {globals} from './globals'; 20import { 21 isLegacyTrace, 22 openFileWithLegacyTraceViewer, 23} from './legacy_trace_viewer'; 24 25const ALL_PROCESSES_QUERY = 'select name, pid from process order by name;'; 26 27const CPU_TIME_FOR_PROCESSES = ` 28select 29 process.name, 30 tot_proc/1e9 as cpu_sec 31from 32 (select 33 upid, 34 sum(tot_thd) as tot_proc 35 from 36 (select 37 utid, 38 sum(dur) as tot_thd 39 from sched group by utid) 40 join thread using(utid) group by upid) 41join process using(upid) 42order by cpu_sec desc limit 100;`; 43 44const CYCLES_PER_P_STATE_PER_CPU = ` 45select 46 cpu, 47 freq, 48 dur, 49 sum(dur * freq)/1e6 as mcycles 50from ( 51 select 52 ref as cpu, 53 value as freq, 54 lead(ts) over (partition by ref order by ts) - ts as dur 55 from counters 56 where name = 'cpufreq' 57) group by cpu, freq 58order by mcycles desc limit 32;`; 59 60const CPU_TIME_BY_CLUSTER_BY_PROCESS = ` 61select process.name as process, thread, core, cpu_sec from ( 62 select thread.name as thread, upid, 63 case when cpug = 0 then 'little' else 'big' end as core, 64 cpu_sec from (select cpu/4 as cpug, utid, sum(dur)/1e9 as cpu_sec 65 from sched group by utid, cpug order by cpu_sec desc 66 ) inner join thread using(utid) 67) inner join process using(upid) limit 30;`; 68 69 70const SQL_STATS = ` 71with first as (select started as ts from sqlstats limit 1) 72select query, 73 round((max(ended - started, 0))/1e6) as runtime_ms, 74 round((max(started - queued, 0))/1e6) as latency_ms, 75 round((started - first.ts)/1e6) as t_start_ms 76from sqlstats, first 77order by started desc`; 78 79const TRACE_STATS = 'select * from stats order by severity, source, name, idx'; 80 81function createCannedQuery(query: string): (_: Event) => void { 82 return (e: Event) => { 83 e.preventDefault(); 84 globals.dispatch(Actions.executeQuery({ 85 engineId: '0', 86 queryId: 'command', 87 query, 88 })); 89 }; 90} 91 92const EXAMPLE_ANDROID_TRACE_URL = 93 'https://storage.googleapis.com/perfetto-misc/example_android_trace_30s_1'; 94 95const EXAMPLE_CHROME_TRACE_URL = 96 'https://storage.googleapis.com/perfetto-misc/example_chrome_trace_4s_1.json'; 97 98const SECTIONS = [ 99 { 100 title: 'Navigation', 101 summary: 'Open or record a new trace', 102 expanded: true, 103 items: [ 104 {t: 'Open trace file', a: popupFileSelectionDialog, i: 'folder_open'}, 105 { 106 t: 'Open with legacy UI', 107 a: popupFileSelectionDialogOldUI, 108 i: 'folder_open' 109 }, 110 {t: 'Record new trace', a: navigateRecord, i: 'fiber_smart_record'}, 111 {t: 'Show timeline', a: navigateViewer, i: 'line_style'}, 112 {t: 'Share current trace', a: dispatchCreatePermalink, i: 'share'}, 113 {t: 'Download current trace', a: downloadTrace, i: 'file_download'}, 114 ], 115 }, 116 { 117 title: 'Example Traces', 118 expanded: true, 119 summary: 'Open an example trace', 120 items: [ 121 { 122 t: 'Open Android example', 123 a: openTraceUrl(EXAMPLE_ANDROID_TRACE_URL), 124 i: 'description' 125 }, 126 { 127 t: 'Open Chrome example', 128 a: openTraceUrl(EXAMPLE_CHROME_TRACE_URL), 129 i: 'description' 130 }, 131 ], 132 }, 133 { 134 title: 'Metrics and auditors', 135 summary: 'Compute summary statistics', 136 items: [ 137 { 138 t: 'All Processes', 139 a: createCannedQuery(ALL_PROCESSES_QUERY), 140 i: 'search', 141 }, 142 { 143 t: 'CPU Time by process', 144 a: createCannedQuery(CPU_TIME_FOR_PROCESSES), 145 i: 'search', 146 }, 147 { 148 t: 'Cycles by p-state by CPU', 149 a: createCannedQuery(CYCLES_PER_P_STATE_PER_CPU), 150 i: 'search', 151 }, 152 { 153 t: 'CPU Time by cluster by process', 154 a: createCannedQuery(CPU_TIME_BY_CLUSTER_BY_PROCESS), 155 i: 'search', 156 }, 157 { 158 t: 'Trace stats', 159 a: createCannedQuery(TRACE_STATS), 160 i: 'bug_report', 161 }, 162 { 163 t: 'Debug SQL performance', 164 a: createCannedQuery(SQL_STATS), 165 i: 'bug_report', 166 }, 167 ], 168 }, 169 { 170 title: 'Support', 171 summary: 'Documentation & Bugs', 172 items: [ 173 { 174 t: 'Documentation', 175 a: 'https://perfetto.dev', 176 i: 'help', 177 }, 178 { 179 t: 'Report a bug', 180 a: 'https://goto.google.com/perfetto-ui-bug', 181 i: 'bug_report', 182 }, 183 ], 184 }, 185 186]; 187 188function getFileElement(): HTMLInputElement { 189 return document.querySelector('input[type=file]')! as HTMLInputElement; 190} 191 192function popupFileSelectionDialog(e: Event) { 193 e.preventDefault(); 194 delete getFileElement().dataset['useCatapultLegacyUi']; 195 getFileElement().click(); 196} 197 198function popupFileSelectionDialogOldUI(e: Event) { 199 e.preventDefault(); 200 getFileElement().dataset['useCatapultLegacyUi'] = '1'; 201 getFileElement().click(); 202} 203 204function openTraceUrl(url: string): (e: Event) => void { 205 return e => { 206 e.preventDefault(); 207 globals.dispatch(Actions.openTraceFromUrl({url})); 208 }; 209} 210 211function onInputElementFileSelectionChanged(e: Event) { 212 if (!(e.target instanceof HTMLInputElement)) { 213 throw new Error('Not an input element'); 214 } 215 if (!e.target.files) return; 216 const file = e.target.files[0]; 217 218 if (e.target.dataset['useCatapultLegacyUi'] === '1') { 219 // Switch back the old catapult UI. 220 if (isLegacyTrace(file.name)) { 221 openFileWithLegacyTraceViewer(file); 222 } else { 223 globals.dispatch(Actions.convertTraceToJson({file})); 224 } 225 return; 226 } 227 228 // Open with the current UI. 229 globals.dispatch(Actions.openTraceFromFile({file})); 230} 231 232function navigateRecord(e: Event) { 233 e.preventDefault(); 234 globals.dispatch(Actions.navigate({route: '/record'})); 235} 236 237function navigateViewer(e: Event) { 238 e.preventDefault(); 239 globals.dispatch(Actions.navigate({route: '/viewer'})); 240} 241 242function dispatchCreatePermalink(e: Event) { 243 e.preventDefault(); 244 globals.dispatch(Actions.createPermalink({})); 245} 246 247function downloadTrace(e: Event) { 248 e.preventDefault(); 249 const engine = Object.values(globals.state.engines)[0]; 250 if (!engine) return; 251 const src = engine.source; 252 if (typeof src === 'string') { 253 window.open(src); 254 } else { 255 const url = URL.createObjectURL(src); 256 const a = document.createElement('a'); 257 a.href = url; 258 a.download = src.name; 259 document.body.appendChild(a); 260 a.click(); 261 document.body.removeChild(a); 262 URL.revokeObjectURL(url); 263 } 264} 265 266export class Sidebar implements m.ClassComponent { 267 view() { 268 const vdomSections = []; 269 for (const section of SECTIONS) { 270 const vdomItems = []; 271 for (const item of section.items) { 272 vdomItems.push( 273 m('li', 274 m(`a`, 275 { 276 onclick: typeof item.a === 'function' ? item.a : null, 277 href: typeof item.a === 'string' ? item.a : '#', 278 }, 279 m('i.material-icons', item.i), 280 item.t))); 281 } 282 vdomSections.push( 283 m(`section${section.expanded ? '.expanded' : ''}`, 284 m('.section-header', 285 { 286 onclick: () => { 287 section.expanded = !section.expanded; 288 globals.rafScheduler.scheduleFullRedraw(); 289 } 290 }, 291 m('h1', section.title), 292 m('h2', section.summary), ), 293 m('.section-content', m('ul', vdomItems)))); 294 } 295 return m( 296 'nav.sidebar', 297 m('header', 'Perfetto'), 298 m('input[type=file]', {onchange: onInputElementFileSelectionChanged}), 299 ...vdomSections); 300 } 301} 302