Changeset 51296 in webkit


Ignore:
Timestamp:
Nov 22, 2009 10:25:03 AM (14 years ago)
Author:
pfeldman@chromium.org
Message:

2009-11-22 Pavel Feldman <pfeldman@chromium.org>

Reviewed by Timothy Hatcher.

Web Inspector: Reimplement TimelinePanel to make it fast:

  • Extract grid and overview into separate files
  • Make timeline create only divs for visible rows

https://bugs.webkit.org/show_bug.cgi?id=31784

  • WebCore.gypi:
  • WebCore.vcproj/WebCore.vcproj:
  • inspector/front-end/AbstractTimelinePanel.js: (WebInspector.AbstractTimelinePanel.prototype.createInterface): (WebInspector.AbstractTimelinePanel.prototype.refresh): (WebInspector.AbstractTimelinePanel.prototype.set calculator):
  • inspector/front-end/TimelineGrid.js: Added. (WebInspector.TimelineGrid): (WebInspector.TimelineGrid.prototype.get itemsGraphsElement): (WebInspector.TimelineGrid.prototype.updateDividers): (WebInspector.TimelineGrid.prototype.addEventDivider): (WebInspector.TimelineGrid.prototype.setScrollAndDividerTop):
  • inspector/front-end/TimelineOverviewPane.js: Added. (WebInspector.TimelineOverviewPane): (WebInspector.TimelineOverviewPane.prototype._onCheckboxClicked): (WebInspector.TimelineOverviewPane.prototype.update): (WebInspector.TimelineOverviewPane.prototype.setSidebarWidth): (WebInspector.TimelineOverviewPane.prototype.updateMainViewWidth): (WebInspector.TimelineOverviewPane.prototype.reset): (WebInspector.TimelineOverviewPane.prototype._resizeWindow): (WebInspector.TimelineOverviewPane.prototype._windowResizeDragging): (WebInspector.TimelineOverviewPane.prototype._dragWindow): (WebInspector.TimelineOverviewPane.prototype._windowDragging): (WebInspector.TimelineOverviewPane.prototype._resizeWindowLeft): (WebInspector.TimelineOverviewPane.prototype._resizeWindowRight): (WebInspector.TimelineOverviewPane.prototype._setWindowPosition): (WebInspector.TimelineOverviewPane.prototype._endWindowDragging): (WebInspector.TimelineOverviewCalculator): (WebInspector.TimelineOverviewCalculator.prototype.computeBarGraphPercentages): (WebInspector.TimelineOverviewCalculator.prototype.reset): (WebInspector.TimelineOverviewCalculator.prototype.updateBoundaries): (WebInspector.TimelineOverviewCalculator.prototype.get boundarySpan): (WebInspector.TimelineOverviewCalculator.prototype.formatValue): (WebInspector.TimelineCategoryTreeElement): (WebInspector.TimelineCategoryTreeElement.prototype.onattach): (WebInspector.TimelineCategoryGraph): (WebInspector.TimelineCategoryGraph.prototype.get graphElement): (WebInspector.TimelineCategoryGraph.prototype.addChunk): (WebInspector.TimelineCategoryGraph.prototype.clearChunks): (WebInspector.TimelineCategoryGraph.prototype.set dimmed):
  • inspector/front-end/TimelinePanel.js: (WebInspector.TimelinePanel): (WebInspector.TimelinePanel.prototype._toggleTimelineButtonClicked): (WebInspector.TimelinePanel.prototype.addRecordToTimeline): (WebInspector.TimelinePanel.prototype._formatRecord): (WebInspector.TimelinePanel.prototype.setSidebarWidth): (WebInspector.TimelinePanel.prototype.updateMainViewWidth): (WebInspector.TimelinePanel.prototype.resize): (WebInspector.TimelinePanel.prototype.reset): (WebInspector.TimelinePanel.prototype.show): (WebInspector.TimelinePanel.prototype._onScroll): (WebInspector.TimelinePanel.prototype._scheduleRefresh): (WebInspector.TimelinePanel.prototype._refresh): (WebInspector.TimelinePanel.prototype._refreshRecords): (WebInspector.TimelinePanel.prototype._adjustScrollPosition): (WebInspector.TimelineCategory): (WebInspector.TimelineCalculator): (WebInspector.TimelineCalculator.prototype.get boundarySpan): (WebInspector.TimelineRecordListRow): (WebInspector.TimelineRecordListRow.prototype.update): (WebInspector.TimelineRecordGraphRow): (WebInspector.TimelineRecordGraphRow.prototype.update):
  • inspector/front-end/WebKit.qrc:
  • inspector/front-end/inspector.css:
  • inspector/front-end/inspector.html:
Location:
trunk
Files:
2 added
9 edited

Legend:

