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