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 {{ WINSCOPE_PROXY_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="adbStore.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(devices).length > 0 ? "Connected devices:" : "No devices detected" }}</div> 66 <md-list> 67 <md-list-item v-for="(device, id) in devices" :key="id" @click="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">{{ devices[selectedDevice].model }} ({{ 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(TRACES)" :key="traceKey" v-model="adbStore[traceKey]">{{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="adbStore[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="adbStore[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 {FILE_DECODERS, FILE_TYPES} from './decode.js'; 149import LocalStore from './localstore.js'; 150import FlatCard from './components/FlatCard.vue'; 151 152const STATES = { 153 ERROR: 0, 154 CONNECTING: 1, 155 NO_PROXY: 2, 156 INVALID_VERSION: 3, 157 UNAUTH: 4, 158 DEVICES: 5, 159 START_TRACE: 6, 160 END_TRACE: 7, 161 LOAD_DATA: 8, 162}; 163 164const WINSCOPE_PROXY_VERSION = '0.8'; 165const WINSCOPE_PROXY_URL = 'http://localhost:5544'; 166const PROXY_ENDPOINTS = { 167 DEVICES: '/devices/', 168 START_TRACE: '/start/', 169 END_TRACE: '/end/', 170 CONFIG_TRACE: '/configtrace/', 171 SELECTED_WM_CONFIG_TRACE: '/selectedwmconfigtrace/', 172 SELECTED_SF_CONFIG_TRACE: '/selectedsfconfigtrace/', 173 DUMP: '/dump/', 174 FETCH: '/fetch/', 175 STATUS: '/status/', 176}; 177 178const TRACES = { 179 'window_trace': { 180 name: 'Window Manager', 181 }, 182 'accessibility_trace': { 183 name: 'Accessibility', 184 }, 185 'layers_trace': { 186 name: 'Surface Flinger', 187 }, 188 'transaction': { 189 name: 'Transactions', 190 }, 191 'proto_log': { 192 name: 'ProtoLog', 193 }, 194 'screen_recording': { 195 name: 'Screen Recording', 196 }, 197 'ime_trace_clients': { 198 name: 'Input Method Clients', 199 }, 200 'ime_trace_service': { 201 name: 'Input Method Service', 202 }, 203 'ime_trace_managerservice': { 204 name: 'Input Method Manager Service', 205 }, 206}; 207 208const TRACE_CONFIG = { 209 'layers_trace': [ 210 'composition', 211 'metadata', 212 'hwc', 213 ], 214}; 215 216const SF_SELECTED_CONFIG = { 217 'sfbuffersize': [ 218 '4000', 219 '8000', 220 '16000', 221 '32000', 222 ], 223}; 224 225const WM_SELECTED_CONFIG = { 226 'wmbuffersize': [ 227 '4000', 228 '8000', 229 '16000', 230 '32000', 231 ], 232 'tracingtype': [ 233 'frame', 234 'transaction', 235 ], 236 'tracinglevel': [ 237 'all', 238 'trim', 239 'critical', 240 ], 241}; 242 243const DUMPS = { 244 'window_dump': { 245 name: 'Window Manager', 246 }, 247 'layers_dump': { 248 name: 'Surface Flinger', 249 }, 250}; 251 252const proxyFileTypeAdapter = { 253 'window_trace': FILE_TYPES.WINDOW_MANAGER_TRACE, 254 'accessibility_trace': FILE_TYPES.ACCESSIBILITY_TRACE, 255 'layers_trace': FILE_TYPES.SURFACE_FLINGER_TRACE, 256 'wl_trace': FILE_TYPES.WAYLAND_TRACE, 257 'layers_dump': FILE_TYPES.SURFACE_FLINGER_DUMP, 258 'window_dump': FILE_TYPES.WINDOW_MANAGER_DUMP, 259 'wl_dump': FILE_TYPES.WAYLAND_DUMP, 260 'screen_recording': FILE_TYPES.SCREEN_RECORDING, 261 'transactions': FILE_TYPES.TRANSACTIONS_TRACE, 262 'proto_log': FILE_TYPES.PROTO_LOG, 263 'system_ui_trace': FILE_TYPES.SYSTEM_UI, 264 'launcher_trace': FILE_TYPES.LAUNCHER, 265 'ime_trace_clients': FILE_TYPES.IME_TRACE_CLIENTS, 266 'ime_trace_service': FILE_TYPES.IME_TRACE_SERVICE, 267 'ime_trace_managerservice': FILE_TYPES.IME_TRACE_MANAGERSERVICE, 268}; 269 270const CONFIGS = Object.keys(TRACE_CONFIG).flatMap((file) => TRACE_CONFIG[file]); 271 272export default { 273 name: 'dataadb', 274 data() { 275 return { 276 STATES, 277 TRACES, 278 TRACE_CONFIG, 279 SF_SELECTED_CONFIG, 280 WM_SELECTED_CONFIG, 281 SF_SELECTED_CONFIG_VALUES: {}, 282 WM_SELECTED_CONFIG_VALUES: {}, 283 DUMPS, 284 FILE_DECODERS, 285 WINSCOPE_PROXY_VERSION, 286 status: STATES.CONNECTING, 287 dataFiles: [], 288 devices: {}, 289 selectedDevice: '', 290 refresh_worker: null, 291 keep_alive_worker: null, 292 errorText: '', 293 loadProgress: 0, 294 adbStore: LocalStore( 295 'adb', 296 Object.assign( 297 { 298 proxyKey: '', 299 lastDevice: '', 300 }, 301 Object.keys(TRACES) 302 .concat(Object.keys(DUMPS)) 303 .concat(CONFIGS) 304 .reduce(function(obj, key) { 305 obj[key] = true; return obj; 306 }, {}), 307 ), 308 ), 309 downloadProxyUrl: 'https://android.googlesource.com/platform/development/+/master/tools/winscope/adb_proxy/winscope_proxy.py', 310 }; 311 }, 312 props: ['store'], 313 components: { 314 'flat-card': FlatCard, 315 }, 316 methods: { 317 getDevices() { 318 if (this.status !== STATES.DEVICES && this.status !== STATES.CONNECTING) { 319 clearInterval(this.refresh_worker); 320 this.refresh_worker = null; 321 return; 322 } 323 this.callProxy('GET', PROXY_ENDPOINTS.DEVICES, this, function(request, view) { 324 try { 325 view.devices = JSON.parse(request.responseText); 326 if (view.adbStore.lastDevice && view.devices[view.adbStore.lastDevice] && view.devices[view.adbStore.lastDevice].authorised) { 327 view.selectDevice(view.adbStore.lastDevice); 328 } else { 329 if (view.refresh_worker === null) { 330 view.refresh_worker = setInterval(view.getDevices, 1000); 331 } 332 view.status = STATES.DEVICES; 333 } 334 } catch (err) { 335 console.error(err); 336 view.errorText = request.responseText; 337 view.status = STATES.ERROR; 338 } 339 }); 340 }, 341 keepAliveTrace() { 342 if (this.status !== STATES.END_TRACE) { 343 clearInterval(this.keep_alive_worker); 344 this.keep_alive_worker = null; 345 return; 346 } 347 this.callProxy('GET', `${PROXY_ENDPOINTS.STATUS}${this.deviceId()}/`, this, function(request, view) { 348 if (request.responseText !== 'True') { 349 view.endTrace(); 350 } else if (view.keep_alive_worker === null) { 351 view.keep_alive_worker = setInterval(view.keepAliveTrace, 1000); 352 } 353 }); 354 }, 355 startTrace() { 356 const requested = this.toTrace(); 357 const requestedConfig = this.toTraceConfig(); 358 const requestedSelectedSfConfig = this.toSelectedSfTraceConfig(); 359 const requestedSelectedWmConfig = this.toSelectedWmTraceConfig(); 360 if (requested.length < 1) { 361 this.errorText = 'No targets selected'; 362 this.status = STATES.ERROR; 363 this.newEventOccurred("No targets selected"); 364 return; 365 } 366 367 this.newEventOccurred("Start Trace"); 368 this.callProxy('POST', `${PROXY_ENDPOINTS.CONFIG_TRACE}${this.deviceId()}/`, this, null, null, requestedConfig); 369 this.callProxy('POST', `${PROXY_ENDPOINTS.SELECTED_SF_CONFIG_TRACE}${this.deviceId()}/`, this, null, null, requestedSelectedSfConfig); 370 this.callProxy('POST', `${PROXY_ENDPOINTS.SELECTED_WM_CONFIG_TRACE}${this.deviceId()}/`, this, null, null, requestedSelectedWmConfig); 371 this.status = STATES.END_TRACE; 372 this.callProxy('POST', `${PROXY_ENDPOINTS.START_TRACE}${this.deviceId()}/`, this, function(request, view) { 373 view.keepAliveTrace(); 374 }, null, requested); 375 }, 376 dumpState() { 377 this.buttonClicked("Dump State"); 378 const requested = this.toDump(); 379 if (requested.length < 1) { 380 this.errorText = 'No targets selected'; 381 this.status = STATES.ERROR; 382 this.newEventOccurred("No targets selected"); 383 return; 384 } 385 this.status = STATES.LOAD_DATA; 386 this.callProxy('POST', `${PROXY_ENDPOINTS.DUMP}${this.deviceId()}/`, this, function(request, view) { 387 view.loadFile(requested, 0); 388 }, null, requested); 389 }, 390 endTrace() { 391 this.status = STATES.LOAD_DATA; 392 this.callProxy('POST', `${PROXY_ENDPOINTS.END_TRACE}${this.deviceId()}/`, this, function(request, view) { 393 view.loadFile(view.toTrace(), 0); 394 }); 395 this.newEventOccurred("Ended Trace"); 396 }, 397 loadFile(files, idx) { 398 this.callProxy('GET', `${PROXY_ENDPOINTS.FETCH}${this.deviceId()}/${files[idx]}/`, this, function(request, view) { 399 try { 400 const enc = new TextDecoder('utf-8'); 401 const resp = enc.decode(request.response); 402 const filesByType = JSON.parse(resp); 403 404 for (const filetype in filesByType) { 405 if (filesByType.hasOwnProperty(filetype)) { 406 const files = filesByType[filetype]; 407 const fileDecoder = FILE_DECODERS[proxyFileTypeAdapter[filetype]]; 408 409 for (const encodedFileBuffer of files) { 410 const buffer = Uint8Array.from(atob(encodedFileBuffer), (c) => c.charCodeAt(0)); 411 const data = fileDecoder.decoder(buffer, fileDecoder.decoderParams, fileDecoder.name, view.store); 412 view.dataFiles.push(data); 413 view.loadProgress = 100 * (idx + 1) / files.length; // TODO: Update this 414 } 415 } 416 } 417 418 if (idx < files.length - 1) { 419 view.loadFile(files, idx + 1); 420 } else { 421 view.$emit('dataReady', view.dataFiles); 422 } 423 } catch (err) { 424 console.error(err); 425 view.errorText = err; 426 view.status = STATES.ERROR; 427 } 428 }, 'arraybuffer'); 429 }, 430 toTrace() { 431 return Object.keys(TRACES) 432 .filter((traceKey) => this.adbStore[traceKey]); 433 }, 434 toTraceConfig() { 435 return Object.keys(TRACE_CONFIG) 436 .filter((file) => this.adbStore[file]) 437 .flatMap((file) => TRACE_CONFIG[file]) 438 .filter((config) => this.adbStore[config]); 439 }, 440 toSelectedSfTraceConfig() { 441 const requestedSelectedConfig = {}; 442 for (const config in this.SF_SELECTED_CONFIG_VALUES) { 443 if (this.SF_SELECTED_CONFIG_VALUES[config] !== "") { 444 requestedSelectedConfig[config] = this.SF_SELECTED_CONFIG_VALUES[config]; 445 } 446 } 447 return requestedSelectedConfig; 448 }, 449 toSelectedWmTraceConfig() { 450 const requestedSelectedConfig = {}; 451 for (const config in this.WM_SELECTED_CONFIG_VALUES) { 452 if (this.WM_SELECTED_CONFIG_VALUES[config] !== "") { 453 requestedSelectedConfig[config] = this.WM_SELECTED_CONFIG_VALUES[config]; 454 } 455 } 456 return requestedSelectedConfig; 457 }, 458 toDump() { 459 return Object.keys(DUMPS) 460 .filter((dumpKey) => this.adbStore[dumpKey]); 461 }, 462 selectDevice(device_id) { 463 this.selectedDevice = device_id; 464 this.adbStore.lastDevice = device_id; 465 this.status = STATES.START_TRACE; 466 }, 467 deviceId() { 468 return this.selectedDevice; 469 }, 470 restart() { 471 this.buttonClicked("Connect / Retry"); 472 this.status = STATES.CONNECTING; 473 }, 474 resetLastDevice() { 475 this.buttonClicked("Change Device"); 476 this.adbStore.lastDevice = ''; 477 this.restart(); 478 }, 479 callProxy(method, path, view, onSuccess, type, jsonRequest) { 480 const request = new XMLHttpRequest(); 481 var view = this; 482 request.onreadystatechange = function() { 483 if (this.readyState !== 4) { 484 return; 485 } 486 if (this.status === 0) { 487 view.status = STATES.NO_PROXY; 488 } else if (this.status === 200) { 489 if (this.getResponseHeader('Winscope-Proxy-Version') !== WINSCOPE_PROXY_VERSION) { 490 view.status = STATES.INVALID_VERSION; 491 } else if (onSuccess) { 492 onSuccess(this, view); 493 } 494 } else if (this.status === 403) { 495 view.status = STATES.UNAUTH; 496 } else { 497 if (this.responseType === 'text' || !this.responseType) { 498 view.errorText = this.responseText; 499 } else if (this.responseType === 'arraybuffer') { 500 view.errorText = String.fromCharCode.apply(null, new Uint8Array(this.response)); 501 } 502 view.status = STATES.ERROR; 503 } 504 }; 505 request.responseType = type || ''; 506 request.open(method, WINSCOPE_PROXY_URL + path); 507 request.setRequestHeader('Winscope-Token', this.adbStore.proxyKey); 508 if (jsonRequest) { 509 const json = JSON.stringify(jsonRequest); 510 request.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); 511 request.send(json); 512 } else { 513 request.send(); 514 } 515 }, 516 }, 517 created() { 518 const urlParams = new URLSearchParams(window.location.search); 519 if (urlParams.has('token')) { 520 this.adbStore.proxyKey = urlParams.get('token'); 521 } 522 this.getDevices(); 523 }, 524 watch: { 525 status: { 526 handler(st) { 527 if (st == STATES.CONNECTING) { 528 this.getDevices(); 529 } 530 }, 531 }, 532 }, 533}; 534 535</script> 536<style scoped> 537.config-selection { 538 width: 150px; 539 display: inline-flex; 540 margin-left: 5px; 541 margin-right: 5px; 542} 543.device-choice { 544 display: inline-flex; 545} 546h3 { 547 margin-bottom: 0; 548} 549.trace-btn, .dump-btn { 550 margin-top: 0; 551} 552pre { 553 white-space: pre-wrap; 554} 555</style> 556