[DO NOT MERGE] Sync flicker from master to sc-v2

Flicker on master diverged form sc-v2, to make it easier to debug
flicker issues on sc-v2, push the current version of flicker into sc-v2

Test: atest FlickerTests WMShellFlickerTests
Bug: 183993924
Change-Id: I0bd06578e7c93271d4f84361c15818385a8a4fdb
This commit is contained in:
Nataniel Borges
2021-09-22 09:19:41 +00:00
parent 325b0476f5
commit 3907154c2c
32 changed files with 668 additions and 250 deletions

View File

@@ -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"
},

View File

@@ -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("Tag Transformation", () => {
})
});
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 @@ describe("Error Transformation", () => {
expect(data.entries[1].errors).toEqual([new Error("","",66,"",66)]);
expect(data.entries[2].errors).toEqual([new Error("","",99,"",99)]);
})
});
});

View File

@@ -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 FocusedDataViewFinder from './mixins/FocusedDataViewFinder';
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 @@ export default {
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 @@ export default {
},
/** 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 @@ export default {
//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 @@ export default {
},
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 @@ export default {
},
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 @@ export default {
},
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 @@ export default {
`${ 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 @@ export default {
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() {

View File

@@ -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 @@ export default {
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 @@ export default {
}, 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 @@ export default {
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 @@ export default {
return this.selectedDevice;
},
restart() {
this.buttonClicked("Connect / Retry");
this.status = STATES.CONNECTING;
},
resetLastDevice() {
this.buttonClicked("Change Device");
this.adbStore.lastDevice = '';
this.restart();
},

View File

@@ -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 @@ export default {
},
hideSnackbarMessage() {
this.showSnackbar = false;
this.buttonClicked("Hide Snackbar Message")
},
getFetchFilesLoadingAnimation() {
let frame = 0;
@@ -226,20 +236,24 @@ export default {
});
}
},
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 @@ export default {
},
};
</script>
</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>

View File

@@ -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 @@ export default {
// 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() {

View File

@@ -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 @@ export default {
transitionTooltip(transition) {
return transitionMap.get(transition).desc;
},
errorTooltip(errorMessage) {
if (errorMessage.length>100) {
return `Error: ${errorMessage.substring(0,100)}...`;
}
return `Error: ${errorMessage}`;
},
},
components: {
Arrow,

View File

@@ -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 MdIconOption from './components/IconSelection/IconSelectOption.vue';
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 @@ export default {
cropIntent: null,
TRACE_ICONS,
search: false,
tags: [],
errors: [],
searchInput: "",
isSeekTimeInputMode: false,
};
},
created() {
@@ -326,6 +344,7 @@ export default {
},
destroyed() {
this.$store.commit('removeMergedTimeline', this.mergedTimeline);
this.updateInputMode(false);
},
watch: {
navigationStyle(style) {
@@ -433,8 +452,6 @@ export default {
}
},
minimizedTimeline() {
this.updateFlickerMode(this.navigationStyle);
if (this.navigationStyle === NAVIGATION_STYLE.GLOBAL) {
return this.mergedTimeline;
}
@@ -471,7 +488,7 @@ export default {
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 @@ export default {
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 @@ export default {
},
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 @@ export default {
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 @@ export default {
color: rgba(0,0,0,0.54);
font-size: 12px;
font-family: inherit;
cursor: text;
}
.minimized-timeline-content .minimized-timeline {
@@ -921,6 +927,27 @@ export default {
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;
}

View File

@@ -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 @@ export default {
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 @@ export default {
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 @@ export default {
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 @@ export default {
/** 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 @@ export default {
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 @@ export default {
});
},
},
destroyed() {
this.updateInputMode(false);
},
};
</script>
<style>
<style scoped>
.searchbar {
background-color: rgb(250, 243, 233) !important;
top: 0;
@@ -241,8 +276,14 @@ export default {
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 @@ export default {
.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 @@ export default {
.dropdown-content table {
overflow-y: scroll;
height: 150px;
max-height: 150px;
display: block;
}
@@ -283,7 +331,7 @@ export default {
}
.inline-time:hover {
background: #9af39f;
background: rgb(216, 250, 218);
cursor: pointer;
}
@@ -292,7 +340,7 @@ export default {
}
.inline-transition:hover {
background: #9af39f;
background: rgb(216, 250, 218);
cursor: pointer;
}
@@ -302,7 +350,7 @@ export default {
}
.inline-error:hover {
background: #9af39f;
background: rgb(216, 250, 218);
cursor: pointer;
}
</style>

View File

@@ -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 @@ export default {
}
.error {
stroke: rgb(255, 0, 0);
stroke-width: 2px;
stroke-width: 8px;
cursor: pointer;
}
</style>

View File

@@ -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 PropertiesTreeElement from './PropertiesTreeElement.vue';
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 @@ export default {
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 @@ export default {
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]);

View File

@@ -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 @@ export default {
},
toggleTree() {
this.setCollapseValue(!this.isCollapsed);
if (!this.isCollapsed) {
this.openedToSeeAttributeField(this.item.name)
}
},
expandTree() {
this.setCollapseValue(false);
@@ -446,17 +450,13 @@ export default {
}
},
/** 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 @@ export default {
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 @@ export default {
return transitions;
},
getCurrentErrorTags() {
return this.currentErrors.filter(error => this.isIdMatch(error, this.item));
return this.currentErrors.filter(error => isPropertyMatch(error, this.item));
},
},
computed: {

View File

@@ -554,6 +554,14 @@ function decodeAndTransformProto(buffer, params, displayDefaults) {
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 @@ function protoDecoder(buffer, params, fileName, store) {
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 @@ function videoDecoder(buffer, params, fileName, store) {
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 @@ function dataFile(filename, timeline, data, blobUrl, type) {
timeline: Object.freeze(timeline),
data: data,
blobUrl: blobUrl,
tagGenerationTrace: tagGenerationTrace,
type: type,
selectedIndex: 0,
destroy() {
@@ -678,4 +695,17 @@ function detectAndDecode(buffer, fileName, store) {
*/
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
};

