Merge "Enable "auto rotate" for resizable AVD" into sc-v2-dev
diff --git a/tools/winscope/package.json b/tools/winscope/package.json
index 2f77f41..bf23983 100644
--- a/tools/winscope/package.json
+++ b/tools/winscope/package.json
@@ -18,6 +18,7 @@
"typescript": "^4.3.5",
"vue": "^2.6.14",
"vue-context": "^6.0.0",
+ "vue-gtag": "^1.16.1",
"vue-material": "^1.0.0-beta-15",
"vuex": "^3.6.2"
},
diff --git a/tools/winscope/spec/TagErrorSpec.js b/tools/winscope/spec/TagErrorSpec.js
index e8919a0..59cc138 100644
--- a/tools/winscope/spec/TagErrorSpec.js
+++ b/tools/winscope/spec/TagErrorSpec.js
@@ -1,6 +1,7 @@
import { decodeAndTransformProto, FILE_TYPES, FILE_DECODERS } from '../src/decode';
import Tag from '../src/flickerlib/tags/Tag';
import Error from '../src/flickerlib/errors/Error';
+import { TaggingEngine } from '../src/flickerlib/common.js';
import fs from 'fs';
import path from 'path';
@@ -33,6 +34,26 @@
})
});
+describe("Detect Tag", () => {
+ it("can detect tags", () => {
+ const wmFile = '../spec/traces/regular_rotation_in_last_state_wm_trace.winscope'
+ const layersFile = '../spec/traces/regular_rotation_in_last_state_layers_trace.winscope'
+ const wmBuffer = new Uint8Array(fs.readFileSync(path.resolve(__dirname, wmFile)));
+ const layersBuffer = new Uint8Array(fs.readFileSync(path.resolve(__dirname, layersFile)));
+
+ const wmTrace = decodeAndTransformProto(wmBuffer, FILE_DECODERS[FILE_TYPES.WINDOW_MANAGER_TRACE].decoderParams, true);
+ const layersTrace = decodeAndTransformProto(layersBuffer, FILE_DECODERS[FILE_TYPES.SURFACE_FLINGER_TRACE].decoderParams, true);
+
+ const engine = new TaggingEngine(wmTrace, layersTrace, (text) => { console.log(text) });
+ const tagTrace = engine.run();
+ expect(tagTrace.size).toEqual(4);
+ expect(tagTrace.entries[0].timestamp.toString()).toEqual('280186737540384');
+ expect(tagTrace.entries[1].timestamp.toString()).toEqual('280187243649340');
+ expect(tagTrace.entries[2].timestamp.toString()).toEqual('280188522078113');
+ expect(tagTrace.entries[3].timestamp.toString()).toEqual('280189020672174');
+ })
+});
+
describe("Error Transformation", () => {
it("can transform error traces", () => {
const buffer = new Uint8Array(fs.readFileSync(path.resolve(__dirname, errorTrace)));
@@ -47,4 +68,4 @@
expect(data.entries[1].errors).toEqual([new Error("","",66,"",66)]);
expect(data.entries[2].errors).toEqual([new Error("","",99,"",99)]);
})
-});
\ No newline at end of file
+});
diff --git a/tools/winscope/spec/traces/regular_rotation_in_last_state_layers_trace.winscope b/tools/winscope/spec/traces/regular_rotation_in_last_state_layers_trace.winscope
new file mode 100644
index 0000000..36fc663
--- /dev/null
+++ b/tools/winscope/spec/traces/regular_rotation_in_last_state_layers_trace.winscope
Binary files differ
diff --git a/tools/winscope/spec/traces/regular_rotation_in_last_state_wm_trace.winscope b/tools/winscope/spec/traces/regular_rotation_in_last_state_wm_trace.winscope
new file mode 100644
index 0000000..b5537ac
--- /dev/null
+++ b/tools/winscope/spec/traces/regular_rotation_in_last_state_wm_trace.winscope
Binary files differ
diff --git a/tools/winscope/src/App.vue b/tools/winscope/src/App.vue
index d771761..2621066 100644
--- a/tools/winscope/src/App.vue
+++ b/tools/winscope/src/App.vue
@@ -19,6 +19,11 @@
<h1 class="md-title" style="flex: 1">{{title}}</h1>
<md-button
class="md-primary md-theme-default download-all-btn"
+ @click="generateTags()"
+ v-if="dataLoaded && canGenerateTags"
+ >Generate Tags</md-button>
+ <md-button
+ class="md-primary md-theme-default"
@click="downloadAsZip(files)"
v-if="dataLoaded"
>Download All</md-button>
@@ -62,7 +67,6 @@
<overlay
:presentTags="Object.freeze(presentTags)"
:presentErrors="Object.freeze(presentErrors)"
- :tagAndErrorTraces="tagAndErrorTraces"
:store="store"
:ref="overlayRef"
:searchTypes="searchTypes"
@@ -86,6 +90,8 @@
import {DIRECTION} from './utils/utils';
import Searchbar from './Searchbar.vue';
import {NAVIGATION_STYLE, SEARCH_TYPE} from './utils/consts';
+import {TRACE_TYPES, FILE_TYPES, dataFile} from './decode.js';
+import { TaggingEngine } from './flickerlib/common';
const APP_NAME = 'Winscope';
@@ -107,15 +113,17 @@
navigationStyle: NAVIGATION_STYLE.GLOBAL,
flickerTraceView: false,
showFileTypes: [],
+ isInputMode: false,
}),
overlayRef: 'overlay',
mainContentStyle: {
'padding-bottom': `${CONTENT_BOTTOM_PADDING}px`,
},
+ tagFile: null,
presentTags: [],
presentErrors: [],
searchTypes: [SEARCH_TYPE.TIMESTAMP],
- tagAndErrorTraces: false,
+ hasTagOrErrorTraces: false,
};
},
created() {
@@ -139,11 +147,14 @@
},
/** Get tags from all uploaded tag files*/
getUpdatedTags() {
- var tagStates = this.getUpdatedStates(this.tagFiles);
+ if (this.tagFile === null) return [];
+ const tagStates = this.getUpdatedStates([this.tagFile]);
var tags = [];
tagStates.forEach(tagState => {
tagState.tags.forEach(tag => {
- tag.timestamp = tagState.timestamp;
+ tag.timestamp = Number(tagState.timestamp);
+ // tags generated on frontend have transition.name due to kotlin enum
+ tag.transition = tag.transition.name ?? tag.transition;
tags.push(tag);
});
});
@@ -156,20 +167,31 @@
//TODO (b/196201487) add check if errors empty
errorStates.forEach(errorState => {
errorState.errors.forEach(error => {
- error.timestamp = errorState.timestamp;
+ error.timestamp = Number(errorState.timestamp);
errors.push(error);
});
});
return errors;
},
/** Set flicker mode check for if there are tag/error traces uploaded*/
- shouldUpdateTagAndErrorTraces() {
- return this.tagFiles.length > 0 || this.errorFiles.length > 0;
+ updateHasTagOrErrorTraces() {
+ return this.hasTagTrace() || this.hasErrorTrace();
+ },
+ hasTagTrace() {
+ return this.tagFile !== null;
+ },
+ hasErrorTrace() {
+ return this.errorFiles.length > 0;
},
/** Activate flicker search tab if tags/errors uploaded*/
updateSearchTypes() {
this.searchTypes = [SEARCH_TYPE.TIMESTAMP];
- if (this.tagAndErrorTraces) this.searchTypes.push(SEARCH_TYPE.TAG);
+ if (this.hasTagTrace()) {
+ this.searchTypes.push(SEARCH_TYPE.TRANSITIONS);
+ }
+ if (this.hasErrorTrace()) {
+ this.searchTypes.push(SEARCH_TYPE.ERRORS);
+ }
},
/** Filter data view files by current show settings*/
updateShowFileTypes() {
@@ -179,7 +201,9 @@
},
clear() {
this.store.showFileTypes = [];
+ this.tagFile = null;
this.$store.commit('clearFiles');
+ this.buttonClicked("Clear")
},
onDataViewFocus(file) {
this.$store.commit('setActiveFile', file);
@@ -187,6 +211,7 @@
},
onKeyDown(event) {
event = event || window.event;
+ if (this.store.isInputMode) return false;
if (event.keyCode == 37 /* left */ ) {
this.$store.dispatch('advanceTimeline', DIRECTION.BACKWARD);
} else if (event.keyCode == 39 /* right */ ) {
@@ -203,7 +228,9 @@
},
onDataReady(files) {
this.$store.dispatch('setFiles', files);
- this.tagAndErrorTraces = this.shouldUpdateTagAndErrorTraces();
+
+ this.tagFile = this.tagFiles[0] ?? null;
+ this.hasTagOrErrorTraces = this.updateHasTagOrErrorTraces();
this.presentTags = this.getUpdatedTags();
this.presentErrors = this.getUpdatedErrors();
this.updateSearchTypes();
@@ -224,11 +251,43 @@
`${ CONTENT_BOTTOM_PADDING + newHeight }px`,
);
},
+ generateTags() {
+ // generate tag file
+ this.buttonClicked("Generate Tags");
+ const engine = new TaggingEngine(
+ this.$store.getters.tagGenerationWmTrace,
+ this.$store.getters.tagGenerationSfTrace,
+ (text) => { console.log(text) }
+ );
+ const tagTrace = engine.run();
+ const tagFile = this.generateTagFile(tagTrace);
+
+ // update tag trace in set files, update flicker mode
+ this.tagFile = tagFile;
+ this.hasTagOrErrorTraces = this.updateHasTagOrErrorTraces();
+ this.presentTags = this.getUpdatedTags();
+ this.presentErrors = this.getUpdatedErrors();
+ this.updateSearchTypes();
+ },
+
+ generateTagFile(tagTrace) {
+ const data = tagTrace.entries;
+ const blobUrl = URL.createObjectURL(new Blob([], {type: undefined}));
+ return dataFile(
+ "GeneratedTagTrace.winscope",
+ data.map((x) => x.timestamp),
+ data,
+ blobUrl,
+ FILE_TYPES.TAG_TRACE
+ );
+ },
},
computed: {
files() {
return this.$store.getters.sortedFiles.map(file => {
- if (this.hasDataView(file)) file.show = true;
+ if (this.hasDataView(file)) {
+ file.show = true;
+ }
return file;
});
},
@@ -257,6 +316,11 @@
timelineFiles() {
return this.$store.getters.timelineFiles;
},
+ canGenerateTags() {
+ const fileTypes = this.dataViewFiles.map((file) => file.type);
+ return fileTypes.includes(TRACE_TYPES.WINDOW_MANAGER)
+ && fileTypes.includes(TRACE_TYPES.SURFACE_FLINGER);
+ },
},
watch: {
title() {
diff --git a/tools/winscope/src/DataAdb.vue b/tools/winscope/src/DataAdb.vue
index 0c34f26..1cffa61 100644
--- a/tools/winscope/src/DataAdb.vue
+++ b/tools/winscope/src/DataAdb.vue
@@ -31,7 +31,7 @@
<p>Or get it from the AOSP repository.</p>
</div>
<div class="md-layout">
- <md-button class="md-accent" :href="downloadProxyUrl">Download from AOSP</md-button>
+ <md-button class="md-accent" :href="downloadProxyUrl" @click="buttonClicked(`Download from AOSP`)">Download from AOSP</md-button>
<md-button class="md-accent" @click="restart">Retry</md-button>
</div>
</md-card-content>
@@ -306,8 +306,10 @@
if (requested.length < 1) {
this.errorText = 'No targets selected';
this.status = STATES.ERROR;
+ this.newEventOccurred("No targets selected");
return;
}
+ this.newEventOccurred("Start Trace");
this.callProxy('POST', PROXY_ENDPOINTS.CONFIG_TRACE + this.deviceId() + '/', this, null, null, requestedConfig);
this.status = STATES.END_TRACE;
this.callProxy('POST', PROXY_ENDPOINTS.START_TRACE + this.deviceId() + '/', this, function(request, view) {
@@ -315,10 +317,12 @@
}, null, requested);
},
dumpState() {
+ this.buttonClicked("Dump State");
const requested = this.toDump();
if (requested.length < 1) {
this.errorText = 'No targets selected';
this.status = STATES.ERROR;
+ this.newEventOccurred("No targets selected");
return;
}
this.status = STATES.LOAD_DATA;
@@ -331,6 +335,7 @@
this.callProxy('POST', PROXY_ENDPOINTS.END_TRACE + this.deviceId() + '/', this, function(request, view) {
view.loadFile(view.toTrace(), 0);
});
+ this.newEventOccurred("Ended Trace");
},
loadFile(files, idx) {
this.callProxy('GET', PROXY_ENDPOINTS.FETCH + this.deviceId() + '/' + files[idx] + '/', this, function(request, view) {
@@ -388,9 +393,11 @@
return this.selectedDevice;
},
restart() {
+ this.buttonClicked("Connect / Retry");
this.status = STATES.CONNECTING;
},
resetLastDevice() {
+ this.buttonClicked("Change Device");
this.adbStore.lastDevice = '';
this.restart();
},
diff --git a/tools/winscope/src/DataInput.vue b/tools/winscope/src/DataInput.vue
index 6bd7e35..c55bc99 100644
--- a/tools/winscope/src/DataInput.vue
+++ b/tools/winscope/src/DataInput.vue
@@ -19,25 +19,41 @@
<div class="md-title">Open files</div>
</md-card-header>
<md-card-content>
- <md-list>
- <md-list-item v-for="file in dataFiles" v-bind:key="file.filename">
- <md-icon>{{FILE_ICONS[file.type]}}</md-icon>
- <span class="md-list-item-text">{{file.filename}} ({{file.type}})
- </span>
- <md-button
- class="md-icon-button md-accent"
- @click="onRemoveFile(file.type)"
- >
- <md-icon>close</md-icon>
- </md-button>
- </md-list-item>
- </md-list>
- <md-progress-spinner
- :md-diameter="30"
- :md-stroke="3"
- md-mode="indeterminate"
- v-show="loadingFiles"
- />
+ <div class="dropbox">
+ <md-list style="background: none">
+ <md-list-item v-for="file in dataFiles" v-bind:key="file.filename">
+ <md-icon>{{FILE_ICONS[file.type]}}</md-icon>
+ <span class="md-list-item-text">{{file.filename}} ({{file.type}})
+ </span>
+ <md-button
+ class="md-icon-button md-accent"
+ @click="onRemoveFile(file.type)"
+ >
+ <md-icon>close</md-icon>
+ </md-button>
+ </md-list-item>
+ </md-list>
+ <md-progress-spinner
+ :md-diameter="30"
+ :md-stroke="3"
+ md-mode="indeterminate"
+ v-show="loadingFiles"
+ class="progress-spinner"
+ />
+ <input
+ type="file"
+ @change="onLoadFile"
+ v-on:drop="handleFileDrop"
+ ref="fileUpload"
+ id="dropzone"
+ v-show="false"
+ multiple
+ />
+ <p v-if="!dataReady">
+ Drag your <b>.winscope</b> or <b>.zip</b> file(s) here to begin
+ </p>
+ </div>
+
<div class="md-layout">
<div class="md-layout-item md-small-size-100">
<md-field>
@@ -53,13 +69,6 @@
</div>
</div>
<div class="md-layout">
- <input
- type="file"
- @change="onLoadFile"
- ref="fileUpload"
- v-show="false"
- :multiple="fileType === 'auto'"
- />
<md-button
class="md-primary md-theme-default"
@click="$refs.fileUpload.click()"
@@ -143,6 +152,7 @@
},
hideSnackbarMessage() {
this.showSnackbar = false;
+ this.buttonClicked("Hide Snackbar Message")
},
getFetchFilesLoadingAnimation() {
let frame = 0;
@@ -226,20 +236,24 @@
});
}
},
- fileDragIn(e){
+ fileDragIn(e) {
e.preventDefault();
},
- fileDragOut(e){
+ fileDragOut(e) {
e.preventDefault();
},
handleFileDrop(e) {
e.preventDefault();
let droppedFiles = e.dataTransfer.files;
if(!droppedFiles) return;
+ // Record analytics event
+ this.draggedAndDropped(droppedFiles);
+
this.processFiles(droppedFiles);
},
onLoadFile(e) {
const files = event.target.files || event.dataTransfer.files;
+ this.uploadedFileThroughFilesystem(files);
this.processFiles(files);
},
async processFiles(files) {
@@ -534,4 +548,30 @@
},
};
-</script>
\ No newline at end of file
+</script>
+<style>
+ .dropbox:hover {
+ background: rgb(224, 224, 224);
+ }
+
+ .dropbox p {
+ font-size: 1.2em;
+ text-align: center;
+ padding: 50px 10px;
+ }
+
+ .dropbox {
+ outline: 2px dashed #448aff; /* the dash box */
+ outline-offset: -10px;
+ background: white;
+ color: #448aff;
+ padding: 10px 10px 10px 10px;
+ min-height: 200px; /* minimum height */
+ position: relative;
+ cursor: pointer;
+ }
+
+ .progress-spinner {
+ display: block;
+ }
+</style>
\ No newline at end of file
diff --git a/tools/winscope/src/DataView.vue b/tools/winscope/src/DataView.vue
index a2751f1..5250824 100644
--- a/tools/winscope/src/DataView.vue
+++ b/tools/winscope/src/DataView.vue
@@ -16,13 +16,13 @@
<div @click="onClick($event)">
<flat-card v-if="hasDataView(file)">
<md-card-header>
- <button class="toggle-view-button" @click="toggleView">
- <i aria-hidden="true" class="md-icon md-theme-default material-icons">
- {{ isShowFileType(file.type) ? "expand_more" : "chevron_right" }}
- </i>
- </button>
<md-card-header-text>
<div class="md-title">
+ <button class="toggle-view-button" @click="toggleView">
+ <i aria-hidden="true" class="md-icon md-theme-default material-icons">
+ {{ isShowFileType(file.type) ? "expand_more" : "chevron_right" }}
+ </i>
+ </button>
<md-icon>{{ TRACE_ICONS[file.type] }}</md-icon>
{{ file.type }}
</div>
@@ -160,6 +160,7 @@
// Pass click event to parent, so that click event handler can be attached
// to component.
this.$emit('click', e);
+ this.newEventOccurred(e.toString());
},
/** Filter data view files by current show settings */
updateShowFileTypes() {
diff --git a/tools/winscope/src/DefaultTreeElement.vue b/tools/winscope/src/DefaultTreeElement.vue
index 96c7dcf..ac03f9b 100644
--- a/tools/winscope/src/DefaultTreeElement.vue
+++ b/tools/winscope/src/DefaultTreeElement.vue
@@ -52,7 +52,7 @@
</div>
<div class="flicker-tags" v-for="error in errors" :key="error.message">
<Arrow class="error-arrow"/>
- <md-tooltip md-direction="right"> Error: {{error.message}} </md-tooltip>
+ <md-tooltip md-direction="right"> {{errorTooltip(error.message)}} </md-tooltip>
</div>
</span>
</template>
@@ -80,6 +80,12 @@
transitionTooltip(transition) {
return transitionMap.get(transition).desc;
},
+ errorTooltip(errorMessage) {
+ if (errorMessage.length>100) {
+ return `Error: ${errorMessage.substring(0,100)}...`;
+ }
+ return `Error: ${errorMessage}`;
+ },
},
components: {
Arrow,
diff --git a/tools/winscope/src/Overlay.vue b/tools/winscope/src/Overlay.vue
index d098dd4..a75f4ec 100644
--- a/tools/winscope/src/Overlay.vue
+++ b/tools/winscope/src/Overlay.vue
@@ -65,6 +65,13 @@
md-elevation="0"
class="md-transparent">
+ <md-button
+ @click="toggleSearch()"
+ class="drop-search"
+ >
+ Toggle search bar
+ </md-button>
+
<div class="toolbar" :class="{ expanded: expanded }">
<div class="resize-bar" v-show="expanded">
<div v-if="video" @mousedown="resizeBottomNav">
@@ -75,11 +82,6 @@
</div>
</div>
- <md-button
- @click="toggleSearch()"
- class="drop-search"
- >Show/hide search bar</md-button>
-
<div class="active-timeline" v-show="minimized">
<div
class="active-timeline-icon"
@@ -149,14 +151,20 @@
v-show="minimized"
v-if="hasTimeline"
>
- <label>
- {{ seekTime }}
- </label>
+ <input
+ class="timestamp-search-input"
+ v-model="searchInput"
+ spellcheck="false"
+ :placeholder="seekTime"
+ @focus="updateInputMode(true)"
+ @blur="updateInputMode(false)"
+ @keyup.enter="updateSearchForTimestamp"
+ />
<timeline
:store="store"
:flickerMode="flickerMode"
- :tags="Object.freeze(tags)"
- :errors="Object.freeze(errors)"
+ :tags="Object.freeze(presentTags)"
+ :errors="Object.freeze(presentErrors)"
:timeline="Object.freeze(minimizedTimeline.timeline)"
:selected-index="minimizedTimeline.selectedIndex"
:scale="scale"
@@ -188,11 +196,11 @@
>
<md-icon v-if="minimized">
expand_less
- <md-tooltip md-direction="top">Expand timeline</md-tooltip>
+ <md-tooltip md-direction="top" @click="buttonClicked(`Expand Timeline`)">Expand timeline</md-tooltip>
</md-icon>
<md-icon v-else>
expand_more
- <md-tooltip md-direction="top">Collapse timeline</md-tooltip>
+ <md-tooltip md-direction="top" @click="buttonClicked(`Collapse Timeline`)">Collapse timeline</md-tooltip>
</md-icon>
</md-button>
</div>
@@ -213,7 +221,17 @@
:style="`padding-top: ${resizeOffset}px;`"
>
<div class="seek-time" v-if="seekTime">
- <b>Seek time</b>: {{ seekTime }}
+ <b>Seek time: </b>
+ <input
+ class="timestamp-search-input"
+ :class="{ expanded: expanded }"
+ v-model="searchInput"
+ spellcheck="false"
+ :placeholder="seekTime"
+ @focus="updateInputMode(true)"
+ @blur="updateInputMode(false)"
+ @keyup.enter="updateSearchForTimestamp"
+ />
</div>
<timelines
@@ -283,14 +301,14 @@
import Searchbar from './Searchbar.vue';
import FileType from './mixins/FileType.js';
import {NAVIGATION_STYLE} from './utils/consts';
-import {TRACE_ICONS, FILE_TYPES} from '@/decode.js';
+import {TRACE_ICONS} from '@/decode.js';
// eslint-disable-next-line camelcase
-import {nanos_to_string} from './transform.js';
+import {nanos_to_string, getClosestTimestamp} from './transform.js';
export default {
name: 'overlay',
- props: ['store', 'presentTags', 'presentErrors', 'tagAndErrorTraces', 'searchTypes'],
+ props: ['store', 'presentTags', 'presentErrors', 'searchTypes'],
mixins: [FileType],
data() {
return {
@@ -312,8 +330,8 @@
cropIntent: null,
TRACE_ICONS,
search: false,
- tags: [],
- errors: [],
+ searchInput: "",
+ isSeekTimeInputMode: false,
};
},
created() {
@@ -326,6 +344,7 @@
},
destroyed() {
this.$store.commit('removeMergedTimeline', this.mergedTimeline);
+ this.updateInputMode(false);
},
watch: {
navigationStyle(style) {
@@ -433,8 +452,6 @@
}
},
minimizedTimeline() {
- this.updateFlickerMode(this.navigationStyle);
-
if (this.navigationStyle === NAVIGATION_STYLE.GLOBAL) {
return this.mergedTimeline;
}
@@ -471,7 +488,7 @@
return this.timelineFiles.length > 1;
},
flickerMode() {
- return this.tags.length>0 || this.errors.length>0;
+ return this.presentTags.length>0 || this.presentErrors.length>0;
},
},
updated() {
@@ -486,7 +503,29 @@
methods: {
toggleSearch() {
this.search = !(this.search);
+ this.buttonClicked("Toggle Search Bar");
},
+ /**
+ * determines whether left/right arrow keys should move cursor in input field
+ * and upon click of input field, fills with current timestamp
+ */
+ updateInputMode(isInputMode) {
+ this.isSeekTimeInputMode = isInputMode;
+ this.store.isInputMode = isInputMode;
+ if (!isInputMode) {
+ this.searchInput = "";
+ } else {
+ this.searchInput = this.seekTime;
+ }
+ },
+ /** Navigates to closest timestamp in timeline to search input*/
+ updateSearchForTimestamp() {
+ const closestTimestamp = getClosestTimestamp(this.searchInput, this.mergedTimeline.timeline);
+ this.$store.dispatch("updateTimelineTime", closestTimestamp);
+ this.updateInputMode(false);
+ this.newEventOccurred("Searching for timestamp")
+ },
+
emitBottomHeightUpdate() {
if (this.$refs.bottomNav) {
const newHeight = this.$refs.bottomNav.$el.clientHeight;
@@ -599,12 +638,15 @@
},
closeVideoOverlay() {
this.showVideoOverlay = false;
+ this.buttonClicked("Close Video Overlay")
},
openVideoOverlay() {
this.showVideoOverlay = true;
+ this.buttonClicked("Open Video Overlay")
},
toggleVideoOverlay() {
this.showVideoOverlay = !this.showVideoOverlay;
+ this.buttonClicked("Toggle Video Overlay")
},
videoLoaded() {
this.$refs.videoOverlay.contentLoaded();
@@ -647,43 +689,6 @@
this.$store.commit('setNavigationFilesFilter', navigationStyleFilter);
},
- updateFlickerMode(style) {
- if (style === NAVIGATION_STYLE.GLOBAL ||
- style === NAVIGATION_STYLE.CUSTOM) {
- this.tags = this.presentTags;
- this.errors = this.presentErrors;
-
- } else if (style === NAVIGATION_STYLE.FOCUSED) {
- if (this.focusedFile.timeline) {
- this.tags = this.getTagTimelineComponents(this.presentTags, this.focusedFile);
- this.errors = this.getTagTimelineComponents(this.presentErrors, this.focusedFile);
- }
- } else if (
- style.split('-').length >= 2 &&
- style.split('-')[0] === NAVIGATION_STYLE.TARGETED
- ) {
- const file = this.$store.state.traces[style.split('-')[1]];
- if (file.timeline) {
- this.tags = this.getTagTimelineComponents(this.presentTags, file);
- this.errors = this.getTagTimelineComponents(this.presentErrors, file);
- }
- //Unexpected navigation type or no timeline present in file
- } else {
- console.warn('Unexpected timeline or navigation type; no flicker mode available');
- this.tags = [];
- this.errors = [];
- }
- },
- getTagTimelineComponents(items, file) {
- if (file.type===FILE_TYPES.SURFACE_FLINGER_TRACE) {
- return items.filter(item => item.layerId !== -1);
- }
- if (file.type===FILE_TYPES.WINDOW_MANAGER_TRACE) {
- return items.filter(item => item.taskId !== -1);
- }
- // if focused file is not one supported by tags/errors
- return [];
- },
updateVideoOverlayWidth(width) {
this.videoOverlayExtraWidth = width;
},
@@ -896,6 +901,7 @@
color: rgba(0,0,0,0.54);
font-size: 12px;
font-family: inherit;
+ cursor: text;
}
.minimized-timeline-content .minimized-timeline {
@@ -921,6 +927,27 @@
cursor: help;
}
+.timestamp-search-input {
+ outline: none;
+ border-width: 0 0 1px;
+ border-color: gray;
+ font-family: inherit;
+ color: #448aff;
+ font-size: 12px;
+ padding: 0;
+ letter-spacing: inherit;
+ width: 125px;
+}
+
+.timestamp-search-input:focus {
+ border-color: #448aff;
+}
+
+.timestamp-search-input.expanded {
+ font-size: 14px;
+ width: 150px;
+}
+
.drop-search:hover {
background-color: #9af39f;
}
diff --git a/tools/winscope/src/Searchbar.vue b/tools/winscope/src/Searchbar.vue
index 50aca7b..bbfaa0a 100644
--- a/tools/winscope/src/Searchbar.vue
+++ b/tools/winscope/src/Searchbar.vue
@@ -14,82 +14,114 @@
-->
<template>
<md-content class="searchbar">
- <div class="search-timestamp" v-if="isTimestampSearch()">
- <md-button
- class="search-timestamp-button"
- @click="updateSearchForTimestamp"
- >
- Navigate to timestamp
- </md-button>
- <md-field class="search-input">
- <label>Enter timestamp</label>
- <md-input v-model="searchInput" @keyup.enter.native="updateSearchForTimestamp" />
- </md-field>
- </div>
- <div class="dropdown-content" v-if="isTagSearch()">
- <table>
- <tr class="header">
- <th style="width: 10%">Global Start</th>
- <th style="width: 10%">Global End</th>
- <th style="width: 80%">Description</th>
- </tr>
-
- <tr v-for="item in filteredTransitionsAndErrors" :key="item">
- <td
- v-if="isTransition(item)"
- class="inline-time"
- @click="
- setCurrentTimestamp(transitionStart(transitionTags(item.id)))
- "
- >
- <span>{{ transitionTags(item.id)[0].desc }}</span>
- </td>
- <td
- v-if="isTransition(item)"
- class="inline-time"
- @click="setCurrentTimestamp(transitionEnd(transitionTags(item.id)))"
- >
- <span>{{ transitionTags(item.id)[1].desc }}</span>
- </td>
- <td
- v-if="isTransition(item)"
- class="inline-transition"
- :style="{color: transitionTextColor(item.transition)}"
- @click="
- setCurrentTimestamp(transitionStart(transitionTags(item.id)))
- "
- >
- {{ transitionDesc(item.transition) }}
- </td>
-
- <td
- v-if="!isTransition(item)"
- class="inline-time"
- @click="setCurrentTimestamp(item.timestamp)"
- >
- {{ errorDesc(item.timestamp) }}
- </td>
- <td v-if="!isTransition(item)">-</td>
- <td
- v-if="!isTransition(item)"
- class="inline-error"
- @click="setCurrentTimestamp(item.timestamp)"
- >
- Error: {{item.message}}
- </td>
- </tr>
- </table>
- <md-field class="search-input">
- <label
- >Filter by transition or error message. Click to navigate to closest
- timestamp in active timeline.</label
+ <div class="tabs">
+ <div class="search-timestamp" v-if="isTimestampSearch()">
+ <md-field md-inline class="search-input">
+ <label>Enter timestamp</label>
+ <md-input
+ v-model="searchInput"
+ v-on:focus="updateInputMode(true)"
+ v-on:blur="updateInputMode(false)"
+ @keyup.enter.native="updateSearchForTimestamp"
+ />
+ </md-field>
+ <md-button
+ class="md-dense md-primary search-timestamp-button"
+ @click="updateSearchForTimestamp"
>
- <md-input v-model="searchInput"></md-input>
- </md-field>
+ Go to timestamp
+ </md-button>
+ </div>
+
+ <div class="dropdown-content" v-if="isTransitionSearch()">
+ <table>
+ <tr class="header">
+ <th style="width: 10%">Global Start</th>
+ <th style="width: 10%">Global End</th>
+ <th style="width: 80%">Transition</th>
+ </tr>
+
+ <tr v-for="item in filteredTransitionsAndErrors" :key="item.id">
+ <td
+ v-if="isTransition(item)"
+ class="inline-time"
+ @click="
+ setCurrentTimestamp(transitionStart(transitionTags(item.id)))
+ "
+ >
+ <span>{{ transitionTags(item.id)[0].desc }}</span>
+ </td>
+ <td
+ v-if="isTransition(item)"
+ class="inline-time"
+ @click="setCurrentTimestamp(transitionEnd(transitionTags(item.id)))"
+ >
+ <span>{{ transitionTags(item.id)[1].desc }}</span>
+ </td>
+ <td
+ v-if="isTransition(item)"
+ class="inline-transition"
+ :style="{color: transitionTextColor(item.transition)}"
+ @click="setCurrentTimestamp(transitionStart(transitionTags(item.id)))"
+ >
+ {{ transitionDesc(item.transition) }}
+ </td>
+ </tr>
+ </table>
+ <md-field md-inline class="search-input">
+ <label>
+ Filter by transition name. Click to navigate to closest
+ timestamp in active timeline.
+ </label>
+ <md-input
+ v-model="searchInput"
+ v-on:focus="updateInputMode(true)"
+ v-on:blur="updateInputMode(false)"
+ />
+ </md-field>
+ </div>
+
+ <div class="dropdown-content" v-if="isErrorSearch()">
+ <table>
+ <tr class="header">
+ <th style="width: 10%">Timestamp</th>
+ <th style="width: 90%">Error Message</th>
+ </tr>
+
+ <tr v-for="item in filteredTransitionsAndErrors" :key="item.id">
+ <td
+ v-if="!isTransition(item)"
+ class="inline-time"
+ @click="setCurrentTimestamp(item.timestamp)"
+ >
+ {{ errorDesc(item.timestamp) }}
+ </td>
+ <td
+ v-if="!isTransition(item)"
+ class="inline-error"
+ @click="setCurrentTimestamp(item.timestamp)"
+ >
+ {{item.message}}
+ </td>
+ </tr>
+ </table>
+ <md-field md-inline class="search-input">
+ <label>
+ Filter by error message. Click to navigate to closest
+ timestamp in active timeline.
+ </label>
+ <md-input
+ v-model="searchInput"
+ v-on:focus="updateInputMode(true)"
+ v-on:blur="updateInputMode(false)"
+ />
+ </md-field>
+ </div>
</div>
- <div class="tab-container">
+ <div class="tab-container" v-if="searchTypes.length > 0">
+ Search mode:
<md-button
v-for="searchType in searchTypes"
:key="searchType"
@@ -103,9 +135,7 @@
</template>
<script>
import { transitionMap, SEARCH_TYPE } from "./utils/consts";
-import { nanos_to_string, string_to_nanos } from "./transform";
-
-const regExpTimestampSearch = new RegExp(/^\d+$/);
+import { nanos_to_string, getClosestTimestamp } from "./transform";
export default {
name: "searchbar",
@@ -132,7 +162,8 @@
var tags = [];
var filter = this.searchInput.toUpperCase();
this.presentTags.forEach((tag) => {
- if (tag.transition.includes(filter)) tags.push(tag);
+ const tagTransition = tag.transition.toUpperCase();
+ if (tagTransition.includes(filter)) tags.push(tag);
});
return tags;
},
@@ -141,7 +172,8 @@
var tagsAndErrors = [...this.filteredTags()];
var filter = this.searchInput.toUpperCase();
this.presentErrors.forEach((error) => {
- if (error.message.includes(filter)) tagsAndErrors.push(error);
+ const errorMessage = error.message.toUpperCase();
+ if (errorMessage.includes(filter)) tagsAndErrors.push(error);
});
// sort into chronological order
tagsAndErrors.sort((a, b) => (a.timestamp > b.timestamp ? 1 : -1));
@@ -170,8 +202,10 @@
var times = tags.map((tag) => tag.timestamp);
return times[times.length - 1];
},
- /** Upon selecting a start/end tag in the dropdown;
- * navigates to that timestamp in the timeline */
+ /**
+ * Upon selecting a start/end tag in the dropdown;
+ * navigates to that timestamp in the timeline
+ */
setCurrentTimestamp(timestamp) {
this.$store.dispatch("updateTimelineTime", timestamp);
},
@@ -191,22 +225,15 @@
/** Navigates to closest timestamp in timeline to search input*/
updateSearchForTimestamp() {
- if (regExpTimestampSearch.test(this.searchInput)) {
- var roundedTimestamp = parseInt(this.searchInput);
- } else {
- var roundedTimestamp = string_to_nanos(this.searchInput);
- }
- var closestTimestamp = this.timeline.reduce(function (prev, curr) {
- return Math.abs(curr - roundedTimestamp) <
- Math.abs(prev - roundedTimestamp)
- ? curr
- : prev;
- });
+ const closestTimestamp = getClosestTimestamp(this.searchInput, this.timeline);
this.setCurrentTimestamp(closestTimestamp);
},
- isTagSearch() {
- return this.searchType === SEARCH_TYPE.TAG;
+ isTransitionSearch() {
+ return this.searchType === SEARCH_TYPE.TRANSITIONS;
+ },
+ isErrorSearch() {
+ return this.searchType === SEARCH_TYPE.ERRORS;
},
isTimestampSearch() {
return this.searchType === SEARCH_TYPE.TIMESTAMP;
@@ -214,6 +241,11 @@
isTransition(item) {
return item.stacktrace === undefined;
},
+
+ /** determines whether left/right arrow keys should move cursor in input field */
+ updateInputMode(isInputMode) {
+ this.store.isInputMode = isInputMode;
+ },
},
computed: {
filteredTransitionsAndErrors() {
@@ -227,9 +259,12 @@
});
},
},
+ destroyed() {
+ this.updateInputMode(false);
+ },
};
</script>
-<style>
+<style scoped>
.searchbar {
background-color: rgb(250, 243, 233) !important;
top: 0;
@@ -241,8 +276,14 @@
bottom: 1px;
}
+.tabs {
+ padding-top: 1rem;
+}
+
.tab-container {
- padding: 0px 20px 0px 20px;
+ padding-left: 20px;
+ display: flex;
+ align-items: center;
}
.tab.active {
@@ -255,11 +296,18 @@
.search-timestamp {
padding: 5px 20px 0px 20px;
- display: block;
+ display: inline-flex;
+ width: 100%;
+}
+
+.search-timestamp > .search-input {
+ margin-top: -5px;
+ max-width: 200px;
}
.search-timestamp-button {
left: 0;
+ padding: 0 15px;
}
.dropdown-content {
@@ -269,7 +317,7 @@
.dropdown-content table {
overflow-y: scroll;
- height: 150px;
+ max-height: 150px;
display: block;
}
@@ -283,7 +331,7 @@
}
.inline-time:hover {
- background: #9af39f;
+ background: rgb(216, 250, 218);
cursor: pointer;
}
@@ -292,7 +340,7 @@
}
.inline-transition:hover {
- background: #9af39f;
+ background: rgb(216, 250, 218);
cursor: pointer;
}
@@ -302,7 +350,7 @@
}
.inline-error:hover {
- background: #9af39f;
+ background: rgb(216, 250, 218);
cursor: pointer;
}
</style>
diff --git a/tools/winscope/src/Timeline.vue b/tools/winscope/src/Timeline.vue
index e5a3ce8..5b373d2 100644
--- a/tools/winscope/src/Timeline.vue
+++ b/tools/winscope/src/Timeline.vue
@@ -57,12 +57,13 @@
/>
<line
v-for="error in errorPositions"
- :key="error"
- :x1="`${error}%`"
- :x2="`${error}%`"
+ :key="error.pos"
+ :x1="`${error.pos}%`"
+ :x2="`${error.pos}%`"
y1="0"
y2="18px"
class="error"
+ @click="onErrorClick(error.ts)"
/>
</svg>
</div>
@@ -139,6 +140,7 @@
}
.error {
stroke: rgb(255, 0, 0);
- stroke-width: 2px;
+ stroke-width: 8px;
+ cursor: pointer;
}
</style>
\ No newline at end of file
diff --git a/tools/winscope/src/TraceView.vue b/tools/winscope/src/TraceView.vue
index ab77d35..ef42d65 100644
--- a/tools/winscope/src/TraceView.vue
+++ b/tools/winscope/src/TraceView.vue
@@ -45,7 +45,11 @@
<md-checkbox v-if="hasTagsOrErrors" v-model="store.flickerTraceView">Flicker</md-checkbox>
<md-field md-inline class="filter">
<label>Filter...</label>
- <md-input v-model="hierarchyPropertyFilterString"></md-input>
+ <md-input
+ v-model="hierarchyPropertyFilterString"
+ v-on:focus="updateInputMode(true)"
+ v-on:blur="updateInputMode(false)"
+ />
</md-field>
</md-content>
<div class="tree-view-wrapper">
@@ -98,7 +102,11 @@
</md-checkbox>
<md-field md-inline class="filter">
<label>Filter...</label>
- <md-input v-model="propertyFilterString"></md-input>
+ <md-input
+ v-model="propertyFilterString"
+ v-on:focus="updateInputMode(true)"
+ v-on:blur="updateInputMode(false)"
+ />
</md-field>
</md-content>
<div class="properties-content">
@@ -138,7 +146,7 @@
import {ObjectTransformer} from './transform.js';
import {DiffGenerator, defaultModifiedCheck} from './utils/diff.js';
import {TRACE_TYPES, DUMP_TYPES} from './decode.js';
-import {stableIdCompatibilityFixup} from './utils/utils.js';
+import {isPropertyMatch, stableIdCompatibilityFixup} from './utils/utils.js';
import {CompatibleFeatures} from './utils/compatibility.js';
import {getPropertiesForDisplay} from './flickerlib/mixin';
import ObjectFormatter from './flickerlib/ObjectFormatter';
@@ -318,9 +326,7 @@
matchItems(flickerItems, entryItem) {
var match = false;
flickerItems.forEach(flickerItem => {
- if (flickerItem.taskId===entryItem.taskId || flickerItem.layerId===entryItem.id) {
- match = true;
- }
+ if (isPropertyMatch(flickerItem, entryItem)) match = true;
});
return match;
},
@@ -328,6 +334,11 @@
isEntryTagMatch(entryItem) {
return this.matchItems(this.presentTags, entryItem) || this.matchItems(this.presentErrors, entryItem);
},
+
+ /** determines whether left/right arrow keys should move cursor in input field */
+ updateInputMode(isInputMode) {
+ this.store.isInputMode = isInputMode;
+ },
},
created() {
this.setData(this.file.data[this.file.selectedIndex ?? 0]);
diff --git a/tools/winscope/src/TreeView.vue b/tools/winscope/src/TreeView.vue
index 846924e..e19643c 100644
--- a/tools/winscope/src/TreeView.vue
+++ b/tools/winscope/src/TreeView.vue
@@ -122,6 +122,7 @@
import DefaultTreeElement from './DefaultTreeElement.vue';
import NodeContextMenu from './NodeContextMenu.vue';
import {DiffType} from './utils/diff.js';
+import {isPropertyMatch} from './utils/utils.js';
/* in px, must be kept in sync with css, maybe find a better solution... */
const levelOffset = 24;
@@ -220,6 +221,9 @@
},
toggleTree() {
this.setCollapseValue(!this.isCollapsed);
+ if (!this.isCollapsed) {
+ this.openedToSeeAttributeField(this.item.name)
+ }
},
expandTree() {
this.setCollapseValue(false);
@@ -446,17 +450,13 @@
}
},
- /** Check if tag/error id matches entry id */
- isIdMatch(a, b) {
- return a.taskId===b.taskId || a.layerId===b.id;
- },
/** Performs check for id match between entry and present tags/errors
* exits once match has been found
*/
matchItems(flickerItems) {
var match = false;
flickerItems.every(flickerItem => {
- if (this.isIdMatch(flickerItem, this.item)) {
+ if (isPropertyMatch(flickerItem, this.item)) {
match = true;
return false;
}
@@ -476,7 +476,7 @@
var transitions = [];
var ids = [];
this.currentTags.forEach(tag => {
- if (!ids.includes(tag.id) && this.isIdMatch(tag, this.item)) {
+ if (!ids.includes(tag.id) && isPropertyMatch(tag, this.item)) {
transitions.push(tag.transition);
ids.push(tag.id);
}
@@ -484,7 +484,7 @@
return transitions;
},
getCurrentErrorTags() {
- return this.currentErrors.filter(error => this.isIdMatch(error, this.item));
+ return this.currentErrors.filter(error => isPropertyMatch(error, this.item));
},
},
computed: {
diff --git a/tools/winscope/src/decode.js b/tools/winscope/src/decode.js
index 577993f..dde1041 100644
--- a/tools/winscope/src/decode.js
+++ b/tools/winscope/src/decode.js
@@ -554,6 +554,14 @@
function protoDecoder(buffer, params, fileName, store) {
const transformed = decodeAndTransformProto(buffer, params, store.displayDefaults);
+
+ // add tagGenerationTrace to dataFile for WM/SF traces so tags can be generated
+ var tagGenerationTrace = null;
+ if (params.type === FILE_TYPES.WINDOW_MANAGER_TRACE ||
+ params.type === FILE_TYPES.SURFACE_FLINGER_TRACE) {
+ tagGenerationTrace = transformed;
+ }
+
let data;
if (params.timeline) {
data = transformed.entries ?? transformed.children;
@@ -561,7 +569,15 @@
data = [transformed];
}
const blobUrl = URL.createObjectURL(new Blob([buffer], {type: params.mime}));
- return dataFile(fileName, data.map((x) => x.timestamp), data, blobUrl, params.type);
+
+ return dataFile(
+ fileName,
+ data.map((x) => x.timestamp),
+ data,
+ blobUrl,
+ params.type,
+ tagGenerationTrace
+ );
}
function videoDecoder(buffer, params, fileName, store) {
@@ -570,7 +586,7 @@
return dataFile(fileName, timeline, blobUrl, blobUrl, params.type);
}
-function dataFile(filename, timeline, data, blobUrl, type) {
+function dataFile(filename, timeline, data, blobUrl, type, tagGenerationTrace = null) {
return {
filename: filename,
// Object is frozen for performance reasons
@@ -578,6 +594,7 @@
timeline: Object.freeze(timeline),
data: data,
blobUrl: blobUrl,
+ tagGenerationTrace: tagGenerationTrace,
type: type,
selectedIndex: 0,
destroy() {
@@ -678,4 +695,17 @@
*/
class UndetectableFileType extends Error { }
-export {detectAndDecode, decodeAndTransformProto, FILE_TYPES, TRACE_INFO, TRACE_TYPES, DUMP_TYPES, DUMP_INFO, FILE_DECODERS, FILE_ICONS, UndetectableFileType};
+export {
+ dataFile,
+ detectAndDecode,
+ decodeAndTransformProto,
+ TagTraceMessage,
+ FILE_TYPES,
+ TRACE_INFO,
+ TRACE_TYPES,
+ DUMP_TYPES,
+ DUMP_INFO,
+ FILE_DECODERS,
+ FILE_ICONS,
+ UndetectableFileType
+};
diff --git a/tools/winscope/src/flickerlib/WindowManagerState.ts b/tools/winscope/src/flickerlib/WindowManagerState.ts
index 56b3181..0f13254 100644
--- a/tools/winscope/src/flickerlib/WindowManagerState.ts
+++ b/tools/winscope/src/flickerlib/WindowManagerState.ts
@@ -46,7 +46,7 @@
proto.rootWindowContainer.pendingActivities.map(it => it.title),
rootWindowContainer,
keyguardControllerState,
- timestamp = timestamp
+ /*timestamp */ `${timestamp}`
);
addAttributes(entry, proto);
diff --git a/tools/winscope/src/flickerlib/common.js b/tools/winscope/src/flickerlib/common.js
index d753a70..fefb27f 100644
--- a/tools/winscope/src/flickerlib/common.js
+++ b/tools/winscope/src/flickerlib/common.js
@@ -88,6 +88,9 @@
const ErrorState = require('flicker').com.android.server.wm.traces.common.errors.ErrorState;
const ErrorTrace = require('flicker').com.android.server.wm.traces.common.errors.ErrorTrace;
+// Service
+const TaggingEngine = require('flicker').com.android.server.wm.traces.common.service.TaggingEngine;
+
const EMPTY_BUFFER = new Buffer(0, 0, 0, 0);
const EMPTY_COLOR = new Color(-1, -1, -1, 0);
const EMPTY_RECT = new Rect(0, 0, 0, 0);
@@ -255,6 +258,8 @@
Rect,
RectF,
Region,
+ // Service
+ TaggingEngine,
toSize,
toBuffer,
toColor,
diff --git a/tools/winscope/src/flickerlib/errors/ErrorState.ts b/tools/winscope/src/flickerlib/errors/ErrorState.ts
index 78364c6..533af23 100644
--- a/tools/winscope/src/flickerlib/errors/ErrorState.ts
+++ b/tools/winscope/src/flickerlib/errors/ErrorState.ts
@@ -19,7 +19,7 @@
ErrorState.fromProto = function (protos: any[], timestamp: number): ErrorState {
const errors = protos.map(it => Error.fromProto(it));
- const state = new ErrorState(errors, timestamp);
+ const state = new ErrorState(errors, `${timestamp}`);
return state;
}
diff --git a/tools/winscope/src/flickerlib/tags/Tag.ts b/tools/winscope/src/flickerlib/tags/Tag.ts
index 8ecebc6..fdaa373 100644
--- a/tools/winscope/src/flickerlib/tags/Tag.ts
+++ b/tools/winscope/src/flickerlib/tags/Tag.ts
@@ -22,11 +22,14 @@
['ROTATION', TransitionType.ROTATION],
['PIP_ENTER', TransitionType.PIP_ENTER],
['PIP_RESIZE', TransitionType.PIP_RESIZE],
+ ['PIP_CLOSE', TransitionType.PIP_CLOSE],
['PIP_EXIT', TransitionType.PIP_EXIT],
['APP_LAUNCH', TransitionType.APP_LAUNCH],
['APP_CLOSE', TransitionType.APP_CLOSE],
['IME_APPEAR', TransitionType.IME_APPEAR],
['IME_DISAPPEAR', TransitionType.IME_DISAPPEAR],
+ ['APP_PAIRS_ENTER', TransitionType.APP_PAIRS_ENTER],
+ ['APP_PAIRS_EXIT', TransitionType.APP_PAIRS_EXIT],
]);
Tag.fromProto = function (proto: any): Tag {
diff --git a/tools/winscope/src/flickerlib/tags/TagState.ts b/tools/winscope/src/flickerlib/tags/TagState.ts
index d127a2d..7a3de7b 100644
--- a/tools/winscope/src/flickerlib/tags/TagState.ts
+++ b/tools/winscope/src/flickerlib/tags/TagState.ts
@@ -19,7 +19,7 @@
TagState.fromProto = function (timestamp: number, protos: any[]): TagState {
const tags = protos.map(it => Tag.fromProto(it));
- const state = new TagState(timestamp, tags);
+ const state = new TagState(`${timestamp}`, tags);
return state;
}
diff --git a/tools/winscope/src/flickerlib/tags/TransitionType.ts b/tools/winscope/src/flickerlib/tags/TransitionType.ts
index aebc72f..b4e376a 100644
--- a/tools/winscope/src/flickerlib/tags/TransitionType.ts
+++ b/tools/winscope/src/flickerlib/tags/TransitionType.ts
@@ -18,11 +18,14 @@
ROTATION = 'ROTATION',
PIP_ENTER = 'PIP_ENTER',
PIP_RESIZE ='PIP_RESIZE',
+ PIP_CLOSE = 'PIP_CLOSE',
PIP_EXIT = 'PIP_EXIT',
APP_LAUNCH = 'APP_LAUNCH',
APP_CLOSE = 'APP_CLOSE',
IME_APPEAR = 'IME_APPEAR',
IME_DISAPPEAR = 'IME_DISAPPEAR',
+ APP_PAIRS_ENTER = 'APP_PAIRS_ENTER',
+ APP_PAIRS_EXIT = 'APP_PAIRS_EXIT',
};
export default TransitionType;
\ No newline at end of file
diff --git a/tools/winscope/src/flickerlib/windows/Activity.ts b/tools/winscope/src/flickerlib/windows/Activity.ts
index 3d6149d..60e28cd 100644
--- a/tools/winscope/src/flickerlib/windows/Activity.ts
+++ b/tools/winscope/src/flickerlib/windows/Activity.ts
@@ -16,6 +16,7 @@
import { shortenName } from '../mixin'
import { Activity } from "../common"
+import { VISIBLE_CHIP } from '../treeview/Chips'
import WindowContainer from "./WindowContainer"
Activity.fromProto = function (proto: any): Activity {
@@ -50,6 +51,7 @@
entry.proto = proto;
entry.kind = entry.constructor.name;
entry.shortName = shortenName(entry.name);
+ entry.chips = entry.isVisible ? [VISIBLE_CHIP] : [];
}
export default Activity;
diff --git a/tools/winscope/src/flickerlib/windows/WindowContainer.ts b/tools/winscope/src/flickerlib/windows/WindowContainer.ts
index 5fe3a57..9a99226 100644
--- a/tools/winscope/src/flickerlib/windows/WindowContainer.ts
+++ b/tools/winscope/src/flickerlib/windows/WindowContainer.ts
@@ -58,6 +58,7 @@
name,
token,
proto.orientation,
+ proto.surfaceControl?.layerId ?? 0,
proto.visible,
config,
children
diff --git a/tools/winscope/src/main.js b/tools/winscope/src/main.js
index 26a0f8e..703cced 100644
--- a/tools/winscope/src/main.js
+++ b/tools/winscope/src/main.js
@@ -17,6 +17,7 @@
import Vue from 'vue'
import Vuex from 'vuex'
import VueMaterial from 'vue-material'
+import VueGtag from "vue-gtag";
import App from './App.vue'
import { TRACE_TYPES, DUMP_TYPES, TRACE_INFO, DUMP_INFO } from './decode.js'
@@ -109,6 +110,12 @@
video(state) {
return state.traces[TRACE_TYPES.SCREEN_RECORDING];
},
+ tagGenerationWmTrace(state, getters) {
+ return state.traces[TRACE_TYPES.WINDOW_MANAGER].tagGenerationTrace;
+ },
+ tagGenerationSfTrace(state, getters) {
+ return state.traces[TRACE_TYPES.SURFACE_FLINGER].tagGenerationTrace;
+ }
},
mutations: {
setCurrentTimestamp(state, timestamp) {
@@ -352,6 +359,61 @@
}
})
+/**
+ * Make Google analytics functionalities available for recording events.
+ */
+Vue.use(VueGtag, {
+ config: { id: 'G-RRV0M08Y76'}
+})
+
+Vue.mixin({
+ methods: {
+ buttonClicked(button) {
+ const string = "Clicked " + button + " Button";
+ this.$gtag.event(string, {
+ 'event_category': 'Button Clicked',
+ 'event_label': "Winscope Interactions",
+ 'value': button,
+ });
+ },
+ draggedAndDropped(val) {
+ this.$gtag.event("Dragged And DroppedFile", {
+ 'event_category': 'Uploaded file',
+ 'event_label': "Winscope Interactions",
+ 'value': val,
+ });
+ },
+ uploadedFileThroughFilesystem(val) {
+ this.$gtag.event("Uploaded File From Filesystem", {
+ 'event_category': 'Uploaded file',
+ 'event_label': "Winscope Interactions",
+ 'value': val,
+ });
+ },
+ newEventOccurred(event) {
+ this.$gtag.event(event, {
+ 'event_category': event,
+ 'event_label': "Winscope Interactions",
+ 'value': 1,
+ });
+ },
+ seeingNewScreen(screenname) {
+ this.$gtag.screenview({
+ app_name: "Winscope",
+ screen_name: screenname,
+ })
+ },
+ openedToSeeAttributeField(field) {
+ const string = "Opened field " + field;
+ this.$gtag.event(string, {
+ 'event_category': "Opened attribute field",
+ 'event_label': "Winscope Interactions",
+ 'value': field,
+ });
+ },
+ }
+});
+
new Vue({
el: '#app',
store, // inject the Vuex store into all components
diff --git a/tools/winscope/src/mixins/SaveAsZip.js b/tools/winscope/src/mixins/SaveAsZip.js
index dc57539..cf20fa4 100644
--- a/tools/winscope/src/mixins/SaveAsZip.js
+++ b/tools/winscope/src/mixins/SaveAsZip.js
@@ -57,6 +57,7 @@
},
async downloadAsZip(traces) {
const zip = new JSZip();
+ this.buttonClicked("Download All")
for (const trace of traces) {
const traceFolder = zip.folder(trace.type);
diff --git a/tools/winscope/src/mixins/Timeline.js b/tools/winscope/src/mixins/Timeline.js
index ec9cde9..444d525 100644
--- a/tools/winscope/src/mixins/Timeline.js
+++ b/tools/winscope/src/mixins/Timeline.js
@@ -142,10 +142,8 @@
timelineTransitions() {
const transitions = [];
- //group tags by transition 'id' property
- const groupedTags = _.mapValues(
- _.groupBy(this.tags, 'id'), clist => clist.map(tag => _.omit(tag, 'id')))
- ;
+ //group tags by transition and 'id' property
+ const groupedTags = _.groupBy(this.tags, tag => `"${tag.transition} ${tag.id}"`);
for (const transitionId in groupedTags) {
const id = groupedTags[transitionId];
@@ -154,46 +152,56 @@
const startTimes = id.filter(tag => tag.isStartTag).map(tag => tag.timestamp);
const endTimes = id.filter(tag => !tag.isStartTag).map(tag => tag.timestamp);
- const transitionStartTime = Math.min(startTimes);
- const transitionEndTime = Math.max(endTimes);
+ const transitionStartTime = Math.min(...startTimes);
+ const transitionEndTime = Math.max(...endTimes);
//do not freeze new transition, as overlap still to be handled (defaulted to 0)
const transition = this.generateTransition(
transitionStartTime,
transitionEndTime,
id[0].transition,
- 0
+ 0,
+ id[0].layerId,
+ id[0].taskId,
+ id[0].windowToken
);
transitions.push(transition);
}
//sort transitions in ascending start position in order to handle overlap
- transitions.sort((a, b) => (a.startPos > b.startPos) ? 1: -1);
+ transitions.sort((a, b) => (a.startPos > b.startPos) ? 1 : -1);
//compare each transition to the ones that came before
for (let curr=0; curr<transitions.length; curr++) {
- let overlapStore = [];
+ let processedTransitions = [];
for (let prev=0; prev<curr; prev++) {
- overlapStore.push(transitions[prev].overlap);
+ processedTransitions.push(transitions[prev]);
- if (transitions[prev].startPos <= transitions[curr].startPos
- && transitions[curr].startPos <= transitions[prev].startPos+transitions[prev].width
- && transitions[curr].overlap === transitions[prev].overlap) {
+ if (this.isSimultaneousTransition(transitions[curr], transitions[prev])) {
transitions[curr].overlap++;
}
}
- if (overlapStore.length>0
- && transitions[curr].overlap === Math.max(overlapStore)
- ) transitions[curr].overlap++;
+ let overlapStore = processedTransitions.map(transition => transition.overlap);
+
+ if (transitions[curr].overlap === Math.max(...overlapStore)) {
+ let previousTransition = processedTransitions.find(transition => {
+ return transition.overlap===transitions[curr].overlap;
+ });
+ if (this.isSimultaneousTransition(transitions[curr], previousTransition)) {
+ transitions[curr].overlap++;
+ }
+ }
}
return Object.freeze(transitions);
},
errorPositions() {
if (!this.flickerMode) return [];
- const errorPositions = this.errors.map(error => this.position(error.timestamp));
+ const errorPositions = this.errors.map(
+ error => ({ pos: this.position(error.timestamp), ts: error.timestamp })
+ );
return Object.freeze(errorPositions);
},
},
@@ -244,6 +252,12 @@
return this.position(endTs) - this.position(startTs) + this.pointWidth;
},
+ isSimultaneousTransition(currTransition, prevTransition) {
+ return prevTransition.startPos <= currTransition.startPos
+ && currTransition.startPos <= prevTransition.startPos+prevTransition.width
+ && currTransition.overlap === prevTransition.overlap;
+ },
+
/**
* Converts a position as a percentage of the timeline width to a timestamp.
* @param {number} position - target position as a percentage of the
@@ -342,6 +356,16 @@
},
/**
+ * Handles the error click event.
+ * When an error in the timeline is clicked this function will update the timeline
+ * to match the error timestamp.
+ * @param {number} errorTimestamp
+ */
+ onErrorClick(errorTimestamp) {
+ this.$store.dispatch('updateTimelineTime', errorTimestamp);
+ },
+
+ /**
* Generate a block object that can be used by the timeline SVG to render
* a transformed block that starts at `startTs` and ends at `endTs`.
* @param {number} startTs - The timestamp at which the block starts.
@@ -360,14 +384,24 @@
* @param {number} endTs - The timestamp at which the transition ends.
* @param {string} transitionType - The type of transition.
* @param {number} overlap - The degree to which the transition overlaps with others.
+ * @param {number} layerId - Helps determine if transition is associated with SF trace.
+ * @param {number} taskId - Helps determine if transition is associated with WM trace.
+ * @param {number} windowToken - Helps determine if transition is associated with WM trace.
* @return {Transition} A transition object transformed to the timeline's crop and
* scale parameter.
*/
- generateTransition(startTs, endTs, transitionType, overlap) {
+ generateTransition(startTs, endTs, transitionType, overlap, layerId, taskId, windowToken) {
const transitionWidth = this.objectWidth(startTs, endTs);
const transitionDesc = transitionMap.get(transitionType).desc;
const transitionColor = transitionMap.get(transitionType).color;
- const tooltip = `${transitionDesc}. Start: ${nanos_to_string(startTs)}. End: ${nanos_to_string(endTs)}.`;
+ var tooltip = `${transitionDesc}. Start: ${nanos_to_string(startTs)}. End: ${nanos_to_string(endTs)}.`;
+
+ if (layerId !== 0 && taskId === 0 && windowToken === "") {
+ tooltip += " SF only.";
+ } else if ((taskId !== 0 || windowToken !== "") && layerId === 0) {
+ tooltip += " WM only.";
+ }
+
return new Transition(this.position(startTs), startTs, endTs, transitionWidth, transitionColor, overlap, tooltip);
},
},
diff --git a/tools/winscope/src/traces/SurfaceFlinger.ts b/tools/winscope/src/traces/SurfaceFlinger.ts
index b3f123c..049dc68 100644
--- a/tools/winscope/src/traces/SurfaceFlinger.ts
+++ b/tools/winscope/src/traces/SurfaceFlinger.ts
@@ -20,11 +20,14 @@
export default class SurfaceFlinger extends TraceBase {
sfTraceFile: Object;
+ tagGenerationTrace: Object;
constructor(files) {
const sfTraceFile = files[FILE_TYPES.SURFACE_FLINGER_TRACE];
+ const tagGenerationTrace = files[FILE_TYPES.SURFACE_FLINGER_TRACE].tagGenerationTrace;
super(sfTraceFile.data, sfTraceFile.timeline, files);
+ this.tagGenerationTrace = tagGenerationTrace;
this.sfTraceFile = sfTraceFile;
}
diff --git a/tools/winscope/src/traces/WindowManager.ts b/tools/winscope/src/traces/WindowManager.ts
index 302eeca..eb41ad9 100644
--- a/tools/winscope/src/traces/WindowManager.ts
+++ b/tools/winscope/src/traces/WindowManager.ts
@@ -21,11 +21,14 @@
export default class WindowManager extends TraceBase {
wmTraceFile: Object;
+ tagGenerationTrace: Object;
constructor(files) {
const wmTraceFile = files[FILE_TYPES.WINDOW_MANAGER_TRACE];
+ const tagGenerationTrace = files[FILE_TYPES.WINDOW_MANAGER_TRACE].tagGenerationTrace;
super(wmTraceFile.data, wmTraceFile.timeline, files);
+ this.tagGenerationTrace = tagGenerationTrace;
this.wmTraceFile = wmTraceFile;
}
diff --git a/tools/winscope/src/transform.js b/tools/winscope/src/transform.js
index 96e4785..51bedaf 100644
--- a/tools/winscope/src/transform.js
+++ b/tools/winscope/src/transform.js
@@ -15,6 +15,7 @@
*/
import {DiffType} from './utils/diff.js';
+import {regExpTimestampSearch} from './utils/consts';
// kind - a type used for categorization of different levels
// name - name of the node
@@ -400,5 +401,18 @@
return {short: 'V', long: 'visible', class: 'default'};
}
+// Returns closest timestamp in timeline based on search input*/
+function getClosestTimestamp(searchInput, timeline) {
+ if (regExpTimestampSearch.test(searchInput)) {
+ var roundedTimestamp = parseInt(searchInput);
+ } else {
+ var roundedTimestamp = string_to_nanos(searchInput);
+ }
+ const closestTimestamp = timeline.reduce((prev, curr) => {
+ return Math.abs(curr-roundedTimestamp) < Math.abs(prev-roundedTimestamp) ? curr : prev;
+ });
+ return closestTimestamp;
+}
+
// eslint-disable-next-line camelcase
-export {transform, ObjectTransformer, nanos_to_string, string_to_nanos, get_visible_chip};
+export {transform, ObjectTransformer, nanos_to_string, string_to_nanos, get_visible_chip, getClosestTimestamp};
diff --git a/tools/winscope/src/utils/consts.js b/tools/winscope/src/utils/consts.js
index e43a2c1..ca71b1f 100644
--- a/tools/winscope/src/utils/consts.js
+++ b/tools/winscope/src/utils/consts.js
@@ -34,7 +34,8 @@
};
const SEARCH_TYPE = {
- TAG: 'Transitions and Errors',
+ TRANSITIONS: 'Transitions',
+ ERRORS: 'Errors',
TIMESTAMP: 'Timestamp',
};
@@ -51,11 +52,17 @@
[TransitionType.ROTATION, {desc: 'Rotation', color: '#9900ffff'}],
[TransitionType.PIP_ENTER, {desc: 'Entering PIP mode', color: '#4a86e8ff'}],
[TransitionType.PIP_RESIZE, {desc: 'Resizing PIP mode', color: '#2b9e94ff'}],
+ [TransitionType.PIP_CLOSE, {desc: 'Closing PIP mode', color: 'rgb(57, 57, 182)'}],
[TransitionType.PIP_EXIT, {desc: 'Exiting PIP mode', color: 'darkblue'}],
[TransitionType.APP_LAUNCH, {desc: 'Launching app', color: '#ef6befff'}],
[TransitionType.APP_CLOSE, {desc: 'Closing app', color: '#d10ddfff'}],
[TransitionType.IME_APPEAR, {desc: 'IME appearing', color: '#ff9900ff'}],
[TransitionType.IME_DISAPPEAR, {desc: 'IME disappearing', color: '#ad6800ff'}],
+ [TransitionType.APP_PAIRS_ENTER, {desc: 'Entering app pairs mode', color: 'rgb(58, 151, 39)'}],
+ [TransitionType.APP_PAIRS_EXIT, {desc: 'Exiting app pairs mode', color: 'rgb(45, 110, 32)'}],
])
-export { WebContentScriptMessageType, NAVIGATION_STYLE, SEARCH_TYPE, logLevel, transitionMap };
+//used to split timestamp search input by unit, to convert to nanoseconds
+const regExpTimestampSearch = new RegExp(/^\d+$/);
+
+export { WebContentScriptMessageType, NAVIGATION_STYLE, SEARCH_TYPE, logLevel, transitionMap, regExpTimestampSearch };
diff --git a/tools/winscope/src/utils/utils.js b/tools/winscope/src/utils/utils.js
index f8ec198..fae19f3 100644
--- a/tools/winscope/src/utils/utils.js
+++ b/tools/winscope/src/utils/utils.js
@@ -84,4 +84,21 @@
return parts.reverse().join('');
}
-export { DIRECTION, findLastMatchingSorted, stableIdCompatibilityFixup, nanosToString, TimeUnits }
\ No newline at end of file
+/** Checks for match in window manager properties taskId, layerId, or windowToken,
+ * or surface flinger property id
+ */
+function isPropertyMatch(flickerItem, entryItem) {
+ return flickerItem.taskId === entryItem.taskId ||
+ (flickerItem.windowToken === entryItem.windowToken) ||
+ ((flickerItem.layerId === entryItem.layerId) && flickerItem.layerId !== 0) ||
+ flickerItem.layerId === entryItem.id;
+}
+
+export {
+ DIRECTION,
+ findLastMatchingSorted,
+ isPropertyMatch,
+ stableIdCompatibilityFixup,
+ nanosToString,
+ TimeUnits
+}
\ No newline at end of file
diff --git a/tools/winscope/yarn.lock b/tools/winscope/yarn.lock
index aafaf3b..a25b413 100644
--- a/tools/winscope/yarn.lock
+++ b/tools/winscope/yarn.lock
@@ -7694,6 +7694,11 @@
node-fetch "^2.3.0"
tslib "^1.9.3"
+vue-gtag@^1.16.1:
+ version "1.16.1"
+ resolved "https://registry.yarnpkg.com/vue-gtag/-/vue-gtag-1.16.1.tgz#edb2f20ab4f6c4d4d372dfecf8c1fcc8ab890181"
+ integrity sha512-5vs0pSGxdqrfXqN1Qwt0ZFXG0iTYjRMu/saddc7QIC5yp+DKgjWQRpGYVa7Pq+KbThxwzzMfo0sGi7ISa6NowA==
+
vue-hot-reload-api@^2.3.0:
version "2.3.4"
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2"