| /* |
| * Copyright 2020, The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| import _ from "lodash"; |
| import { nanos_to_string } from "../transform"; |
| import { transitionMap } from "../utils/consts"; |
| |
| /** |
| * Represents a continuous section of the timeline that is rendered into the |
| * timeline svg. |
| */ |
| class Block { |
| /** |
| * Create a block. |
| * @param {number} startPos - The start position of the block as a percentage |
| * of the timeline width. |
| * @param {number} width - The width of the block as a percentage of the |
| * timeline width. |
| */ |
| constructor(startPos, width) { |
| this.startPos = startPos; |
| this.width = width; |
| } |
| } |
| |
| //Represents a continuous section of the tag display that relates to a specific transition |
| class Transition { |
| /** |
| * Create a transition. |
| * @param {number} startPos - The position of the start tag as a percentage |
| * of the timeline width. |
| * @param {number} startTime - The start timestamp in ms of the transition. |
| * @param {number} endTime - The end timestamp in ms of the transition. |
| * @param {number} width - The width of the transition as a percentage of the |
| * timeline width. |
| * @param {string} color - the color of transition depending on type. |
| * @param {number} overlap - number of transitions with which this transition overlaps. |
| * @param {string} tooltip - The tooltip of the transition, minus the type of transition. |
| */ |
| constructor(startPos, startTime, endTime, width, color, overlap, tooltip) { |
| this.startPos = startPos; |
| this.startTime = startTime; |
| this.endTime = endTime; |
| this.width = width; |
| this.color = color; |
| this.overlap = overlap; |
| this.tooltip = tooltip; |
| } |
| } |
| |
| /** |
| * This Mixin should only be injected into components which have the following: |
| * - An element in the template referenced as 'timeline' (this.$refs.timeline). |
| */ |
| |
| export default { |
| name: 'timeline', |
| props: { |
| /** |
| * A 'timeline' as an array of timestamps |
| */ |
| 'timeline': { |
| type: Array, |
| }, |
| /** |
| * A scale factor is an array of two elements, the min and max timestamps of |
| * the timeline |
| */ |
| 'scale': { |
| type: Array, |
| }, |
| 'tags': { |
| type: Array, |
| }, |
| 'errors': { |
| type: Array, |
| }, |
| 'flickerMode': { |
| type: Boolean, |
| } |
| }, |
| data() { |
| return { |
| /** |
| * Is a number representing the percentage of the timeline a block should |
| * be at a minimum or what percentage of the timeline a single entry takes |
| * up when rendered. |
| */ |
| pointWidth: 1, |
| }; |
| }, |
| computed: { |
| /** |
| * Converts the timeline (list of timestamps) to an array of blocks to be |
| * displayed. This is to have fewer elements in the rendered timeline. |
| * Instead of having one rect for each timestamp in the timeline we only |
| * have one for each continuous segment of the timeline. This is to improve |
| * both the Vue patching step's performance and the DOM rendering |
| * performance. |
| */ |
| timelineBlocks() { |
| const blocks = []; |
| |
| // The difference in time between two timestamps after which they are no |
| // longer rendered as a continuous segment/block. |
| const overlapDistanceInTs = (this.scale[1] - this.scale[0]) * |
| ((this.crop?.right ?? 1) - (this.crop?.left ?? 0)) * |
| 1 / (100 - this.pointWidth); |
| |
| let blockStartTs = this.timeline[0]; |
| for (let i = 1; i < this.timeline.length; i++) { |
| const lastTs = this.timeline[i - 1]; |
| const ts = this.timeline[i]; |
| if (ts - lastTs > overlapDistanceInTs) { |
| const block = this.generateTimelineBlock(blockStartTs, lastTs); |
| blocks.push(block); |
| blockStartTs = ts; |
| } |
| } |
| |
| const blockEndTs = this.timeline[this.timeline.length - 1]; |
| const block = this.generateTimelineBlock(blockStartTs, blockEndTs); |
| blocks.push(block); |
| |
| return Object.freeze(blocks); |
| }, |
| |
| //Generates list of transitions to be displayed in flicker mode |
| timelineTransitions() { |
| const transitions = []; |
| |
| //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]; |
| //there are at least two tags per id, maybe more if multiple traces |
| // determine which tag is the start (min of start times), which is end (max of end times) |
| 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); |
| |
| //do not freeze new transition, as overlap still to be handled (defaulted to 0) |
| const transition = this.generateTransition( |
| transitionStartTime, |
| transitionEndTime, |
| id[0].transition, |
| 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); |
| |
| //compare each transition to the ones that came before |
| for (let curr=0; curr<transitions.length; curr++) { |
| let processedTransitions = []; |
| |
| for (let prev=0; prev<curr; prev++) { |
| processedTransitions.push(transitions[prev]); |
| |
| if (this.isSimultaneousTransition(transitions[curr], transitions[prev])) { |
| 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 => ({ pos: this.position(error.timestamp), ts: error.timestamp }) |
| ); |
| return Object.freeze(errorPositions); |
| }, |
| }, |
| methods: { |
| position(item) { |
| let pos; |
| pos = this.translate(item); |
| pos = this.applyCrop(pos); |
| |
| return pos * (100 - this.pointWidth); |
| }, |
| |
| translate(cx) { |
| const scale = [...this.scale]; |
| if (scale[0] >= scale[1]) { |
| return cx; |
| } |
| |
| return (cx - scale[0]) / (scale[1] - scale[0]); |
| }, |
| |
| untranslate(pos) { |
| const scale = [...this.scale]; |
| if (scale[0] >= scale[1]) { |
| return pos; |
| } |
| |
| return pos * (scale[1] - scale[0]) + scale[0]; |
| }, |
| |
| applyCrop(cx) { |
| if (!this.crop) { |
| return cx; |
| } |
| |
| return (cx - this.crop.left) / (this.crop.right - this.crop.left); |
| }, |
| |
| unapplyCrop(pos) { |
| if (!this.crop) { |
| return pos; |
| } |
| |
| return pos * (this.crop.right - this.crop.left) + this.crop.left; |
| }, |
| |
| objectWidth(startTs, endTs) { |
| 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 |
| * timeline's width. |
| * @return {number} The index of the closest timestamp in the timeline to |
| * the target position. |
| */ |
| positionToTsIndex(position) { |
| let targetTimestamp = position / (100 - this.pointWidth); |
| targetTimestamp = this.unapplyCrop(targetTimestamp); |
| targetTimestamp = this.untranslate(targetTimestamp); |
| |
| // The index of the timestamp in the timeline that is closest to the |
| // targetTimestamp. |
| const closestTsIndex = this.findClosestTimestampIndexTo(targetTimestamp); |
| |
| return closestTsIndex; |
| }, |
| |
| indexOfClosestElementTo(target, array) { |
| let smallestDiff = Math.abs(target - array[0]); |
| let closestIndex = 0; |
| for (let i = 1; i < array.length; i++) { |
| const elem = array[i]; |
| if (Math.abs(target - elem) < smallestDiff) { |
| closestIndex = i; |
| smallestDiff = Math.abs(target - elem); |
| } |
| } |
| |
| return closestIndex; |
| }, |
| |
| findClosestTimestampIndexTo(ts) { |
| let left = 0; |
| let right = this.timeline.length - 1; |
| let mid = Math.floor((left + right) / 2); |
| |
| while (left < right) { |
| if (ts < this.timeline[mid]) { |
| right = mid - 1; |
| } else if (ts > this.timeline[mid]) { |
| left = mid + 1; |
| } else { |
| return mid; |
| } |
| mid = Math.floor((left + right) / 2); |
| } |
| |
| const candidateElements = this.timeline.slice(left - 1, right + 2); |
| const closestIndex = |
| this.indexOfClosestElementTo(ts, candidateElements) + (left - 1); |
| return closestIndex; |
| }, |
| |
| /** |
| * Transforms an absolute position in the timeline to a timestamp present in |
| * the timeline. |
| * @param {number} absolutePosition - Pixels from the left of the timeline. |
| * @return {number} The timestamp in the timeline that is closest to the |
| * target position. |
| */ |
| absolutePositionAsTimestamp(absolutePosition) { |
| const timelineWidth = this.$refs.timeline.clientWidth; |
| const position = (absolutePosition / timelineWidth) * 100; |
| |
| return this.timeline[this.positionToTsIndex(position)]; |
| }, |
| |
| /** |
| * Handles the block click event. |
| * When a block in the timeline is clicked this function will determine |
| * the target timeline index and update the timeline to match this index. |
| * @param {MouseEvent} e - The mouse event of the click on a timeline block. |
| */ |
| onBlockClick(e) { |
| const clickOffset = e.offsetX; |
| const timelineWidth = this.$refs.timeline.clientWidth; |
| const clickOffsetAsPercentage = (clickOffset / timelineWidth) * 100; |
| |
| const clickedOnTsIndex = |
| this.positionToTsIndex(clickOffsetAsPercentage - this.pointWidth / 2); |
| |
| if (this.disabled) { |
| return; |
| } |
| |
| var timestamp = parseInt(this.timeline[clickedOnTsIndex]); |
| |
| //pointWidth is always 1 |
| //if offset percentage < 1, clickedOnTsIndex becomes negative, leading to a negative index |
| if (clickedOnTsIndex < 0) { |
| timestamp = parseInt(this.timeline[0]) |
| } |
| 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`. |
| * @param {number} startTs - The timestamp at which the block starts. |
| * @param {number} endTs - The timestamp at which the block ends. |
| * @return {Block} A block object transformed to the timeline's crop and |
| * scale parameter. |
| */ |
| generateTimelineBlock(startTs, endTs) { |
| const blockWidth = this.objectWidth(startTs, endTs); |
| return Object.freeze(new Block(this.position(startTs), blockWidth)); |
| }, |
| /** |
| * Generate a transition object that can be used by the tag-timeline to render |
| * a transformed transition that starts at `startTs` and ends at `endTs`. |
| * @param {number} startTs - The timestamp at which the transition starts. |
| * @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, layerId, taskId, windowToken) { |
| const transitionWidth = this.objectWidth(startTs, endTs); |
| const transitionDesc = transitionMap.get(transitionType).desc; |
| const transitionColor = transitionMap.get(transitionType).color; |
| 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); |
| }, |
| }, |
| }; |