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 |