Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright 2017 the V8 project authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 "use strict" | |
| 6 | |
| 7 let components = []; | |
| 8 | |
| 9 function createViews() { | |
| 10 components.push(new CallTreeView()); | |
| 11 components.push(new TimelineView()); | |
| 12 components.push(new HelpView()); | |
| 13 | |
| 14 let modeBar = document.getElementById("mode-bar"); | |
|
Camillo Bruni
2017/03/03 17:48:57
I always define a toplevel $ function which does t
Jarin
2017/03/04 11:21:23
Done.
| |
| 15 | |
| 16 function addMode(id, text, active) { | |
| 17 let div = document.createElement("div"); | |
| 18 div.classList = "mode-button" + (active ? " active-mode-button" : ""); | |
| 19 div.id = "mode-" + id; | |
| 20 div.textContent = text; | |
| 21 div.onclick = () => { | |
| 22 if (main.currentState.callTree.mode === id) return; | |
| 23 let old = document.getElementById("mode-" + main.currentState.callTree.mod e); | |
| 24 old.classList = "mode-button"; | |
| 25 div.classList = "mode-button active-mode-button"; | |
| 26 main.setMode(id); | |
| 27 }; | |
| 28 modeBar.appendChild(div); | |
| 29 } | |
| 30 | |
| 31 addMode("bottom-up", "Bottom up", true); | |
| 32 addMode("top-down", "Top down"); | |
| 33 addMode("function-list", "Functions"); | |
| 34 | |
| 35 main.setMode("bottom-up"); | |
| 36 } | |
| 37 | |
| 38 function emptyState() { | |
| 39 return { | |
| 40 file : null, | |
| 41 start : 0, | |
| 42 end : Infinity, | |
| 43 timeLine : { | |
| 44 width : 100, | |
| 45 height : 100 | |
| 46 }, | |
| 47 callTree : { | |
| 48 mode : "none", | |
| 49 attribution : "js-exclude-bc", | |
| 50 categories : "code-type", | |
| 51 sort : "time" | |
| 52 } | |
| 53 }; | |
| 54 } | |
| 55 | |
| 56 function setCallTreeState(state, callTreeState) { | |
| 57 state = Object.assign({}, state); | |
| 58 state.callTree = callTreeState; | |
| 59 return state; | |
| 60 } | |
| 61 | |
| 62 let main = { | |
| 63 currentState : emptyState(), | |
| 64 | |
| 65 setMode : function (mode) { | |
|
Camillo Bruni
2017/03/03 17:48:57
no need to add function :) { setMode(mode) { ... }
Jarin
2017/03/04 11:21:23
Whoa, I had no idea this was possible.
Done.
| |
| 66 if (mode != main.currentState.mode) { | |
| 67 let callTreeState = Object.assign({}, main.currentState.callTree); | |
| 68 callTreeState.mode = mode; | |
| 69 switch (mode) { | |
| 70 case "bottom-up": | |
| 71 callTreeState.attribution = "js-exclude-bc"; | |
| 72 callTreeState.categories = "code-type"; | |
| 73 callTreeState.sort = "time"; | |
| 74 break; | |
| 75 case "top-down": | |
| 76 callTreeState.attribution = "js-exclude-bc"; | |
| 77 callTreeState.categories = "none"; | |
| 78 callTreeState.sort = "time"; | |
| 79 break; | |
| 80 case "function-list": | |
| 81 callTreeState.attribution = "js-exclude-bc"; | |
| 82 callTreeState.categories = "none"; | |
| 83 callTreeState.sort = "own-time"; | |
| 84 break; | |
| 85 default: | |
| 86 console.error("Invalid mode"); | |
| 87 } | |
| 88 main.currentState = setCallTreeState(main.currentState, callTreeState); | |
| 89 main.delayRender(); | |
| 90 } | |
| 91 }, | |
| 92 | |
| 93 setCallTreeAttribution : function (attribution) { | |
| 94 if (attribution != main.currentState.attribution) { | |
| 95 let callTreeState = Object.assign({}, main.currentState.callTree); | |
| 96 callTreeState.attribution = attribution; | |
| 97 main.currentState = setCallTreeState(main.currentState, callTreeState); | |
|
Camillo Bruni
2017/03/03 17:48:57
I'm probably too pragmatic, but I'd just modify th
Jarin
2017/03/04 11:21:23
That might not work, the views remember the old st
| |
| 98 main.delayRender(); | |
| 99 } | |
| 100 }, | |
| 101 | |
| 102 setCallTreeSort : function (sort) { | |
| 103 if (sort != main.currentState.sort) { | |
| 104 let callTreeState = Object.assign({}, main.currentState.callTree); | |
| 105 callTreeState.sort = sort; | |
| 106 main.currentState = setCallTreeState(main.currentState, callTreeState); | |
| 107 main.delayRender(); | |
| 108 } | |
| 109 }, | |
| 110 | |
| 111 setCallTreeCategories : function (categories) { | |
| 112 if (categories != main.currentState.categories) { | |
| 113 let callTreeState = Object.assign({}, main.currentState.callTree); | |
| 114 callTreeState.categories = categories; | |
| 115 main.currentState = setCallTreeState(main.currentState, callTreeState); | |
| 116 main.delayRender(); | |
| 117 } | |
| 118 }, | |
| 119 | |
| 120 setViewInterval : function (start, end) { | |
| 121 if (start != main.currentState.start || | |
| 122 end != main.currentState.end) { | |
| 123 main.currentState = Object.assign({}, main.currentState); | |
| 124 main.currentState.start = start; | |
| 125 main.currentState.end = end; | |
| 126 main.delayRender(); | |
| 127 } | |
| 128 }, | |
| 129 | |
| 130 setTimeLineDimensions : (width, height) => { | |
| 131 if (width != main.currentState.timeLine.width || | |
| 132 height != main.currentState.timeLine.height) { | |
| 133 let timeLine = Object.assign({}, main.currentState.timeLine); | |
| 134 timeLine.width = width; | |
| 135 timeLine.height = height; | |
| 136 main.currentState = Object.assign({}, main.currentState); | |
| 137 main.currentState.timeLine = timeLine; | |
| 138 main.delayRender(); | |
| 139 } | |
| 140 }, | |
| 141 | |
| 142 setFile : (file) => { | |
| 143 if (file != main.currentState.file) { | |
| 144 main.currentState = Object.assign({}, main.currentState); | |
| 145 main.currentState.file = file; | |
| 146 main.delayRender(); | |
| 147 } | |
| 148 }, | |
| 149 | |
| 150 onResize : () => { | |
| 151 main.setTimeLineDimensions( | |
| 152 window.innerWidth - 20, window.innerHeight / 8); | |
| 153 }, | |
| 154 | |
| 155 onLoad : () => { | |
| 156 function loadHandler(evt) { | |
| 157 let f = evt.target.files[0]; | |
| 158 if (f) { | |
| 159 let reader = new FileReader(); | |
| 160 reader.onload = function(event) { | |
| 161 let profData = JSON.parse(event.target.result); | |
| 162 main.setViewInterval(0, Infinity); | |
| 163 main.setFile(profData); | |
| 164 }; | |
| 165 reader.onerror = function(event) { | |
| 166 console.error( | |
| 167 "File could not be read! Code " + event.target.error.code); | |
| 168 }; | |
| 169 reader.readAsText(f); | |
| 170 } else { | |
| 171 main.setFile(null); | |
| 172 } | |
| 173 } | |
| 174 document.getElementById("fileinput").addEventListener( | |
| 175 "change", loadHandler, false); | |
| 176 createViews(); | |
| 177 main.onResize(); | |
| 178 }, | |
| 179 | |
| 180 delayRender : function() { | |
| 181 Promise.resolve().then(() => { | |
|
Camillo Bruni
2017/03/03 17:48:57
pfff so modern, when I was young, setTimeout was t
| |
| 182 for (let c of components) { | |
| 183 c.render(main.currentState); | |
| 184 } | |
| 185 }); | |
| 186 } | |
| 187 }; | |
| 188 | |
| 189 let bucketDescriptors = | |
| 190 [ { kinds : [ "JSOPT" ], | |
| 191 color : "#ffb000", | |
| 192 backgroundColor : "#ffe0c0", | |
| 193 text : "JS Optimized" }, | |
| 194 { kinds : [ "JSUNOPT", "BC" ], | |
| 195 color : "#00ff00", | |
| 196 backgroundColor : "#c0ffc0", | |
| 197 text : "JS Unoptimized" }, | |
| 198 { kinds : [ "IC" ], | |
| 199 color : "#ffff00", | |
| 200 backgroundColor : "#ffffc0", | |
| 201 text : "IC" }, | |
| 202 { kinds : [ "STUB", "BUILTIN", "REGEXP" ], | |
| 203 color : "#ffb0b0", | |
| 204 backgroundColor : "#fff0f0", | |
| 205 text : "Other generated" }, | |
| 206 { kinds : [ "CPP", "LIB" ], | |
| 207 color : "#0000ff", | |
| 208 backgroundColor : "#c0c0ff", | |
| 209 text : "C++" }, | |
| 210 { kinds : [ "CPPEXT" ], | |
| 211 color : "#8080ff", | |
| 212 backgroundColor : "#e0e0ff", | |
| 213 text : "C++/external" }, | |
| 214 { kinds : [ "CPPCOMP" ], | |
| 215 color : "#00ffff", | |
| 216 backgroundColor : "#c0ffff", | |
| 217 text : "C++/Compiler" }, | |
| 218 { kinds : [ "CPPGC" ], | |
| 219 color : "#ff00ff", | |
| 220 backgroundColor : "#ffc0ff", | |
| 221 text : "C++/GC" }, | |
| 222 { kinds : [ "UNKNOWN" ], | |
| 223 color : "#f0f0f0", | |
| 224 backgroundColor : "#e0e0e0", | |
| 225 text : "Unknown" } | |
| 226 ]; | |
| 227 | |
| 228 function bucketFromKind(kind) { | |
| 229 for (let i = 0; i < bucketDescriptors.length; i++) { | |
| 230 let bucket = bucketDescriptors[i]; | |
| 231 for (let j = 0; j < bucket.kinds.length; j++) { | |
| 232 if (bucket.kinds[j] === kind) { | |
| 233 return bucket; | |
| 234 } | |
| 235 } | |
| 236 } | |
| 237 return null; | |
| 238 } | |
| 239 | |
| 240 class CallTreeView { | |
| 241 constructor() { | |
| 242 this.element = document.getElementById("calltree"); | |
| 243 this.treeElement = document.getElementById("calltree-table"); | |
| 244 this.selectAttribution = document.getElementById("calltree-attribution"); | |
| 245 this.selectCategories = | |
| 246 document.getElementById("calltree-categories"); | |
| 247 this.selectSort = document.getElementById("calltree-sort"); | |
| 248 | |
| 249 this.selectAttribution.onchange = () => { | |
| 250 main.setCallTreeAttribution(this.selectAttribution.value); | |
| 251 }; | |
| 252 | |
| 253 this.selectCategories.onchange = () => { | |
| 254 main.setCallTreeCategories(this.selectCategories.value); | |
| 255 }; | |
| 256 | |
| 257 this.selectSort.onchange = () => { | |
| 258 main.setCallTreeSort(this.selectSort.value); | |
| 259 }; | |
| 260 | |
| 261 this.currentState = null; | |
| 262 } | |
| 263 | |
| 264 filterFromFilterId(id) { | |
| 265 switch (id) { | |
| 266 case "full-tree": | |
| 267 return (type, kind) => true; | |
| 268 case "js-funs": | |
| 269 return (type, kind) => type !== 'CODE'; | |
| 270 case "js-exclude-bc": | |
| 271 return (type, kind) => | |
| 272 type !== 'CODE' || !CallTreeView.IsBytecodeHandler(kind); | |
| 273 } | |
| 274 } | |
| 275 | |
| 276 sortFromId(id) { | |
| 277 switch (id) { | |
| 278 case "time": | |
| 279 return (c1, c2) => c2.ticks - c1.ticks; | |
| 280 case "own-time": | |
| 281 return (c1, c2) => c2.ownTicks - c1.ownTicks; | |
| 282 case "category-time": | |
| 283 return (c1, c2) => { | |
| 284 if (c1.type === c2.type) return c2.ticks - c1.ticks; | |
| 285 if (c1.type < c2.type) return 1; | |
| 286 return -1; | |
| 287 }; | |
| 288 case "category-own-time": | |
| 289 return (c1, c2) => { | |
| 290 if (c1.type === c2.type) return c2.ownTicks - c1.ownTicks; | |
| 291 if (c1.type < c2.type) return 1; | |
| 292 return -1; | |
| 293 }; | |
| 294 } | |
| 295 } | |
| 296 | |
| 297 static IsBytecodeHandler(kind) { | |
| 298 return kind === "BytecodeHandler"; | |
| 299 } | |
| 300 | |
| 301 createExpander(indent) { | |
| 302 let div = document.createElement("div"); | |
| 303 div.style.width = (1 + indent) + "em"; | |
| 304 div.style.display = "inline-block"; | |
| 305 div.style.textAlign = "right"; | |
| 306 return div; | |
| 307 } | |
| 308 | |
| 309 codeTypeToText(type) { | |
| 310 switch (type) { | |
| 311 case "UNKNOWN": | |
| 312 return "Unknown"; | |
| 313 case "CPPCOMP": | |
| 314 return "C++ (compiler)"; | |
| 315 case "CPPGC": | |
| 316 return "C++"; | |
| 317 case "CPPEXT": | |
| 318 return "C++ External"; | |
| 319 case "CPP": | |
| 320 return "C++"; | |
| 321 case "LIB": | |
| 322 return "Library"; | |
| 323 case "IC": | |
| 324 return "IC"; | |
| 325 case "BC": | |
| 326 return "Bytecode"; | |
| 327 case "STUB": | |
| 328 return "Stub"; | |
| 329 case "BUILTIN": | |
| 330 return "Builtin"; | |
| 331 case "REGEXP": | |
| 332 return "RegExp"; | |
| 333 case "JSOPT": | |
| 334 return "JS opt"; | |
| 335 case "JSUNOPT": | |
| 336 return "JS unopt"; | |
| 337 } | |
| 338 console.error("Unknown type: " + type); | |
| 339 } | |
| 340 | |
| 341 createTypeDiv(type) { | |
| 342 if (type === "CAT") { | |
| 343 return document.createTextNode(""); | |
| 344 } | |
| 345 let div = document.createElement("div"); | |
| 346 div.classList.add("code-type-chip"); | |
| 347 | |
| 348 let span = document.createElement("span"); | |
| 349 span.classList.add("code-type-chip"); | |
| 350 span.textContent = this.codeTypeToText(type); | |
| 351 div.appendChild(span); | |
| 352 | |
| 353 span = document.createElement("span"); | |
| 354 span.classList.add("code-type-chip-space"); | |
| 355 div.appendChild(span); | |
| 356 | |
| 357 return div; | |
| 358 } | |
| 359 | |
| 360 expandTree(tree, indent) { | |
| 361 let that = this; | |
| 362 let index = 0; | |
| 363 let id = "R/"; | |
| 364 let row = tree.row; | |
| 365 let expander = tree.expander; | |
| 366 | |
| 367 if (row) { | |
| 368 console.assert("expander"); | |
| 369 index = row.rowIndex; | |
| 370 id = row.id; | |
| 371 | |
| 372 // Make sure we collapse the children when the row is clicked | |
| 373 // again. | |
| 374 expander.textContent = "\u25BE"; | |
| 375 let expandHandler = expander.onclick; | |
| 376 expander.onclick = () => { | |
| 377 that.collapseRow(tree, expander, expandHandler); | |
| 378 } | |
| 379 } | |
| 380 | |
| 381 // Collect the children, and sort them by ticks. | |
| 382 let children = []; | |
| 383 for (let child in tree.children) { | |
| 384 if (tree.children[child].ticks > 0) { | |
| 385 children.push(tree.children[child]); | |
| 386 } | |
| 387 } | |
| 388 children.sort(this.sortFromId(this.currentState.callTree.sort)); | |
| 389 | |
| 390 for (let i = 0; i < children.length; i++) { | |
| 391 let node = children[i]; | |
| 392 let row = this.rows.insertRow(index); | |
| 393 row.id = id + i + "/"; | |
| 394 | |
| 395 if (node.type != "CAT") { | |
| 396 row.style.backgroundColor = bucketFromKind(node.type).backgroundColor; | |
| 397 } | |
| 398 | |
| 399 // Inclusive time % cell. | |
| 400 let c = row.insertCell(); | |
| 401 c.textContent = (node.ticks * 100 / this.tickCount).toFixed(2) + "%"; | |
| 402 c.style.textAlign = "right"; | |
| 403 // Percent-of-parent cell. | |
| 404 c = row.insertCell(); | |
| 405 c.textContent = (node.ticks * 100 / tree.ticks).toFixed(2) + "%"; | |
| 406 c.style.textAlign = "right"; | |
| 407 // Exclusive time % cell. | |
| 408 if (this.currentState.callTree.mode !== "bottom-up") { | |
| 409 c = row.insertCell(-1); | |
| 410 c.textContent = (node.ownTicks * 100 / this.tickCount).toFixed(2) + "%"; | |
| 411 c.style.textAlign = "right"; | |
| 412 } | |
| 413 | |
| 414 // Create the name cell. | |
| 415 let nameCell = row.insertCell(); | |
| 416 let expander = this.createExpander(indent); | |
| 417 nameCell.appendChild(expander); | |
| 418 nameCell.appendChild(this.createTypeDiv(node.type)); | |
| 419 nameCell.appendChild(document.createTextNode(node.name)); | |
| 420 | |
| 421 // Inclusive ticks cell. | |
| 422 c = row.insertCell(); | |
| 423 c.textContent = node.ticks; | |
| 424 c.style.textAlign = "right"; | |
| 425 if (this.currentState.callTree.mode !== "bottom-up") { | |
| 426 // Exclusive ticks cell. | |
| 427 c = row.insertCell(-1); | |
| 428 c.textContent = node.ownTicks; | |
| 429 c.style.textAlign = "right"; | |
| 430 } | |
| 431 if (node.children.length > 0) { | |
| 432 expander.textContent = "\u25B8"; | |
| 433 expander.onclick = () => { that.expandTree(node, indent + 1); }; | |
| 434 } | |
| 435 | |
| 436 node.row = row; | |
| 437 node.expander = expander; | |
| 438 | |
| 439 index++; | |
| 440 } | |
| 441 } | |
| 442 | |
| 443 collapseRow(tree, expander, expandHandler) { | |
| 444 let row = tree.row; | |
| 445 let id = row.id; | |
| 446 let index = row.rowIndex; | |
| 447 while (row.rowIndex < this.rows.rows.length && | |
| 448 this.rows.rows[index].id.startsWith(id)) { | |
| 449 this.rows.deleteRow(index); | |
| 450 } | |
| 451 | |
| 452 expander.textContent = "\u25B8"; | |
| 453 expander.onclick = expandHandler; | |
| 454 } | |
| 455 | |
| 456 fillSelects(calltree) { | |
| 457 function addOptions(e, values, current) { | |
| 458 while (e.options.length > 0) { | |
| 459 e.remove(0); | |
| 460 } | |
| 461 for (let i = 0; i < values.length; i++) { | |
| 462 let option = document.createElement("option"); | |
| 463 option.value = values[i].value; | |
| 464 option.textContent = values[i].text; | |
| 465 e.appendChild(option); | |
| 466 } | |
| 467 e.value = current; | |
| 468 } | |
| 469 | |
| 470 let attributions = [ | |
| 471 { value : "js-exclude-bc", | |
| 472 text : "Attribute bytecode handlers to caller" }, | |
| 473 { value : "full-tree", | |
| 474 text : "Count each code object separately" }, | |
| 475 { value : "js-funs", | |
| 476 text : "Attribute non-functions to JS functions" } | |
| 477 ]; | |
| 478 | |
| 479 switch (calltree.mode) { | |
| 480 case "bottom-up": | |
| 481 addOptions(this.selectAttribution, attributions, calltree.attribution); | |
| 482 addOptions(this.selectCategories, [ | |
| 483 { value : "code-type", text : "Code type" }, | |
| 484 { value : "none", text : "None" } | |
| 485 ], calltree.categories); | |
| 486 addOptions(this.selectSort, [ | |
| 487 { value : "time", text : "Time (including children)" }, | |
| 488 { value : "category-time", text : "Code category, time" }, | |
| 489 ], calltree.sort); | |
| 490 return; | |
| 491 case "top-down": | |
| 492 addOptions(this.selectAttribution, attributions, calltree.attribution); | |
| 493 addOptions(this.selectCategories, [ | |
| 494 { value : "none", text : "None" } | |
| 495 ], calltree.categories); | |
| 496 addOptions(this.selectSort, [ | |
| 497 { value : "time", text : "Time (including children)" }, | |
| 498 { value : "own-time", text : "Own time" }, | |
| 499 { value : "category-time", text : "Code category, time" }, | |
| 500 { value : "category-own-time", text : "Code category, own time"} | |
| 501 ], calltree.sort); | |
| 502 return; | |
| 503 case "function-list": | |
| 504 addOptions(this.selectAttribution, attributions, calltree.attribution); | |
| 505 addOptions(this.selectCategories, [ | |
| 506 { value : "none", text : "None" } | |
| 507 ], calltree.categories); | |
| 508 addOptions(this.selectSort, [ | |
| 509 { value : "own-time", text : "Own time" }, | |
| 510 { value : "time", text : "Time (including children)" }, | |
| 511 { value : "category-own-time", text : "Code category, own time"}, | |
| 512 { value : "category-time", text : "Code category, time" }, | |
| 513 ], calltree.sort); | |
| 514 return; | |
| 515 } | |
| 516 console.error("Unexpected mode"); | |
| 517 } | |
| 518 | |
| 519 render(newState) { | |
| 520 let oldState = this.currentState; | |
| 521 if (!newState.file) { | |
| 522 this.element.style.display = "none"; | |
| 523 return; | |
| 524 } | |
| 525 | |
| 526 this.currentState = newState; | |
| 527 if (oldState) { | |
| 528 if (newState.file === oldState.file && | |
| 529 newState.start === oldState.start && | |
| 530 newState.end === oldState.end && | |
| 531 newState.callTree.mode === oldState.callTree.mode && | |
| 532 newState.callTree.attribution === oldState.callTree.attribution && | |
| 533 newState.callTree.categories === oldState.callTree.categories && | |
| 534 newState.callTree.sort === oldState.callTree.sort) { | |
| 535 // No change => just return. | |
| 536 return; | |
| 537 } | |
| 538 } | |
| 539 | |
| 540 this.element.style.display = "inherit"; | |
| 541 | |
| 542 let mode = this.currentState.callTree.mode; | |
| 543 if (!oldState || mode !== oldState.callTree.mode) { | |
| 544 // Technically, we should also call this if attribution, categories or | |
| 545 // sort change, but the selection is already highlighted by the combobox | |
| 546 // itself, so we do need to do anything here. | |
| 547 this.fillSelects(newState.callTree); | |
| 548 } | |
| 549 | |
| 550 let inclusiveDisplay = (mode === "bottom-up") ? "none" : "inherit"; | |
| 551 let ownTimeTh = | |
| 552 document.getElementById(this.treeElement.id + "-own-time-header"); | |
| 553 ownTimeTh.style.display = inclusiveDisplay; | |
| 554 let ownTicksTh = | |
| 555 document.getElementById(this.treeElement.id + "-own-ticks-header"); | |
| 556 ownTicksTh.style.display = inclusiveDisplay; | |
| 557 | |
| 558 // Build the tree. | |
| 559 let stackProcessor; | |
| 560 let filter = this.filterFromFilterId(this.currentState.callTree.attribution) ; | |
| 561 if (mode === "top-down") { | |
| 562 stackProcessor = | |
| 563 new PlainCallTreeProcessor(filter, false); | |
| 564 } else if (mode === "function-list") { | |
| 565 stackProcessor = | |
| 566 new FunctionListTree(filter); | |
| 567 | |
| 568 } else { | |
| 569 console.assert(mode === "bottom-up"); | |
| 570 if (this.currentState.callTree.categories == "none") { | |
| 571 stackProcessor = | |
| 572 new PlainCallTreeProcessor(filter, true); | |
| 573 } else { | |
| 574 console.assert(this.currentState.callTree.categories === "code-type"); | |
| 575 stackProcessor = | |
| 576 new CategorizedCallTreeProcessor(filter, true); | |
| 577 } | |
| 578 } | |
| 579 this.tickCount = | |
| 580 generateTree(this.currentState.file, | |
| 581 this.currentState.start, | |
| 582 this.currentState.end, | |
| 583 stackProcessor); | |
| 584 // TODO(jarin) Handle the case when tick count is negative. | |
| 585 | |
| 586 this.tree = stackProcessor.tree; | |
| 587 | |
| 588 // Remove old content of the table, replace with new one. | |
| 589 let oldRows = this.treeElement.getElementsByTagName("tbody"); | |
| 590 let newRows = document.createElement("tbody"); | |
| 591 this.rows = newRows; | |
| 592 | |
| 593 // Populate the table. | |
| 594 this.expandTree(this.tree, 0); | |
| 595 | |
| 596 // Swap in the new rows. | |
| 597 this.treeElement.replaceChild(newRows, oldRows[0]); | |
| 598 } | |
| 599 } | |
| 600 | |
| 601 class TimelineView { | |
| 602 constructor() { | |
| 603 this.element = document.getElementById("timeline"); | |
| 604 this.canvas = document.getElementById("timeline-canvas"); | |
| 605 this.legend = document.getElementById("timeline-legend"); | |
| 606 | |
| 607 this.canvas.onmousedown = this.onMouseDown.bind(this); | |
| 608 this.canvas.onmouseup = this.onMouseUp.bind(this); | |
| 609 this.canvas.onmousemove = this.onMouseMove.bind(this); | |
| 610 | |
| 611 this.selectionStart = null; | |
| 612 this.selectionEnd = null; | |
| 613 this.selecting = false; | |
| 614 | |
| 615 this.currentState = null; | |
| 616 } | |
| 617 | |
| 618 onMouseDown(e) { | |
| 619 this.selectionStart = | |
| 620 e.clientX - this.canvas.getBoundingClientRect().left; | |
| 621 this.selectionEnd = this.selectionStart + 1; | |
| 622 this.selecting = true; | |
| 623 } | |
| 624 | |
| 625 onMouseMove(e) { | |
| 626 if (this.selecting) { | |
| 627 this.selectionEnd = | |
| 628 e.clientX - this.canvas.getBoundingClientRect().left; | |
| 629 this.drawSelection(); | |
| 630 } | |
| 631 } | |
| 632 | |
| 633 onMouseUp(e) { | |
| 634 if (this.selectionStart !== null) { | |
| 635 let x = e.clientX - this.canvas.getBoundingClientRect().left; | |
| 636 if (Math.abs(x - this.selectionStart) < 10) { | |
| 637 this.selectionStart = null; | |
| 638 this.selectionEnd = null; | |
| 639 let ctx = this.canvas.getContext("2d"); | |
| 640 ctx.drawImage(this.buffer, 0, 0); | |
| 641 } else { | |
| 642 this.selectionEnd = x; | |
| 643 this.drawSelection(); | |
| 644 } | |
| 645 let file = this.currentState.file; | |
| 646 if (file) { | |
| 647 let start = this.selectionStart === null ? 0 : this.selectionStart; | |
| 648 let end = this.selectionEnd === null ? Infinity : this.selectionEnd; | |
| 649 let firstTime = file.ticks[0].tm; | |
| 650 let lastTime = file.ticks[file.ticks.length - 1].tm; | |
| 651 | |
| 652 let width = this.buffer.width; | |
| 653 | |
| 654 start = (start / width) * (lastTime - firstTime) + firstTime; | |
| 655 end = (end / width) * (lastTime - firstTime) + firstTime; | |
| 656 | |
| 657 if (end < start) { | |
| 658 let temp = start; | |
| 659 start = end; | |
| 660 end = temp; | |
| 661 } | |
| 662 | |
| 663 main.setViewInterval(start, end); | |
| 664 } | |
| 665 } | |
| 666 this.selecting = false; | |
| 667 } | |
| 668 | |
| 669 drawSelection() { | |
| 670 let ctx = this.canvas.getContext("2d"); | |
| 671 ctx.drawImage(this.buffer, 0, 0); | |
| 672 | |
| 673 if (this.selectionStart !== null && this.selectionEnd !== null) { | |
| 674 ctx.fillStyle = "rgba(0, 0, 0, 0.3)"; | |
| 675 let left = Math.min(this.selectionStart, this.selectionEnd); | |
| 676 let right = Math.max(this.selectionStart, this.selectionEnd); | |
| 677 ctx.fillRect(0, 0, left, this.buffer.height); | |
| 678 ctx.fillRect(right, 0, this.buffer.width - right, this.buffer.height); | |
| 679 } | |
| 680 } | |
| 681 | |
| 682 | |
| 683 render(newState) { | |
| 684 let oldState = this.currentState; | |
| 685 | |
| 686 if (!newState.file) { | |
| 687 this.element.style.display = "none"; | |
| 688 return; | |
| 689 } | |
| 690 | |
| 691 this.currentState = newState; | |
| 692 if (oldState) { | |
| 693 if (newState.timeLine.width === oldState.timeLine.width && | |
| 694 newState.timeLine.height === oldState.timeLine.height && | |
| 695 newState.file === oldState.file && | |
| 696 newState.start === oldState.start && | |
| 697 newState.end === oldState.end) { | |
| 698 // No change, nothing to do. | |
| 699 return; | |
| 700 } | |
| 701 } | |
| 702 | |
| 703 this.element.style.display = "inherit"; | |
| 704 | |
| 705 // Make sure the canvas has the right dimensions. | |
| 706 let width = this.currentState.timeLine.width; | |
| 707 this.canvas.width = width; | |
| 708 this.canvas.height = this.currentState.timeLine.height; | |
| 709 | |
| 710 let file = this.currentState.file; | |
| 711 if (!file) return; | |
| 712 | |
| 713 let firstTime = file.ticks[0].tm; | |
| 714 let lastTime = file.ticks[file.ticks.length - 1].tm; | |
| 715 let start = Math.max(this.currentState.start, firstTime); | |
| 716 let end = Math.min(this.currentState.end, lastTime); | |
| 717 | |
| 718 this.selectionStart = (start - firstTime) / (lastTime - firstTime) * width; | |
| 719 this.selectionEnd = (end - firstTime) / (lastTime - firstTime) * width; | |
| 720 | |
| 721 let tickCount = file.ticks.length; | |
| 722 | |
| 723 let minBucketPixels = 10; | |
| 724 let minBucketSamples = 30; | |
| 725 let bucketCount = Math.min(width / minBucketPixels, | |
| 726 tickCount / minBucketSamples); | |
| 727 | |
| 728 let stackProcessor = new CategorySampler(file, bucketCount); | |
| 729 generateTree(file, 0, Infinity, stackProcessor); | |
| 730 | |
| 731 let buffer = document.createElement("canvas"); | |
| 732 | |
| 733 buffer.width = this.canvas.width; | |
| 734 buffer.height = this.canvas.height; | |
| 735 | |
| 736 // Calculate the bar heights for each bucket. | |
| 737 let graphHeight = buffer.height; | |
| 738 let buckets = stackProcessor.buckets; | |
| 739 let bucketsGraph = []; | |
| 740 for (let i = 0; i < buckets.length; i++) { | |
| 741 let sum = 0; | |
| 742 let bucketData = []; | |
| 743 let total = buckets[i].total; | |
| 744 for (let j = 0; j < bucketDescriptors.length; j++) { | |
| 745 let desc = bucketDescriptors[j]; | |
| 746 for (let k = 0; k < desc.kinds.length; k++) { | |
| 747 sum += buckets[i][desc.kinds[k]]; | |
| 748 } | |
| 749 bucketData.push(graphHeight * sum / total); | |
| 750 } | |
| 751 bucketsGraph.push(bucketData); | |
| 752 } | |
| 753 | |
| 754 // Draw the graph into the buffer. | |
| 755 let bucketWidth = width / bucketCount; | |
| 756 let ctx = buffer.getContext('2d'); | |
| 757 for (let i = 0; i < bucketsGraph.length - 1; i++) { | |
| 758 let bucketData = bucketsGraph[i]; | |
| 759 let nextBucketData = bucketsGraph[i + 1]; | |
| 760 for (let j = 0; j < bucketData.length; j++) { | |
| 761 ctx.beginPath(); | |
| 762 ctx.moveTo(i * bucketWidth, j && bucketData[j - 1]); | |
| 763 ctx.lineTo((i + 1) * bucketWidth, j && nextBucketData[j - 1]); | |
| 764 ctx.lineTo((i + 1) * bucketWidth, nextBucketData[j]); | |
| 765 ctx.lineTo(i * bucketWidth, bucketData[j]); | |
| 766 ctx.closePath(); | |
| 767 ctx.fillStyle = bucketDescriptors[j].color; | |
| 768 ctx.fill(); | |
|
Camillo Bruni
2017/03/03 17:48:57
ha! I would have drawn the full width for every ty
Jarin
2017/03/04 11:21:23
You can actually see the stitches between buckets,
| |
| 769 } | |
| 770 } | |
| 771 | |
| 772 // Remember stuff for later. | |
| 773 this.buffer = buffer; | |
| 774 | |
| 775 // Draw the buffer. | |
| 776 this.drawSelection(); | |
| 777 | |
| 778 // (Re-)Populate the graph legend. | |
| 779 while (this.legend.cells.length > 0) { | |
| 780 this.legend.deleteCell(0); | |
| 781 } | |
| 782 let cell = this.legend.insertCell(-1); | |
| 783 cell.textContent = "Legend: "; | |
| 784 cell.style.padding = "1ex"; | |
| 785 for (let i = 0; i < bucketDescriptors.length; i++) { | |
| 786 let cell = this.legend.insertCell(-1); | |
| 787 cell.style.padding = "1ex"; | |
| 788 let desc = bucketDescriptors[i]; | |
| 789 let div = document.createElement("div"); | |
| 790 div.style.display = "inline-block"; | |
| 791 div.style.width = "0.6em"; | |
| 792 div.style.height = "1.2ex"; | |
| 793 div.style.backgroundColor = desc.color; | |
| 794 div.style.borderStyle = "solid"; | |
| 795 div.style.borderWidth = "1px"; | |
| 796 div.style.borderColor = "Black"; | |
| 797 cell.appendChild(div); | |
| 798 cell.appendChild(document.createTextNode(" " + desc.text)); | |
| 799 } | |
| 800 } | |
| 801 } | |
| 802 | |
| 803 class HelpView { | |
| 804 constructor() { | |
| 805 this.element = document.getElementById("help"); | |
| 806 } | |
| 807 | |
| 808 render(newState) { | |
| 809 this.element.style.display = newState.file ? "none" : "inherit"; | |
| 810 } | |
| 811 } | |
| OLD | NEW |