1/*
2 * Copyright (C) 2009 Google Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
7 *
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
13 * distribution.
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.
17 *
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.
29 */
30
31WebInspector.TimelinePanel = function()
32{
33    WebInspector.Panel.call(this);
34    this.element.addStyleClass("timeline");
35
36    this._overviewPane = new WebInspector.TimelineOverviewPane(this.categories);
37    this._overviewPane.addEventListener("window changed", this._windowChanged, this);
38    this._overviewPane.addEventListener("filter changed", this._refresh, this);
39    this.element.appendChild(this._overviewPane.element);
40
41    this._sidebarBackgroundElement = document.createElement("div");
42    this._sidebarBackgroundElement.className = "sidebar timeline-sidebar-background";
43    this.element.appendChild(this._sidebarBackgroundElement);
44
45    this._containerElement = document.createElement("div");
46    this._containerElement.id = "timeline-container";
47    this._containerElement.addEventListener("scroll", this._onScroll.bind(this), false);
48    this.element.appendChild(this._containerElement);
49
50    this.createSidebar(this._containerElement, this._containerElement);
51    var itemsTreeElement = new WebInspector.SidebarSectionTreeElement(WebInspector.UIString("RECORDS"), {}, true);
52    itemsTreeElement.expanded = true;
53    this.sidebarTree.appendChild(itemsTreeElement);
54
55    this._sidebarListElement = document.createElement("div");
56    this.sidebarElement.appendChild(this._sidebarListElement);
57
58    this._containerContentElement = document.createElement("div");
59    this._containerContentElement.id = "resources-container-content";
60    this._containerElement.appendChild(this._containerContentElement);
61
62    this._timelineGrid = new WebInspector.TimelineGrid();
63    this._itemsGraphsElement = this._timelineGrid.itemsGraphsElement;
64    this._itemsGraphsElement.id = "timeline-graphs";
65    this._containerContentElement.appendChild(this._timelineGrid.element);
66
67    this._topGapElement = document.createElement("div");
68    this._topGapElement.className = "timeline-gap";
69    this._itemsGraphsElement.appendChild(this._topGapElement);
70
71    this._graphRowsElement = document.createElement("div");
72    this._itemsGraphsElement.appendChild(this._graphRowsElement);
73
74    this._bottomGapElement = document.createElement("div");
75    this._bottomGapElement.className = "timeline-gap";
76    this._itemsGraphsElement.appendChild(this._bottomGapElement);
77
78    this._createStatusbarButtons();
79
80    this._records = [];
81    this._sendRequestRecords = {};
82    this._calculator = new WebInspector.TimelineCalculator();
83    this._boundariesAreValid = true;
84}
85
86WebInspector.TimelinePanel.prototype = {
87    toolbarItemClass: "timeline",
88
89    get toolbarItemLabel()
90    {
91        return WebInspector.UIString("Timeline");
92    },
93
94    get statusBarItems()
95    {
96        return [this.toggleTimelineButton.element, this.clearButton.element];
97    },
98
99    get categories()
100    {
101        if (!this._categories) {
102            this._categories = {
103                loading: new WebInspector.TimelineCategory("loading", WebInspector.UIString("Loading"), "rgb(47,102,236)"),
104                scripting: new WebInspector.TimelineCategory("scripting", WebInspector.UIString("Scripting"), "rgb(157,231,119)"),
105                rendering: new WebInspector.TimelineCategory("rendering", WebInspector.UIString("Rendering"), "rgb(164,60,255)")
106            };
107        }
108        return this._categories;
109    },
110
111    _createStatusbarButtons: function()
112    {
113        this.toggleTimelineButton = new WebInspector.StatusBarButton("", "record-profile-status-bar-item");
114        this.toggleTimelineButton.addEventListener("click", this._toggleTimelineButtonClicked.bind(this), false);
115
116        this.clearButton = new WebInspector.StatusBarButton("", "timeline-clear-status-bar-item");
117        this.clearButton.addEventListener("click", this.reset.bind(this), false);
118    },
119
120    _toggleTimelineButtonClicked: function()
121    {
122        if (this.toggleTimelineButton.toggled)
123            InspectorBackend.stopTimelineProfiler();
124        else
125            InspectorBackend.startTimelineProfiler();
126    },
127
128    timelineWasStarted: function()
129    {
130        this.toggleTimelineButton.toggled = true;
131    },
132
133    timelineWasStopped: function()
134    {
135        this.toggleTimelineButton.toggled = false;
136    },
137
138    addRecordToTimeline: function(record)
139    {
140        this._innerAddRecordToTimeline(record, this._records);
141        this._scheduleRefresh();
142    },
143
144    _innerAddRecordToTimeline: function(record, collection)
145    {
146        var formattedRecord = this._formatRecord(record);
147
148        // Glue subsequent records with same category and title together if they are closer than 100ms to each other.
149        if (this._lastRecord && (!record.children || !record.children.length) &&
150                this._lastRecord.category == formattedRecord.category &&
151                this._lastRecord.title == formattedRecord.title &&
152                this._lastRecord.details == formattedRecord.details &&
153                formattedRecord.startTime - this._lastRecord.endTime < 0.1) {
154            this._lastRecord.endTime = formattedRecord.endTime;
155            this._lastRecord.count++;
156        } else {
157            collection.push(formattedRecord);
158            for (var i = 0; record.children && i < record.children.length; ++i) {
159                if (!formattedRecord.children)
160                    formattedRecord.children = [];
161                var formattedChild = this._innerAddRecordToTimeline(record.children[i], formattedRecord.children);
162                formattedChild.parent = formattedRecord;
163            }
164            this._lastRecord = record.children && record.children.length ? null : formattedRecord;
165        }
166        return formattedRecord;
167    },
168
169    _formatRecord: function(record)
170    {
171        var recordTypes = WebInspector.TimelineAgent.RecordType;
172        if (!this._recordStyles) {
173            this._recordStyles = {};
174            this._recordStyles[recordTypes.EventDispatch] = { title: WebInspector.UIString("Event"), category: this.categories.scripting };
175            this._recordStyles[recordTypes.Layout] = { title: WebInspector.UIString("Layout"), category: this.categories.rendering };
176            this._recordStyles[recordTypes.RecalculateStyles] = { title: WebInspector.UIString("Recalculate Style"), category: this.categories.rendering };
177            this._recordStyles[recordTypes.Paint] = { title: WebInspector.UIString("Paint"), category: this.categories.rendering };
178            this._recordStyles[recordTypes.ParseHTML] = { title: WebInspector.UIString("Parse"), category: this.categories.loading };
179            this._recordStyles[recordTypes.TimerInstall] = { title: WebInspector.UIString("Install Timer"), category: this.categories.scripting };
180            this._recordStyles[recordTypes.TimerRemove] = { title: WebInspector.UIString("Remove Timer"), category: this.categories.scripting };
181            this._recordStyles[recordTypes.TimerFire] = { title: WebInspector.UIString("Timer Fired"), category: this.categories.scripting };
182            this._recordStyles[recordTypes.XHRReadyStateChange] = { title: WebInspector.UIString("XHR Ready State Change"), category: this.categories.scripting };
183            this._recordStyles[recordTypes.XHRLoad] = { title: WebInspector.UIString("XHR Load"), category: this.categories.scripting };
184            this._recordStyles[recordTypes.EvaluateScript] = { title: WebInspector.UIString("Evaluate Script"), category: this.categories.scripting };
185            this._recordStyles[recordTypes.MarkTimeline] = { title: WebInspector.UIString("Mark"), category: this.categories.scripting };
186            this._recordStyles[recordTypes.ResourceSendRequest] = { title: WebInspector.UIString("Send Request"), category: this.categories.loading };
187            this._recordStyles[recordTypes.ResourceReceiveResponse] = { title: WebInspector.UIString("Receive Response"), category: this.categories.loading };
188            this._recordStyles[recordTypes.ResourceFinish] = { title: WebInspector.UIString("Finish Loading"), category: this.categories.loading };
189        }
190
191        var style = this._recordStyles[record.type];
192        if (!style)
193            style = this._recordStyles[recordTypes.EventDispatch];
194
195        var formattedRecord = {};
196        formattedRecord.category = style.category;
197        formattedRecord.title = style.title;
198        formattedRecord.startTime = record.startTime / 1000;
199        formattedRecord.data = record.data;
200        formattedRecord.count = 1;
201        formattedRecord.type = record.type;
202        formattedRecord.endTime = (typeof record.endTime !== "undefined") ? record.endTime / 1000 : formattedRecord.startTime;
203        formattedRecord.record = record;
204
205        // Make resource receive record last since request was sent; make finish record last since response received.
206        if (record.type === WebInspector.TimelineAgent.RecordType.ResourceSendRequest) {
207            this._sendRequestRecords[record.data.identifier] = formattedRecord;
208        } else if (record.type === WebInspector.TimelineAgent.RecordType.ResourceReceiveResponse) {
209            var sendRequestRecord = this._sendRequestRecords[record.data.identifier];
210            if (sendRequestRecord) { // False if we started instrumentation in the middle of request.
211                sendRequestRecord._responseReceivedFormattedTime = formattedRecord.startTime;
212                formattedRecord.startTime = sendRequestRecord.startTime;
213                sendRequestRecord.details = this._getRecordDetails(record);
214            }
215        } else if (record.type === WebInspector.TimelineAgent.RecordType.ResourceFinish) {
216            var sendRequestRecord = this._sendRequestRecords[record.data.identifier];
217            if (sendRequestRecord) // False for main resource.
218                formattedRecord.startTime = sendRequestRecord._responseReceivedFormattedTime;
219        }
220        formattedRecord.details = this._getRecordDetails(record);
221
222        return formattedRecord;
223    },
224
225    _getRecordDetails: function(record)
226    {
227        switch (record.type) {
228        case WebInspector.TimelineAgent.RecordType.EventDispatch:
229            return record.data ? record.data.type : "";
230        case WebInspector.TimelineAgent.RecordType.Paint:
231            return record.data.width + "\u2009\u00d7\u2009" + record.data.height;
232        case WebInspector.TimelineAgent.RecordType.TimerInstall:
233        case WebInspector.TimelineAgent.RecordType.TimerRemove:
234        case WebInspector.TimelineAgent.RecordType.TimerFire:
235            return record.data.timerId;
236        case WebInspector.TimelineAgent.RecordType.XHRReadyStateChange:
237        case WebInspector.TimelineAgent.RecordType.XHRLoad:
238        case WebInspector.TimelineAgent.RecordType.EvaluateScript:
239        case WebInspector.TimelineAgent.RecordType.ResourceSendRequest:
240            return WebInspector.displayNameForURL(record.data.url);
241        case WebInspector.TimelineAgent.RecordType.ResourceReceiveResponse:
242        case WebInspector.TimelineAgent.RecordType.ResourceFinish:
243            var sendRequestRecord = this._sendRequestRecords[record.data.identifier];
244            return sendRequestRecord ? WebInspector.displayNameForURL(sendRequestRecord.data.url) : "";
245        case WebInspector.TimelineAgent.RecordType.MarkTimeline:
246            return record.data.message;
247        default:
248            return "";
249        }
250    },
251
252    setSidebarWidth: function(width)
253    {
254        WebInspector.Panel.prototype.setSidebarWidth.call(this, width);
255        this._sidebarBackgroundElement.style.width = width + "px";
256        this._overviewPane.setSidebarWidth(width);
257    },
258
259    updateMainViewWidth: function(width)
260    {
261        this._containerContentElement.style.left = width + "px";
262        this._scheduleRefresh();
263        this._overviewPane.updateMainViewWidth(width);
264    },
265
266    resize: function() {
267        this._scheduleRefresh();
268    },
269
270    reset: function()
271    {
272        this._lastRecord = null;
273        this._sendRequestRecords = {};
274        this._records = [];
275        this._boundariesAreValid = false;
276        this._overviewPane.reset();
277        this._adjustScrollPosition(0);
278        this._refresh();
279    },
280
281    show: function()
282    {
283        WebInspector.Panel.prototype.show.call(this);
284
285        if (this._needsRefresh)
286            this._refresh();
287    },
288
289    _onScroll: function(event)
290    {
291        var scrollTop = this._containerElement.scrollTop;
292        var dividersTop = Math.max(0, scrollTop);
293        this._timelineGrid.setScrollAndDividerTop(scrollTop, dividersTop);
294        this._scheduleRefresh(true);
295    },
296
297    _windowChanged: function()
298    {
299        this._scheduleRefresh();
300    },
301
302    _scheduleRefresh: function(preserveBoundaries)
303    {
304        this._boundariesAreValid &= preserveBoundaries;
305        if (this._needsRefresh)
306            return;
307        this._needsRefresh = true;
308
309        if (this.visible && !("_refreshTimeout" in this)) {
310            if (preserveBoundaries)
311                this._refresh();
312            else
313                this._refreshTimeout = setTimeout(this._refresh.bind(this), 100);
314        }
315    },
316
317    _refresh: function()
318    {
319        this._needsRefresh = false;
320        if ("_refreshTimeout" in this) {
321            clearTimeout(this._refreshTimeout);
322            delete this._refreshTimeout;
323        }
324
325        if (!this._boundariesAreValid)
326            this._overviewPane.update(this._records);
327        this._refreshRecords(!this._boundariesAreValid);
328        this._boundariesAreValid = true;
329    },
330
331    _refreshRecords: function(updateBoundaries)
332    {
333        if (updateBoundaries) {
334            this._calculator.reset();
335            this._calculator.windowLeft = this._overviewPane.windowLeft;
336            this._calculator.windowRight = this._overviewPane.windowRight;
337
338            for (var i = 0; i < this._records.length; ++i)
339                this._calculator.updateBoundaries(this._records[i]);
340
341            this._calculator.calculateWindow();
342        }
343
344        var recordsInWindow = [];
345        for (var i = 0; i < this._records.length; ++i) {
346            var record = this._records[i];
347            var percentages = this._calculator.computeBarGraphPercentages(record);
348            if (percentages.start < 100 && percentages.end >= 0 && !record.category.hidden)
349                this._addToRecordsWindow(record, recordsInWindow);
350        }
351
352        // Calculate the visible area.
353        var visibleTop = this._containerElement.scrollTop;
354        var visibleBottom = visibleTop + this._containerElement.clientHeight;
355
356        // Define row height, should be in sync with styles for timeline graphs.
357        const rowHeight = 18;
358        const expandOffset = 15;
359
360        // Convert visible area to visible indexes. Always include top-level record for a visible nested record.
361        var startIndex = Math.max(0, Math.min(Math.floor(visibleTop / rowHeight) - 1, recordsInWindow.length - 1));
362        while (startIndex > 0 && recordsInWindow[startIndex].parent)
363            startIndex--;
364        var endIndex = Math.min(recordsInWindow.length, Math.ceil(visibleBottom / rowHeight));
365        while (endIndex < recordsInWindow.length - 1 && recordsInWindow[endIndex].parent)
366            endIndex++;
367
368        // Resize gaps first.
369        const top = (startIndex * rowHeight) + "px";
370        this._topGapElement.style.height = top;
371        this.sidebarElement.style.top = top;
372        this.sidebarResizeElement.style.top = top;
373        this._bottomGapElement.style.height = (recordsInWindow.length - endIndex) * rowHeight + "px";
374
375        // Update visible rows.
376        var listRowElement = this._sidebarListElement.firstChild;
377        var width = this._graphRowsElement.offsetWidth;
378        this._itemsGraphsElement.removeChild(this._graphRowsElement);
379        var graphRowElement = this._graphRowsElement.firstChild;
380        var scheduleRefreshCallback = this._scheduleRefresh.bind(this, true);
381        for (var i = startIndex; i < endIndex; ++i) {
382            var record = recordsInWindow[i];
383            var isEven = !(i % 2);
384
385            if (!listRowElement) {
386                listRowElement = new WebInspector.TimelineRecordListRow().element;
387                this._sidebarListElement.appendChild(listRowElement);
388            }
389            if (!graphRowElement) {
390                graphRowElement = new WebInspector.TimelineRecordGraphRow(this._itemsGraphsElement, scheduleRefreshCallback, rowHeight).element;
391                this._graphRowsElement.appendChild(graphRowElement);
392            }
393
394            listRowElement.listRow.update(record, isEven);
395            graphRowElement.graphRow.update(record, isEven, this._calculator, width, expandOffset, i);
396
397            listRowElement = listRowElement.nextSibling;
398            graphRowElement = graphRowElement.nextSibling;
399        }
400
401        // Remove extra rows.
402        while (listRowElement) {
403            var nextElement = listRowElement.nextSibling;
404            listRowElement.listRow.dispose();
405            listRowElement = nextElement;
406        }
407        while (graphRowElement) {
408            var nextElement = graphRowElement.nextSibling;
409            graphRowElement.graphRow.dispose();
410            graphRowElement = nextElement;
411        }
412
413        this._itemsGraphsElement.insertBefore(this._graphRowsElement, this._bottomGapElement);
414        // Reserve some room for expand / collapse controls to the left for records that start at 0ms.
415        var timelinePaddingLeft = this._calculator.windowLeft === 0 ? expandOffset : 0;
416        if (updateBoundaries)
417            this._timelineGrid.updateDividers(true, this._calculator, timelinePaddingLeft);
418        this._adjustScrollPosition((recordsInWindow.length + 1) * rowHeight);
419    },
420
421    _addToRecordsWindow: function(record, recordsWindow)
422    {
423        recordsWindow.push(record);
424        if (!record.collapsed) {
425            var index = recordsWindow.length;
426            for (var i = 0; record.children && i < record.children.length; ++i)
427                this._addToRecordsWindow(record.children[i], recordsWindow);
428            record.visibleChildrenCount = recordsWindow.length - index;
429        }
430    },
431
432    _adjustScrollPosition: function(totalHeight)
433    {
434        // Prevent the container from being scrolled off the end.
435        if ((this._containerElement.scrollTop + this._containerElement.offsetHeight) > totalHeight + 1)
436            this._containerElement.scrollTop = (totalHeight - this._containerElement.offsetHeight);
437    }
438}
439
440WebInspector.TimelinePanel.prototype.__proto__ = WebInspector.Panel.prototype;
441
442
443WebInspector.TimelineCategory = function(name, title, color)
444{
445    this.name = name;
446    this.title = title;
447    this.color = color;
448}
449
450
451WebInspector.TimelineCalculator = function()
452{
453    this.reset();
454    this.windowLeft = 0.0;
455    this.windowRight = 1.0;
456    this._uiString = WebInspector.UIString.bind(WebInspector);
457}
458
459WebInspector.TimelineCalculator.prototype = {
460    computeBarGraphPercentages: function(record)
461    {
462        var start = (record.startTime - this.minimumBoundary) / this.boundarySpan * 100;
463        var end = (record.endTime - this.minimumBoundary) / this.boundarySpan * 100;
464        return {start: start, end: end};
465    },
466
467    calculateWindow: function()
468    {
469        this.minimumBoundary = this._absoluteMinimumBoundary + this.windowLeft * (this._absoluteMaximumBoundary - this._absoluteMinimumBoundary);
470        this.maximumBoundary = this._absoluteMinimumBoundary + this.windowRight * (this._absoluteMaximumBoundary - this._absoluteMinimumBoundary);
471        this.boundarySpan = this.maximumBoundary - this.minimumBoundary;
472    },
473
474    reset: function()
475    {
476        this._absoluteMinimumBoundary = -1;
477        this._absoluteMaximumBoundary = -1;
478    },
479
480    updateBoundaries: function(record)
481    {
482        var lowerBound = record.startTime;
483        if (this._absoluteMinimumBoundary === -1 || lowerBound < this._absoluteMinimumBoundary)
484            this._absoluteMinimumBoundary = lowerBound;
485
486        var upperBound = record.endTime;
487        if (this._absoluteMaximumBoundary === -1 || upperBound > this._absoluteMaximumBoundary)
488            this._absoluteMaximumBoundary = upperBound;
489    },
490
491    formatValue: function(value)
492    {
493        return Number.secondsToString(value + this.minimumBoundary - this._absoluteMinimumBoundary, this._uiString);
494    }
495}
496
497
498WebInspector.TimelineRecordListRow = function()
499{
500    this.element = document.createElement("div");
501    this.element.listRow = this;
502    var iconElement = document.createElement("span");
503    iconElement.className = "timeline-tree-icon";
504    this.element.appendChild(iconElement);
505
506    this._typeElement = document.createElement("span");
507    this._typeElement.className = "type";
508    this.element.appendChild(this._typeElement);
509
510    var separatorElement = document.createElement("span");
511    separatorElement.className = "separator";
512    separatorElement.textContent = " ";
513
514    this._dataElement = document.createElement("span");
515    this._dataElement.className = "data dimmed";
516
517    this._repeatCountElement = document.createElement("span");
518    this._repeatCountElement.className = "count";
519
520    this.element.appendChild(separatorElement);
521    this.element.appendChild(this._dataElement);
522    this.element.appendChild(this._repeatCountElement);
523}
524
525WebInspector.TimelineRecordListRow.prototype = {
526    update: function(record, isEven)
527    {
528        this.element.className = "timeline-tree-item timeline-category-" + record.category.name + (isEven ? " even" : "");
529        this._typeElement.textContent = record.title;
530
531        if (record.details) {
532            this._dataElement.textContent = "(" + record.details + ")";
533            this._dataElement.title = record.details;
534        } else {
535            this._dataElement.textContent = "";
536            this._dataElement.title = "";
537        }
538
539        if (record.count > 1)
540            this._repeatCountElement.textContent = "\u2009\u00d7\u2009" + record.count;
541        else
542            this._repeatCountElement.textContent = "";
543    },
544
545    dispose: function()
546    {
547        this.element.parentElement.removeChild(this.element);
548    }
549}
550
551
552WebInspector.TimelineRecordGraphRow = function(graphContainer, refreshCallback, rowHeight)
553{
554    this.element = document.createElement("div");
555    this.element.graphRow = this;
556
557    this._barAreaElement = document.createElement("div");
558    this._barAreaElement.className = "timeline-graph-bar-area";
559    this.element.appendChild(this._barAreaElement);
560
561    this._barElement = document.createElement("div");
562    this._barElement.className = "timeline-graph-bar";
563    this._barAreaElement.appendChild(this._barElement);
564
565    this._expandElement = document.createElement("div");
566    this._expandElement.className = "timeline-expandable";
567    graphContainer.appendChild(this._expandElement);
568
569    var leftBorder = document.createElement("div");
570    leftBorder.className = "timeline-expandable-left";
571    this._expandElement.appendChild(leftBorder);
572
573    this._expandElement.addEventListener("click", this._onClick.bind(this));
574    this._refreshCallback = refreshCallback;
575    this._rowHeight = rowHeight;
576}
577
578WebInspector.TimelineRecordGraphRow.prototype = {
579    update: function(record, isEven, calculator, clientWidth, expandOffset, index)
580    {
581        this._record = record;
582        this.element.className = "timeline-graph-side timeline-category-" + record.category.name + (isEven ? " even" : "");
583        var percentages = calculator.computeBarGraphPercentages(record);
584        var left = percentages.start / 100 * clientWidth;
585        var width = (percentages.end - percentages.start) / 100 * clientWidth;
586        this._barElement.style.left = (left + expandOffset) + "px";
587        this._barElement.style.width = width + "px";
588
589        if (record.visibleChildrenCount) {
590            this._expandElement.style.top = index * this._rowHeight + "px";
591            this._expandElement.style.left = left + "px";
592            this._expandElement.style.width = Math.max(12, width + 25) + "px";
593            if (!record.collapsed) {
594                this._expandElement.style.height = (record.visibleChildrenCount + 1) * this._rowHeight + "px";
595                this._expandElement.addStyleClass("timeline-expandable-expanded");
596                this._expandElement.removeStyleClass("timeline-expandable-collapsed");
597            } else {
598                this._expandElement.style.height = this._rowHeight + "px";
599                this._expandElement.addStyleClass("timeline-expandable-collapsed");
600                this._expandElement.removeStyleClass("timeline-expandable-expanded");
601            }
602            this._expandElement.removeStyleClass("hidden");
603        } else {
604            this._expandElement.addStyleClass("hidden");
605        }
606    },
607
608    _onClick: function(event)
609    {
610        this._record.collapsed = !this._record.collapsed;
611        this._refreshCallback();
612    },
613
614    dispose: function()
615    {
616        this.element.parentElement.removeChild(this.element);
617        this._expandElement.parentElement.removeChild(this._expandElement);
618    }
619}
620