2 * Copyright (C) 2014 Google Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
14 * * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33 * @implements {WebInspector.FlameChartDataProvider}
34 * @implements {WebInspector.TimelineFlameChart.SelectionProvider}
35 * @param {!WebInspector.TimelineModelImpl} model
36 * @param {!WebInspector.TimelineFrameModelBase} frameModel
38 WebInspector.TimelineFlameChartDataProvider = function(model, frameModel)
40 WebInspector.FlameChartDataProvider.call(this);
42 this._frameModel = frameModel;
43 this._font = "12px " + WebInspector.fontFamily();
44 this._linkifier = new WebInspector.Linkifier();
47 WebInspector.TimelineFlameChartDataProvider.prototype = {
59 textBaseline: function()
67 textPadding: function()
73 * @param {number} entryIndex
76 entryFont: function(entryIndex)
82 * @param {number} entryIndex
85 entryTitle: function(entryIndex)
87 var record = this._records[entryIndex];
88 if (record === this._cpuThreadRecord)
89 return WebInspector.UIString("CPU");
90 else if (record === this._gpuThreadRecord)
91 return WebInspector.UIString("GPU");
92 var details = WebInspector.TimelineUIUtilsImpl.buildDetailsNode(record, this._linkifier, this._model.loadedFromFile());
93 var title = WebInspector.TimelineUIUtilsImpl.recordTitle(record);
94 return details ? WebInspector.UIString("%s (%s)", title, details.textContent) : title;
98 * @param {number} startTime
99 * @param {number} endTime
100 * @return {?Array.<number>}
102 dividerOffsets: function(startTime, endTime)
104 // While we have tracing and timeline flame chart on screen at a time,
105 // we don't want to render frame-based grid.
111 this._timelineData = null;
115 * @return {!WebInspector.FlameChart.TimelineData}
117 timelineData: function()
119 if (this._timelineData)
120 return this._timelineData;
122 this._linkifier.reset();
125 * @type {?WebInspector.FlameChart.TimelineData}
127 this._timelineData = {
134 this._entryThreadDepths = {};
135 this._minimumBoundary = this._model.minimumRecordTime();
137 var cpuThreadRecordPayload = { type: WebInspector.TimelineModel.RecordType.Program };
138 this._cpuThreadRecord = new WebInspector.TimelineModel.RecordImpl(this._model, /** @type {!TimelineAgent.TimelineEvent} */ (cpuThreadRecordPayload), null);
139 this._pushRecord(this._cpuThreadRecord, 0, this.minimumBoundary(), Math.max(this._model.maximumRecordTime(), this.totalTime() + this.minimumBoundary()));
141 this._gpuThreadRecord = null;
143 var records = this._model.records();
144 for (var i = 0; i < records.length; ++i) {
145 var record = records[i];
146 var thread = record.thread();
147 if (thread === "gpu")
150 for (var j = 0; j < record.children().length; ++j)
151 this._appendRecord(record.children()[j], 1);
153 var visible = this._appendRecord(records[i], 1);
154 if (visible && !this._gpuThreadRecord) {
155 var gpuThreadRecordPayload = { type: WebInspector.TimelineModel.RecordType.Program };
156 this._gpuThreadRecord = new WebInspector.TimelineModel.RecordImpl(this._model, /** @type {!TimelineAgent.TimelineEvent} */ (gpuThreadRecordPayload), null);
157 this._pushRecord(this._gpuThreadRecord, 0, this.minimumBoundary(), Math.max(this._model.maximumRecordTime(), this.totalTime() + this.minimumBoundary()));
162 var cpuStackDepth = Math.max(4, this._entryThreadDepths[undefined]);
163 delete this._entryThreadDepths[undefined];
164 this._maxStackDepth = cpuStackDepth;
166 if (this._gpuThreadRecord) {
167 // We have multiple threads, update levels.
168 var threadBaselines = {};
169 var threadBaseline = cpuStackDepth + 2;
171 for (var thread in this._entryThreadDepths) {
172 threadBaselines[thread] = threadBaseline;
173 threadBaseline += this._entryThreadDepths[thread];
175 this._maxStackDepth = threadBaseline;
177 for (var i = 0; i < this._records.length; ++i) {
178 var record = this._records[i];
179 var level = this._timelineData.entryLevels[i];
180 if (record === this._cpuThreadRecord)
182 else if (record === this._gpuThreadRecord)
183 level = cpuStackDepth + 2;
184 else if (record.thread())
185 level += threadBaselines[record.thread()];
186 this._timelineData.entryLevels[i] = level;
190 return this._timelineData;
196 minimumBoundary: function()
198 return this._minimumBoundary;
204 totalTime: function()
206 return Math.max(1000, this._model.maximumRecordTime() - this._model.minimumRecordTime());
212 maxStackDepth: function()
214 return this._maxStackDepth;
218 * @param {!WebInspector.TimelineModel.Record} record
219 * @param {number} level
222 _appendRecord: function(record, level)
225 if (!this._model.isVisible(record)) {
226 for (var i = 0; i < record.children().length; ++i)
227 result = this._appendRecord(record.children()[i], level) || result;
231 this._pushRecord(record, level, record.startTime(), record.endTime());
232 for (var i = 0; i < record.children().length; ++i)
233 this._appendRecord(record.children()[i], level + 1);
238 * @param {!WebInspector.TimelineModel.Record} record
239 * @param {number} level
240 * @param {number} startTime
241 * @param {number} endTime
244 _pushRecord: function(record, level, startTime, endTime)
246 var index = this._records.length;
247 this._records.push(record);
248 this._timelineData.entryStartTimes[index] = startTime;
249 this._timelineData.entryLevels[index] = level;
250 this._timelineData.entryTotalTimes[index] = endTime - startTime;
251 this._entryThreadDepths[record.thread()] = Math.max(level, this._entryThreadDepths[record.thread()] || 0);
256 * @param {number} entryIndex
257 * @return {?Array.<!{title: string, text: string}>}
259 prepareHighlightedEntryInfo: function(entryIndex)
265 * @param {number} entryIndex
268 canJumpToEntry: function(entryIndex)
274 * @param {number} entryIndex
277 entryColor: function(entryIndex)
279 var record = this._records[entryIndex];
280 if (record === this._cpuThreadRecord || record === this._gpuThreadRecord)
283 if (record.type() === WebInspector.TimelineModel.RecordType.JSFrame)
284 return WebInspector.TimelineFlameChartDataProvider.jsFrameColorGenerator().colorForID(record.data()["functionName"]);
286 return record.category().fillColorStop1;
291 * @param {number} entryIndex
292 * @param {!CanvasRenderingContext2D} context
293 * @param {?string} text
294 * @param {number} barX
295 * @param {number} barY
296 * @param {number} barWidth
297 * @param {number} barHeight
298 * @param {function(number):number} offsetToPosition
301 decorateEntry: function(entryIndex, context, text, barX, barY, barWidth, barHeight, offsetToPosition)
306 var record = this._records[entryIndex];
307 var timelineData = this._timelineData;
309 var category = record.category();
310 // Paint text using white color on dark background.
313 context.fillStyle = "white";
314 context.shadowColor = "rgba(0, 0, 0, 0.1)";
315 context.shadowOffsetX = 1;
316 context.shadowOffsetY = 1;
317 context.font = this._font;
318 context.fillText(text, barX + this.textPadding(), barY + barHeight - this.textBaseline());
322 if (record.children().length) {
323 var entryStartTime = timelineData.entryStartTimes[entryIndex];
324 var barSelf = offsetToPosition(entryStartTime + record.selfTime())
327 context.fillStyle = category.backgroundColor;
328 context.rect(barSelf, barY, barX + barWidth - barSelf, barHeight);
331 // Paint text using dark color on light background.
335 context.fillStyle = category.borderColor;
336 context.shadowColor = "rgba(0, 0, 0, 0.1)";
337 context.shadowOffsetX = 1;
338 context.shadowOffsetY = 1;
339 context.fillText(text, barX + this.textPadding(), barY + barHeight - this.textBaseline());
344 if (record.warnings()) {
347 context.rect(barX, barY, barWidth, this.barHeight());
351 context.fillStyle = record.warnings() ? "red" : "rgba(255, 0, 0, 0.5)";
352 context.moveTo(barX + barWidth - 15, barY + 1);
353 context.lineTo(barX + barWidth - 1, barY + 1);
354 context.lineTo(barX + barWidth - 1, barY + 15);
364 * @param {number} entryIndex
367 forceDecoration: function(entryIndex)
369 var record = this._records[entryIndex];
370 return !!record.warnings();
374 * @param {number} entryIndex
375 * @return {?{startTime: number, endTime: number}}
377 highlightTimeRange: function(entryIndex)
379 var record = this._records[entryIndex];
380 if (record === this._cpuThreadRecord || record === this._gpuThreadRecord)
383 startTime: record.startTime(),
384 endTime: record.endTime()
391 paddingLeft: function()
397 * @param {number} entryIndex
400 textColor: function(entryIndex)
406 * @param {number} entryIndex
407 * @return {?WebInspector.TimelineSelection}
409 createSelection: function(entryIndex)
411 var record = this._records[entryIndex];
412 if (record instanceof WebInspector.TimelineModel.RecordImpl) {
413 this._lastSelection = new WebInspector.TimelineFlameChart.Selection(WebInspector.TimelineSelection.fromRecord(record), entryIndex);
414 return this._lastSelection.timelineSelection;
420 * @param {?WebInspector.TimelineSelection} selection
423 entryIndexForSelection: function(selection)
425 if (!selection || selection.type() !== WebInspector.TimelineSelection.Type.Record)
427 var record = /** @type{!WebInspector.TimelineModel.Record} */ (selection.object());
428 if (this._lastSelection && this._lastSelection.timelineSelection.object() === record)
429 return this._lastSelection.entryIndex;
430 var entryRecords = this._records;
431 for (var entryIndex = 0; entryIndex < entryRecords.length; ++entryIndex) {
432 if (entryRecords[entryIndex] === record) {
433 this._lastSelection = new WebInspector.TimelineFlameChart.Selection(WebInspector.TimelineSelection.fromRecord(record), entryIndex);
443 * @implements {WebInspector.FlameChartDataProvider}
444 * @implements {WebInspector.TimelineFlameChart.SelectionProvider}
445 * @param {!WebInspector.TracingTimelineModel} model
446 * @param {!WebInspector.TimelineFrameModelBase} frameModel
447 * @param {!WebInspector.Target} target
449 WebInspector.TracingBasedTimelineFlameChartDataProvider = function(model, frameModel, target)
451 WebInspector.FlameChartDataProvider.call(this);
453 this._frameModel = frameModel;
454 this._target = target;
455 this._font = "12px " + WebInspector.fontFamily();
456 this._linkifier = new WebInspector.Linkifier();
457 this._palette = new WebInspector.TraceViewPalette();
458 this._entryIndexToTitle = {};
461 WebInspector.TracingBasedTimelineFlameChartDataProvider.prototype = {
465 barHeight: function()
473 textBaseline: function()
481 textPadding: function()
487 * @param {number} entryIndex
490 entryFont: function(entryIndex)
496 * @param {number} entryIndex
499 entryTitle: function(entryIndex)
501 var event = this._entryEvents[entryIndex];
503 var name = WebInspector.TracingTimelineUIUtils.styleForTraceEvent(event.name).title;
504 // TODO(yurys): support event dividers
505 var details = WebInspector.TracingTimelineUIUtils.buildDetailsNodeForTraceEvent(event, this._linkifier, false, this._target);
506 return details ? WebInspector.UIString("%s (%s)", name, details.textContent) : name;
508 var title = this._entryIndexToTitle[entryIndex];
510 title = WebInspector.UIString("Unexpected entryIndex %d", entryIndex);
511 console.error(title);
517 * @param {number} startTime
518 * @param {number} endTime
519 * @return {?Array.<number>}
521 dividerOffsets: function(startTime, endTime)
528 this._timelineData = null;
529 /** @type {!Array.<!WebInspector.TracingModel.Event>} */
530 this._entryEvents = [];
531 this._entryIndexToTitle = {};
535 * @return {!WebInspector.FlameChart.TimelineData}
537 timelineData: function()
539 if (this._timelineData)
540 return this._timelineData;
543 * @type {?WebInspector.FlameChart.TimelineData}
545 this._timelineData = {
551 this._currentLevel = 0;
552 this._minimumBoundary = this._model.minimumRecordTime();
553 this._timeSpan = Math.max(this._model.maximumRecordTime() - this._minimumBoundary, 1000);
554 this._appendHeaderRecord("CPU");
555 var events = this._model.mainThreadEvents();
556 var maxStackDepth = 0;
557 for (var eventIndex = 0; eventIndex < events.length; ++eventIndex) {
558 var event = events[eventIndex];
559 var category = event.category;
560 if (category !== "disabled-by-default-devtools.timeline" && category !== "devtools")
562 if (event.duration || event.phase === WebInspector.TracingModel.Phase.Instant) {
563 this._appendEvent(event);
564 if (maxStackDepth < event.level)
565 maxStackDepth = event.level;
568 this._currentLevel += maxStackDepth + 1;
570 this._appendHeaderRecord("GPU");
571 return this._timelineData;
577 minimumBoundary: function()
579 return this._minimumBoundary;
585 totalTime: function()
587 return this._timeSpan;
593 maxStackDepth: function()
595 return this._currentLevel;
599 * @param {number} entryIndex
600 * @return {?Array.<!{title: string, text: string}>}
602 prepareHighlightedEntryInfo: function(entryIndex)
608 * @param {number} entryIndex
611 canJumpToEntry: function(entryIndex)
617 * @param {number} entryIndex
620 entryColor: function(entryIndex)
622 var event = this._entryEvents[entryIndex];
625 var style = WebInspector.TracingTimelineUIUtils.styleForTraceEvent(event.name);
626 return style.category.fillColorStop1;
630 * @param {number} entryIndex
631 * @param {!CanvasRenderingContext2D} context
632 * @param {?string} text
633 * @param {number} barX
634 * @param {number} barY
635 * @param {number} barWidth
636 * @param {number} barHeight
637 * @param {function(number):number} offsetToPosition
640 decorateEntry: function(entryIndex, context, text, barX, barY, barWidth, barHeight, offsetToPosition)
645 var timelineData = this._timelineData;
647 // Paint text using white color on dark background.
650 context.fillStyle = "white";
651 context.shadowColor = "rgba(0, 0, 0, 0.1)";
652 context.shadowOffsetX = 1;
653 context.shadowOffsetY = 1;
654 context.font = this._font;
655 context.fillText(text, barX + this.textPadding(), barY + barHeight - this.textBaseline());
659 var event = this._entryEvents[entryIndex];
660 if (event && event.warning) {
663 context.rect(barX, barY, barWidth, this.barHeight());
667 context.fillStyle = "red";
668 context.moveTo(barX + barWidth - 15, barY + 1);
669 context.lineTo(barX + barWidth - 1, barY + 1);
670 context.lineTo(barX + barWidth - 1, barY + 15);
680 * @param {number} entryIndex
683 forceDecoration: function(entryIndex)
685 var event = this._entryEvents[entryIndex];
688 return !!event.warning;
692 * @param {number} entryIndex
693 * @return {?{startTime: number, endTime: number}}
695 highlightTimeRange: function(entryIndex)
697 var event = this._entryEvents[entryIndex];
701 startTime: event.startTime,
702 endTime: event.endTime
709 paddingLeft: function()
715 * @param {number} entryIndex
718 textColor: function(entryIndex)
724 * @param {string} title
726 _appendHeaderRecord: function(title)
728 var index = this._entryEvents.length;
729 this._entryIndexToTitle[index] = title;
730 this._entryEvents.push(null);
731 this._timelineData.entryLevels[index] = this._currentLevel++;
732 this._timelineData.entryTotalTimes[index] = this._timeSpan;
733 this._timelineData.entryStartTimes[index] = this._minimumBoundary;
737 * @param {!WebInspector.TracingModel.Event} event
739 _appendEvent: function(event)
741 var index = this._entryEvents.length;
742 this._entryEvents.push(event);
743 this._timelineData.entryLevels[index] = this._currentLevel + event.level;
744 this._timelineData.entryTotalTimes[index] = event.duration || 1;
745 this._timelineData.entryStartTimes[index] = event.startTime;
749 * @param {number} entryIndex
750 * @return {?WebInspector.TimelineSelection}
752 createSelection: function(entryIndex)
754 var event = this._entryEvents[entryIndex];
757 this._lastSelection = new WebInspector.TimelineFlameChart.Selection(WebInspector.TimelineSelection.fromTraceEvent(event), entryIndex);
758 return this._lastSelection.timelineSelection;
762 * @param {?WebInspector.TimelineSelection} selection
765 entryIndexForSelection: function(selection)
767 if (!selection || selection.type() !== WebInspector.TimelineSelection.Type.TraceEvent)
769 var event = /** @type{!WebInspector.TracingModel.Event} */ (selection.object());
770 if (this._lastSelection && this._lastSelection.timelineSelection.object() === event)
771 return this._lastSelection.entryIndex;
772 var entryEvents = this._entryEvents;
773 for (var entryIndex = 0; entryIndex < entryEvents.length; ++entryIndex) {
774 if (entryEvents[entryIndex] === event) {
775 this._lastSelection = new WebInspector.TimelineFlameChart.Selection(WebInspector.TimelineSelection.fromTraceEvent(event), entryIndex);
784 * @return {!WebInspector.FlameChart.ColorGenerator}
786 WebInspector.TimelineFlameChartDataProvider.jsFrameColorGenerator = function()
788 if (!WebInspector.TimelineFlameChartDataProvider._jsFrameColorGenerator) {
789 var hueSpace = { min: 30, max: 55, count: 5 };
790 var satSpace = { min: 70, max: 100, count: 6 };
791 var colorGenerator = new WebInspector.FlameChart.ColorGenerator(hueSpace, satSpace, 50);
792 colorGenerator.setColorForID("(idle)", "hsl(0, 0%, 60%)");
793 colorGenerator.setColorForID("(program)", "hsl(0, 0%, 60%)");
794 colorGenerator.setColorForID("(garbage collector)", "hsl(0, 0%, 60%)");
795 WebInspector.TimelineFlameChartDataProvider._jsFrameColorGenerator = colorGenerator;
797 return WebInspector.TimelineFlameChartDataProvider._jsFrameColorGenerator;
802 * @extends {WebInspector.VBox}
803 * @implements {WebInspector.TimelineModeView}
804 * @implements {WebInspector.FlameChartDelegate}
805 * @param {!WebInspector.TimelineModeViewDelegate} delegate
806 * @param {!WebInspector.TimelineModel} model
807 * @param {?WebInspector.TracingTimelineModel} tracingModel
808 * @param {!WebInspector.TimelineFrameModelBase} frameModel
810 WebInspector.TimelineFlameChart = function(delegate, model, tracingModel, frameModel)
812 WebInspector.VBox.call(this);
813 this.element.classList.add("timeline-flamechart");
814 this.registerRequiredCSS("flameChart.css");
815 this._delegate = delegate;
817 this._dataProvider = tracingModel
818 ? new WebInspector.TracingBasedTimelineFlameChartDataProvider(tracingModel, frameModel, model.target())
819 : new WebInspector.TimelineFlameChartDataProvider(/** @type {!WebInspector.TimelineModelImpl} */(model), frameModel);
820 this._mainView = new WebInspector.FlameChart(this._dataProvider, this, true);
821 this._mainView.show(this.element);
822 this._model.addEventListener(WebInspector.TimelineModel.Events.RecordingStarted, this._onRecordingStarted, this);
823 this._mainView.addEventListener(WebInspector.FlameChart.Events.EntrySelected, this._onEntrySelected, this);
826 WebInspector.TimelineFlameChart.prototype = {
829 this._model.removeEventListener(WebInspector.TimelineModel.Events.RecordingStarted, this._onRecordingStarted, this);
830 this._mainView.removeEventListener(WebInspector.FlameChart.Events.EntrySelected, this._onEntrySelected, this);
834 * @param {number} windowStartTime
835 * @param {number} windowEndTime
837 requestWindowTimes: function(windowStartTime, windowEndTime)
839 this._delegate.requestWindowTimes(windowStartTime, windowEndTime);
843 * @param {?RegExp} textFilter
845 refreshRecords: function(textFilter)
847 this._dataProvider.reset();
848 this._mainView._scheduleUpdate();
853 this._mainView._scheduleUpdate();
858 * @return {!WebInspector.View}
867 this._automaticallySizeWindow = true;
868 this._dataProvider.reset();
869 this._mainView.reset();
870 this._mainView.setWindowTimes(0, Infinity);
873 _onRecordingStarted: function()
875 this._automaticallySizeWindow = true;
876 this._mainView.reset();
880 * @param {!WebInspector.TimelineModel.Record} record
882 addRecord: function(record)
884 this._dataProvider.reset();
885 if (this._automaticallySizeWindow) {
886 var minimumRecordTime = this._model.minimumRecordTime();
887 if (record.startTime() > (minimumRecordTime + 1000)) {
888 this._automaticallySizeWindow = false;
889 this._delegate.requestWindowTimes(minimumRecordTime, minimumRecordTime + 1000);
891 this._mainView._scheduleUpdate();
893 if (!this._pendingUpdateTimer)
894 this._pendingUpdateTimer = window.setTimeout(this._updateOnAddRecord.bind(this), 300);
898 _updateOnAddRecord: function()
900 delete this._pendingUpdateTimer;
901 this._mainView._scheduleUpdate();
905 * @param {number} startTime
906 * @param {number} endTime
908 setWindowTimes: function(startTime, endTime)
910 this._mainView.setWindowTimes(startTime, endTime);
911 this._delegate.select(null);
915 * @param {number} width
917 setSidebarSize: function(width)
922 * @param {?WebInspector.TimelineModel.Record} record
923 * @param {string=} regex
924 * @param {boolean=} selectRecord
926 highlightSearchResult: function(record, regex, selectRecord)
931 * @param {?WebInspector.TimelineSelection} selection
933 setSelection: function(selection)
935 var index = this._dataProvider.entryIndexForSelection(selection);
936 this._mainView.setSelectedEntry(index);
940 * @param {!WebInspector.Event} event
942 _onEntrySelected: function(event)
944 var entryIndex = /** @type{number} */ (event.data);
945 var timelineSelection = this._dataProvider.createSelection(entryIndex);
946 if (timelineSelection)
947 this._delegate.select(timelineSelection);
950 __proto__: WebInspector.VBox.prototype
955 * @param {!WebInspector.TimelineSelection} selection
956 * @param {number} entryIndex
958 WebInspector.TimelineFlameChart.Selection = function(selection, entryIndex)
960 this.timelineSelection = selection;
961 this.entryIndex = entryIndex;
967 WebInspector.TimelineFlameChart.SelectionProvider = function() { }
969 WebInspector.TimelineFlameChart.SelectionProvider.prototype = {
971 * @param {number} entryIndex
972 * @return {?WebInspector.TimelineSelection}
974 createSelection: function(entryIndex) { },
976 * @param {?WebInspector.TimelineSelection} selection
979 entryIndexForSelection: function(selection) { }