Merge "Refactor file decoding, implement video view"
This commit is contained in:
@@ -85,11 +85,11 @@ export default {
|
||||
},
|
||||
onTimelineItemSelected(index, timelineIndex) {
|
||||
this.files[timelineIndex].selectedIndex = index;
|
||||
var t = parseInt(this.files[timelineIndex].timeline[index].timestamp);
|
||||
var t = parseInt(this.files[timelineIndex].timeline[index]);
|
||||
for (var i = 0; i < this.files.length; i++) {
|
||||
if (i != timelineIndex) {
|
||||
this.files[i].selectedIndex = findLastMatchingSorted(this.files[i].timeline, function(array, idx) {
|
||||
return parseInt(array[idx].timestamp) <= t;
|
||||
return parseInt(array[idx]) <= t;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -104,7 +104,7 @@ export default {
|
||||
if (cur + direction < 0 || cur + direction >= this.files[idx].timeline.length) {
|
||||
continue;
|
||||
}
|
||||
var d = Math.abs(parseInt(file.timeline[cur + direction].timestamp) - this.currentTimestamp);
|
||||
var d = Math.abs(parseInt(file.timeline[cur + direction]) - this.currentTimestamp);
|
||||
if (timeDiff > d) {
|
||||
timeDiff = d;
|
||||
closestTimeline = idx;
|
||||
@@ -112,7 +112,7 @@ export default {
|
||||
}
|
||||
if (closestTimeline >= 0) {
|
||||
this.files[closestTimeline].selectedIndex += direction;
|
||||
this.currentTimestamp = parseInt(this.files[closestTimeline].timeline[this.files[closestTimeline].selectedIndex].timestamp);
|
||||
this.currentTimestamp = parseInt(this.files[closestTimeline].timeline[this.files[closestTimeline].selectedIndex]);
|
||||
}
|
||||
},
|
||||
onDataViewFocus(view) {
|
||||
@@ -149,8 +149,8 @@ export default {
|
||||
prettyDump: function() { return JSON.stringify(this.dump, null, 2); },
|
||||
dataLoaded: function() { return this.files.length > 0 },
|
||||
scale() {
|
||||
var mx = Math.max(...(this.files.map(f => Math.max(...f.timeline.map(t => t.timestamp)))));
|
||||
var mi = Math.min(...(this.files.map(f => Math.min(...f.timeline.map(t => t.timestamp)))));
|
||||
var mx = Math.max(...(this.files.map(f => Math.max(...f.timeline))));
|
||||
var mi = Math.min(...(this.files.map(f => Math.min(...f.timeline))));
|
||||
return [mi, mx];
|
||||
},
|
||||
activeView: function() {
|
||||
|
||||
@@ -55,23 +55,7 @@
|
||||
</md-layout>
|
||||
</template>
|
||||
<script>
|
||||
import jsonProtoDefs from 'frameworks/base/core/proto/android/server/windowmanagertrace.proto'
|
||||
import jsonProtoDefsSF from 'frameworks/native/services/surfaceflinger/layerproto/layerstrace.proto'
|
||||
import protobuf from 'protobufjs'
|
||||
|
||||
import { detectFile, dataFile, FILE_TYPES, DATA_TYPES } from './detectfile.js'
|
||||
|
||||
import { fill_transform_data } from './matrix_utils.js'
|
||||
|
||||
var protoDefs = protobuf.Root.fromJSON(jsonProtoDefs)
|
||||
.addJSON(jsonProtoDefsSF.nested);
|
||||
|
||||
var TraceMessage = protoDefs.lookupType(
|
||||
"com.android.server.wm.WindowManagerTraceFileProto");
|
||||
var ServiceMessage = protoDefs.lookupType(
|
||||
"com.android.server.wm.WindowManagerServiceDumpProto");
|
||||
var LayersMessage = protoDefs.lookupType("android.surfaceflinger.LayersProto");
|
||||
var LayersTraceMessage = protoDefs.lookupType("android.surfaceflinger.LayersTraceFileProto");
|
||||
import { detectAndDecode, dataFile, FILE_TYPES, DATA_TYPES } from './decode.js'
|
||||
|
||||
export default {
|
||||
name: 'datainput',
|
||||
@@ -92,7 +76,7 @@ export default {
|
||||
// No file selected.
|
||||
return;
|
||||
}
|
||||
this.$emit('statusChange', this.filename + " (loading)");
|
||||
this.$emit('statusChange', file.name + " (loading)");
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
@@ -100,13 +84,9 @@ export default {
|
||||
try {
|
||||
if (FILE_TYPES[type]) {
|
||||
var filetype = FILE_TYPES[type];
|
||||
var decoded = filetype.protoType.decode(buffer);
|
||||
modifyProtoFields(decoded, this.store.displayDefaults);
|
||||
var transformed = filetype.transform(decoded);
|
||||
var data = fileType.decoder(buffer, filetype, file.name, this.store);
|
||||
} else {
|
||||
var [filetype, decoded] = detectFile(buffer);
|
||||
modifyProtoFields(decoded, this.store.displayDefaults);
|
||||
var transformed = filetype.transform(decoded);
|
||||
var [filetype, data] = detectAndDecode(buffer, file.name, this.store);
|
||||
}
|
||||
} catch (ex) {
|
||||
this.$emit('statusChange', this.filename + ': ' + ex);
|
||||
@@ -114,51 +94,8 @@ export default {
|
||||
} finally {
|
||||
event.target.value = ''
|
||||
}
|
||||
this.$emit('statusChange', this.filename + " (loading " + filetype.name + ")");
|
||||
|
||||
// Replace enum values with string representation and
|
||||
// add default values to the proto objects. This function also handles
|
||||
// a special case with TransformProtos where the matrix may be derived
|
||||
// from the transform type.
|
||||
function modifyProtoFields(protoObj, displayDefaults) {
|
||||
if (!protoObj || protoObj !== Object(protoObj) || !protoObj.$type) {
|
||||
return;
|
||||
}
|
||||
for (var fieldName in protoObj.$type.fields) {
|
||||
var fieldProperties = protoObj.$type.fields[fieldName];
|
||||
var field = protoObj[fieldName];
|
||||
|
||||
if (Array.isArray(field)) {
|
||||
field.forEach((item, _) => {
|
||||
modifyProtoFields(item, displayDefaults);
|
||||
})
|
||||
continue;
|
||||
}
|
||||
|
||||
if (displayDefaults && !(field)) {
|
||||
protoObj[fieldName] = fieldProperties.defaultValue;
|
||||
}
|
||||
|
||||
if (fieldProperties.type === 'TransformProto') {
|
||||
fill_transform_data(protoObj[fieldName]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fieldProperties.resolvedType && fieldProperties.resolvedType.valuesById) {
|
||||
protoObj[fieldName] = fieldProperties.resolvedType.valuesById[protoObj[fieldProperties.name]];
|
||||
continue;
|
||||
}
|
||||
modifyProtoFields(protoObj[fieldName], displayDefaults);
|
||||
}
|
||||
}
|
||||
var timeline;
|
||||
if (filetype.timeline) {
|
||||
timeline = transformed.children;
|
||||
} else {
|
||||
timeline = [transformed];
|
||||
}
|
||||
|
||||
this.$set(this.dataFiles, filetype.dataType.name, dataFile(file.name, timeline, filetype.dataType));
|
||||
this.$set(this.dataFiles, filetype.dataType.name, data);
|
||||
this.$emit('statusChange', null);
|
||||
}
|
||||
reader.readAsArrayBuffer(files[0]);
|
||||
|
||||
@@ -13,203 +13,47 @@
|
||||
limitations under the License.
|
||||
-->
|
||||
<template>
|
||||
<md-card v-if="tree">
|
||||
<md-card v-if="file">
|
||||
<md-card-header>
|
||||
<div class="md-title">
|
||||
<md-icon>{{file.type.icon}}</md-icon> {{file.filename}}
|
||||
</div>
|
||||
</md-card-header>
|
||||
<md-card-content class="container">
|
||||
<md-card class="rects">
|
||||
<md-whiteframe md-tag="md-toolbar" md-elevation="0" class="card-toolbar md-transparent md-dense">
|
||||
<h2 class="md-title">Screen</h2>
|
||||
</md-whiteframe>
|
||||
<md-whiteframe md-elevation="8">
|
||||
<rects :bounds="bounds" :rects="rects" :highlight="highlight" @rect-click="onRectClick" />
|
||||
</md-whiteframe>
|
||||
</md-card>
|
||||
<md-card class="hierarchy">
|
||||
<md-whiteframe md-tag="md-toolbar" md-elevation="0" class="card-toolbar md-transparent md-dense">
|
||||
<h2 class="md-title" style="flex: 1;">Hierarchy</h2>
|
||||
<md-checkbox v-model="store.onlyVisible">Only visible</md-checkbox>
|
||||
<md-checkbox v-model="store.flattened">Flat</md-checkbox>
|
||||
</md-whiteframe>
|
||||
<tree-view :item="tree" @item-selected="itemSelected" :selected="hierarchySelected" :filter="hierarchyFilter" :flattened="store.flattened" ref="hierarchy" />
|
||||
</md-card>
|
||||
<md-card class="properties">
|
||||
<md-whiteframe md-tag="md-toolbar" md-elevation="0" class="card-toolbar md-transparent md-dense">
|
||||
<h2 class="md-title" style="flex: 1">Properties</h2>
|
||||
<div class="filter">
|
||||
<input id="filter" type="search" placeholder="Filter..." v-model="propertyFilterString" />
|
||||
</div>
|
||||
</md-whiteframe>
|
||||
<tree-view :item="selectedTree" :filter="propertyFilter" />
|
||||
</md-card>
|
||||
</md-card-content>
|
||||
<traceview v-if="isTrace" :store="store" :file="file" ref="view" />
|
||||
<videoview v-if="isVideo" :file="file" ref="view" />
|
||||
</md-card>
|
||||
</template>
|
||||
<script>
|
||||
import TreeView from './TreeView.vue'
|
||||
import Timeline from './Timeline.vue'
|
||||
import Rects from './Rects.vue'
|
||||
|
||||
import { transform_json } from './transform.js'
|
||||
import { format_transform_type, is_simple_transform } from './matrix_utils.js'
|
||||
|
||||
function formatProto(obj) {
|
||||
if (!obj || !obj.$type) {
|
||||
return;
|
||||
}
|
||||
if (obj.$type.fullName === '.android.surfaceflinger.RectProto' ||
|
||||
obj.$type.fullName === '.android.graphics.RectProto') {
|
||||
return `(${obj.left}, ${obj.top}) - (${obj.right}, ${obj.bottom})`;
|
||||
} else if (obj.$type.fullName === '.android.surfaceflinger.FloatRectProto') {
|
||||
return `(${obj.left.toFixed(3)}, ${obj.top.toFixed(3)}) - (${obj.right.toFixed(3)}, ${obj.bottom.toFixed(3)})`;
|
||||
} else if (obj.$type.fullName === '.android.surfaceflinger.PositionProto') {
|
||||
return `(${obj.x.toFixed(3)}, ${obj.y.toFixed(3)})`;
|
||||
} else if (obj.$type.fullName === '.android.surfaceflinger.SizeProto') {
|
||||
return `${obj.w} x ${obj.h}`;
|
||||
} else if (obj.$type.fullName === '.android.surfaceflinger.ColorProto') {
|
||||
return `r:${obj.r} g:${obj.g} \n b:${obj.b} a:${obj.a}`;
|
||||
} else if (obj.$type.fullName === '.android.surfaceflinger.TransformProto') {
|
||||
var transform_type = format_transform_type(obj);
|
||||
if (is_simple_transform(obj)) {
|
||||
return `${transform_type}`;
|
||||
}
|
||||
return `${transform_type} dsdx:${obj.dsdx.toFixed(3)} dtdx:${obj.dtdx.toFixed(3)} dsdy:${obj.dsdy.toFixed(3)} dtdy:${obj.dtdy.toFixed(3)}`;
|
||||
}
|
||||
}
|
||||
import TraceView from './TraceView.vue'
|
||||
import VideoView from './VideoView.vue'
|
||||
import { DATA_TYPES } from './decode.js'
|
||||
|
||||
export default {
|
||||
name: 'dataview',
|
||||
data() {
|
||||
return {
|
||||
propertyFilterString: "",
|
||||
selectedTree: {},
|
||||
hierarchySelected: null,
|
||||
lastSelectedStableId: null,
|
||||
bounds: {},
|
||||
rects: [],
|
||||
tree: null,
|
||||
highlight: null,
|
||||
}
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
itemSelected(item) {
|
||||
this.hierarchySelected = item;
|
||||
this.selectedTree = transform_json(item.obj, item.name, {
|
||||
skip: item.skip,
|
||||
formatter: formatProto
|
||||
});
|
||||
this.highlight = item.highlight;
|
||||
this.lastSelectedStableId = item.stableId;
|
||||
this.$emit('focus');
|
||||
},
|
||||
onRectClick(item) {
|
||||
if (item) {
|
||||
this.itemSelected(item);
|
||||
}
|
||||
},
|
||||
setData(item) {
|
||||
this.tree = item;
|
||||
this.rects = [...item.rects].reverse();
|
||||
this.bounds = item.bounds;
|
||||
|
||||
this.hierarchySelected = null;
|
||||
this.selectedTree = {};
|
||||
this.highlight = null;
|
||||
|
||||
function find_item(item, stableId) {
|
||||
if (item.stableId === stableId) {
|
||||
return item;
|
||||
}
|
||||
if (Array.isArray(item.children)) {
|
||||
for (var child of item.children) {
|
||||
var found = find_item(child, stableId);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.lastSelectedStableId) {
|
||||
var found = find_item(item, this.lastSelectedStableId);
|
||||
if (found) {
|
||||
this.itemSelected(found);
|
||||
}
|
||||
}
|
||||
},
|
||||
arrowUp() {
|
||||
return this.$refs.hierarchy.selectPrev();
|
||||
return this.$refs.view.arrowUp();
|
||||
},
|
||||
arrowDown() {
|
||||
return this.$refs.hierarchy.selectNext();
|
||||
return this.$refs.view.arrowDown();
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.setData(this.file.timeline[this.file.selectedIndex]);
|
||||
},
|
||||
watch: {
|
||||
selectedIndex() {
|
||||
this.setData(this.file.timeline[this.file.selectedIndex]);
|
||||
}
|
||||
},
|
||||
props: ['store', 'file'],
|
||||
computed: {
|
||||
selectedIndex() {
|
||||
return this.file.selectedIndex;
|
||||
isTrace() {
|
||||
return this.file.type == DATA_TYPES.WINDOW_MANAGER || this.file.type == DATA_TYPES.SURFACE_FLINGER;
|
||||
},
|
||||
hierarchyFilter() {
|
||||
return this.store.onlyVisible ? (c, flattened) => {
|
||||
return c.visible || c.childrenVisible && !flattened;
|
||||
} : null;
|
||||
},
|
||||
propertyFilter() {
|
||||
var filterStrings = this.propertyFilterString.split(",");
|
||||
var positive = [];
|
||||
var negative = [];
|
||||
filterStrings.forEach((f) => {
|
||||
if (f.startsWith("!")) {
|
||||
var str = f.substring(1);
|
||||
negative.push((s) => s.indexOf(str) === -1);
|
||||
} else {
|
||||
var str = f;
|
||||
positive.push((s) => s.indexOf(str) !== -1);
|
||||
}
|
||||
});
|
||||
var filter = (item) => {
|
||||
var apply = (f) => f(item.name);
|
||||
return (positive.length === 0 || positive.some(apply)) &&
|
||||
(negative.length === 0 || negative.every(apply));
|
||||
};
|
||||
filter.includeChildren = true;
|
||||
return filter;
|
||||
isVideo() {
|
||||
return this.file.type == DATA_TYPES.SCREEN_RECORDING;
|
||||
}
|
||||
},
|
||||
components: {
|
||||
'tree-view': TreeView,
|
||||
'rects': Rects,
|
||||
'traceview': TraceView,
|
||||
'videoview': VideoView,
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<style>
|
||||
.rects {
|
||||
flex: none;
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.hierarchy,
|
||||
.properties {
|
||||
flex: 1;
|
||||
margin: 8px;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.hierarchy>.tree-view,
|
||||
.properties>.tree-view {
|
||||
margin: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -27,7 +27,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
position(item) {
|
||||
return this.translate(item.timestamp);
|
||||
return this.translate(item);
|
||||
},
|
||||
translate(cx) {
|
||||
var scale = [...this.scale];
|
||||
@@ -45,7 +45,7 @@ export default {
|
||||
if (this.items.length == 1) {
|
||||
return [0];
|
||||
}
|
||||
return this.items.map((e) => parseInt(e.timestamp));
|
||||
return this.items;
|
||||
},
|
||||
selected() {
|
||||
return this.items[this.selectedIndex];
|
||||
|
||||
209
tools/winscope/src/TraceView.vue
Normal file
209
tools/winscope/src/TraceView.vue
Normal file
@@ -0,0 +1,209 @@
|
||||
<!-- Copyright (C) 2019 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.
|
||||
-->
|
||||
<template>
|
||||
<md-card-content class="container">
|
||||
<md-card class="rects">
|
||||
<md-whiteframe md-tag="md-toolbar" md-elevation="0" class="card-toolbar md-transparent md-dense">
|
||||
<h2 class="md-title">Screen</h2>
|
||||
</md-whiteframe>
|
||||
<md-whiteframe md-elevation="8">
|
||||
<rects :bounds="bounds" :rects="rects" :highlight="highlight" @rect-click="onRectClick" />
|
||||
</md-whiteframe>
|
||||
</md-card>
|
||||
<md-card class="hierarchy">
|
||||
<md-whiteframe md-tag="md-toolbar" md-elevation="0" class="card-toolbar md-transparent md-dense">
|
||||
<h2 class="md-title" style="flex: 1;">Hierarchy</h2>
|
||||
<md-checkbox v-model="store.onlyVisible">Only visible</md-checkbox>
|
||||
<md-checkbox v-model="store.flattened">Flat</md-checkbox>
|
||||
</md-whiteframe>
|
||||
<tree-view :item="tree" @item-selected="itemSelected" :selected="hierarchySelected" :filter="hierarchyFilter" :flattened="store.flattened" ref="hierarchy" />
|
||||
</md-card>
|
||||
<md-card class="properties">
|
||||
<md-whiteframe md-tag="md-toolbar" md-elevation="0" class="card-toolbar md-transparent md-dense">
|
||||
<h2 class="md-title" style="flex: 1">Properties</h2>
|
||||
<div class="filter">
|
||||
<input id="filter" type="search" placeholder="Filter..." v-model="propertyFilterString" />
|
||||
</div>
|
||||
</md-whiteframe>
|
||||
<tree-view :item="selectedTree" :filter="propertyFilter" />
|
||||
</md-card>
|
||||
</md-card-content>
|
||||
</template>
|
||||
<script>
|
||||
import TreeView from './TreeView.vue'
|
||||
import Timeline from './Timeline.vue'
|
||||
import Rects from './Rects.vue'
|
||||
|
||||
import { transform_json } from './transform.js'
|
||||
import { format_transform_type, is_simple_transform } from './matrix_utils.js'
|
||||
|
||||
function formatProto(obj) {
|
||||
if (!obj || !obj.$type) {
|
||||
return;
|
||||
}
|
||||
if (obj.$type.fullName === '.android.surfaceflinger.RectProto' ||
|
||||
obj.$type.fullName === '.android.graphics.RectProto') {
|
||||
return `(${obj.left}, ${obj.top}) - (${obj.right}, ${obj.bottom})`;
|
||||
} else if (obj.$type.fullName === '.android.surfaceflinger.FloatRectProto') {
|
||||
return `(${obj.left.toFixed(3)}, ${obj.top.toFixed(3)}) - (${obj.right.toFixed(3)}, ${obj.bottom.toFixed(3)})`;
|
||||
} else if (obj.$type.fullName === '.android.surfaceflinger.PositionProto') {
|
||||
return `(${obj.x.toFixed(3)}, ${obj.y.toFixed(3)})`;
|
||||
} else if (obj.$type.fullName === '.android.surfaceflinger.SizeProto') {
|
||||
return `${obj.w} x ${obj.h}`;
|
||||
} else if (obj.$type.fullName === '.android.surfaceflinger.ColorProto') {
|
||||
return `r:${obj.r} g:${obj.g} \n b:${obj.b} a:${obj.a}`;
|
||||
} else if (obj.$type.fullName === '.android.surfaceflinger.TransformProto') {
|
||||
var transform_type = format_transform_type(obj);
|
||||
if (is_simple_transform(obj)) {
|
||||
return `${transform_type}`;
|
||||
}
|
||||
return `${transform_type} dsdx:${obj.dsdx.toFixed(3)} dtdx:${obj.dtdx.toFixed(3)} dsdy:${obj.dsdy.toFixed(3)} dtdy:${obj.dtdy.toFixed(3)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'traceview',
|
||||
data() {
|
||||
return {
|
||||
propertyFilterString: "",
|
||||
selectedTree: {},
|
||||
hierarchySelected: null,
|
||||
lastSelectedStableId: null,
|
||||
bounds: {},
|
||||
rects: [],
|
||||
tree: null,
|
||||
highlight: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
itemSelected(item) {
|
||||
this.hierarchySelected = item;
|
||||
this.selectedTree = transform_json(item.obj, item.name, {
|
||||
skip: item.skip,
|
||||
formatter: formatProto
|
||||
});
|
||||
this.highlight = item.highlight;
|
||||
this.lastSelectedStableId = item.stableId;
|
||||
this.$emit('focus');
|
||||
},
|
||||
onRectClick(item) {
|
||||
if (item) {
|
||||
this.itemSelected(item);
|
||||
}
|
||||
},
|
||||
setData(item) {
|
||||
this.tree = item;
|
||||
this.rects = [...item.rects].reverse();
|
||||
this.bounds = item.bounds;
|
||||
|
||||
this.hierarchySelected = null;
|
||||
this.selectedTree = {};
|
||||
this.highlight = null;
|
||||
|
||||
function find_item(item, stableId) {
|
||||
if (item.stableId === stableId) {
|
||||
return item;
|
||||
}
|
||||
if (Array.isArray(item.children)) {
|
||||
for (var child of item.children) {
|
||||
var found = find_item(child, stableId);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.lastSelectedStableId) {
|
||||
var found = find_item(item, this.lastSelectedStableId);
|
||||
if (found) {
|
||||
this.itemSelected(found);
|
||||
}
|
||||
}
|
||||
},
|
||||
arrowUp() {
|
||||
return this.$refs.hierarchy.selectPrev();
|
||||
},
|
||||
arrowDown() {
|
||||
return this.$refs.hierarchy.selectNext();
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.setData(this.file.data[this.file.selectedIndex]);
|
||||
},
|
||||
watch: {
|
||||
selectedIndex() {
|
||||
this.setData(this.file.data[this.file.selectedIndex]);
|
||||
}
|
||||
},
|
||||
props: ['store', 'file'],
|
||||
computed: {
|
||||
selectedIndex() {
|
||||
return this.file.selectedIndex;
|
||||
},
|
||||
hierarchyFilter() {
|
||||
return this.store.onlyVisible ? (c, flattened) => {
|
||||
return c.visible || c.childrenVisible && !flattened;
|
||||
} : null;
|
||||
},
|
||||
propertyFilter() {
|
||||
var filterStrings = this.propertyFilterString.split(",");
|
||||
var positive = [];
|
||||
var negative = [];
|
||||
filterStrings.forEach((f) => {
|
||||
if (f.startsWith("!")) {
|
||||
var str = f.substring(1);
|
||||
negative.push((s) => s.indexOf(str) === -1);
|
||||
} else {
|
||||
var str = f;
|
||||
positive.push((s) => s.indexOf(str) !== -1);
|
||||
}
|
||||
});
|
||||
var filter = (item) => {
|
||||
var apply = (f) => f(item.name);
|
||||
return (positive.length === 0 || positive.some(apply)) &&
|
||||
(negative.length === 0 || negative.every(apply));
|
||||
};
|
||||
filter.includeChildren = true;
|
||||
return filter;
|
||||
}
|
||||
},
|
||||
components: {
|
||||
'tree-view': TreeView,
|
||||
'rects': Rects,
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<style>
|
||||
.rects {
|
||||
flex: none;
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.hierarchy,
|
||||
.properties {
|
||||
flex: 1;
|
||||
margin: 8px;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.hierarchy>.tree-view,
|
||||
.properties>.tree-view {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
</style>
|
||||
78
tools/winscope/src/VideoView.vue
Normal file
78
tools/winscope/src/VideoView.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<!-- Copyright (C) 2019 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.
|
||||
-->
|
||||
<template>
|
||||
<md-card-content class="container">
|
||||
<md-card class="rects">
|
||||
<md-whiteframe md-tag="md-toolbar" md-elevation="0" class="card-toolbar md-transparent md-dense">
|
||||
<h2 class="md-title">Screen</h2>
|
||||
</md-whiteframe>
|
||||
<md-whiteframe md-elevation="8">
|
||||
<video :id="file.filename" class="screen" :src="videoUri" />
|
||||
</md-whiteframe>
|
||||
</md-card>
|
||||
</md-card-content>
|
||||
</template>
|
||||
<script>
|
||||
const EPSILON = 0.00001
|
||||
|
||||
function uint8ToString(array) {
|
||||
var chunk = 0x8000;
|
||||
var out = [];
|
||||
for (var i = 0; i < array.length; i += chunk) {
|
||||
out.push(String.fromCharCode.apply(null, array.subarray(i, i + chunk)));
|
||||
}
|
||||
return out.join("");
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'videoview',
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
arrowUp() {
|
||||
return true
|
||||
},
|
||||
arrowDown() {
|
||||
return true;
|
||||
},
|
||||
selectFrame(idx) {
|
||||
var time = (this.file.timeline[idx] - this.file.timeline[0]) / 1000000000 + EPSILON;
|
||||
document.getElementById(this.file.filename).currentTime = time;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selectedIndex() {
|
||||
this.selectFrame(this.file.selectedIndex);
|
||||
}
|
||||
},
|
||||
props: ['file'],
|
||||
computed: {
|
||||
selectedIndex() {
|
||||
return this.file.selectedIndex;
|
||||
},
|
||||
videoUri() {
|
||||
return "data:video/mp4;base64," + btoa(uint8ToString(this.file.data));
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
</script>
|
||||
<style>
|
||||
.screen {
|
||||
max-height: 50em;
|
||||
}
|
||||
|
||||
</style>
|
||||
211
tools/winscope/src/decode.js
Normal file
211
tools/winscope/src/decode.js
Normal file
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright 2017, 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 jsonProtoDefs from 'frameworks/base/core/proto/android/server/windowmanagertrace.proto'
|
||||
import jsonProtoDefsSF from 'frameworks/native/services/surfaceflinger/layerproto/layerstrace.proto'
|
||||
import protobuf from 'protobufjs'
|
||||
import { transform_layers, transform_layers_trace } from './transform_sf.js'
|
||||
import { transform_window_service, transform_window_trace } from './transform_wm.js'
|
||||
import { fill_transform_data } from './matrix_utils.js'
|
||||
import { mp4Decoder } from './decodeVideo.js'
|
||||
|
||||
var protoDefs = protobuf.Root.fromJSON(jsonProtoDefs)
|
||||
.addJSON(jsonProtoDefsSF.nested);
|
||||
|
||||
var WindowTraceMessage = protoDefs.lookupType(
|
||||
"com.android.server.wm.WindowManagerTraceFileProto");
|
||||
var WindowMessage = protoDefs.lookupType(
|
||||
"com.android.server.wm.WindowManagerServiceDumpProto");
|
||||
var LayersMessage = protoDefs.lookupType("android.surfaceflinger.LayersProto");
|
||||
var LayersTraceMessage = protoDefs.lookupType("android.surfaceflinger.LayersTraceFileProto");
|
||||
|
||||
|
||||
const LAYER_TRACE_MAGIC_NUMBER = [0x09, 0x4c, 0x59, 0x52, 0x54, 0x52, 0x41, 0x43, 0x45] // .LYRTRACE
|
||||
const WINDOW_TRACE_MAGIC_NUMBER = [0x09, 0x57, 0x49, 0x4e, 0x54, 0x52, 0x41, 0x43, 0x45] // .WINTRACE
|
||||
const MPEG4_MAGIC_NMBER = [0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32] // ....ftypmp42
|
||||
|
||||
const DATA_TYPES = {
|
||||
WINDOW_MANAGER: {
|
||||
name: "WindowManager",
|
||||
icon: "view_compact",
|
||||
},
|
||||
SURFACE_FLINGER: {
|
||||
name: "SurfaceFlinger",
|
||||
icon: "filter_none",
|
||||
},
|
||||
SCREEN_RECORDING: {
|
||||
name: "Screen recording",
|
||||
icon: "videocam",
|
||||
}
|
||||
}
|
||||
|
||||
const FILE_TYPES = {
|
||||
'window_trace': {
|
||||
name: "WindowManager trace",
|
||||
dataType: DATA_TYPES.WINDOW_MANAGER,
|
||||
decoder: protoDecoder,
|
||||
decoderParams: {
|
||||
protoType: WindowTraceMessage,
|
||||
transform: transform_window_trace,
|
||||
timeline: true,
|
||||
},
|
||||
},
|
||||
'layers_trace': {
|
||||
name: "SurfaceFlinger trace",
|
||||
dataType: DATA_TYPES.SURFACE_FLINGER,
|
||||
decoder: protoDecoder,
|
||||
decoderParams: {
|
||||
protoType: LayersTraceMessage,
|
||||
transform: transform_layers_trace,
|
||||
timeline: true,
|
||||
},
|
||||
},
|
||||
'layers_dump': {
|
||||
name: "SurfaceFlinger dump",
|
||||
dataType: DATA_TYPES.SURFACE_FLINGER,
|
||||
decoder: protoDecoder,
|
||||
decoderParams: {
|
||||
protoType: LayersMessage,
|
||||
transform: transform_layers,
|
||||
timeline: false,
|
||||
},
|
||||
},
|
||||
'window_dump': {
|
||||
name: "WindowManager dump",
|
||||
dataType: DATA_TYPES.WINDOW_MANAGER,
|
||||
decoder: protoDecoder,
|
||||
decoderParams: {
|
||||
protoType: WindowMessage,
|
||||
transform: transform_window_service,
|
||||
timeline: false,
|
||||
},
|
||||
},
|
||||
'screen_recording': {
|
||||
name: "Screen recording",
|
||||
dataType: DATA_TYPES.SCREEN_RECORDING,
|
||||
decoder: videoDecoder,
|
||||
decoderParams: {
|
||||
videoDecoder: mp4Decoder,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// Replace enum values with string representation and
|
||||
// add default values to the proto objects. This function also handles
|
||||
// a special case with TransformProtos where the matrix may be derived
|
||||
// from the transform type.
|
||||
function modifyProtoFields(protoObj, displayDefaults) {
|
||||
if (!protoObj || protoObj !== Object(protoObj) || !protoObj.$type) {
|
||||
return;
|
||||
}
|
||||
for (var fieldName in protoObj.$type.fields) {
|
||||
var fieldProperties = protoObj.$type.fields[fieldName];
|
||||
var field = protoObj[fieldName];
|
||||
|
||||
if (Array.isArray(field)) {
|
||||
field.forEach((item, _) => {
|
||||
modifyProtoFields(item, displayDefaults);
|
||||
})
|
||||
continue;
|
||||
}
|
||||
|
||||
if (displayDefaults && !(field)) {
|
||||
protoObj[fieldName] = fieldProperties.defaultValue;
|
||||
}
|
||||
|
||||
if (fieldProperties.type === 'TransformProto') {
|
||||
fill_transform_data(protoObj[fieldName]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fieldProperties.resolvedType && fieldProperties.resolvedType.valuesById) {
|
||||
protoObj[fieldName] = fieldProperties.resolvedType.valuesById[protoObj[fieldProperties.name]];
|
||||
continue;
|
||||
}
|
||||
modifyProtoFields(protoObj[fieldName], displayDefaults);
|
||||
}
|
||||
}
|
||||
|
||||
function protoDecoder(buffer, fileType, fileName, store) {
|
||||
var decoded = fileType.decoderParams.protoType.decode(buffer);
|
||||
modifyProtoFields(decoded, store.displayDefaults);
|
||||
var transformed = fileType.decoderParams.transform(decoded);
|
||||
var data
|
||||
if (fileType.decoderParams.timeline) {
|
||||
data = transformed.children;
|
||||
} else {
|
||||
data = [transformed];
|
||||
}
|
||||
return dataFile(fileName, data.map(x => x.timestamp), data, fileType.dataType);
|
||||
}
|
||||
|
||||
function videoDecoder(buffer, fileType, fileName, store) {
|
||||
var [data, timeline] = fileType.decoderParams.videoDecoder(buffer)
|
||||
return dataFile(fileName, timeline, data, fileType.dataType);
|
||||
}
|
||||
|
||||
function dataFile(filename, timeline, data, type) {
|
||||
return {
|
||||
filename: filename,
|
||||
timeline: timeline,
|
||||
data: data,
|
||||
type: type,
|
||||
selectedIndex: 0,
|
||||
}
|
||||
}
|
||||
|
||||
function arrayEquals(a, b) {
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
for (var i = 0; i < a.length; i++) {
|
||||
if (a[i] != b[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function arrayStartsWith(array, prefix) {
|
||||
return arrayEquals(array.slice(0, prefix.length), prefix);
|
||||
}
|
||||
|
||||
function decodedFile(fileType, buffer, fileName, store) {
|
||||
return [fileType, fileType.decoder(buffer, fileType, fileName, store)];
|
||||
}
|
||||
|
||||
function detectAndDecode(buffer, fileName, store) {
|
||||
if (arrayStartsWith(buffer, LAYER_TRACE_MAGIC_NUMBER)) {
|
||||
return decodedFile(FILE_TYPES['layers_trace'], buffer, fileName, store);
|
||||
}
|
||||
if (arrayStartsWith(buffer, WINDOW_TRACE_MAGIC_NUMBER)) {
|
||||
return decodedFile(FILE_TYPES['window_trace'], buffer, fileName, store);
|
||||
}
|
||||
if (arrayStartsWith(buffer, MPEG4_MAGIC_NMBER)) {
|
||||
return decodedFile(FILE_TYPES['screen_recording'], buffer, fileName, store);
|
||||
}
|
||||
for (var name of ['layers_dump', 'window_dump']) {
|
||||
try {
|
||||
return decodedFile(FILE_TYPES[name], buffer, fileName, store);
|
||||
} catch (ex) {
|
||||
// ignore exception and try next filetype
|
||||
}
|
||||
}
|
||||
throw new Error('Unable to detect file');
|
||||
}
|
||||
|
||||
export { detectAndDecode, dataFile, DATA_TYPES, FILE_TYPES };
|
||||
69
tools/winscope/src/decodeVideo.js
Normal file
69
tools/winscope/src/decodeVideo.js
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2017, 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.
|
||||
*/
|
||||
|
||||
const WINSCOPE_META_MAGIC_STRING = [0x23, 0x56, 0x56, 0x31, 0x4e, 0x53, 0x43, 0x30, 0x50, 0x45, 0x54, 0x31, 0x4d, 0x45, 0x21, 0x23]; // #VV1NSC0PET1ME!#
|
||||
|
||||
// Suitable only for short patterns
|
||||
function findFirstInArray(array, pattern) {
|
||||
for (var i = 0; i < array.length; i++) {
|
||||
var match = true;
|
||||
for (var j = 0; j < pattern.length; j++) {
|
||||
if (array[i + j] != pattern[j]) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function parseUintNLE(buffer, position, bytes) {
|
||||
var num = 0;
|
||||
for (var i = bytes - 1; i >= 0; i--) {
|
||||
num = num * 256
|
||||
num += buffer[position + i];
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
function parseUint32LE(buffer, position) {
|
||||
return parseUintNLE(buffer, position, 4)
|
||||
}
|
||||
|
||||
function parseUint64LE(buffer, position) {
|
||||
return parseUintNLE(buffer, position, 8)
|
||||
}
|
||||
|
||||
function mp4Decoder(buffer) {
|
||||
var dataStart = findFirstInArray(buffer, WINSCOPE_META_MAGIC_STRING);
|
||||
if (dataStart < 0) {
|
||||
throw new Error('Unable to find sync metadata in the file');
|
||||
}
|
||||
dataStart += WINSCOPE_META_MAGIC_STRING.length;
|
||||
var frameNum = parseUint32LE(buffer, dataStart);
|
||||
dataStart += 4;
|
||||
var timeline = [];
|
||||
for (var i = 0; i < frameNum; i++) {
|
||||
timeline.push(parseUint64LE(buffer, dataStart) * 1000);
|
||||
dataStart += 8;
|
||||
}
|
||||
return [buffer, timeline]
|
||||
}
|
||||
|
||||
export { mp4Decoder };
|
||||
@@ -1,129 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017, 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 jsonProtoDefs from 'frameworks/base/core/proto/android/server/windowmanagertrace.proto'
|
||||
import jsonProtoDefsSF from 'frameworks/native/services/surfaceflinger/layerproto/layerstrace.proto'
|
||||
import protobuf from 'protobufjs'
|
||||
import { transform_layers, transform_layers_trace } from './transform_sf.js'
|
||||
import { transform_window_service, transform_window_trace } from './transform_wm.js'
|
||||
|
||||
var protoDefs = protobuf.Root.fromJSON(jsonProtoDefs)
|
||||
.addJSON(jsonProtoDefsSF.nested);
|
||||
|
||||
var WindowTraceMessage = protoDefs.lookupType(
|
||||
"com.android.server.wm.WindowManagerTraceFileProto");
|
||||
var WindowMessage = protoDefs.lookupType(
|
||||
"com.android.server.wm.WindowManagerServiceDumpProto");
|
||||
var LayersMessage = protoDefs.lookupType("android.surfaceflinger.LayersProto");
|
||||
var LayersTraceMessage = protoDefs.lookupType("android.surfaceflinger.LayersTraceFileProto");
|
||||
|
||||
|
||||
const LAYER_TRACE_MAGIC_NUMBER = [0x09, 0x4c, 0x59, 0x52, 0x54, 0x52, 0x41, 0x43, 0x45] // .LYRTRACE
|
||||
const WINDOW_TRACE_MAGIC_NUMBER = [0x09, 0x57, 0x49, 0x4e, 0x54, 0x52, 0x41, 0x43, 0x45] // .WINTRACE
|
||||
|
||||
const DATA_TYPES = {
|
||||
WINDOW_MANAGER: {
|
||||
name: "WindowManager",
|
||||
icon: "view_compact",
|
||||
},
|
||||
SURFACE_FLINGER: {
|
||||
name: "SurfaceFlinger",
|
||||
icon: "filter_none",
|
||||
},
|
||||
}
|
||||
|
||||
const FILE_TYPES = {
|
||||
'window_trace': {
|
||||
protoType: WindowTraceMessage,
|
||||
transform: transform_window_trace,
|
||||
name: "WindowManager trace",
|
||||
timeline: true,
|
||||
dataType: DATA_TYPES.WINDOW_MANAGER,
|
||||
},
|
||||
'layers_trace': {
|
||||
protoType: LayersTraceMessage,
|
||||
transform: transform_layers_trace,
|
||||
name: "SurfaceFlinger trace",
|
||||
timeline: true,
|
||||
dataType: DATA_TYPES.SURFACE_FLINGER,
|
||||
},
|
||||
'layers_dump': {
|
||||
protoType: LayersMessage,
|
||||
transform: transform_layers,
|
||||
name: "SurfaceFlinger dump",
|
||||
timeline: false,
|
||||
dataType: DATA_TYPES.SURFACE_FLINGER,
|
||||
},
|
||||
'window_dump': {
|
||||
protoType: WindowMessage,
|
||||
transform: transform_window_service,
|
||||
name: "WindowManager dump",
|
||||
timeline: false,
|
||||
dataType: DATA_TYPES.WINDOW_MANAGER,
|
||||
},
|
||||
};
|
||||
|
||||
function dataFile(filename, timeline, type) {
|
||||
return {
|
||||
filename: filename,
|
||||
timeline: timeline,
|
||||
type: type,
|
||||
selectedIndex: 0,
|
||||
}
|
||||
}
|
||||
|
||||
function arrayEquals(a, b) {
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
for (var i = 0; i < a.length; i++) {
|
||||
if (a[i] != b[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function arrayStartsWith(array, prefix) {
|
||||
return arrayEquals(array.slice(0, prefix.length), prefix);
|
||||
}
|
||||
|
||||
function decodedFile(filename, buffer) {
|
||||
var decoded = FILE_TYPES[filename].protoType.decode(buffer);
|
||||
return [FILE_TYPES[filename], decoded];
|
||||
}
|
||||
|
||||
function detectFile(buffer) {
|
||||
if (arrayStartsWith(buffer, LAYER_TRACE_MAGIC_NUMBER)) {
|
||||
return decodedFile('layers_trace', buffer);
|
||||
}
|
||||
if (arrayStartsWith(buffer, WINDOW_TRACE_MAGIC_NUMBER)) {
|
||||
return decodedFile('window_trace', buffer);
|
||||
}
|
||||
for (var filename of ['layers_dump', 'window_dump']) {
|
||||
try {
|
||||
var [filetype, decoded] = decodedFile(filename, buffer);
|
||||
var transformed = filetype.transform(decoded);
|
||||
return [FILE_TYPES[filename], decoded];
|
||||
} catch (ex) {
|
||||
// ignore exception and try next filetype
|
||||
}
|
||||
}
|
||||
throw new Error('Unable to detect file');
|
||||
}
|
||||
|
||||
export { detectFile, dataFile, DATA_TYPES, FILE_TYPES };
|
||||
Reference in New Issue
Block a user