1<!-- Copyright (C) 2019 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--> 15<template> 16 <flat-card style="min-width: 50em"> 17 <md-card-header> 18 <div class="md-title">ADB Connect</div> 19 </md-card-header> 20 <md-card-content v-if="status === STATES.CONNECTING"> 21 <md-progress-spinner md-indeterminate></md-progress-spinner> 22 </md-card-content> 23 <md-card-content v-if="status === STATES.NO_PROXY"> 24 <md-icon class="md-accent">error</md-icon> 25 <span class="md-subheading">Unable to connect to Winscope ADB proxy</span> 26 <div class="md-body-2"> 27 <p>Launch the Winscope ADB Connect proxy to capture traces directly from your browser.</p> 28 <p>Python 3.5+ and ADB is required.</p> 29 <p>Run:</p> 30 <pre>python3 $ANDROID_BUILD_TOP/development/tools/winscope/adb_proxy/winscope_proxy.py</pre> 31 <p>Or get it from the AOSP repository.</p> 32 </div> 33 <div class="md-layout"> 34 <md-button class="md-accent" :href="downloadProxyUrl" @click="buttonClicked(`Download from AOSP`)">Download from AOSP</md-button> 35 <md-button class="md-accent" @click="restart">Retry</md-button> 36 </div> 37 </md-card-content> 38 <md-card-content v-if="status === STATES.INVALID_VERSION"> 39 <md-icon class="md-accent">update</md-icon> 40 <span class="md-subheading">The version of Winscope ADB Connect proxy running on your machine is incopatibile with Winscope.</span> 41 <div class="md-body-2"> 42 <p>Please update the proxy to version {{ proxyClient.VERSION }}</p> 43 <p>Run:</p> 44 <pre>python3 $ANDROID_BUILD_TOP/development/tools/winscope/adb_proxy/winscope_proxy.py</pre> 45 <p>Or get it from the AOSP repository.</p> 46 </div> 47 <div class="md-layout"> 48 <md-button class="md-accent" :href="downloadProxyUrl">Download from AOSP</md-button> 49 <md-button class="md-accent" @click="restart">Retry</md-button> 50 </div> 51 </md-card-content> 52 <md-card-content v-if="status === STATES.UNAUTH"> 53 <md-icon class="md-accent">lock</md-icon> 54 <span class="md-subheading">Proxy authorisation required</span> 55 <md-field> 56 <label>Enter Winscope proxy token</label> 57 <md-input v-model="proxyClient.store.proxyKey"></md-input> 58 </md-field> 59 <div class="md-body-2">The proxy token is printed to console on proxy launch, copy and paste it above.</div> 60 <div class="md-layout"> 61 <md-button class="md-primary" @click="restart">Connect</md-button> 62 </div> 63 </md-card-content> 64 <md-card-content v-if="status === STATES.DEVICES"> 65 <div class="md-subheading">{{ Object.keys(proxyClient.devices).length > 0 ? "Connected devices:" : "No devices detected" }}</div> 66 <md-list> 67 <md-list-item v-for="(device, id) in proxyClient.devices" :key="id" @click="proxyClient.selectDevice(id)" :disabled="!device.authorised"> 68 <md-icon>{{ device.authorised ? "smartphone" : "screen_lock_portrait" }}</md-icon> 69 <span class="md-list-item-text">{{ device.authorised ? device.model : "unauthorised" }} ({{ id }})</span> 70 </md-list-item> 71 </md-list> 72 <md-progress-spinner :md-size="30" md-indeterminate></md-progress-spinner> 73 </md-card-content> 74 <md-card-content v-if="status === STATES.START_TRACE"> 75 <div class="device-choice"> 76 <md-list> 77 <md-list-item> 78 <md-icon>smartphone</md-icon> 79 <span class="md-list-item-text">{{ proxyClient.devices[proxyClient.selectedDevice].model }} ({{ proxyClient.selectedDevice }})</span> 80 </md-list-item> 81 </md-list> 82 <md-button class="md-primary" @click="resetLastDevice">Change device</md-button> 83 </div> 84 <div class="trace-section"> 85 <h3>Trace targets:</h3> 86 <div class="selection"> 87 <md-checkbox class="md-primary" v-for="traceKey in Object.keys(DYNAMIC_TRACES)" :key="traceKey" v-model="traceStore[traceKey]">{{ DYNAMIC_TRACES[traceKey].name }}</md-checkbox> 88 </div> 89 <div class="trace-config"> 90 <h4>Surface Flinger config</h4> 91 <div class="selection"> 92 <md-checkbox class="md-primary" v-for="config in TRACE_CONFIG['layers_trace']" :key="config" v-model="traceStore[config]">{{config}}</md-checkbox> 93 <div class="selection"> 94 <md-field class="config-selection" v-for="selectConfig in Object.keys(SF_SELECTED_CONFIG)" :key="selectConfig"> 95 <md-select v-model="SF_SELECTED_CONFIG_VALUES[selectConfig]" :placeholder="selectConfig"> 96 <md-option value="">{{selectConfig}}</md-option> 97 <md-option v-for="option in SF_SELECTED_CONFIG[selectConfig]" :key="option" :value="option">{{ option }}</md-option> 98 </md-select> 99 </md-field> 100 </div> 101 </div> 102 </div> 103 <div class="trace-config"> 104 <h4>Window Manager config</h4> 105 <div class="selection"> 106 <md-field class="config-selection" v-for="selectConfig in Object.keys(WM_SELECTED_CONFIG)" :key="selectConfig"> 107 <md-select v-model="WM_SELECTED_CONFIG_VALUES[selectConfig]" :placeholder="selectConfig"> 108 <md-option value="">{{selectConfig}}</md-option> 109 <md-option v-for="option in WM_SELECTED_CONFIG[selectConfig]" :key="option" :value="option">{{ option }}</md-option> 110 </md-select> 111 </md-field> 112 </div> 113 </div> 114 <md-button class="md-primary trace-btn" @click="startTrace">Start trace</md-button> 115 </div> 116 <div class="dump-section"> 117 <h3>Dump targets:</h3> 118 <div class="selection"> 119 <md-checkbox class="md-primary" v-for="dumpKey in Object.keys(DUMPS)" :key="dumpKey" v-model="traceStore[dumpKey]">{{DUMPS[dumpKey].name}}</md-checkbox> 120 </div> 121 <div class="md-layout"> 122 <md-button class="md-primary dump-btn" @click="dumpState">Dump state</md-button> 123 </div> 124 </div> 125 </md-card-content> 126 <md-card-content v-if="status === STATES.ERROR"> 127 <md-icon class="md-accent">error</md-icon> 128 <span class="md-subheading">Error:</span> 129 <pre> 130 {{ errorText }} 131 </pre> 132 <md-button class="md-primary" @click="restart">Retry</md-button> 133 </md-card-content> 134 <md-card-content v-if="status === STATES.END_TRACE"> 135 <span class="md-subheading">Tracing...</span> 136 <md-progress-bar md-mode="indeterminate"></md-progress-bar> 137 <div class="md-layout"> 138 <md-button class="md-primary" @click="endTrace">End trace</md-button> 139 </div> 140 </md-card-content> 141 <md-card-content v-if="status === STATES.LOAD_DATA"> 142 <span class="md-subheading">Loading data...</span> 143 <md-progress-bar md-mode="determinate" :md-value="loadProgress"></md-progress-bar> 144 </md-card-content> 145 </flat-card> 146</template> 147<script> 148import LocalStore from './localstore.js'; 149import FlatCard from './components/FlatCard.vue'; 150import {proxyClient, ProxyState, ProxyEndpoint} from './proxyclient/ProxyClient.ts'; 151 152// trace options should be added in a nested category 153const TRACES = { 154 'default': { 155 'window_trace': { 156 name: 'Window Manager', 157 }, 158 'accessibility_trace': { 159 name: 'Accessibility', 160 }, 161 'layers_trace': { 162 name: 'Surface Flinger', 163 }, 164 'transactions': { 165 name: 'Transaction', 166 }, 167 'proto_log': { 168 name: 'ProtoLog', 169 }, 170 'screen_recording': { 171 name: 'Screen Recording', 172 }, 173 'ime_trace_clients': { 174 name: 'Input Method Clients', 175 }, 176 'ime_trace_service': { 177 name: 'Input Method Service', 178 }, 179 'ime_trace_managerservice': { 180 name: 'Input Method Manager Service', 181 }, 182 }, 183 'arc': { 184 'wayland_trace': { 185 name: 'Wayland', 186 }, 187 }, 188}; 189 190const TRACE_CONFIG = { 191 'layers_trace': [ 192 'composition', 193 'metadata', 194 'hwc', 195 'tracebuffers', 196 ], 197}; 198 199const SF_SELECTED_CONFIG = { 200 'sfbuffersize': [ 201 '4000', 202 '8000', 203 '16000', 204 '32000', 205 ], 206}; 207 208const WM_SELECTED_CONFIG = { 209 'wmbuffersize': [ 210 '4000', 211 '8000', 212 '16000', 213 '32000', 214 ], 215 'tracingtype': [ 216 'frame', 217 'transaction', 218 ], 219 'tracinglevel': [ 220 'verbose', 221 'debug', 222 'critical', 223 ], 224}; 225 226const DUMPS = { 227 'window_dump': { 228 name: 'Window Manager', 229 }, 230 'layers_dump': { 231 name: 'Surface Flinger', 232 }, 233}; 234 235const CONFIGS = Object.keys(TRACE_CONFIG).flatMap((file) => TRACE_CONFIG[file]); 236 237export default { 238 name: 'dataadb', 239 data() { 240 return { 241 proxyClient, 242 ProxyState, 243 STATES: ProxyState, 244 TRACES, 245 DYNAMIC_TRACES: TRACES['default'], 246 TRACE_CONFIG, 247 SF_SELECTED_CONFIG, 248 WM_SELECTED_CONFIG, 249 SF_SELECTED_CONFIG_VALUES: {}, 250 WM_SELECTED_CONFIG_VALUES: {}, 251 DUMPS, 252 status: ProxyState.CONNECTING, 253 dataFiles: [], 254 keep_alive_worker: null, 255 errorText: '', 256 loadProgress: 0, 257 traceStore: LocalStore( 258 'trace', 259 Object.assign( 260 this.getAllTraceKeys(TRACES) 261 .concat(Object.keys(DUMPS)) 262 .concat(CONFIGS) 263 .reduce(function(obj, key) { 264 obj[key] = true; return obj; 265 }, {}), 266 ), 267 ), 268 downloadProxyUrl: 'https://android.googlesource.com/platform/development/+/master/tools/winscope/adb_proxy/winscope_proxy.py', 269 onStateChangeFn: (newState, errorText) => { 270 this.status = newState; 271 this.errorText = errorText; 272 }, 273 }; 274 }, 275 props: ['store'], 276 components: { 277 'flat-card': FlatCard, 278 }, 279 methods: { 280 getAllTraceKeys(traces) { 281 let keys = []; 282 for (let dict_key in traces) { 283 for (let key in traces[dict_key]) { 284 keys.push(key); 285 } 286 } 287 return keys; 288 }, 289 setAvailableTraces() { 290 this.DYNAMIC_TRACES = this.TRACES['default']; 291 proxyClient.call('GET', ProxyEndpoint.CHECK_WAYLAND, this, function(request, view) { 292 try { 293 if(request.responseText == 'true') { 294 view.appendOptionalTraces('arc'); 295 } 296 } catch(err) { 297 console.error(err); 298 proxyClient.setState(ProxyState.ERROR, request.responseText); 299 } 300 }); 301 }, 302 appendOptionalTraces(device_key) { 303 for(let key in this.TRACES[device_key]) { 304 this.$set(this.DYNAMIC_TRACES, key, this.TRACES[device_key][key]); 305 } 306 }, 307 keepAliveTrace() { 308 if (this.status !== ProxyState.END_TRACE) { 309 clearInterval(this.keep_alive_worker); 310 this.keep_alive_worker = null; 311 return; 312 } 313 proxyClient.call('GET', `${ProxyEndpoint.STATUS}${proxyClient.deviceId()}/`, this, function(request, view) { 314 if (request.responseText !== 'True') { 315 view.endTrace(); 316 } else if (view.keep_alive_worker === null) { 317 view.keep_alive_worker = setInterval(view.keepAliveTrace, 1000); 318 } 319 }); 320 }, 321 startTrace() { 322 const requested = this.toTrace(); 323 const requestedConfig = this.toTraceConfig(); 324 const requestedSelectedSfConfig = this.toSelectedSfTraceConfig(); 325 const requestedSelectedWmConfig = this.toSelectedWmTraceConfig(); 326 if (requested.length < 1) { 327 proxyClient.setState(ProxyState.ERROR, 'No targets selected'); 328 this.recordNewEvent("No targets selected"); 329 return; 330 } 331 332 this.recordNewEvent("Start Trace"); 333 proxyClient.call('POST', `${ProxyEndpoint.CONFIG_TRACE}${proxyClient.deviceId()}/`, this, null, null, requestedConfig); 334 proxyClient.call('POST', `${ProxyEndpoint.SELECTED_SF_CONFIG_TRACE}${proxyClient.deviceId()}/`, this, null, null, requestedSelectedSfConfig); 335 proxyClient.call('POST', `${ProxyEndpoint.SELECTED_WM_CONFIG_TRACE}${proxyClient.deviceId()}/`, this, null, null, requestedSelectedWmConfig); 336 proxyClient.setState(ProxyState.END_TRACE); 337 proxyClient.call('POST', `${ProxyEndpoint.START_TRACE}${proxyClient.deviceId()}/`, this, function(request, view) { 338 view.keepAliveTrace(); 339 }, null, requested); 340 }, 341 dumpState() { 342 this.recordButtonClickedEvent("Dump State"); 343 const requested = this.toDump(); 344 if (requested.length < 1) { 345 proxyClient.setState(ProxyState.ERROR, 'No targets selected'); 346 this.recordNewEvent("No targets selected"); 347 return; 348 } 349 proxyClient.setState(ProxyState.LOAD_DATA); 350 proxyClient.call('POST', `${ProxyEndpoint.DUMP}${proxyClient.deviceId()}/`, this, function(request, view) { 351 proxyClient.loadFile(requested, 0, "dump", view); 352 }, null, requested); 353 }, 354 endTrace() { 355 proxyClient.setState(ProxyState.LOAD_DATA); 356 proxyClient.call('POST', `${ProxyEndpoint.END_TRACE}${proxyClient.deviceId()}/`, this, function(request, view) { 357 proxyClient.loadFile(view.toTrace(), 0, "trace", view); 358 }); 359 this.recordNewEvent("Ended Trace"); 360 }, 361 toTrace() { 362 return Object.keys(this.DYNAMIC_TRACES) 363 .filter((traceKey) => this.traceStore[traceKey]); 364 }, 365 toTraceConfig() { 366 return Object.keys(TRACE_CONFIG) 367 .filter((file) => this.traceStore[file]) 368 .flatMap((file) => TRACE_CONFIG[file]) 369 .filter((config) => this.traceStore[config]); 370 }, 371 toSelectedSfTraceConfig() { 372 const requestedSelectedConfig = {}; 373 for (const config in this.SF_SELECTED_CONFIG_VALUES) { 374 if (this.SF_SELECTED_CONFIG_VALUES[config] !== "") { 375 requestedSelectedConfig[config] = this.SF_SELECTED_CONFIG_VALUES[config]; 376 } 377 } 378 return requestedSelectedConfig; 379 }, 380 toSelectedWmTraceConfig() { 381 const requestedSelectedConfig = {}; 382 for (const config in this.WM_SELECTED_CONFIG_VALUES) { 383 if (this.WM_SELECTED_CONFIG_VALUES[config] !== "") { 384 requestedSelectedConfig[config] = this.WM_SELECTED_CONFIG_VALUES[config]; 385 } 386 } 387 return requestedSelectedConfig; 388 }, 389 toDump() { 390 return Object.keys(DUMPS) 391 .filter((dumpKey) => this.traceStore[dumpKey]); 392 }, 393 restart() { 394 this.recordButtonClickedEvent("Connect / Retry"); 395 proxyClient.setState(ProxyState.CONNECTING); 396 }, 397 resetLastDevice() { 398 this.recordButtonClickedEvent("Change Device"); 399 this.proxyClient.resetLastDevice(); 400 this.restart(); 401 }, 402 }, 403 created() { 404 proxyClient.setState(ProxyState.CONNECTING); 405 this.proxyClient.onStateChange(this.onStateChangeFn); 406 const urlParams = new URLSearchParams(window.location.search); 407 if (urlParams.has('token')) { 408 this.proxyClient.proxyKey = urlParams.get('token'); 409 } 410 this.proxyClient.getDevices(); 411 }, 412 beforeDestroy() { 413 this.proxyClient.removeOnStateChange(this.onStateChangeFn); 414 }, 415 watch: { 416 status: { 417 handler(st) { 418 if (st == ProxyState.CONNECTING) { 419 this.proxyClient.getDevices(); 420 } 421 if (st == ProxyState.START_TRACE) { 422 this.setAvailableTraces(); 423 } 424 }, 425 }, 426 }, 427}; 428 429</script> 430<style scoped> 431.config-selection { 432 width: 150px; 433 display: inline-flex; 434 margin-left: 5px; 435 margin-right: 5px; 436} 437.device-choice { 438 display: inline-flex; 439} 440h3 { 441 margin-bottom: 0; 442} 443.trace-btn, .dump-btn { 444 margin-top: 0; 445} 446pre { 447 white-space: pre-wrap; 448} 449</style> 450