View File

@@ -46,7 +46,7 @@ WindowManagerState.fromProto = function (proto: any, timestamp: number = 0, wher
proto.rootWindowContainer.pendingActivities.map(it => it.title),
rootWindowContainer,
keyguardControllerState,
timestamp = timestamp
/*timestamp */ `${timestamp}`
);
addAttributes(entry, proto);

View File

@@ -88,6 +88,9 @@ const Error = require('flicker').com.android.server.wm.traces.common.errors.Erro
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 @@ export {
Rect,
RectF,
Region,
// Service
TaggingEngine,
toSize,
toBuffer,
toColor,

View File

@@ -19,7 +19,7 @@ import Error from './Error';
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;
}

View File

@@ -22,11 +22,14 @@ const transitionTypeMap = new Map([
['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 {

View File

@@ -19,7 +19,7 @@ import Tag from './Tag';
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;
}

View File

@@ -18,11 +18,14 @@ enum TransitionType {
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;

View File

@@ -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 @@ function addAttributes(entry: Activity, proto: any) {
entry.proto = proto;
entry.kind = entry.constructor.name;
entry.shortName = shortenName(entry.name);
entry.chips = entry.isVisible ? [VISIBLE_CHIP] : [];
}
export default Activity;

View File

@@ -58,6 +58,7 @@ WindowContainer.fromProto = function (
name,
token,
proto.orientation,
proto.surfaceControl?.layerId ?? 0,
proto.visible,
config,
children

View File

@@ -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 @@ const store = new Vuex.Store({
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 @@ const store = new Vuex.Store({
}
})
/**
* 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

View File

@@ -57,6 +57,7 @@ export default {
},
async downloadAsZip(traces) {
const zip = new JSZip();
this.buttonClicked("Download All")
for (const trace of traces) {
const traceFolder = zip.folder(trace.type);

View File

@@ -142,10 +142,8 @@ export default {
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 @@ export default {
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 @@ export default {
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
@@ -341,6 +355,16 @@ export default {
this.$store.dispatch('updateTimelineTime', timestamp);
},
/**
* 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`.
@@ -360,14 +384,24 @@ export default {
* @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);
},
},

View File

@@ -20,11 +20,14 @@ import { LayersTrace } from '@/flickerlib';
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;
}

View File

@@ -21,11 +21,14 @@ import { WindowManagerTrace } from '@/flickerlib';
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;
}

View File

@@ -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 @@ function get_visible_chip() {
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};

View File

@@ -34,7 +34,8 @@ const NAVIGATION_STYLE = {
};
const SEARCH_TYPE = {
TAG: 'Transitions and Errors',
TRANSITIONS: 'Transitions',
ERRORS: 'Errors',
TIMESTAMP: 'Timestamp',
};
@@ -51,11 +52,17 @@ const transitionMap = new Map([
[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 };

View File

@@ -84,4 +84,21 @@ function nanosToString(elapsedRealtimeNanos, precision) {
return parts.reverse().join('');
}
export { DIRECTION, findLastMatchingSorted, stableIdCompatibilityFixup, nanosToString, TimeUnits }
/** 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
}

View File

@@ -7694,6 +7694,11 @@ vue-github-buttons@^3.1.0:
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"