// Copyright 2020 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import {FocusEvent, SelectionEvent, SelectTimeEvent} from './events.mjs'; import {delay, DOM, V8CustomElement} from './helper.mjs'; import {Group} from './ic-model.mjs'; import {IcLogEntry} from './log/ic.mjs'; import {MapLogEntry} from './log/map.mjs'; DOM.defineCustomElement( 'ic-panel', (templateText) => class ICPanel extends V8CustomElement { _selectedLogEntries; _timeline; constructor() { super(templateText); this.initGroupKeySelect(); this.groupKey.addEventListener('change', e => this.updateTable(e)); } set timeline(value) { console.assert(value !== undefined, 'timeline undefined!'); this._timeline = value; this.selectedLogEntries = this._timeline.all; this.update(); } get groupKey() { return this.$('#group-key'); } get table() { return this.$('#table'); } get tableBody() { return this.$('#table-body'); } get count() { return this.$('#count'); } get spanSelectAll() { return this.querySelectorAll('span'); } set selectedLogEntries(value) { this._selectedLogEntries = value; this.update(); } _update() { this._updateCount(); this._updateTable(); } _updateCount() { this.count.innerHTML = `length=${this._selectedLogEntries.length}`; } _updateTable(event) { let select = this.groupKey; let key = select.options[select.selectedIndex].text; DOM.removeAllChildren(this.tableBody); let groups = Group.groupBy(this._selectedLogEntries, key, true); this._render(groups, this.tableBody); } escapeHtml(unsafe) { if (!unsafe) return ''; return unsafe.toString() .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } handleMapClick(e) { const group = e.target.parentNode.entry; const id = group.key; const selectedMapLogEntries = this.searchIcLogEntryToMapLogEntry(id, group.entries); this.dispatchEvent(new SelectionEvent(selectedMapLogEntries)); } searchIcLogEntryToMapLogEntry(id, icLogEntries) { // searches for mapLogEntries using the id, time const selectedMapLogEntriesSet = new Set(); for (const icLogEntry of icLogEntries) { const selectedMap = MapLogEntry.get(id, icLogEntry.time); selectedMapLogEntriesSet.add(selectedMap); } return Array.from(selectedMapLogEntriesSet); } // TODO(zcankara) Handle in the processor for events with source // positions. handleFilePositionClick(e) { const tr = e.target.parentNode; const sourcePosition = tr.group.entries[0].sourcePosition; this.dispatchEvent(new FocusEvent(sourcePosition)); } _render(groups, parent) { const fragment = document.createDocumentFragment(); const max = Math.min(1000, groups.length) const detailsClickHandler = this.handleDetailsClick.bind(this); const mapClickHandler = this.handleMapClick.bind(this); const fileClickHandler = this.handleFilePositionClick.bind(this); for (let i = 0; i < max; i++) { const group = groups[i]; const tr = DOM.tr(); tr.group = group; const details = tr.appendChild(DOM.td('', 'toggle')); details.onclick = detailsClickHandler; tr.appendChild(DOM.td(group.percentage + '%', 'percentage')); tr.appendChild(DOM.td(group.count, 'count')); const valueTd = tr.appendChild(DOM.td(group.key, 'key')); if (group.property === 'map') { valueTd.onclick = mapClickHandler; valueTd.classList.add('clickable'); } else if (group.property == 'filePosition') { valueTd.classList.add('clickable'); valueTd.onclick = fileClickHandler; } fragment.appendChild(tr); } const omitted = groups.length - max; if (omitted > 0) { const tr = DOM.tr(); const tdNode = tr.appendChild(DOM.td(`Omitted ${omitted} entries.`)); tdNode.colSpan = 4; fragment.appendChild(tr); } parent.appendChild(fragment); } handleDetailsClick(event) { const tr = event.target.parentNode; const group = tr.group; // Create subgroup in-place if the don't exist yet. if (group.groups === undefined) { group.createSubGroups(); this.renderDrilldown(group, tr); } let detailsTr = tr.nextSibling; if (tr.classList.contains('open')) { tr.classList.remove('open'); detailsTr.style.display = 'none'; } else { tr.classList.add('open'); detailsTr.style.display = 'table-row'; } } renderDrilldown(group, previousSibling) { let tr = DOM.tr('entry-details'); tr.style.display = 'none'; // indent by one td. tr.appendChild(DOM.td()); let td = DOM.td(); td.colSpan = 3; for (let key in group.groups) { this.renderDrilldownGroup(td, group.groups[key], key); } tr.appendChild(td); // Append the new TR after previousSibling. previousSibling.parentNode.insertBefore(tr, previousSibling.nextSibling) } renderDrilldownGroup(td, children, key) { const max = 20; const div = DOM.div('drilldown-group-title'); div.textContent = `Grouped by ${key} [top ${max} out of ${children.length}]`; td.appendChild(div); const table = DOM.table(); this._render(children.slice(0, max), table, false) td.appendChild(table); } initGroupKeySelect() { const select = this.groupKey; select.options.length = 0; for (const propertyName of IcLogEntry.propertyNames) { const option = document.createElement('option'); option.text = propertyName; select.add(option); } } });