1<!-- Copyright (C) 2017 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 <div id="app"> 17 <md-whiteframe md-tag="md-toolbar"> 18 <h1 class="md-title" style="flex: 1">{{title}}</h1> 19 20 <input type="file" @change="onLoadFile" id="upload-file" v-show="false"/> 21 <label class="md-button md-accent md-raised md-theme-default" for="upload-file">Open File</label> 22 23 <div> 24 <md-select v-model="fileType" id="file-type" placeholder="File type"> 25 <md-option value="auto">Detect type</md-option> 26 <md-option :value="k" v-for="(v,k) in FILE_TYPES">{{v.name}}</md-option> 27 </md-select> 28 </div> 29 30 </md-whiteframe> 31 32 <div class="main-content" v-if="timeline.length"> 33 <md-card class="timeline-card"> 34 <md-whiteframe md-tag="md-toolbar" md-elevation="0" class="card-toolbar md-transparent md-dense"><h2 class="md-title">Timeline</h2></md-whiteframe> 35 <timeline :items="timeline" :selected="tree" @item-selected="onTimelineItemSelected" class="timeline" /> 36 </md-card> 37 38 <div class="container"> 39 <md-card class="rects"> 40 <md-whiteframe md-tag="md-toolbar" md-elevation="0" class="card-toolbar md-transparent md-dense"><h2 class="md-title">Screen</h2></md-whiteframe> 41 <md-whiteframe md-elevation="8"> 42 <rects :bounds="bounds" :rects="rects" :highlight="highlight" @rect-click="onRectClick" /> 43 </md-whiteframe> 44 </md-card> 45 46 <md-card class="hierarchy"> 47 <md-whiteframe md-tag="md-toolbar" md-elevation="0" class="card-toolbar md-transparent md-dense"> 48 <h2 class="md-title" style="flex: 1;">Hierarchy</h2> 49 <md-checkbox v-model="store.onlyVisible">Only visible</md-checkbox> 50 <md-checkbox v-model="store.flattened">Flat</md-checkbox> 51 </md-whiteframe> 52 <tree-view :item="tree" @item-selected="itemSelected" :selected="hierarchySelected" :filter="hierarchyFilter" :flattened="store.flattened" ref="hierarchy" /> 53 </md-card> 54 55 <md-card class="properties"> 56 <md-whiteframe md-tag="md-toolbar" md-elevation="0" class="card-toolbar md-transparent md-dense"> 57 <h2 class="md-title" style="flex: 1">Properties</h2> 58 <div class="filter"> 59 <input id="filter" type="search" placeholder="Filter..." v-model="propertyFilterString" /> 60 </div> 61 </md-whiteframe> 62 <tree-view :item="selectedTree" :filter="propertyFilter" /> 63 </md-card> 64 </div> 65 </div> 66 </div> 67</template> 68 69<script> 70 71import jsonProtoDefs from 'frameworks/base/core/proto/android/server/windowmanagertrace.proto' 72import jsonProtoDefsSF from 'frameworks/native/services/surfaceflinger/layerproto/layerstrace.proto' 73import protobuf from 'protobufjs' 74 75import TreeView from './TreeView.vue' 76import Timeline from './Timeline.vue' 77import Rects from './Rects.vue' 78 79import detectFile from './detectfile.js' 80import LocalStore from './localstore.js' 81 82import {transform_json} from './transform.js' 83import {transform_layers, transform_layers_trace} from './transform_sf.js' 84import {transform_window_service, transform_window_trace} from './transform_wm.js' 85 86 87var protoDefs = protobuf.Root.fromJSON(jsonProtoDefs) 88 .addJSON(jsonProtoDefsSF.nested); 89 90var TraceMessage = protoDefs.lookupType( 91 "com.android.server.wm.WindowManagerTraceFileProto"); 92var ServiceMessage = protoDefs.lookupType( 93 "com.android.server.wm.WindowManagerServiceDumpProto"); 94var LayersMessage = protoDefs.lookupType("android.surfaceflinger.LayersProto"); 95var LayersTraceMessage = protoDefs.lookupType("android.surfaceflinger.LayersTraceFileProto"); 96 97function formatProto(obj) { 98 if (!obj || !obj.$type) { 99 return; 100 } 101 if (obj.$type.fullName === '.android.surfaceflinger.RectProto') { 102 return `(${obj.left},${obj.top})-(${obj.right},${obj.bottom})`; 103 } else if (obj.$type.fullName === '.android.surfaceflinger.PositionProto') { 104 return `(${obj.x},${obj.y})`; 105 } else if (obj.$type.fullName === '.android.surfaceflinger.SizeProto') { 106 return `${obj.w}x${obj.h}`; 107 } else if (obj.$type.fullName === '.android.surfaceflinger.ColorProto') { 108 return `r:${obj.r} g:${obj.g} b:${obj.b} a:${obj.a}`; 109 } 110} 111 112const FILE_TYPES = { 113 'window_dump': { 114 protoType: ServiceMessage, 115 transform: transform_window_service, 116 name: "WindowManager dump", 117 timeline: false, 118 }, 119 'window_trace': { 120 protoType: TraceMessage, 121 transform: transform_window_trace, 122 name: "WindowManager trace", 123 timeline: true, 124 }, 125 'layers_dump': { 126 protoType: LayersMessage, 127 transform: transform_layers, 128 name: "SurfaceFlinger dump", 129 timeline: false, 130 }, 131 'layers_trace': { 132 protoType: LayersTraceMessage, 133 transform: transform_layers_trace, 134 name: "SurfaceFlinger trace", 135 timeline: true, 136 }, 137}; 138 139export default { 140 name: 'app', 141 data() { 142 return { 143 selectedTree: {}, 144 hierarchySelected: null, 145 tree: {}, 146 timeline: [], 147 bounds: {}, 148 rects: [], 149 highlight: null, 150 timelineIndex: 0, 151 title: "The Tool", 152 filename: "", 153 lastSelectedStableId: null, 154 propertyFilterString: "", 155 store: LocalStore('app', { 156 flattened: false, 157 onlyVisible: false, 158 }), 159 FILE_TYPES, 160 fileType: "auto", 161 } 162 }, 163 created() { 164 window.addEventListener('keydown', this.onKeyDown); 165 document.title = this.title; 166 }, 167 methods: { 168 onLoadFile(e) { 169 return this.onLoadProtoFile(e, this.fileType); 170 }, 171 onLoadProtoFile(event, type) { 172 var files = event.target.files || event.dataTransfer.files; 173 var file = files[0]; 174 if (!file) { 175 // No file selected. 176 return; 177 } 178 this.filename = file.name; 179 this.title = this.filename + " (loading)"; 180 181 var reader = new FileReader(); 182 reader.onload = (e) => { 183 var buffer = new Uint8Array(e.target.result); 184 var filetype = FILE_TYPES[type] || FILE_TYPES[detectFile(buffer)]; 185 if (!filetype) { 186 this.title = this.filename + ": Could not detect file type." 187 event.target.value = ''; 188 return; 189 } 190 this.title = this.filename + " (loading " + filetype.name + ")"; 191 192 try { 193 var decoded = filetype.protoType.decode(buffer); 194 decoded = filetype.protoType.toObject(decoded, {enums: String, defaults: true}); 195 var transformed = filetype.transform(decoded); 196 } catch (ex) { 197 this.title = this.filename + " (loading " + filetype.name + "):" + ex; 198 return; 199 } finally { 200 event.target.value = ''; 201 } 202 203 if (filetype.timeline) { 204 this.timeline = transformed.children; 205 } else { 206 this.timeline = [transformed]; 207 } 208 209 this.title = this.filename + " (" + filetype.name + ")"; 210 211 this.lastSelectedStableId = null; 212 this.onTimelineItemSelected(this.timeline[0], 0); 213 } 214 reader.readAsArrayBuffer(files[0]); 215 }, 216 itemSelected(item) { 217 this.hierarchySelected = item; 218 this.selectedTree = transform_json(item.obj, item.name, { 219 skip: item.skip, 220 formatter: formatProto}); 221 this.highlight = item.highlight; 222 this.lastSelectedStableId = item.stableId; 223 }, 224 onRectClick(item) { 225 if (item) { 226 this.itemSelected(item); 227 } 228 }, 229 onTimelineItemSelected(item, index) { 230 this.timelineIndex = index; 231 this.tree = item; 232 this.rects = [...item.rects].reverse(); 233 this.bounds = item.bounds; 234 235 this.hierarchySelected = null; 236 this.selectedTree = {}; 237 this.highlight = null; 238 239 function find_item(item, stableId) { 240 if (item.stableId === stableId) { 241 return item; 242 } 243 if (Array.isArray(item.children)) { 244 for (var child of item.children) { 245 var found = find_item(child, stableId); 246 if (found) { 247 return found; 248 } 249 } 250 } 251 return null; 252 } 253 254 if (this.lastSelectedStableId) { 255 var found = find_item(item, this.lastSelectedStableId); 256 if (found) { 257 this.itemSelected(found); 258 } 259 } 260 }, 261 onKeyDown(event) { 262 event = event || window.event; 263 if (event.keyCode == 37 /* left */) { 264 this.advanceTimeline(-1); 265 } else if (event.keyCode == 39 /* right */) { 266 this.advanceTimeline(1); 267 } else if (event.keyCode == 38 /* up */) { 268 this.$refs.hierarchy.selectPrev(); 269 } else if (event.keyCode == 40 /* down */) { 270 this.$refs.hierarchy.selectNext(); 271 } else { 272 return false; 273 } 274 event.preventDefault(); 275 return true; 276 }, 277 advanceTimeline(frames) { 278 if (!Array.isArray(this.timeline) || this.timeline.length == 0) { 279 return false; 280 } 281 var nextIndex = this.timelineIndex + frames; 282 if (nextIndex < 0) { 283 nextIndex = 0; 284 } 285 if (nextIndex >= this.timeline.length) { 286 nextIndex = this.timeline.length - 1; 287 } 288 this.onTimelineItemSelected(this.timeline[nextIndex], nextIndex); 289 return true; 290 }, 291 }, 292 computed: { 293 prettyDump: function() { return JSON.stringify(this.dump, null, 2); }, 294 hierarchyFilter() { 295 return this.store.onlyVisible ? (c, flattened) => { 296 return c.visible || c.childrenVisible && !flattened; 297 } : null; 298 }, 299 propertyFilter() { 300 var filterStrings = this.propertyFilterString.split(","); 301 var positive = []; 302 var negative = []; 303 filterStrings.forEach((f) => { 304 if (f.startsWith("!")) { 305 var str = f.substring(1); 306 negative.push((s) => s.indexOf(str) === -1); 307 } else { 308 var str = f; 309 positive.push((s) => s.indexOf(str) !== -1); 310 } 311 }); 312 var filter = (item) => { 313 var apply = (f) => f(item.name); 314 return (positive.length === 0 || positive.some(apply)) 315 && (negative.length === 0 || negative.every(apply)); 316 }; 317 filter.includeChildren = true; 318 return filter; 319 }, 320 }, 321 watch: { 322 title() { 323 document.title = this.title; 324 } 325 }, 326 components: { 327 'tree-view': TreeView, 328 'timeline': Timeline, 329 'rects': Rects, 330 } 331} 332</script> 333 334<style> 335#app { 336} 337 338.main-content { 339 padding: 8px; 340} 341 342.card-toolbar { 343 border-bottom: 1px solid rgba(0, 0, 0, .12); 344} 345 346.timeline-card { 347 margin: 8px; 348} 349 350.timeline { 351 margin: 16px; 352} 353 354.screen { 355 border: 1px solid black; 356} 357 358.container { 359 display: flex; 360 flex-wrap: wrap; 361} 362 363.rects { 364 flex: none; 365 margin: 8px; 366} 367 368.hierarchy, .properties { 369 flex: 1; 370 margin: 8px; 371 min-width: 400px; 372} 373 374.hierarchy > .tree-view, .properties > .tree-view { 375 margin: 16px; 376} 377 378h1, h2 { 379 font-weight: normal; 380} 381 382ul { 383 list-style-type: none; 384 padding: 0; 385} 386 387li { 388 display: inline-block; 389 margin: 0 10px; 390} 391 392a { 393 color: #42b983; 394} 395</style> 396