1/* 2 * Copyright 2020, The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import _ from "lodash"; 18import { nanos_to_string } from "../transform"; 19import { transitionMap } from "../utils/consts"; 20 21/** 22 * Represents a continuous section of the timeline that is rendered into the 23 * timeline svg. 24 */ 25class Block { 26 /** 27 * Create a block. 28 * @param {number} startPos - The start position of the block as a percentage 29 * of the timeline width. 30 * @param {number} width - The width of the block as a percentage of the 31 * timeline width. 32 */ 33 constructor(startPos, width) { 34 this.startPos = startPos; 35 this.width = width; 36 } 37} 38 39//Represents a continuous section of the tag display that relates to a specific transition 40class Transition { 41 /** 42 * Create a transition. 43 * @param {number} startPos - The position of the start tag as a percentage 44 * of the timeline width. 45 * @param {number} startTime - The start timestamp in ms of the transition. 46 * @param {number} endTime - The end timestamp in ms of the transition. 47 * @param {number} width - The width of the transition as a percentage of the 48 * timeline width. 49 * @param {string} color - the color of transition depending on type. 50 * @param {number} overlap - number of transitions with which this transition overlaps. 51 * @param {string} tooltip - The tooltip of the transition, minus the type of transition. 52 */ 53 constructor(startPos, startTime, endTime, width, color, overlap, tooltip) { 54 this.startPos = startPos; 55 this.startTime = startTime; 56 this.endTime = endTime; 57 this.width = width; 58 this.color = color; 59 this.overlap = overlap; 60 this.tooltip = tooltip; 61 } 62} 63 64/** 65 * This Mixin should only be injected into components which have the following: 66 * - An element in the template referenced as 'timeline' (this.$refs.timeline). 67 */ 68 69export default { 70 name: 'timeline', 71 props: { 72 /** 73 * A 'timeline' as an array of timestamps 74 */ 75 'timeline': { 76 type: Array, 77 }, 78 /** 79 * A scale factor is an array of two elements, the min and max timestamps of 80 * the timeline 81 */ 82 'scale': { 83 type: Array, 84 }, 85 'tags': { 86 type: Array, 87 }, 88 'errors': { 89 type: Array, 90 }, 91 'flickerMode': { 92 type: Boolean, 93 } 94 }, 95 data() { 96 return { 97 /** 98 * Is a number representing the percentage of the timeline a block should 99 * be at a minimum or what percentage of the timeline a single entry takes 100 * up when rendered. 101 */ 102 pointWidth: 1, 103 }; 104 }, 105 computed: { 106 /** 107 * Converts the timeline (list of timestamps) to an array of blocks to be 108 * displayed. This is to have fewer elements in the rendered timeline. 109 * Instead of having one rect for each timestamp in the timeline we only 110 * have one for each continuous segment of the timeline. This is to improve 111 * both the Vue patching step's performance and the DOM rendering 112 * performance. 113 */ 114 timelineBlocks() { 115 const blocks = []; 116 117 // The difference in time between two timestamps after which they are no 118 // longer rendered as a continuous segment/block. 119 const overlapDistanceInTs = (this.scale[1] - this.scale[0]) * 120 ((this.crop?.right ?? 1) - (this.crop?.left ?? 0)) * 121 1 / (100 - this.pointWidth); 122 123 let blockStartTs = this.timeline[0]; 124 for (let i = 1; i < this.timeline.length; i++) { 125 const lastTs = this.timeline[i - 1]; 126 const ts = this.timeline[i]; 127 if (ts - lastTs > overlapDistanceInTs) { 128 const block = this.generateTimelineBlock(blockStartTs, lastTs); 129 blocks.push(block); 130 blockStartTs = ts; 131 } 132 } 133 134 const blockEndTs = this.timeline[this.timeline.length - 1]; 135 const block = this.generateTimelineBlock(blockStartTs, blockEndTs); 136 blocks.push(block); 137 138 return Object.freeze(blocks); 139 }, 140 141 //Generates list of transitions to be displayed in flicker mode 142 timelineTransitions() { 143 const transitions = []; 144 145 //group tags by transition and 'id' property 146 const groupedTags = _.groupBy(this.tags, tag => `"${tag.transition} ${tag.id}"`); 147 148 for (const transitionId in groupedTags) { 149 const id = groupedTags[transitionId]; 150 //there are at least two tags per id, maybe more if multiple traces 151 // determine which tag is the start (min of start times), which is end (max of end times) 152 const startTimes = id.filter(tag => tag.isStartTag).map(tag => tag.timestamp); 153 const endTimes = id.filter(tag => !tag.isStartTag).map(tag => tag.timestamp); 154 155 const transitionStartTime = Math.min(...startTimes); 156 const transitionEndTime = Math.max(...endTimes); 157 158 //do not freeze new transition, as overlap still to be handled (defaulted to 0) 159 const transition = this.generateTransition( 160 transitionStartTime, 161 transitionEndTime, 162 id[0].transition, 163 0, 164 id[0].layerId, 165 id[0].taskId, 166 id[0].windowToken 167 ); 168 transitions.push(transition); 169 } 170 171 //sort transitions in ascending start position in order to handle overlap 172 transitions.sort((a, b) => (a.startPos > b.startPos) ? 1 : -1); 173 174 //compare each transition to the ones that came before 175 for (let curr=0; curr<transitions.length; curr++) { 176 let processedTransitions = []; 177 178 for (let prev=0; prev<curr; prev++) { 179 processedTransitions.push(transitions[prev]); 180 181 if (this.isSimultaneousTransition(transitions[curr], transitions[prev])) { 182 transitions[curr].overlap++; 183 } 184 } 185 186 let overlapStore = processedTransitions.map(transition => transition.overlap); 187 188 if (transitions[curr].overlap === Math.max(...overlapStore)) { 189 let previousTransition = processedTransitions.find(transition => { 190 return transition.overlap===transitions[curr].overlap; 191 }); 192 if (this.isSimultaneousTransition(transitions[curr], previousTransition)) { 193 transitions[curr].overlap++; 194 } 195 } 196 } 197 198 return Object.freeze(transitions); 199 }, 200 errorPositions() { 201 if (!this.flickerMode) return []; 202 const errorPositions = this.errors.map( 203 error => ({ pos: this.position(error.timestamp), ts: error.timestamp }) 204 ); 205 return Object.freeze(errorPositions); 206 }, 207 }, 208 methods: { 209 position(item) { 210 let pos; 211 pos = this.translate(item); 212 pos = this.applyCrop(pos); 213 214 return pos * (100 - this.pointWidth); 215 }, 216 217 translate(cx) { 218 const scale = [...this.scale]; 219 if (scale[0] >= scale[1]) { 220 return cx; 221 } 222 223 return (cx - scale[0]) / (scale[1] - scale[0]); 224 }, 225 226 untranslate(pos) { 227 const scale = [...this.scale]; 228 if (scale[0] >= scale[1]) { 229 return pos; 230 } 231 232 return pos * (scale[1] - scale[0]) + scale[0]; 233 }, 234 235 applyCrop(cx) { 236 if (!this.crop) { 237 return cx; 238 } 239 240 return (cx - this.crop.left) / (this.crop.right - this.crop.left); 241 }, 242 243 unapplyCrop(pos) { 244 if (!this.crop) { 245 return pos; 246 } 247 248 return pos * (this.crop.right - this.crop.left) + this.crop.left; 249 }, 250 251 objectWidth(startTs, endTs) { 252 return this.position(endTs) - this.position(startTs) + this.pointWidth; 253 }, 254 255 isSimultaneousTransition(currTransition, prevTransition) { 256 return prevTransition.startPos <= currTransition.startPos 257 && currTransition.startPos <= prevTransition.startPos+prevTransition.width 258 && currTransition.overlap === prevTransition.overlap; 259 }, 260 261 /** 262 * Converts a position as a percentage of the timeline width to a timestamp. 263 * @param {number} position - target position as a percentage of the 264 * timeline's width. 265 * @return {number} The index of the closest timestamp in the timeline to 266 * the target position. 267 */ 268 positionToTsIndex(position) { 269 let targetTimestamp = position / (100 - this.pointWidth); 270 targetTimestamp = this.unapplyCrop(targetTimestamp); 271 targetTimestamp = this.untranslate(targetTimestamp); 272 273 // The index of the timestamp in the timeline that is closest to the 274 // targetTimestamp. 275 const closestTsIndex = this.findClosestTimestampIndexTo(targetTimestamp); 276 277 return closestTsIndex; 278 }, 279 280 indexOfClosestElementTo(target, array) { 281 let smallestDiff = Math.abs(target - array[0]); 282 let closestIndex = 0; 283 for (let i = 1; i < array.length; i++) { 284 const elem = array[i]; 285 if (Math.abs(target - elem) < smallestDiff) { 286 closestIndex = i; 287 smallestDiff = Math.abs(target - elem); 288 } 289 } 290 291 return closestIndex; 292 }, 293 294 findClosestTimestampIndexTo(ts) { 295 let left = 0; 296 let right = this.timeline.length - 1; 297 let mid = Math.floor((left + right) / 2); 298 299 while (left < right) { 300 if (ts < this.timeline[mid]) { 301 right = mid - 1; 302 } else if (ts > this.timeline[mid]) { 303 left = mid + 1; 304 } else { 305 return mid; 306 } 307 mid = Math.floor((left + right) / 2); 308 } 309 310 const candidateElements = this.timeline.slice(left - 1, right + 2); 311 const closestIndex = 312 this.indexOfClosestElementTo(ts, candidateElements) + (left - 1); 313 return closestIndex; 314 }, 315 316 /** 317 * Transforms an absolute position in the timeline to a timestamp present in 318 * the timeline. 319 * @param {number} absolutePosition - Pixels from the left of the timeline. 320 * @return {number} The timestamp in the timeline that is closest to the 321 * target position. 322 */ 323 absolutePositionAsTimestamp(absolutePosition) { 324 const timelineWidth = this.$refs.timeline.clientWidth; 325 const position = (absolutePosition / timelineWidth) * 100; 326 327 return this.timeline[this.positionToTsIndex(position)]; 328 }, 329 330 /** 331 * Handles the block click event. 332 * When a block in the timeline is clicked this function will determine 333 * the target timeline index and update the timeline to match this index. 334 * @param {MouseEvent} e - The mouse event of the click on a timeline block. 335 */ 336 onBlockClick(e) { 337 const clickOffset = e.offsetX; 338 const timelineWidth = this.$refs.timeline.clientWidth; 339 const clickOffsetAsPercentage = (clickOffset / timelineWidth) * 100; 340 341 const clickedOnTsIndex = 342 this.positionToTsIndex(clickOffsetAsPercentage - this.pointWidth / 2); 343 344 if (this.disabled) { 345 return; 346 } 347 348 var timestamp = parseInt(this.timeline[clickedOnTsIndex]); 349 350 //pointWidth is always 1 351 //if offset percentage < 1, clickedOnTsIndex becomes negative, leading to a negative index 352 if (clickedOnTsIndex < 0) { 353 timestamp = parseInt(this.timeline[0]) 354 } 355 this.$store.dispatch('updateTimelineTime', timestamp); 356 }, 357 358 /** 359 * Handles the error click event. 360 * When an error in the timeline is clicked this function will update the timeline 361 * to match the error timestamp. 362 * @param {number} errorTimestamp 363 */ 364 onErrorClick(errorTimestamp) { 365 this.$store.dispatch('updateTimelineTime', errorTimestamp); 366 }, 367 368 /** 369 * Generate a block object that can be used by the timeline SVG to render 370 * a transformed block that starts at `startTs` and ends at `endTs`. 371 * @param {number} startTs - The timestamp at which the block starts. 372 * @param {number} endTs - The timestamp at which the block ends. 373 * @return {Block} A block object transformed to the timeline's crop and 374 * scale parameter. 375 */ 376 generateTimelineBlock(startTs, endTs) { 377 const blockWidth = this.objectWidth(startTs, endTs); 378 return Object.freeze(new Block(this.position(startTs), blockWidth)); 379 }, 380 /** 381 * Generate a transition object that can be used by the tag-timeline to render 382 * a transformed transition that starts at `startTs` and ends at `endTs`. 383 * @param {number} startTs - The timestamp at which the transition starts. 384 * @param {number} endTs - The timestamp at which the transition ends. 385 * @param {string} transitionType - The type of transition. 386 * @param {number} overlap - The degree to which the transition overlaps with others. 387 * @param {number} layerId - Helps determine if transition is associated with SF trace. 388 * @param {number} taskId - Helps determine if transition is associated with WM trace. 389 * @param {number} windowToken - Helps determine if transition is associated with WM trace. 390 * @return {Transition} A transition object transformed to the timeline's crop and 391 * scale parameter. 392 */ 393 generateTransition(startTs, endTs, transitionType, overlap, layerId, taskId, windowToken) { 394 const transitionWidth = this.objectWidth(startTs, endTs); 395 const transitionDesc = transitionMap.get(transitionType).desc; 396 const transitionColor = transitionMap.get(transitionType).color; 397 var tooltip = `${transitionDesc}. Start: ${nanos_to_string(startTs)}. End: ${nanos_to_string(endTs)}.`; 398 399 if (layerId !== 0 && taskId === 0 && windowToken === "") { 400 tooltip += " SF only."; 401 } else if ((taskId !== 0 || windowToken !== "") && layerId === 0) { 402 tooltip += " WM only."; 403 } 404 405 return new Transition(this.position(startTs), startTs, endTs, transitionWidth, transitionColor, overlap, tooltip); 406 }, 407 }, 408};