UNPKG

chrome-devtools-frontend

Version:
816 lines (716 loc) 26.6 kB
/* * Copyright (C) 2008 Apple Inc. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import * as Common from '../common/common.js'; import * as i18n from '../i18n/i18n.js'; import * as SDK from '../sdk/sdk.js'; import * as UI from '../ui/ui.js'; import {DataDisplayDelegate, // eslint-disable-line no-unused-vars Events as ProfileHeaderEvents, // eslint-disable-line no-unused-vars ProfileEvents as ProfileTypeEvents, ProfileHeader, ProfileType,} from './ProfileHeader.js'; // eslint-disable-line no-unused-vars import {Events as ProfileLauncherEvents, ProfileLauncherView} from './ProfileLauncherView.js'; import {ProfileSidebarTreeElement, setSharedFileSelectorElement} from './ProfileSidebarTreeElement.js'; // eslint-disable-line no-unused-vars import {instance} from './ProfileTypeRegistry.js'; export const UIStrings = { /** *@description Tooltip text that appears when hovering over the largeicon clear button in the Profiles Panel of a profiler tool */ clearAllProfiles: 'Clear all profiles', /** *@description Text in Profiles Panel of a profiler tool *@example {'.js', '.json'} PH1 */ cantLoadFileSupportedFile: 'Can’t load file. Supported file extensions: \'{PH1}.\'', /** *@description Text in Profiles Panel of a profiler tool */ cantLoadProfileWhileAnother: 'Can’t load profile while another profile is being recorded.', /** *@description Text in Profiles Panel of a profiler tool *@example {cannot open file} PH1 */ profileLoadingFailedS: 'Profile loading failed: {PH1}.', /** *@description A context menu item in the Profiles Panel of a profiler tool */ load: 'Load…', /** *@description Text in Profiles Panel of a profiler tool *@example {2} PH1 */ runD: 'Run {PH1}', /** *@description Text in Profiles Panel of a profiler tool */ profiles: 'Profiles', }; const str_ = i18n.i18n.registerUIStrings('profiler/ProfilesPanel.js', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); /** * @implements {DataDisplayDelegate} */ export class ProfilesPanel extends UI.Panel.PanelWithSidebar { /** * @param {string} name * @param {!Array.<!ProfileType>} profileTypes * @param {string} recordingActionId */ constructor(name, profileTypes, recordingActionId) { super(name); this._profileTypes = profileTypes; this.registerRequiredCSS('profiler/heapProfiler.css', {enableLegacyPatching: true}); this.registerRequiredCSS('profiler/profilesPanel.css', {enableLegacyPatching: true}); this.registerRequiredCSS('object_ui/objectValue.css', {enableLegacyPatching: true}); const mainContainer = new UI.Widget.VBox(); this.splitWidget().setMainWidget(mainContainer); this.profilesItemTreeElement = new ProfilesSidebarTreeElement(this); this._sidebarTree = new UI.TreeOutline.TreeOutlineInShadow(); this._sidebarTree.registerRequiredCSS('profiler/profilesSidebarTree.css', {enableLegacyPatching: true}); this._sidebarTree.element.classList.add('profiles-sidebar-tree-box'); this.panelSidebarElement().appendChild(this._sidebarTree.element); this._sidebarTree.appendChild(this.profilesItemTreeElement); this._sidebarTree.element.addEventListener('keydown', this._onKeyDown.bind(this), false); this.profileViews = document.createElement('div'); this.profileViews.id = 'profile-views'; this.profileViews.classList.add('vbox'); mainContainer.element.appendChild(this.profileViews); this._toolbarElement = document.createElement('div'); this._toolbarElement.classList.add('profiles-toolbar'); mainContainer.element.insertBefore(this._toolbarElement, mainContainer.element.firstChild); this.panelSidebarElement().classList.add('profiles-tree-sidebar'); const toolbarContainerLeft = document.createElement('div'); toolbarContainerLeft.classList.add('profiles-toolbar'); this.panelSidebarElement().insertBefore(toolbarContainerLeft, this.panelSidebarElement().firstChild); const toolbar = new UI.Toolbar.Toolbar('', toolbarContainerLeft); /** @type {!UI.ActionRegistration.Action} */ this._toggleRecordAction = /** @type {!UI.ActionRegistration.Action} */ ( UI.ActionRegistry.ActionRegistry.instance().action(recordingActionId)); this._toggleRecordButton = UI.Toolbar.Toolbar.createActionButton(this._toggleRecordAction); toolbar.appendToolbarItem(this._toggleRecordButton); this.clearResultsButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.clearAllProfiles), 'largeicon-clear'); this.clearResultsButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this._reset, this); toolbar.appendToolbarItem(this.clearResultsButton); toolbar.appendSeparator(); toolbar.appendToolbarItem(UI.Toolbar.Toolbar.createActionButtonForId('components.collect-garbage')); this._profileViewToolbar = new UI.Toolbar.Toolbar('', this._toolbarElement); this._profileGroups = {}; this._launcherView = new ProfileLauncherView(this); this._launcherView.addEventListener(ProfileLauncherEvents.ProfileTypeSelected, this._onProfileTypeSelected, this); /** @type {!UI.Widget.Widget|undefined} */ this.visibleView; /** @type {!Array<!{profile: !ProfileHeader, view: !UI.Widget.Widget}>} */ this._profileToView = []; /** @type {!Object<string, !ProfileTypeSidebarSection>} */ this._typeIdToSidebarSection = {}; const types = this._profileTypes; for (let i = 0; i < types.length; i++) { this._registerProfileType(types[i]); } this._launcherView.restoreSelectedProfileType(); this.profilesItemTreeElement.select(); this._showLauncherView(); /** @type {!HTMLInputElement} */ this._fileSelectorElement; this._createFileSelectorElement(); this.element.addEventListener('contextmenu', this._handleContextMenuEvent.bind(this), false); SDK.SDKModel.TargetManager.instance().addEventListener( SDK.SDKModel.Events.SuspendStateChanged, this._onSuspendStateChanged, this); UI.Context.Context.instance().addFlavorChangeListener( SDK.CPUProfilerModel.CPUProfilerModel, this._updateProfileTypeSpecificUI, this); UI.Context.Context.instance().addFlavorChangeListener( SDK.HeapProfilerModel.HeapProfilerModel, this._updateProfileTypeSpecificUI, this); } /** * @param {!Event} ev */ _onKeyDown(ev) { const event = /** @type {!KeyboardEvent} */ (ev); let handled = false; if (event.key === 'ArrowDown' && !event.altKey) { handled = this._sidebarTree.selectNext(); } else if (event.key === 'ArrowUp' && !event.altKey) { handled = this._sidebarTree.selectPrevious(); } if (handled) { event.consume(true); } } /** * @override * @return {?UI.SearchableView.SearchableView} */ searchableView() { const visibleView = /** @type {*} */ (this.visibleView); return visibleView && visibleView.searchableView ? visibleView.searchableView() : null; } _createFileSelectorElement() { if (this._fileSelectorElement) { this.element.removeChild(this._fileSelectorElement); } this._fileSelectorElement = UI.UIUtils.createFileSelectorElement(this._loadFromFile.bind(this)); setSharedFileSelectorElement(this._fileSelectorElement); this.element.appendChild(this._fileSelectorElement); } /** * @param {string} fileName * @return {?ProfileType} */ _findProfileTypeByExtension(fileName) { return this._profileTypes.find( type => Boolean(type.fileExtension()) && fileName.endsWith(type.fileExtension() || '')) || null; } /** * @param {!File} file */ async _loadFromFile(file) { this._createFileSelectorElement(); const profileType = this._findProfileTypeByExtension(file.name); if (!profileType) { const extensions = new Set(this._profileTypes.map(type => type.fileExtension()).filter(ext => ext)); Common.Console.Console.instance().error( i18nString(UIStrings.cantLoadFileSupportedFile, {PH1: Array.from(extensions).join('\', \'')})); return; } if (Boolean(profileType.profileBeingRecorded())) { Common.Console.Console.instance().error(i18nString(UIStrings.cantLoadProfileWhileAnother)); return; } const error = await profileType.loadFromFile(file); if (error && 'message' in error) { UI.UIUtils.MessageDialog.show(i18nString(UIStrings.profileLoadingFailedS, {PH1: error.message})); } } /** * @return {boolean} */ toggleRecord() { if (!this._toggleRecordAction.enabled()) { return true; } const toggleButton = this.element.ownerDocument.deepActiveElement(); const type = this._selectedProfileType; if (!type) { return true; } const isProfiling = type.buttonClicked(); this._updateToggleRecordAction(isProfiling); if (isProfiling) { this._launcherView.profileStarted(); if (type.hasTemporaryView()) { this.showProfile(type.profileBeingRecorded()); } } else { this._launcherView.profileFinished(); } if (toggleButton) { /** @type {!HTMLElement} */ (toggleButton).focus(); } return true; } _onSuspendStateChanged() { this._updateToggleRecordAction(this._toggleRecordAction.toggled()); } /** * @param {boolean} toggled */ _updateToggleRecordAction(toggled) { const hasSelectedTarget = Boolean( UI.Context.Context.instance().flavor(SDK.CPUProfilerModel.CPUProfilerModel) || UI.Context.Context.instance().flavor(SDK.HeapProfilerModel.HeapProfilerModel)); const enable = toggled || (!SDK.SDKModel.TargetManager.instance().allTargetsSuspended() && hasSelectedTarget); this._toggleRecordAction.setEnabled(enable); this._toggleRecordAction.setToggled(toggled); if (enable) { this._toggleRecordButton.setTitle(this._selectedProfileType ? this._selectedProfileType.buttonTooltip : ''); } else { this._toggleRecordButton.setTitle(UI.UIUtils.anotherProfilerActiveLabel()); } if (this._selectedProfileType) { this._launcherView.updateProfileType(this._selectedProfileType, enable); } } _profileBeingRecordedRemoved() { this._updateToggleRecordAction(false); this._launcherView.profileFinished(); } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _onProfileTypeSelected(event) { this._selectedProfileType = /** @type {!ProfileType} */ (event.data); this._updateProfileTypeSpecificUI(); } _updateProfileTypeSpecificUI() { this._updateToggleRecordAction(this._toggleRecordAction.toggled()); } _reset() { this._profileTypes.forEach(type => type.reset()); delete this.visibleView; this._profileGroups = {}; this._updateToggleRecordAction(false); this._launcherView.profileFinished(); this._sidebarTree.element.classList.remove('some-expandable'); this._launcherView.detach(); this.profileViews.removeChildren(); this._profileViewToolbar.removeToolbarItems(); this.clearResultsButton.element.classList.remove('hidden'); this.profilesItemTreeElement.select(); this._showLauncherView(); } _showLauncherView() { this.closeVisibleView(); this._profileViewToolbar.removeToolbarItems(); this._launcherView.show(this.profileViews); this.visibleView = this._launcherView; this._toolbarElement.classList.add('hidden'); } /** * @param {!ProfileType} profileType */ _registerProfileType(profileType) { this._launcherView.addProfileType(profileType); const profileTypeSection = new ProfileTypeSidebarSection(this, profileType); this._typeIdToSidebarSection[profileType.id] = profileTypeSection; this._sidebarTree.appendChild(profileTypeSection); profileTypeSection.childrenListElement.addEventListener( 'contextmenu', this._handleContextMenuEvent.bind(this), false); /** * @param {!Common.EventTarget.EventTargetEvent} event * @this {ProfilesPanel} */ function onAddProfileHeader(event) { this._addProfileHeader(/** @type {!ProfileHeader} */ (event.data)); } /** * @param {!Common.EventTarget.EventTargetEvent} event * @this {ProfilesPanel} */ function onRemoveProfileHeader(event) { this._removeProfileHeader(/** @type {!ProfileHeader} */ (event.data)); } /** * @param {!Common.EventTarget.EventTargetEvent} event * @this {ProfilesPanel} */ function profileComplete(event) { this.showProfile(/** @type {!ProfileHeader} */ (event.data)); } profileType.addEventListener(ProfileTypeEvents.ViewUpdated, this._updateProfileTypeSpecificUI, this); profileType.addEventListener(ProfileTypeEvents.AddProfileHeader, onAddProfileHeader, this); profileType.addEventListener(ProfileTypeEvents.RemoveProfileHeader, onRemoveProfileHeader, this); profileType.addEventListener(ProfileTypeEvents.ProfileComplete, profileComplete, this); const profiles = profileType.getProfiles(); for (let i = 0; i < profiles.length; i++) { this._addProfileHeader(profiles[i]); } } /** * @param {!Event} event */ _handleContextMenuEvent(event) { const contextMenu = new UI.ContextMenu.ContextMenu(event); if (this.panelSidebarElement().isSelfOrAncestor(/** @type {?Node} */ (event.target))) { contextMenu.defaultSection().appendItem( i18nString(UIStrings.load), this._fileSelectorElement.click.bind(this._fileSelectorElement)); } contextMenu.show(); } showLoadFromFileDialog() { this._fileSelectorElement.click(); } /** * @param {!ProfileHeader} profile */ _addProfileHeader(profile) { const profileType = profile.profileType(); const typeId = profileType.id; this._typeIdToSidebarSection[typeId].addProfileHeader(profile); if (!this.visibleView || this.visibleView === this._launcherView) { this.showProfile(profile); } } /** * @param {!ProfileHeader} profile */ _removeProfileHeader(profile) { if (profile.profileType().profileBeingRecorded() === profile) { this._profileBeingRecordedRemoved(); } const i = this._indexOfViewForProfile(profile); if (i !== -1) { this._profileToView.splice(i, 1); } const typeId = profile.profileType().id; const sectionIsEmpty = this._typeIdToSidebarSection[typeId].removeProfileHeader(profile); // No other item will be selected if there aren't any other profiles, so // make sure that view gets cleared when the last profile is removed. if (sectionIsEmpty) { this.profilesItemTreeElement.select(); this._showLauncherView(); } } /** * @override * @param {?ProfileHeader} profile * @return {?UI.Widget.Widget} */ showProfile(profile) { if (!profile || (profile.profileType().profileBeingRecorded() === profile) && !profile.profileType().hasTemporaryView()) { return null; } const view = this.viewForProfile(profile); if (view === this.visibleView) { return view; } this.closeVisibleView(); view.show(this.profileViews); this._toolbarElement.classList.remove('hidden'); this.visibleView = view; const profileTypeSection = this._typeIdToSidebarSection[profile.profileType().id]; const sidebarElement = profileTypeSection.sidebarElementForProfile(profile); if (sidebarElement) { sidebarElement.revealAndSelect(); } this._profileViewToolbar.removeToolbarItems(); /** @type {!UI.View.View} */ (/** @type {*} */ (view)).toolbarItems().then(items => { items.map(item => this._profileViewToolbar.appendToolbarItem(item)); }); return view; } /** * @override * @param {!Protocol.HeapProfiler.HeapSnapshotObjectId} snapshotObjectId * @param {string} perspectiveName */ showObject(snapshotObjectId, perspectiveName) { } /** * @override * @param {number} nodeIndex * @return {!Promise<?Element>} */ async linkifyObject(nodeIndex) { return null; } /** * @param {!ProfileHeader} profile * @return {!UI.Widget.Widget} */ viewForProfile(profile) { const index = this._indexOfViewForProfile(profile); if (index !== -1) { return this._profileToView[index].view; } const view = profile.createView(this); view.element.classList.add('profile-view'); this._profileToView.push({profile: profile, view: view}); return view; } /** * @param {!ProfileHeader} profile * @return {number} */ _indexOfViewForProfile(profile) { return this._profileToView.findIndex(item => item.profile === profile); } closeVisibleView() { if (this.visibleView) { this.visibleView.detach(); } delete this.visibleView; } /** * @override */ focus() { this._sidebarTree.focus(); } } export class ProfileTypeSidebarSection extends UI.TreeOutline.TreeElement { /** * @param {!DataDisplayDelegate} dataDisplayDelegate * @param {!ProfileType} profileType */ constructor(dataDisplayDelegate, profileType) { super(profileType.treeItemTitle, true); this.selectable = false; this._dataDisplayDelegate = dataDisplayDelegate; /** @type {!Array<!ProfileSidebarTreeElement>} */ this._profileTreeElements = []; /** @type {!Object<string, !ProfileGroup>} */ this._profileGroups = {}; this.expand(); this.hidden = true; this.setCollapsible(false); } /** * @param {!ProfileHeader} profile */ addProfileHeader(profile) { this.hidden = false; const profileType = profile.profileType(); /** @type {?UI.TreeOutline.TreeElement} */ let sidebarParent = this; const profileTreeElement = /** @type {!ProfileSidebarTreeElement} */ (profile.createSidebarTreeElement(this._dataDisplayDelegate)); this._profileTreeElements.push(profileTreeElement); if (!profile.fromFile() && profileType.profileBeingRecorded() !== profile) { const profileTitle = profile.title; let group = this._profileGroups[profileTitle]; if (!group) { group = new ProfileGroup(); this._profileGroups[profileTitle] = group; } group.profileSidebarTreeElements.push(profileTreeElement); const groupSize = group.profileSidebarTreeElements.length; if (groupSize === 2) { // Make a group UI.TreeOutline.TreeElement now that there are 2 profiles. group.sidebarTreeElement = new ProfileGroupSidebarTreeElement(this._dataDisplayDelegate, profile.title); const firstProfileTreeElement = group.profileSidebarTreeElements[0]; // Insert at the same index for the first profile of the group. const index = this.children().indexOf(firstProfileTreeElement); this.insertChild(group.sidebarTreeElement, index); // Move the first profile to the group. const selected = firstProfileTreeElement.selected; this.removeChild(firstProfileTreeElement); group.sidebarTreeElement.appendChild(firstProfileTreeElement); if (selected) { firstProfileTreeElement.revealAndSelect(); } firstProfileTreeElement.setSmall(true); firstProfileTreeElement.setMainTitle(i18nString(UIStrings.runD, {PH1: 1})); if (this.treeOutline) { this.treeOutline.element.classList.add('some-expandable'); } } if (groupSize >= 2) { sidebarParent = group.sidebarTreeElement; profileTreeElement.setSmall(true); profileTreeElement.setMainTitle(i18nString(UIStrings.runD, {PH1: groupSize})); } } if (sidebarParent) { sidebarParent.appendChild(profileTreeElement); } } /** * @param {!ProfileHeader} profile * @return {boolean} */ removeProfileHeader(profile) { const index = this._sidebarElementIndex(profile); if (index === -1) { return false; } const profileTreeElement = this._profileTreeElements[index]; this._profileTreeElements.splice(index, 1); /** @type {?UI.TreeOutline.TreeElement} */ let sidebarParent = this; const group = this._profileGroups[profile.title]; if (group) { const groupElements = group.profileSidebarTreeElements; groupElements.splice(groupElements.indexOf(profileTreeElement), 1); if (groupElements.length === 1) { // Move the last profile out of its group and remove the group. const pos = sidebarParent.children().indexOf( /** @type {!ProfileGroupSidebarTreeElement} */ (group.sidebarTreeElement)); if (group.sidebarTreeElement) { group.sidebarTreeElement.removeChild(groupElements[0]); } this.insertChild(groupElements[0], pos); groupElements[0].setSmall(false); groupElements[0].setMainTitle(profile.title); if (group.sidebarTreeElement) { this.removeChild(group.sidebarTreeElement); } } if (groupElements.length !== 0) { sidebarParent = group.sidebarTreeElement; } } if (sidebarParent) { sidebarParent.removeChild(profileTreeElement); } profileTreeElement.dispose(); if (this.childCount()) { return false; } this.hidden = true; return true; } /** * @param {!ProfileHeader} profile * @return {?ProfileSidebarTreeElement} */ sidebarElementForProfile(profile) { const index = this._sidebarElementIndex(profile); return index === -1 ? null : this._profileTreeElements[index]; } /** * @param {!ProfileHeader} profile * @return {number} */ _sidebarElementIndex(profile) { const elements = this._profileTreeElements; for (let i = 0; i < elements.length; i++) { if (elements[i].profile === profile) { return i; } } return -1; } /** * @override */ onattach() { this.listItemElement.classList.add('profiles-tree-section'); } } export class ProfileGroup { constructor() { /** @type {!Array<!ProfileSidebarTreeElement>} */ this.profileSidebarTreeElements = []; /** @type {?ProfileGroupSidebarTreeElement} */ this.sidebarTreeElement = null; } } export class ProfileGroupSidebarTreeElement extends UI.TreeOutline.TreeElement { /** * @param {!DataDisplayDelegate} dataDisplayDelegate * @param {string} title */ constructor(dataDisplayDelegate, title) { super('', true); this.selectable = false; this._dataDisplayDelegate = dataDisplayDelegate; this._profileTitle = title; this.expand(); this.toggleOnClick = true; } /** * @override * @return {boolean} */ onselect() { const hasChildren = this.childCount() > 0; if (hasChildren) { const lastChild = this.lastChild(); if (lastChild instanceof ProfileSidebarTreeElement) { this._dataDisplayDelegate.showProfile(lastChild.profile); } } return hasChildren; } /** * @override */ onattach() { this.listItemElement.classList.add('profile-group-sidebar-tree-item'); this.listItemElement.createChild('div', 'icon'); this.listItemElement.createChild('div', 'titles no-subtitle') .createChild('span', 'title-container') .createChild('span', 'title') .textContent = this._profileTitle; } } export class ProfilesSidebarTreeElement extends UI.TreeOutline.TreeElement { /** * @param {!ProfilesPanel} panel */ constructor(panel) { super('', false); this.selectable = true; this._panel = panel; } /** * @override * @return {boolean} */ onselect() { this._panel._showLauncherView(); return true; } /** * @override */ onattach() { this.listItemElement.classList.add('profile-launcher-view-tree-item'); this.listItemElement.createChild('div', 'icon'); this.listItemElement.createChild('div', 'titles no-subtitle') .createChild('span', 'title-container') .createChild('span', 'title') .textContent = i18nString(UIStrings.profiles); } } /** @type {!JSProfilerPanel} */ let jsProfilerPanelInstance; /** * @implements {UI.ActionRegistration.ActionDelegate} */ export class JSProfilerPanel extends ProfilesPanel { constructor() { const registry = instance; super('js_profiler', [registry.cpuProfileType], 'profiler.js-toggle-recording'); } /** * @param {{forceNew: ?boolean}} opts */ static instance(opts = {forceNew: null}) { const {forceNew} = opts; if (!jsProfilerPanelInstance || forceNew) { jsProfilerPanelInstance = new JSProfilerPanel(); } return jsProfilerPanelInstance; } /** * @override */ wasShown() { UI.Context.Context.instance().setFlavor(JSProfilerPanel, this); } /** * @override */ willHide() { UI.Context.Context.instance().setFlavor(JSProfilerPanel, null); } /** * @override * @param {!UI.Context.Context} context * @param {string} actionId * @return {boolean} */ handleAction(context, actionId) { const panel = UI.Context.Context.instance().flavor(JSProfilerPanel); if (panel instanceof JSProfilerPanel) { panel.toggleRecord(); } else { throw new Error('non-null JSProfilerPanel expected!'); } return true; } }