Unmodified
Added
Removed
  • trunk/LayoutTests/inspector/timeline-test.js

    r51196 r51296  
    119119function frontend_getTimelineResults() {
    120120    var result = [];
    121     var items = WebInspector.panels.timeline._items;
    122     for (var i = 0; i < items.length; ++i) {
    123         result.push(items[i].record);
     121    var records = WebInspector.panels.timeline._records;
     122    for (var i = 0; i < records.length; ++i) {
     123        result.push(records[i].record);
    124124    }
    125125    return result;
  • trunk/WebCore/ChangeLog

    r51295 r51296  
     12009-11-22  Pavel Feldman  <pfeldman@chromium.org>
     2
     3        Reviewed by Timothy Hatcher.
     4
     5        Web Inspector: Reimplement TimelinePanel to make it fast:
     6        - Extract grid and overview into separate files
     7        - Make timeline create only divs for visible rows
     8
     9        https://bugs.webkit.org/show_bug.cgi?id=31784
     10
     11        * WebCore.gypi:
     12        * WebCore.vcproj/WebCore.vcproj:
     13        * inspector/front-end/AbstractTimelinePanel.js:
     14        (WebInspector.AbstractTimelinePanel.prototype.createInterface):
     15        (WebInspector.AbstractTimelinePanel.prototype.refresh):
     16        (WebInspector.AbstractTimelinePanel.prototype.set calculator):
     17        * inspector/front-end/TimelineGrid.js: Added.
     18        (WebInspector.TimelineGrid):
     19        (WebInspector.TimelineGrid.prototype.get itemsGraphsElement):
     20        (WebInspector.TimelineGrid.prototype.updateDividers):
     21        (WebInspector.TimelineGrid.prototype.addEventDivider):
     22        (WebInspector.TimelineGrid.prototype.setScrollAndDividerTop):
     23        * inspector/front-end/TimelineOverviewPane.js: Added.
     24        (WebInspector.TimelineOverviewPane):
     25        (WebInspector.TimelineOverviewPane.prototype._onCheckboxClicked):
     26        (WebInspector.TimelineOverviewPane.prototype.update):
     27        (WebInspector.TimelineOverviewPane.prototype.setSidebarWidth):
     28        (WebInspector.TimelineOverviewPane.prototype.updateMainViewWidth):
     29        (WebInspector.TimelineOverviewPane.prototype.reset):
     30        (WebInspector.TimelineOverviewPane.prototype._resizeWindow):
     31        (WebInspector.TimelineOverviewPane.prototype._windowResizeDragging):
     32        (WebInspector.TimelineOverviewPane.prototype._dragWindow):
     33        (WebInspector.TimelineOverviewPane.prototype._windowDragging):
     34        (WebInspector.TimelineOverviewPane.prototype._resizeWindowLeft):
     35        (WebInspector.TimelineOverviewPane.prototype._resizeWindowRight):
     36        (WebInspector.TimelineOverviewPane.prototype._setWindowPosition):
     37        (WebInspector.TimelineOverviewPane.prototype._endWindowDragging):
     38        (WebInspector.TimelineOverviewCalculator):
     39        (WebInspector.TimelineOverviewCalculator.prototype.computeBarGraphPercentages):
     40        (WebInspector.TimelineOverviewCalculator.prototype.reset):
     41        (WebInspector.TimelineOverviewCalculator.prototype.updateBoundaries):
     42        (WebInspector.TimelineOverviewCalculator.prototype.get boundarySpan):
     43        (WebInspector.TimelineOverviewCalculator.prototype.formatValue):
     44        (WebInspector.TimelineCategoryTreeElement):
     45        (WebInspector.TimelineCategoryTreeElement.prototype.onattach):
     46        (WebInspector.TimelineCategoryGraph):
     47        (WebInspector.TimelineCategoryGraph.prototype.get graphElement):
     48        (WebInspector.TimelineCategoryGraph.prototype.addChunk):
     49        (WebInspector.TimelineCategoryGraph.prototype.clearChunks):
     50        (WebInspector.TimelineCategoryGraph.prototype.set dimmed):
     51        * inspector/front-end/TimelinePanel.js:
     52        (WebInspector.TimelinePanel):
     53        (WebInspector.TimelinePanel.prototype._toggleTimelineButtonClicked):
     54        (WebInspector.TimelinePanel.prototype.addRecordToTimeline):
     55        (WebInspector.TimelinePanel.prototype._formatRecord):
     56        (WebInspector.TimelinePanel.prototype.setSidebarWidth):
     57        (WebInspector.TimelinePanel.prototype.updateMainViewWidth):
     58        (WebInspector.TimelinePanel.prototype.resize):
     59        (WebInspector.TimelinePanel.prototype.reset):
     60        (WebInspector.TimelinePanel.prototype.show):
     61        (WebInspector.TimelinePanel.prototype._onScroll):
     62        (WebInspector.TimelinePanel.prototype._scheduleRefresh):
     63        (WebInspector.TimelinePanel.prototype._refresh):
     64        (WebInspector.TimelinePanel.prototype._refreshRecords):
     65        (WebInspector.TimelinePanel.prototype._adjustScrollPosition):
     66        (WebInspector.TimelineCategory):
     67        (WebInspector.TimelineCalculator):
     68        (WebInspector.TimelineCalculator.prototype.get boundarySpan):
     69        (WebInspector.TimelineRecordListRow):
     70        (WebInspector.TimelineRecordListRow.prototype.update):
     71        (WebInspector.TimelineRecordGraphRow):
     72        (WebInspector.TimelineRecordGraphRow.prototype.update):
     73        * inspector/front-end/WebKit.qrc:
     74        * inspector/front-end/inspector.css:
     75        * inspector/front-end/inspector.html:
     76
    1772009-11-22  Chris Evans  <cevans@chromium.org>
    278
  • trunk/WebCore/WebCore.gypi

    r51245 r51296  
    36703670            'inspector/front-end/TextPrompt.js',
    36713671            'inspector/front-end/TimelineAgent.js',
     3672            'inspector/front-end/TimelineOverviewPane.js',
     3673            'inspector/front-end/TimelineGrid.js',
    36723674            'inspector/front-end/TimelinePanel.js',
    36733675            'inspector/front-end/TopDownProfileDataGridTree.js',
  • trunk/WebCore/WebCore.vcproj/WebCore.vcproj

    r51251 r51296  
    4249942499                                </File>
    4250042500                                <File
     42501                                        RelativePath="..\inspector\front-end\TimelineOverviewPane.js"
     42502                                        >
     42503                                </File>
     42504                                <File
     42505                                        RelativePath="..\inspector\front-end\TimelineGrid.js"
     42506                                        >
     42507                                </File>
     42508                                <File
    4250142509                                        RelativePath="..\inspector\front-end\TimelinePanel.js"
    4250242510                                        >
  • trunk/WebCore/inspector/front-end/AbstractTimelinePanel.js

    r51250 r51296  
    8282        this._containerContentElement.appendChild(this.summaryBar.element);
    8383
    84         this._timelineGrid = new WebInspector.TimelineGrid(this._containerContentElement);
     84        this._timelineGrid = new WebInspector.TimelineGrid();
     85        this._containerContentElement.appendChild(this._timelineGrid.element);
    8586        this.itemsGraphsElement = this._timelineGrid.itemsGraphsElement;
    8687    },
     
    301302        if (boundariesChanged) {
    302303            // The boundaries changed, so all item graphs are stale.
    303             this._staleItems = this._items;
     304            this._staleItems = this._items.slice();
    304305            staleItemsLength = this._staleItems.length;
    305306        }
     
    350351        this._calculator.reset();
    351352
    352         this._staleItems = this._items;
     353        this._staleItems = this._items.slice();
    353354        this.refresh();
    354355    },
     
    507508    }
    508509}
    509 
    510 WebInspector.TimelineGrid = function(container)
    511 {
    512     this._itemsGraphsElement = document.createElement("div");
    513     this._itemsGraphsElement.id = "resources-graphs";
    514     container.appendChild(this._itemsGraphsElement);
    515 
    516     this._dividersElement = document.createElement("div");
    517     this._dividersElement.id = "resources-dividers";
    518     container.appendChild(this._dividersElement);
    519 
    520     this._eventDividersElement = document.createElement("div");
    521     this._eventDividersElement.id = "resources-event-dividers";
    522     container.appendChild(this._eventDividersElement);
    523 
    524     this._dividersLabelBarElement = document.createElement("div");
    525     this._dividersLabelBarElement.id = "resources-dividers-label-bar";
    526     container.appendChild(this._dividersLabelBarElement);
    527 }
    528 
    529 WebInspector.TimelineGrid.prototype = {
    530     get itemsGraphsElement()
    531     {
    532         return this._itemsGraphsElement;
    533     },
    534 
    535     updateDividers: function(force, calculator)
    536     {
    537         var dividerCount = Math.round(this._dividersElement.offsetWidth / 64);
    538         var slice = calculator.boundarySpan / dividerCount;
    539         if (!force && this._currentDividerSlice === slice)
    540             return false;
    541 
    542         this._currentDividerSlice = slice;
    543 
    544         this._dividersElement.removeChildren();
    545         this._eventDividersElement.removeChildren();
    546         this._dividersLabelBarElement.removeChildren();
    547 
    548         for (var i = 1; i <= dividerCount; ++i) {
    549             var divider = document.createElement("div");
    550             divider.className = "resources-divider";
    551             if (i === dividerCount)
    552                 divider.addStyleClass("last");
    553             divider.style.left = ((i / dividerCount) * 100) + "%";
    554 
    555             this._dividersElement.appendChild(divider.cloneNode());
    556 
    557             var label = document.createElement("div");
    558             label.className = "resources-divider-label";
    559             if (!isNaN(slice))
    560                 label.textContent = calculator.formatValue(slice * i);
    561             divider.appendChild(label);
    562 
    563             this._dividersLabelBarElement.appendChild(divider);
    564         }
    565         return true;
    566     },
    567 
    568     addEventDivider: function(divider)
    569     {
    570         this._eventDividersElement.appendChild(divider);
    571     },
    572 
    573     setScrollAndDividerTop: function(scrollTop, dividersTop)
    574     {
    575         this._dividersElement.style.top = scrollTop + "px";
    576         this._eventDividersElement.style.top = scrollTop + "px";
    577         this._dividersLabelBarElement.style.top = dividersTop + "px";
    578     }
    579 }
  • trunk/WebCore/inspector/front-end/TimelinePanel.js

    r51072 r51296  
    3131WebInspector.TimelinePanel = function()
    3232{
    33     WebInspector.AbstractTimelinePanel.call(this);
    34 
     33    WebInspector.Panel.call(this);
    3534    this.element.addStyleClass("timeline");
    3635
    37     this._createOverview();
    38     this.createInterface();
    39     this.containerElement.id = "timeline-container";
    40     this.summaryBar.element.id = "timeline-summary";
    41     this.itemsGraphsElement.id = "timeline-graphs";
     36    this._overviewPane = new WebInspector.TimelineOverviewPane(this.categories);
     37    this._overviewPane.addEventListener("window changed", this._scheduleRefresh, this);
     38    this._overviewPane.addEventListener("filter changed", this._refresh, this);
     39    this.element.appendChild(this._overviewPane.element);
     40
     41    this._containerElement = document.createElement("div");
     42    this._containerElement.id = "timeline-container";
     43    this._containerElement.addEventListener("scroll", this._onScroll.bind(this), false);
     44    this.element.appendChild(this._containerElement);
     45
     46    this.createSidebar(this._containerElement, this._containerElement);
     47    this.sidebarElement.id = "timeline-sidebar";
     48    this.itemsTreeElement = new WebInspector.SidebarSectionTreeElement(WebInspector.UIString("RECORDS"), {}, true);
     49    this.itemsTreeElement.expanded = true;
     50    this.sidebarTree.appendChild(this.itemsTreeElement);
     51
     52    this._sidebarListElement = document.createElement("div");
     53    this.sidebarElement.appendChild(this._sidebarListElement);
     54
     55    this._containerContentElement = document.createElement("div");
     56    this._containerContentElement.id = "resources-container-content";
     57    this._containerElement.appendChild(this._containerContentElement);
     58
     59    this._timelineGrid = new WebInspector.TimelineGrid();
     60    this._itemsGraphsElement = this._timelineGrid.itemsGraphsElement;
     61    this._itemsGraphsElement.id = "timeline-graphs";
     62    this._containerContentElement.appendChild(this._timelineGrid.element);
     63
     64    this._topGapElement = document.createElement("div");
     65    this._topGapElement.className = "timeline-gap";
     66    this._itemsGraphsElement.appendChild(this._topGapElement);
     67
     68    this._graphRowsElement = document.createElement("div");
     69    this._itemsGraphsElement.appendChild(this._graphRowsElement);
     70
     71    this._bottomGapElement = document.createElement("div");
     72    this._bottomGapElement.className = "timeline-gap";
     73    this._itemsGraphsElement.appendChild(this._bottomGapElement);
    4274
    4375    this._createStatusbarButtons();
    4476
    45     this.calculator = new WebInspector.TimelineCalculator();
    46     for (category in this.categories)
    47         this.showCategory(category);
     77    this._records = [];
    4878    this._sendRequestRecords = {};
     79    this._calculator = new WebInspector.TimelineCalculator();
    4980}
    5081
     
    74105    },
    75106
    76     populateSidebar: function()
    77     {
    78         this.itemsTreeElement = new WebInspector.SidebarSectionTreeElement(WebInspector.UIString("RECORDS"), {}, true);
    79         this.itemsTreeElement.expanded = true;
    80         this.sidebarTree.appendChild(this.itemsTreeElement);
    81     },
    82 
    83107    _createStatusbarButtons: function()
    84108    {
     
    88112        this.clearButton = new WebInspector.StatusBarButton("", "timeline-clear-status-bar-item");
    89113        this.clearButton.addEventListener("click", this.reset.bind(this), false);
     114    },
     115
     116    _toggleTimelineButtonClicked: function()
     117    {
     118        if (this.toggleTimelineButton.toggled)
     119            InspectorController.stopTimelineProfiler();
     120        else
     121            InspectorController.startTimelineProfiler();
    90122    },
    91123
     
    111143            this._lastRecord.endTime = formattedRecord.endTime;
    112144            this._lastRecord.count++;
    113             this.refreshItem(this._lastRecord);
    114145        } else {
    115             this.addItem(formattedRecord);
     146            this._records.push(formattedRecord);
    116147
    117148            for (var i = 0; record.children && i < record.children.length; ++i)
     
    119150            this._lastRecord = record.children && record.children.length ? null : formattedRecord;
    120151        }
    121     },
    122 
    123     createItemTreeElement: function(item)
    124     {
    125         return new WebInspector.TimelineRecordTreeElement(item);
    126     },
    127 
    128     createItemGraph: function(item)
    129     {
    130         return new WebInspector.TimelineGraph(item);
    131     },
    132 
    133     _toggleTimelineButtonClicked: function()
    134     {
    135         if (this.toggleTimelineButton.toggled)
    136             InspectorController.stopTimelineProfiler();
    137         else
    138             InspectorController.startTimelineProfiler();
     152        this._scheduleRefresh();
    139153    },
    140154
     
    183197            formattedRecord.startTime = sendRequestRecord.startTime;
    184198            sendRequestRecord.details = this._getRecordDetails(record);
    185             this.refreshItem(sendRequestRecord);
    186199        } else if (record.type === WebInspector.TimelineAgent.RecordType.ResourceFinish) {
    187200            var sendRequestRecord = this._sendRequestRecords[record.data.identifier];
     
    221234    },
    222235
     236    setSidebarWidth: function(width)
     237    {
     238        WebInspector.Panel.prototype.setSidebarWidth.call(this, width);
     239        this._overviewPane.setSidebarWidth(width);
     240    },
     241
     242    updateMainViewWidth: function(width)
     243    {
     244        this._containerContentElement.style.left = width + "px";
     245        this._scheduleRefresh();
     246        this._overviewPane.updateMainViewWidth(width);
     247    },
     248
     249    resize: function() {
     250        this._scheduleRefresh();
     251    },
     252
    223253    reset: function()
    224254    {
    225         WebInspector.AbstractTimelinePanel.prototype.reset.call(this);
    226255        this._lastRecord = null;
    227         this._overviewCalculator.reset();
    228         for (var category in this.categories)
    229             this._categoryGraphs[category].clearChunks();
    230         this._setWindowPosition(0, this._overviewGridElement.clientWidth);
    231256        this._sendRequestRecords = {};
    232     },
    233 
    234     _createOverview: function()
    235     {
    236         var overviewPanelElement = document.createElement("div");
    237         overviewPanelElement.id = "timeline-overview-panel";
    238         this.element.appendChild(overviewPanelElement);
    239 
    240         this._overviewSidebarElement = document.createElement("div");
    241         this._overviewSidebarElement.id = "timeline-overview-sidebar";
    242         overviewPanelElement.appendChild(this._overviewSidebarElement);
    243 
    244         var overviewTreeElement = document.createElement("ol");
    245         overviewTreeElement.className = "sidebar-tree";
    246         this._overviewSidebarElement.appendChild(overviewTreeElement);
    247         var sidebarTree = new TreeOutline(overviewTreeElement);
    248 
    249         var categoriesTreeElement = new WebInspector.SidebarSectionTreeElement(WebInspector.UIString("TIMELINES"), {}, true);
    250         categoriesTreeElement.expanded = true;
    251         sidebarTree.appendChild(categoriesTreeElement);
    252 
    253         for (var category in this.categories)
    254             categoriesTreeElement.appendChild(new WebInspector.TimelineCategoryTreeElement(this.categories[category]));
    255 
    256         this._overviewGridElement = document.createElement("div");
    257         this._overviewGridElement.id = "timeline-overview-grid";
    258         overviewPanelElement.appendChild(this._overviewGridElement);
    259         this._overviewGrid = new WebInspector.TimelineGrid(this._overviewGridElement);
    260         this._overviewGrid.itemsGraphsElement.id = "timeline-overview-graphs";
    261 
    262         this._categoryGraphs = {};
    263         for (var category in this.categories) {
    264             var categoryGraph = new WebInspector.TimelineCategoryGraph(this.categories[category]);
    265             this._categoryGraphs[category] = categoryGraph;
    266             this._overviewGrid.itemsGraphsElement.appendChild(categoryGraph.graphElement);
    267         }
    268         this._overviewGrid.setScrollAndDividerTop(0, 0);
    269 
    270         this._overviewWindowElement = document.createElement("div");
    271         this._overviewWindowElement.id = "timeline-overview-window";
    272         this._overviewWindowElement.addEventListener("mousedown", this._dragWindow.bind(this), false);
    273         this._overviewGridElement.appendChild(this._overviewWindowElement);
    274 
    275         this._leftResizeElement = document.createElement("div");
    276         this._leftResizeElement.className = "timeline-window-resizer";
    277         this._leftResizeElement.style.left = 0;
    278         this._overviewGridElement.appendChild(this._leftResizeElement);
    279         this._leftResizeElement.addEventListener("mousedown", this._resizeWindow.bind(this, this._leftResizeElement), false);
    280 
    281         this._rightResizeElement = document.createElement("div");
    282         this._rightResizeElement.className = "timeline-window-resizer timeline-window-resizer-right";
    283         this._rightResizeElement.style.right = 0;
    284         this._overviewGridElement.appendChild(this._rightResizeElement);
    285         this._rightResizeElement.addEventListener("mousedown", this._resizeWindow.bind(this, this._rightResizeElement), false);
    286 
    287         this._overviewCalculator = new WebInspector.TimelineCalculator();
    288 
    289         var separatorElement = document.createElement("div");
    290         separatorElement.id = "timeline-overview-separator";
    291         this.element.appendChild(separatorElement);
    292     },
    293 
    294     setSidebarWidth: function(width)
    295     {
    296         WebInspector.AbstractTimelinePanel.prototype.setSidebarWidth.call(this, width);
    297         this._overviewSidebarElement.style.width = width + "px";
    298     },
    299 
    300     updateMainViewWidth: function(width)
    301     {
    302         WebInspector.AbstractTimelinePanel.prototype.updateMainViewWidth.call(this, width);
    303         this._overviewGridElement.style.left = width + "px";
    304     },
    305 
    306     updateGraphDividersIfNeeded: function()
    307     {
    308         WebInspector.AbstractTimelinePanel.prototype.updateGraphDividersIfNeeded.call(this);
    309         this._overviewGrid.updateDividers(true, this._overviewCalculator);
    310     },
    311 
    312     refresh: function()
    313     {
    314         WebInspector.AbstractTimelinePanel.prototype.refresh.call(this);
    315         this.adjustScrollPosition();
    316 
    317         // Clear summary bars.
    318         var timelines = {};
    319         for (var category in this.categories) {
    320             timelines[category] = [];
    321             this._categoryGraphs[category].clearChunks();
    322         }
    323 
    324         // Create sparse arrays with 101 cells each to fill with chunks for a given category.
    325         for (var i = 0; i < this.items.length; ++i) {
    326             var record = this.items[i];
    327             this._overviewCalculator.updateBoundaries(record);
    328             var percentages = this._overviewCalculator.computeBarGraphPercentages(record);
    329             var end = Math.round(percentages.end);
    330             var categoryName = record.category.name;
    331             for (var j = Math.round(percentages.start); j <= end; ++j)
    332                 timelines[categoryName][j] = true;
    333         }
    334 
    335         // Convert sparse arrays to continuous segments, render graphs for each.
    336         for (var category in this.categories) {
    337             var timeline = timelines[category];
    338             window.timelineSaved = timeline;
    339             var chunkStart = -1;
    340             for (var j = 0; j < 101; ++j) {
    341                 if (timeline[j]) {
    342                     if (chunkStart === -1)
    343                         chunkStart = j;
    344                 } else {
    345                     if (chunkStart !== -1) {
    346                         this._categoryGraphs[category].addChunk(chunkStart, j);
    347                         chunkStart = -1;
    348                     }
    349                 }
     257        this._overviewPane.reset();
     258        this._records = [];
     259        this._refresh();
     260    },
     261
     262    show: function()
     263    {
     264        WebInspector.Panel.prototype.show.call(this);
     265
     266        if (this._needsRefresh)
     267            this._refresh();
     268    },
     269
     270    _onScroll: function(event)
     271    {
     272        var scrollTop = this._containerElement.scrollTop;
     273        var dividersTop = Math.max(0, scrollTop);
     274        this._timelineGrid.setScrollAndDividerTop(scrollTop, dividersTop);
     275        this._scheduleRefresh();
     276    },
     277
     278    _scheduleRefresh: function()
     279    {
     280        if (this._needsRefresh)
     281            return;
     282        this._needsRefresh = true;
     283
     284        if (this.visible && !("_refreshTimeout" in this))
     285            this._refreshTimeout = setTimeout(this._refresh.bind(this), 100);
     286    },
     287
     288    _refresh: function()
     289    {
     290        this._needsRefresh = false;
     291        if ("_refreshTimeout" in this) {
     292            clearTimeout(this._refreshTimeout);
     293            delete this._refreshTimeout;
     294        }
     295        this._overviewPane.update(this._records);
     296        this._refreshRecords();
     297    },
     298
     299    _refreshRecords: function()
     300    {
     301        this._calculator.windowLeft = this._overviewPane.windowLeft;
     302        this._calculator.windowRight = this._overviewPane.windowRight;
     303        this._calculator.reset();
     304
     305        for (var i = 0; i < this._records.length; ++i)
     306            this._calculator.updateBoundaries(this._records[i]);
     307
     308        var recordsInWindow = [];
     309        for (var i = 0; i < this._records.length; ++i) {
     310            var record = this._records[i];
     311            var percentages = this._calculator.computeBarGraphPercentages(record);
     312            if (percentages.start < 100 && percentages.end >= 0 && !record.category.hidden)
     313                recordsInWindow.push(record);
     314        }
     315
     316        // Calculate the visible area.
     317        var visibleTop = this._containerElement.scrollTop;
     318        var visibleBottom = visibleTop + this._containerElement.clientHeight;
     319        const rowHeight = 18;
     320
     321        // Convert visible area to visible indexes.
     322        var startIndex = Math.max(0, Math.floor(visibleTop / rowHeight) - 1);
     323        var endIndex = Math.min(recordsInWindow.length, Math.ceil(visibleBottom / rowHeight));
     324
     325        var listRowElement = this._sidebarListElement.firstChild;
     326        var graphRowElement = this._graphRowsElement.firstChild;
     327        for (var i = startIndex; i < endIndex; ++i) {
     328            var record = recordsInWindow[i];
     329            var isEven = !(i % 2);
     330
     331            if (!listRowElement) {
     332                listRowElement = new WebInspector.TimelineRecordListRow().element;
     333                this._sidebarListElement.appendChild(listRowElement);
    350334            }
    351             if (chunkStart !== -1) {
    352                 this._categoryGraphs[category].addChunk(chunkStart, 100);
    353                 chunkStart = -1;
     335            if (!graphRowElement) {
     336                graphRowElement = new WebInspector.TimelineRecordGraphRow().element;
     337                this._graphRowsElement.appendChild(graphRowElement);
    354338            }
    355         }
    356         this._overviewGrid.updateDividers(true, this._overviewCalculator);
    357     },
    358 
    359     _resizeWindow: function(resizeElement, event)
    360     {
    361         WebInspector.elementDragStart(resizeElement, this._windowResizeDragging.bind(this, resizeElement), this._endWindowDragging.bind(this), event, "col-resize");
    362     },
    363 
    364     _windowResizeDragging: function(resizeElement, event)
    365     {
    366         if (resizeElement === this._leftResizeElement)
    367             this._resizeWindowLeft(event.pageX - this._overviewGridElement.offsetLeft);
    368         else
    369             this._resizeWindowRight(event.pageX - this._overviewGridElement.offsetLeft);
    370         event.preventDefault();
    371     },
    372 
    373     _dragWindow: function(event)
    374     {
    375         WebInspector.elementDragStart(this._overviewWindowElement, this._windowDragging.bind(this, event.pageX,
    376             this._leftResizeElement.offsetLeft, this._rightResizeElement.offsetLeft), this._endWindowDragging.bind(this), event, "ew-resize");
    377     },
    378 
    379     _windowDragging: function(startX, windowLeft, windowRight, event)
    380     {
    381         var delta = event.pageX - startX;
    382         var start = windowLeft + delta;
    383         var end = windowRight + delta;
    384         var windowSize = windowRight - windowLeft;
    385 
    386         if (start < 0) {
    387             start = 0;
    388             end = windowSize;
    389         }
    390 
    391         if (end > this._overviewGridElement.clientWidth) {
    392             end = this._overviewGridElement.clientWidth;
    393             start = end - windowSize;
    394         }
    395         this._setWindowPosition(start, end);
    396 
    397         event.preventDefault();
    398     },
    399 
    400     _resizeWindowLeft: function(start)
    401     {
    402         // Glue to edge.
    403         if (start < 10)
    404             start = 0;
    405         this._setWindowPosition(start, null);
    406     },
    407 
    408     _resizeWindowRight: function(end)
    409     {
    410         // Glue to edge.
    411         if (end > this._overviewGridElement.clientWidth - 10)
    412             end = this._overviewGridElement.clientWidth;
    413         this._setWindowPosition(null, end);
    414     },
    415 
    416     _setWindowPosition: function(start, end)
    417     {
    418         this.calculator.reset();
    419         this.invalidateAllItems();
    420         if (typeof start === "number") {
    421             if (start > this._rightResizeElement.offsetLeft - 25)
    422                 start = this._rightResizeElement.offsetLeft - 25;
    423 
    424             var windowLeft = start / this._overviewGridElement.clientWidth;
    425             this.calculator.windowLeft = windowLeft;
    426             this._leftResizeElement.style.left = windowLeft * 100 + "%";
    427             this._overviewWindowElement.style.left = windowLeft * 100 + "%";
    428         }
    429         if (typeof end === "number") {
    430             if (end < this._leftResizeElement.offsetLeft + 30)
    431                 end = this._leftResizeElement.offsetLeft + 30;
    432 
    433             var windowRight = end / this._overviewGridElement.clientWidth;
    434             this.calculator.windowRight = windowRight;
    435             this._rightResizeElement.style.left = windowRight * 100 + "%";
    436         }
    437         this._overviewWindowElement.style.width = (this.calculator.windowRight - this.calculator.windowLeft) * 100 + "%";
    438         this.needsRefresh = true;
    439     },
    440 
    441     _endWindowDragging: function(event)
    442     {
    443         WebInspector.elementDragEnd(event);
     339
     340            listRowElement.listRow.update(record, isEven);
     341            graphRowElement.graphRow.update(record, isEven, this._calculator);
     342
     343            listRowElement = listRowElement.nextSibling;
     344            graphRowElement = graphRowElement.nextSibling;
     345        }
     346
     347        while (listRowElement) {
     348            var nextElement = listRowElement.nextSibling;
     349            listRowElement.parentElement.removeChild(listRowElement);
     350            listRowElement = nextElement;
     351        }
     352
     353        while (graphRowElement) {
     354            var nextElement = graphRowElement.nextSibling;
     355            graphRowElement.parentElement.removeChild(graphRowElement);
     356            graphRowElement = nextElement;
     357        }
     358
     359        this._timelineGrid.updateDividers(true, this._calculator);
     360
     361        const top = (startIndex * rowHeight) + "px";
     362        this._topGapElement.style.height = top;
     363        this.sidebarElement.style.top = top;
     364        this.sidebarResizeElement.style.top = top;
     365        this._bottomGapElement.style.height = (recordsInWindow.length - endIndex) * rowHeight + "px";
     366        this._adjustScrollPosition((recordsInWindow.length + 1) * rowHeight);
     367    },
     368
     369    _adjustScrollPosition: function(totalHeight)
     370    {
     371        // Prevent the container from being scrolled off the end.
     372        if ((this._containerElement.scrollTop + this._containerElement.offsetHeight) > totalHeight + 1)
     373            this._containerElement.scrollTop = (totalHeight - this._containerElement.offsetHeight);
    444374    }
    445375}
    446376
    447 WebInspector.TimelinePanel.prototype.__proto__ = WebInspector.AbstractTimelinePanel.prototype;
     377WebInspector.TimelinePanel.prototype.__proto__ = WebInspector.Panel.prototype;
    448378
    449379
    450380WebInspector.TimelineCategory = function(name, title, color)
    451381{
    452     WebInspector.AbstractTimelineCategory.call(this, name, title, color);
    453 }
    454 
    455 WebInspector.TimelineCategory.prototype = {
    456 }
    457 
    458 WebInspector.TimelineCategory.prototype.__proto__ = WebInspector.AbstractTimelineCategory.prototype;
    459 
    460 
    461 
    462 WebInspector.TimelineCategoryTreeElement = function(category)
    463 {
    464     this._category = category;
    465 
    466     // Pass an empty title, the title gets made later in onattach.
    467     TreeElement.call(this, "", null, false);
    468 }
    469 
    470 WebInspector.TimelineCategoryTreeElement.prototype = {
    471     onattach: function()
    472     {
    473         this.listItemElement.removeChildren();
    474         this.listItemElement.addStyleClass("timeline-category-tree-item");
    475         this.listItemElement.addStyleClass("timeline-category-" + this._category.name);
    476 
    477         var label = document.createElement("label");
    478 
    479         var checkElement = document.createElement("input");
    480         checkElement.type = "checkbox";
    481         checkElement.className = "timeline-category-checkbox";
    482         checkElement.checked = true;
    483         checkElement.addEventListener("click", this._onCheckboxClicked.bind(this));
    484         label.appendChild(checkElement);
    485 
    486         var typeElement = document.createElement("span");
    487         typeElement.className = "type";
    488         typeElement.textContent = this._category.title;
    489         label.appendChild(typeElement);
    490 
    491         this.listItemElement.appendChild(label);
    492     },
    493 
    494     _onCheckboxClicked: function (event) {
    495         if (event.target.checked)
    496             WebInspector.panels.timeline.showCategory(this._category.name);
    497         else {
    498             WebInspector.panels.timeline.hideCategory(this._category.name);
    499             WebInspector.panels.timeline.adjustScrollPosition();
    500         }
    501         WebInspector.panels.timeline._categoryGraphs[this._category.name].dimmed = !event.target.checked;
    502     }
    503 }
    504 
    505 WebInspector.TimelineCategoryTreeElement.prototype.__proto__ = TreeElement.prototype;
    506 
    507 
    508 WebInspector.TimelineRecordTreeElement = function(record)
    509 {
    510     this._record = record;
    511 
    512     // Pass an empty title, the title gets made later in onattach.
    513     TreeElement.call(this, "", null, false);
    514 }
    515 
    516 WebInspector.TimelineRecordTreeElement.prototype = {
    517     onattach: function()
    518     {
    519         this.listItemElement.removeChildren();
    520         this.listItemElement.addStyleClass("timeline-tree-item");
    521         this.listItemElement.addStyleClass("timeline-category-" + this._record.category.name);
    522 
    523         var iconElement = document.createElement("span");
    524         iconElement.className = "timeline-tree-icon";
    525         this.listItemElement.appendChild(iconElement);
    526 
    527         this.typeElement = document.createElement("span");
    528         this.typeElement.className = "type";
    529         this.typeElement.textContent = this._record.title;
    530         this.listItemElement.appendChild(this.typeElement);
    531 
    532         if (this._record.details) {
    533             var separatorElement = document.createElement("span");
    534             separatorElement.className = "separator";
    535             separatorElement.textContent = " ";
    536 
    537             this.dataElement = document.createElement("span");
    538             this.dataElement.className = "data dimmed";
    539             this._updateDetails();
    540 
    541             this.listItemElement.appendChild(separatorElement);
    542             this.listItemElement.appendChild(this.dataElement);
    543         }
    544     },
    545 
    546     _updateDetails: function()
    547     {
    548         if (this.dataElement && this._record.details !== this._details) {
    549             this._details = this._record.details;
    550             this.dataElement.textContent = "(" + this._details + ")";
    551             this.dataElement.title = this._details;
    552         }
    553     },
    554 
    555     refresh: function()
    556     {
    557         this._updateDetails();
    558 
    559         if (this._record.count <= 1)
    560             return;
    561 
    562         if (!this.repeatCountElement) {
    563             this.repeatCountElement = document.createElement("span");
    564             this.repeatCountElement.className = "count";
    565             this.listItemElement.appendChild(this.repeatCountElement);
    566         }
    567 
    568         this.repeatCountElement.textContent = "\u2009\u00d7\u2009" + this._record.count;
    569     }
    570 }
    571 
    572 WebInspector.TimelineRecordTreeElement.prototype.__proto__ = TreeElement.prototype;
     382    this.name = name;
     383    this.title = title;
     384    this.color = color;
     385}
    573386
    574387
    575388WebInspector.TimelineCalculator = function()
    576389{
    577     WebInspector.AbstractTimelineCalculator.call(this);
    578390    this.windowLeft = 0.0;
    579391    this.windowRight = 1.0;
     
    587399        var end = (record.endTime - this.minimumBoundary) / this.boundarySpan * 100;
    588400        return {start: start, end: end};
    589     },
    590 
    591     computePercentageFromEventTime: function(eventTime)
    592     {
    593         return ((eventTime - this.minimumBoundary) / this.boundarySpan) * 100;
    594     },
    595 
    596     computeBarGraphLabels: function(record)
    597     {
    598         return {tooltip: record.title};
    599401    },
    600402
     
    653455    },
    654456
     457    get boundarySpan()
     458    {
     459        return this.maximumBoundary - this.minimumBoundary;
     460    },
     461
    655462    formatValue: function(value)
    656463    {
     
    659466}
    660467
    661 WebInspector.TimelineCalculator.prototype.__proto__ = WebInspector.AbstractTimelineCalculator.prototype;
    662 
    663 
    664 WebInspector.TimelineCategoryGraph = function(category)
     468
     469WebInspector.TimelineRecordListRow = function()
    665470{
    666     this._category = category;
    667 
    668     this._graphElement = document.createElement("div");
    669     this._graphElement.className = "timeline-graph-side timeline-overview-graph-side";
    670 
    671     this._barAreaElement = document.createElement("div");
    672     this._barAreaElement.className = "timeline-graph-bar-area timeline-category-" + category.name;
    673     this._graphElement.appendChild(this._barAreaElement);
    674 }
    675 
    676 WebInspector.TimelineCategoryGraph.prototype = {
    677     get graphElement()
    678     {
    679         return this._graphElement;
    680     },
    681 
    682     addChunk: function(start, end)
    683     {
    684         var chunk = document.createElement("div");
    685         chunk.className = "timeline-graph-bar";
    686         this._barAreaElement.appendChild(chunk);
    687         chunk.style.setProperty("left", start + "%");
    688         chunk.style.setProperty("width", (end - start) + "%");
    689     },
    690 
    691     clearChunks: function()
    692     {
    693         this._barAreaElement.removeChildren();
    694     },
    695 
    696     set dimmed(dimmed)
    697     {
    698         if (dimmed)
    699             this._barAreaElement.removeStyleClass("timeline-category-" + this._category.name);
     471    this.element = document.createElement("div");
     472    this.element.listRow = this;
     473
     474    var iconElement = document.createElement("span");
     475    iconElement.className = "timeline-tree-icon";
     476    this.element.appendChild(iconElement);
     477
     478    this._typeElement = document.createElement("span");
     479    this._typeElement.className = "type";
     480    this.element.appendChild(this._typeElement);
     481
     482    var separatorElement = document.createElement("span");
     483    separatorElement.className = "separator";
     484    separatorElement.textContent = " ";
     485
     486    this._dataElement = document.createElement("span");
     487    this._dataElement.className = "data dimmed";
     488
     489    this._repeatCountElement = document.createElement("span");
     490    this._repeatCountElement.className = "count";
     491
     492    this.element.appendChild(separatorElement);
     493    this.element.appendChild(this._dataElement);
     494    this.element.appendChild(this._repeatCountElement);
     495}
     496
     497WebInspector.TimelineRecordListRow.prototype = {
     498    update: function(record, isEven)
     499    {
     500        this.element.className = "timeline-tree-item timeline-category-" + record.category.name + (isEven ? " even" : "");
     501        this._typeElement.textContent = record.title;
     502
     503        if (record.details) {
     504            this._dataElement.textContent = "(" + record.details + ")";
     505            this._dataElement.title = record.details;
     506        } else {
     507            this._dataElement.textContent = "";
     508            this._dataElement.title = "";
     509        }
     510
     511        if (record.count > 1)
     512            this._repeatCountElement.textContent = "\u2009\u00d7\u2009" + record.count;
    700513        else
    701             this._barAreaElement.addStyleClass("timeline-category-" + this._category.name);
     514            this._repeatCountElement.textContent = "";
    702515    }
    703516}
    704517
    705518
    706 WebInspector.TimelineGraph = function(record)
     519WebInspector.TimelineRecordGraphRow = function()
    707520{
    708     this.record = record;
    709 
    710     this._graphElement = document.createElement("div");
    711     this._graphElement.className = "timeline-graph-side";
     521    this.element = document.createElement("div");
     522    this.element.graphRow = this;
    712523
    713524    this._barAreaElement = document.createElement("div");
    714525    this._barAreaElement.className = "timeline-graph-bar-area";
    715     this._graphElement.appendChild(this._barAreaElement);
     526    this.element.appendChild(this._barAreaElement);
    716527
    717528    this._barElement = document.createElement("div");
    718529    this._barElement.className = "timeline-graph-bar";
    719530    this._barAreaElement.appendChild(this._barElement);
    720 
    721     this._graphElement.addStyleClass("timeline-category-" + record.category.name);
    722     this._hidden = false;
    723 }
    724 
    725 WebInspector.TimelineGraph.prototype = {
    726     get graphElement()
    727     {
    728         return this._graphElement;
    729     },
    730 
    731     refreshLabelPositions: function()
    732     {
    733     },
    734 
    735     refresh: function(calculator)
    736     {
    737         var percentages = calculator.computeBarGraphPercentages(this.record);
    738         var labels = calculator.computeBarGraphLabels(this.record);
    739 
    740         this._percentages = percentages;
    741 
    742         if (percentages.start > 100 || percentages.end < 0) {
    743             if (!this._hidden) {
    744                 this._graphElement.addStyleClass("hidden");
    745                 this.record._itemsTreeElement.listItemElement.addStyleClass("hidden");
    746                 this._hidden = true;
    747             }
    748         } else {
    749             this._barElement.style.setProperty("left", percentages.start + "%");
    750             this._barElement.style.setProperty("right", (100 - percentages.end) + "%");
    751             if (this._hidden) {
    752                 this._graphElement.removeStyleClass("hidden");
    753                 this.record._itemsTreeElement.listItemElement.removeStyleClass("hidden");
    754                 this._hidden = false;
    755             }
    756         }
    757         var tooltip = (labels.tooltip || "");
    758         this._barElement.title = tooltip;
     531}
     532
     533WebInspector.TimelineRecordGraphRow.prototype = {
     534    update: function(record, isEven, calculator)
     535    {
     536        this.element.className = "timeline-graph-side timeline-category-" + record.category.name + (isEven ? " even" : "");
     537        var percentages = calculator.computeBarGraphPercentages(record);
     538        this._barElement.style.setProperty("left", percentages.start + "%");
     539        this._barElement.style.setProperty("right", (100 - percentages.end) + "%");
    759540    }
    760541}
  • trunk/WebCore/inspector/front-end/WebKit.qrc

    r51245 r51296  
    6363    <file>TextPrompt.js</file>
    6464    <file>TimelineAgent.js</file>
     65    <file>TimelineGrid.js</file>
     66    <file>TimelineOverviewPane.js</file>
    6567    <file>TimelinePanel.js</file>
    6668    <file>TopDownProfileDataGridTree.js</file>
  • trunk/WebCore/inspector/front-end/inspector.css

    r51255 r51296  
    14371437
    14381438.event-bars .event-bar .header .subtitle {
    1439     color: rgba(90, 90, 90, 0.742188);
     1439    color: rgba(90, 90, 90, 0.75);
    14401440}
    14411441
     
    17821782    position: absolute;
    17831783    top: 0;
     1784    min-height: 100%;
    17841785    left: 0;
    1785     bottom: 0;
    17861786    width: 200px;
    17871787    overflow-y: auto;
     
    25952595#resources-dividers-label-bar {
    25962596    position: absolute;
    2597     top: 93px;
     2597    top: 0;
    25982598    left: 0px;
    25992599    right: 0;
     
    28672867    position: absolute;
    28682868    top: 0;
    2869     bottom: 0;
     2869    min-height: 100%;
    28702870    width: 5px;
    28712871    z-index: 500;
     
    33473347}
    33483348
    3349 .timeline .sidebar-resizer-vertical {
    3350     top: 90px;
    3351 }
    3352 
    33533349#timeline-overview-grid #resources-graphs {
    33543350    position: absolute;
     
    33763372.timeline-category-tree-item {
    33773373    height: 20px;
     3374    line-height: 20px;
    33783375    padding-left: 6px;
    3379     padding-top: 3px;
    33803376    white-space: nowrap;
    33813377    text-overflow: ellipsis;
     
    34183414.timeline-tree-item {
    34193415    height: 18px;
     3416    line-height: 15px;
    34203417    padding-left: 10px;
    34213418    padding-top: 2px;
     
    34423439}
    34433440
    3444 .timeline-tree-item:nth-of-type(2n) {
     3441.timeline-tree-item.even {
    34453442    background-color: rgba(0, 0, 0, 0.05);
    34463443}
     
    34483445.timeline-tree-item .data.dimmed {
    34493446    color: rgba(0, 0, 0, 0.7);
    3450 }
    3451 
    3452 #timeline-container .timeline-category-loading, #timeline-container .timeline-category-scripting, #timeline-container .timeline-category-rendering {
    3453     display: none;
    3454 }
    3455 
    3456 #timeline-container .filter-all .timeline-category-loading, #timeline-container .filter-loading .timeline-category-loading,
    3457 #timeline-container .filter-all .timeline-category-scripting, #timeline-container .filter-scripting .timeline-category-scripting,
    3458 #timeline-container .filter-all .timeline-category-rendering, #timeline-container .filter-rendering .timeline-category-rendering {
    3459     display: list-item;
    34603447}
    34613448
     
    35193506}
    35203507
    3521 .timeline-graph-side:nth-of-type(2n) {
     3508.timeline-graph-side.even {
    35223509    background-color: rgba(0, 0, 0, 0.05);
    35233510}
  • trunk/WebCore/inspector/front-end/inspector.html

    r51245 r51296  
    4747    <script type="text/javascript" src="ConsoleView.js"></script>
    4848    <script type="text/javascript" src="Panel.js"></script>
     49    <script type="text/javascript" src="TimelineGrid.js"></script>   
    4950    <script type="text/javascript" src="AbstractTimelinePanel.js"></script>
    5051    <script type="text/javascript" src="Resource.js"></script>
     
    9899    <script type="text/javascript" src="TimelineAgent.js"></script>
    99100    <script type="text/javascript" src="TimelinePanel.js"></script>
     101    <script type="text/javascript" src="TimelineOverviewPane.js"></script>
    100102    <script type="text/javascript" src="TestController.js"></script>
    101103</head>
Note: See TracChangeset for help on using the changeset viewer.