Changeset 248425 in webkit


Ignore:
Timestamp:
Aug 8, 2019 10:53:51 AM (5 years ago)
Author:
Jonathan Bedard
Message:

results.webkit.org: Use canvas for timeline
https://bugs.webkit.org/show_bug.cgi?id=200172

Rubber-stamped by Aakash Jain.

  • resultsdbpy/resultsdbpy/view/static/js/commit.js:

(Commit.constructor): Make uuid a member variable instead of a member function for efficiency.
(Commit.compare): Ditto.
(_CommitBank.commitByUuid): Ditto.
(_CommitBank._loadSiblings): Ditto.
(_CommitBank._load): Ditto.

  • resultsdbpy/resultsdbpy/view/static/js/timeline.js:

(tickForCommit): Deleted.
(minimumUuidForResults): Given a dictionary of result lists, determine the minimum UUID
which encompasses all results. Crucially, this function must exclude an UUIDs which may
refer to results excluded because of the limit argument.
(renderTimeline): Deleted.
(commitsForResults): Given a dictionary of result lists, return a list of commits associated
with those results.
(scaleForCommits): Given a list of commits, generate a scale to be consumed by the canvas Timeline.
(repositoriesForCommits): Given a list of commits, return a sorted list of associated repository ids.
(xAxisFromScale): Create a canvas-based x-axis based on the provided scale and a repository id.
(inPlaceCombine): Combine result objects together.
(statsForSingleResult): Turn a single result into a stat object.
(combineResults): Given lists of results, combine these lists while keeping the original lists unchanged.
(Dot): Deleted.
(TimelineFromEndpoint): Renamed from Timeline.
(TimelineFromEndpoint.constructor): Canvas Timeline manages expansion and collapsing of nested timelines.
(TimelineFromEndpoint.teardown): Detach callbacks from CommitBank.
(TimelineFromEndpoint.update): Update with any new commit information, force a re-draw of the current
cache contents.
(TimelineFromEndpoint.reload): Remove management of nested timelines.
(TimelineFromEndpoint.render): Use canvas Timeline instead of html timeline to visualize results.

  • resultsdbpy/resultsdbpy/view/templates/search.html: Use TimelineFromEndpoint class.
  • resultsdbpy/resultsdbpy/view/templates/suite_results.html: Ditto.
Location:
trunk/Tools
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/Tools/ChangeLog

    r248410 r248425  
     12019-08-08  Jonathan Bedard  <jbedard@apple.com>
     2
     3        results.webkit.org: Use canvas for timeline
     4        https://bugs.webkit.org/show_bug.cgi?id=200172
     5
     6        Rubber-stamped by Aakash Jain.
     7
     8        * resultsdbpy/resultsdbpy/view/static/js/commit.js:
     9        (Commit.constructor): Make uuid a member variable instead of a member function for efficiency.
     10        (Commit.compare): Ditto.
     11        (_CommitBank.commitByUuid): Ditto.
     12        (_CommitBank._loadSiblings): Ditto.
     13        (_CommitBank._load): Ditto.
     14        * resultsdbpy/resultsdbpy/view/static/js/timeline.js:
     15        (tickForCommit): Deleted.
     16        (minimumUuidForResults): Given a dictionary of result lists, determine the minimum UUID
     17        which encompasses all results. Crucially, this function must exclude an UUIDs which may
     18        refer to results excluded because of the limit argument.
     19        (renderTimeline): Deleted.
     20        (commitsForResults): Given a dictionary of result lists, return a list of commits associated
     21        with those results.
     22        (scaleForCommits): Given a list of commits, generate a scale to be consumed by the canvas Timeline.
     23        (repositoriesForCommits): Given a list of commits, return a sorted list of associated repository ids.
     24        (xAxisFromScale): Create a canvas-based x-axis based on the provided scale and a repository id.
     25        (inPlaceCombine): Combine result objects together.
     26        (statsForSingleResult): Turn a single result into a stat object.
     27        (combineResults): Given lists of results, combine these lists while keeping the original lists unchanged.
     28        (Dot): Deleted.
     29        (TimelineFromEndpoint): Renamed from Timeline.
     30        (TimelineFromEndpoint.constructor): Canvas Timeline manages expansion and collapsing of nested timelines.
     31        (TimelineFromEndpoint.teardown): Detach callbacks from CommitBank.
     32        (TimelineFromEndpoint.update): Update with any new commit information, force a re-draw of the current
     33        cache contents.
     34        (TimelineFromEndpoint.reload): Remove management of nested timelines.
     35        (TimelineFromEndpoint.render): Use canvas Timeline instead of html timeline to visualize results.
     36        * resultsdbpy/resultsdbpy/view/templates/search.html: Use TimelineFromEndpoint class.
     37        * resultsdbpy/resultsdbpy/view/templates/suite_results.html: Ditto.
     38
    1392019-08-08  Brady Eidson  <beidson@apple.com>
    240
  • trunk/Tools/resultsdbpy/resultsdbpy/view/static/js/commit.js

    r247628 r248425  
    128128        this.repository_id = json.repository_id;
    129129        this.timestamp = json.timestamp;
    130     }
    131     uuid() {
    132         return this.timestamp * TIMESTAMP_TO_UUID_MULTIPLIER + this.order;
     130        this.uuid = this.timestamp * TIMESTAMP_TO_UUID_MULTIPLIER + this.order;
    133131    }
    134132    compare(commit) {
    135         return this.uuid() - commit.uuid();
     133        return this.uuid - commit.uuid;
    136134    }
    137135};
     
    156154            const mid = Math.ceil((begin + end) / 2);
    157155            const candidate = this.commits[mid];
    158             if (candidate.uuid() === uuid)
     156            if (candidate.uuid === uuid)
    159157                return candidate;
    160             if (candidate.uuid() < uuid)
     158            if (candidate.uuid < uuid)
    161159                begin = mid + 1;
    162160            else
     
    184182                        const commit = new Commit(commitJson);
    185183                        while (index >= 0) {
    186                             if (self.commits[index].uuid() < commit.uuid()) {
     184                            if (self.commits[index].uuid < commit.uuid) {
    187185                                self.commits.splice(index, 0, commit);
    188186                                --index;
    189187                                break;
    190188                            }
    191                             if (self.commits[index].uuid() === commit.uuid())
     189                            if (self.commits[index].uuid === commit.uuid)
    192190                                break;
    193191                            --index;
     
    241239
    242240                    while (commitsIndex < self.commits.length) {
    243                         if (self.commits[commitsIndex].uuid() > commit.uuid()) {
     241                        if (self.commits[commitsIndex].uuid > commit.uuid) {
    244242                            self.commits.splice(commitsIndex, 0, commit);
    245243                            ++commitsIndex;
    246244                            break;
    247245                        }
    248                         if (self.commits[commitsIndex].uuid() === commit.uuid())
     246                        if (self.commits[commitsIndex].uuid === commit.uuid)
    249247                            break;
    250248                        ++commitsIndex;
     
    261259                    if (count === limit) {
    262260                        let commit = new Commit(json[firstIndexForRepository.get(repo)]);
    263                         minFound = Math.max(minFound, commit.uuid());
     261                        minFound = Math.max(minFound, commit.uuid);
    264262                    }
    265263                });
  • trunk/Tools/resultsdbpy/resultsdbpy/view/static/js/timeline.js

    r247877 r248425  
    2525import {Configuration} from '/assets/js/configuration.js';
    2626import {deepCompare, ErrorDisplay, paramsToQuery, queryToParams} from '/assets/js/common.js';
    27 import {DOM, EventStream, REF} from '/library/js/Ref.js';
     27import {DOM, EventStream, REF, FP} from '/library/js/Ref.js';
     28import {Timeline} from '/library/js/components/TimelineComponents.js';
    2829
    2930
     
    7273let willFilterExpected = false;
    7374
    74 function tickForCommit(commit, scale) {
    75     let params = {
    76         branch: commit.branch ? [commit.branch] : queryToParams(document.URL.split('?')[1]).branch,
    77         uuid: [commit.uuid()],
    78     }
    79     if (!params.branch)
    80         delete params.branch;
    81     const query = paramsToQuery(params);
    82 
    83     if (scale <= 0)
    84         return '';
    85     if (scale === 1)
    86         return `<div class="scale">
    87                 <div class="line"></div>
    88                     <div class="text">
    89                         <a href="/commit?${query}" target="_blank">${String(commit.id).substring(0,10)}</a>
    90                     </div>
    91                 </div>`;
    92     let result = '';
    93     while (scale > 0) {
    94         let countToUse = scale;
    95         if (countToUse === 11 || countToUse == 12)
    96             countToUse = 6;
    97         else if (countToUse > 10)
    98             countToUse = 10;
    99         result += `<div class="scale group-${countToUse}">
    100                 <div class="border-line-left"></div>
    101                 <div class="line"></div>
    102                 <div class="text">
    103                     <a href="/commit?${query}" target="_blank">${String(commit.id).substring(0,10)}</a>
    104                 </div>
    105                 <div class="border-line-right"></div>
    106             </div>`;
    107         scale -= countToUse;
    108     }
    109     return result;
    110 }
    111 
    112 function renderTimeline(commits, repositories = [], top = false) {
    113     // FIXME: This function breaks with more than 3 repositories because of <rdar://problem/51042981>
    114     if (repositories.length === 0) {
    115         if (top)
    116             return '';
    117         repositories = [null];
    118     }
    119     const start = top ? Math.ceil(repositories.length / 2) : 0;
    120     const end = top ? repositories.length : Math.ceil(repositories.length / 2);
    121     const numberOfElements = commits.length - Math.max(repositories.length - 1, 0)
    122     return repositories.slice(start, end).map(repository => {
    123         let commitsFromOtherRepos = 0;
    124         let renderedElements = 0;
    125         return `<div class="x-axis ${top ? 'top' : ''}">
    126                 ${commits.map(commit => {
    127                     if (commit.repository_id && commit.repository_id !== repository) {
    128                         ++commitsFromOtherRepos;
    129                         return '';
     75function minimumUuidForResults(results, limit) {
     76    const now = Math.floor(Date.now() / 10);
     77    let minDisplayedUuid = now;
     78    let maxLimitedUuid = 0;
     79
     80    Object.keys(results).forEach((key) => {
     81        results[key].forEach(pair => {
     82            if (!pair.results.length)
     83                return;
     84            if (limit !== 1 && limit === pair.results.length)
     85                maxLimitedUuid = Math.max(pair.results[0].uuid, maxLimitedUuid);
     86            else if (limit === 1)
     87                minDisplayedUuid = Math.min(pair.results[pair.results.length - 1].uuid, minDisplayedUuid);
     88            else
     89                minDisplayedUuid = Math.min(pair.results[0].uuid, minDisplayedUuid);
     90        });
     91    });
     92
     93    if (minDisplayedUuid === now)
     94        return maxLimitedUuid;
     95    return Math.max(minDisplayedUuid, maxLimitedUuid);
     96}
     97
     98function commitsForResults(results, limit, allCommits = true) {
     99    const minDisplayedUuid = minimumUuidForResults(limit);
     100    let commits = [];
     101    let repositories = new Set();
     102    let currentCommitIndex = CommitBank.commits.length - 1;
     103    Object.keys(results).forEach((key) => {
     104        results[key].forEach(pair => {
     105            pair.results.forEach(result => {
     106                if (result.uuid < minDisplayedUuid)
     107                    return;
     108                let candidateCommits = [];
     109
     110                if (!allCommits)
     111                    currentCommitIndex = CommitBank.commits.length - 1;
     112                while (currentCommitIndex >= 0) {
     113                    if (CommitBank.commits[currentCommitIndex].uuid < result.uuid)
     114                        break;
     115                    if (allCommits || CommitBank.commits[currentCommitIndex].uuid === result.uuid)
     116                        candidateCommits.push(CommitBank.commits[currentCommitIndex]);
     117                    --currentCommitIndex;
     118                }
     119                if (candidateCommits.length === 0 || candidateCommits[candidateCommits.length - 1].uuid !== result.uuid)
     120                    candidateCommits.push({
     121                        id: '?',
     122                        uuid: result.uuid,
     123                    });
     124
     125                let index = 0;
     126                candidateCommits.forEach(commit => {
     127                    if (commit.repository_id)
     128                        repositories.add(commit.repository_id);
     129                    while (index < commits.length) {
     130                        if (commit.uuid === commits[index].uuid)
     131                            return;
     132                        if (commit.uuid > commits[index].uuid) {
     133                            commits.splice(index, 0, commit);
     134                            return;
     135                        }
     136                        ++index;
    130137                    }
    131                     const scale =  Math.min(commitsFromOtherRepos + 1, numberOfElements - renderedElements);
    132                     commitsFromOtherRepos = 0;
    133                     renderedElements += scale;
    134                     return tickForCommit(commit, scale);
    135                 }).join('')}
    136             </div>`;
    137     }).join('');
    138 }
    139 
    140 class Dot {
    141     static merge(dots = {}) {
    142         let result = [];
    143         let index = 0;
    144         let hasData = true;
    145 
    146         while (hasData) {
    147             let dot = new Dot();
    148             hasData = false;
    149 
    150             Object.keys(dots).forEach(key => {
    151                 if (dots[key].length <= index)
    152                     return;
    153                 hasData = true;
    154                 if (!dots[key][index].count)
    155                     return;
    156                 if (dot.count)
    157                     dot.combined = true;
    158 
    159                 dot.count += dots[key][index].count;
    160                 dot.failed += dots[key][index].failed;
    161                 dot.timeout += dots[key][index].timeout;
    162                 dot.crash += dots[key][index].crash;
    163                 if (dot.combined)
    164                     dot.link = null;
    165                 else
    166                     dot.link = dots[key][index].link;
     138                    commits.push(commit);
     139                });
    167140            });
    168             if (hasData)
    169                 result.push(dot);
    170             ++index;
    171         }
    172         return result;
    173     }
    174     constructor(count = 0, failed = 0, timeout = 0, crash = 0, combined = false, link = null) {
    175         this.count = count;
    176         this.failed = failed;
    177         this.timeout = timeout;
    178         this.crash = crash;
    179         this.combined = combined;
    180         this.link = link;
    181     }
    182     toString() {
    183         if (!this.count)
    184             return `<div class="dot empty"></div>`;
    185 
    186         const self = this;
    187 
    188         function render(cssClass, inside='') {
    189             if (self.link)
    190                 return `<a href="${self.link}" target="_blank" class="${cssClass}">${inside}</a>`;
    191             return `<div class="${cssClass}">${inside}</div>`;
    192         }
    193 
    194         if (!this.failed)
    195             return render('dot success');
    196 
    197         let key = 'failed';
    198         if (this.timeout)
    199             key = 'timeout';
    200         if (this.crash)
    201             key = 'crash';
    202 
    203         if (!this.combined)
    204             return render(`dot ${key}`, `<div class="tag" style="color:var(--boldInverseColor)">${this.failed}</div>`);
    205 
    206         return render(`dot ${key}`, `<div class="tag" style="color:var(--boldInverseColor)">
    207                 ${function() {
    208                     let percent = Math.ceil(self.failed / self.count * 100 - .5);
    209                     if (percent > 0)
    210                         return percent;
    211                     return '<1';
    212                 }()} %
    213             </div>`);
    214     }
    215 }
    216 
    217 class Timeline {
     141        });
     142    });
     143    if (currentCommitIndex >= 0 && commits.length) {
     144        let trailingRepositories = new Set(repositories);
     145        trailingRepositories.delete(commits[commits.length - 1].repository_id);
     146        while (currentCommitIndex >= 0 && trailingRepositories.size) {
     147            const commit = CommitBank.commits[currentCommitIndex];
     148            if (trailingRepositories.has(commit.repository_id)) {
     149                commits.push(commit);
     150                trailingRepositories.delete(commit.repository_id);
     151            }
     152            --currentCommitIndex;
     153        }
     154    }
     155
     156    repositories = [...repositories];
     157    repositories.sort();
     158    return commits;
     159}
     160
     161function scaleForCommits(commits) {
     162    let scale = [];
     163    for (let i = commits.length - 1; i >= 0; --i) {
     164        const repository_id = commits[i].repository_id ? commits[i].repository_id : '?';
     165        scale.unshift({});
     166        scale[0][repository_id] = commits[i];
     167        if (scale.length < 2)
     168            continue;
     169        Object.keys(scale[1]).forEach((key) => {
     170            if (key === repository_id || key === '?' || key === 'uuid')
     171                return;
     172            scale[0][key] = scale[1][key];
     173        });
     174        scale[0].uuid = Math.max(...Object.keys(scale[0]).map((key) => {
     175            return scale[0][key].uuid;
     176        }));
     177    }
     178    return scale;
     179}
     180
     181function repositoriesForCommits(commits) {
     182    let repositories = new Set();
     183    commits.forEach((commit) => {
     184        if (commit.repository_id)
     185            repositories.add(commit.repository_id);
     186    });
     187    repositories = [...repositories];
     188    if (!repositories.length)
     189        repositories = ['?'];
     190    repositories.sort();
     191    return repositories;
     192}
     193
     194function xAxisFromScale(scale, repository, updatesArray, isTop=false)
     195{
     196    function scaleForRepository(scale) {
     197        return scale.map(node => {
     198            let commit = node[repository];
     199            if (!commit)
     200                commit = node['?'];
     201            if (!commit)
     202                return {id: '', uuid: null};
     203            return commit;
     204        });
     205    }
     206
     207    return Timeline.CanvasXAxisComponent(scaleForRepository(scale), {
     208        isTop: isTop,
     209        height: 130,
     210        onScaleClick: (node) => {
     211            if (!node.label.id)
     212                return;
     213            let params = {
     214                branch: node.label.branch ? [node.label.branch] : queryToParams(document.URL.split('?')[1]).branch,
     215                uuid: [node.label.uuid],
     216            }
     217            if (!params.branch)
     218                delete params.branch;
     219            const query = paramsToQuery(params);
     220            window.open(`/commit?${query}`, '_blank');
     221        },
     222        // Per the birthday paradox, 10% change of collision with 7.7 million commits with 12 character commits
     223        getLabelFunc: (commit) => {return commit ? commit.id.substring(0,12) : '?';},
     224        getScaleFunc: (commit) => commit.uuid,
     225        exporter: (updateFunction) => {
     226            updatesArray.push((scale) => {updateFunction(scaleForRepository(scale));});
     227        },
     228    });
     229}
     230
     231const testsRegex = /tests_([a-z])+/;
     232const failureTypeOrder = ['failed', 'timedout', 'crashed'];
     233const failureTypeMapping = {
     234    failed: 'ERROR',
     235    timedout: 'TIMEOUT',
     236    crashed: 'CRASH',
     237}
     238
     239function inPlaceCombine(out, obj)
     240{
     241    if (!obj)
     242        return out;
     243
     244    if (!out) {
     245        out = {};
     246        Object.keys(obj).forEach(key => {
     247            if (key[0] === '_')
     248                return;
     249            if (obj[key] instanceof Object)
     250                out[key] = inPlaceCombine(out[key], obj[key]);
     251            else
     252                out[key] = obj[key];
     253        });
     254    } else {
     255        Object.keys(out).forEach(key => {
     256            if (key[0] === '_')
     257                return;
     258
     259            if (out[key] instanceof Object) {
     260                out[key] = inPlaceCombine(out[key], obj[key]);
     261                return;
     262            }
     263
     264            // Set of special case keys which need to be added together
     265            if (key.match(testsRegex)) {
     266                out[key] += obj[key];
     267                return;
     268            }
     269
     270            // If the key exists, but doesn't match, delete it
     271            if (!(key in obj) || out[key] !== obj[key]) {
     272                delete out[key];
     273                return;
     274            }
     275        });
     276        Object.keys(obj).forEach(key => {
     277            if (key.match(testsRegex) && !(key in out))
     278                out[key] = obj[key];
     279        });
     280    }
     281    return out;
     282}
     283
     284function statsForSingleResult(result) {
     285    const actualId = Expectations.stringToStateId(result.actual);
     286    const unexpectedId = Expectations.stringToStateId(Expectations.unexpectedResults(result.actual, result.expected));
     287    let stats = {
     288        tests_run: 1,
     289        tests_skipped: 0,
     290    }
     291    failureTypeOrder.forEach(type => {
     292        const idForType = Expectations.stringToStateId(failureTypeMapping[type]);
     293        stats[`tests_${type}`] = actualId > idForType  ? 0 : 1;
     294        stats[`tests_unexpected_${type}`] = unexpectedId > idForType  ? 0 : 1;
     295    });
     296    return stats;
     297}
     298
     299function combineResults() {
     300    let counts = new Array(arguments.length).fill(0);
     301    let data = [];
     302
     303    while (true) {
     304        // Find candidate uuid
     305        let uuid = 0;
     306        for (let i = 0; i < counts.length; ++i) {
     307            let candidateUuid = null;
     308            while (arguments[i] && arguments[i].length > counts[i]) {
     309                candidateUuid = arguments[i][counts[i]].uuid;
     310                if (candidateUuid)
     311                    break;
     312                ++counts[i];
     313            }
     314            if (candidateUuid)
     315                uuid = Math.max(uuid, candidateUuid);
     316        }
     317
     318        if (!uuid)
     319            return data;
     320
     321        // Combine relevant results
     322        let dataNode = null;
     323        for (let i = 0; i < counts.length; ++i) {
     324            while (counts[i] < arguments[i].length && arguments[i][counts[i]] && arguments[i][counts[i]].uuid === uuid) {
     325                if (dataNode && !dataNode.stats)
     326                    dataNode.stats = statsForSingleResult(dataNode);
     327
     328                dataNode = inPlaceCombine(dataNode, arguments[i][counts[i]]);
     329
     330                if (dataNode.stats && !arguments[i][counts[i]].stats)
     331                    dataNode.stats = inPlaceCombine(dataNode.stats, statsForSingleResult(arguments[i][counts[i]]));
     332
     333                ++counts[i];
     334            }
     335        }
     336        if (dataNode)
     337            data.push(dataNode);
     338    }
     339    return data;
     340}
     341
     342class TimelineFromEndpoint {
    218343    constructor(endpoint, suite = null) {
    219344        this.endpoint = endpoint;
     
    222347        this.configurations = Configuration.fromQuery();
    223348        this.results = {};
    224         this.collapsed = {};
    225         this.expandedSDKs = {};
    226349        this.suite = suite;  // Suite is often implied by the endpoint, but trying to determine suite from endpoint is not trivial.
    227350
     351        this.updates = [];
     352        this.xaxisUpdates = [];
     353        this.timelineUpdate = null;
     354        this.repositories = [];
     355
    228356        const self = this;
    229         this.configurations.forEach(configuration => {
    230             this.results[configuration.toKey()] = [];
    231             this.collapsed[configuration.toKey()] = true;
    232         });
    233357
    234358        this.latestDispatch = Date.now();
     
    245369        });
    246370
    247         CommitBank.callbacks.push(() => {
    248             const params = queryToParams(document.URL.split('?')[1]);
    249             self.ref.setState(params.limit ? parseInt(params.limit[params.limit.length - 1]) : DEFAULT_LIMIT);
    250         });
     371        this.commit_callback = () => {
     372            self.update();
     373        };
     374        CommitBank.callbacks.push(this.commit_callback);
    251375
    252376        this.reload();
    253377    }
     378    teardown() {
     379        CommitBank.callbacks = CommitBank.callbacks.filter((value, index, arr) => {
     380            return this.commit_callback === value;
     381        });
     382    }
     383    update() {
     384        const params = queryToParams(document.URL.split('?')[1]);
     385        const commits = commitsForResults(this.results, params.limit ? parseInt(params.limit[params.limit.length - 1]) : DEFAULT_LIMIT, this.allCommits);
     386        const scale = scaleForCommits(commits);
     387
     388        const newRepositories = repositoriesForCommits(commits);
     389        let haveNewRepos = this.repositories.length !== newRepositories.length;
     390        for (let i = 0; !haveNewRepos && i < this.repositories.length && i < newRepositories.length; ++i)
     391            haveNewRepos = this.repositories[i] !== newRepositories[i];
     392        if (haveNewRepos && this.timelineUpdate) {
     393            this.xaxisUpdates = [];
     394            let top = true;
     395            let components = [];
     396
     397            newRepositories.forEach(repository => {
     398                components.push(xAxisFromScale(scale, repository, this.xaxisUpdates, top));
     399                top = false;
     400            });
     401
     402            this.timelineUpdate(components);
     403            this.repositories = newRepositories;
     404        }
     405
     406        this.updates.forEach(func => {func(scale);})
     407        this.xaxisUpdates.forEach(func => {func(scale);});
     408    }
    254409    rerender() {
    255         let params = queryToParams(document.URL.split('?')[1]);
     410        const params = queryToParams(document.URL.split('?')[1]);
    256411        this.ref.setState(params.limit ? parseInt(params.limit[params.limit.length - 1]) : DEFAULT_LIMIT);
    257412    }
     
    274429            this.configurations = newConfigs;
    275430            this.results = {};
    276             this.collapsed = {};
    277             this.expandedSDKs = {};
    278431            this.configurations.forEach(configuration => {
    279432                this.results[configuration.toKey()] = [];
    280                 this.collapsed[configuration.toKey()] = true;
    281433            });
    282434        }
     
    332484            onStateUpdate: (element, state) => {
    333485                if (state.error)
    334                     element.innerHTML = ErrorDisplay(state);
     486                    DOM.inject(element, ErrorDisplay(state));
    335487                else if (state > 0)
    336488                    DOM.inject(element, this.render(state));
    337489                else
    338                     element.innerHTML = this.placeholder();
     490                    DOM.inject(element, this.placeholder());
    339491            }
    340492        });
     
    342494        return `<div class="content" ref="${this.ref}"></div>`;
    343495    }
     496
    344497    render(limit) {
    345         let now = Math.floor(Date.now() / 10);
    346         let minDisplayedUuid = now;
    347         let maxLimitedUuid = 0;
     498        const branch = queryToParams(document.URL.split('?')[1]).branch;
     499        const self = this;
     500        const commits = commitsForResults(this.results, limit, this.allCommits);
     501        const scale = scaleForCommits(commits);
     502
     503        const computedStyle = getComputedStyle(document.body);
     504        const colorMap = {
     505            success: computedStyle.getPropertyValue('--greenLight').trim(),
     506            failed: computedStyle.getPropertyValue('--redLight').trim(),
     507            timedout: computedStyle.getPropertyValue('--orangeLight').trim(),
     508            crashed: computedStyle.getPropertyValue('--purpleLight').trim(),
     509        }
     510
     511        this.updates = [];
     512        const options = {
     513            getScaleFunc: (value) => {
     514                if (value && value.uuid)
     515                    return {uuid: value.uuid};
     516                return {};
     517            },
     518            compareFunc: (a, b) => {return b.uuid - a.uuid;},
     519            renderFactory: (drawDot) => (data, context, x, y) => {
     520                if (!data)
     521                    return drawDot(context, x, y, true);
     522
     523                let tag = null;
     524                let color = colorMap.success;
     525                if (data.stats) {
     526                    tag = data.stats[`tests${willFilterExpected ? '_unexpected_' : '_'}failed`];
     527
     528                    // If we have failures that are a result of multiple runs, combine them.
     529                    if (tag && !data.start_time) {
     530                        tag = Math.ceil(tag / data.stats.tests_run * 100 - .5);
     531                        if (!tag)
     532                            tag = '<1';
     533                        tag = `${tag} %`
     534                    }
     535
     536                    failureTypeOrder.forEach(type => {
     537                        if (data.stats[`tests${willFilterExpected ? '_unexpected_' : '_'}${type}`] > 0)
     538                            color = colorMap[type];
     539                    });
     540                } else {
     541                    let resultId = Expectations.stringToStateId(data.actual);
     542                    if (willFilterExpected)
     543                        resultId = Expectations.stringToStateId(Expectations.unexpectedResults(data.actual, data.expected));
     544                    failureTypeOrder.forEach(type => {
     545                        if (Expectations.stringToStateId(failureTypeMapping[type]) >= resultId)
     546                            color = colorMap[type];
     547                    });
     548                }
     549
     550                return drawDot(context, x, y, false, tag ? tag : null, false, color);
     551            },
     552        };
     553
     554        function onDotClickFactory(configuration) {
     555            return (data) => {
     556                // FIXME: We should do something sane here, but we probably need another endpoint
     557                if (!data.start_time) {
     558                    alert('Node is a combination of multiple runs');
     559                    return;
     560                }
     561
     562                let buildParams = configuration.toParams();
     563                buildParams['suite'] = [self.suite];
     564                buildParams['uuid'] = [data.uuid];
     565                buildParams['after_time'] = [data.start_time];
     566                buildParams['before_time'] = [data.start_time];
     567                if (branch)
     568                    buildParams['branch'] = branch;
     569                window.open(`/urls/build?${paramsToQuery(buildParams)}`, '_blank');
     570            }
     571        }
     572
     573        function exporterFactory(data) {
     574            return (updateFunction) => {
     575                self.updates.push((scale) => {updateFunction(data, scale);});
     576            }
     577        }
     578
     579        let children = [];
    348580        this.configurations.forEach(configuration => {
     581            if (!this.results[configuration.toKey()] || Object.keys(this.results[configuration.toKey()]).length === 0)
     582                return;
     583
     584            // Create a list of configurations to display with SDKs stripped
     585            let mappedChildrenConfigs = {};
     586            let childrenConfigsBySDK = {}
     587            let resultsByKey = {};
    349588            this.results[configuration.toKey()].forEach(pair => {
    350                 if (!pair.results.length)
    351                     return;
    352                 if (limit !== 1 && limit === pair.results.length)
    353                     maxLimitedUuid = Math.max(pair.results[0].uuid, maxLimitedUuid);
    354                 else if (limit === 1)
    355                     minDisplayedUuid = Math.min(pair.results[pair.results.length - 1].uuid, minDisplayedUuid);
    356                 else
    357                     minDisplayedUuid = Math.min(pair.results[0].uuid, minDisplayedUuid);
     589                const strippedConfig = new Configuration(pair.configuration);
     590                resultsByKey[strippedConfig.toKey()] = combineResults([], [...pair.results].sort(function(a, b) {return b.uuid - a.uuid;}));
     591                delete strippedConfig.sdk;
     592                mappedChildrenConfigs[strippedConfig.toKey()] = strippedConfig;
     593                if (!childrenConfigsBySDK[strippedConfig.toKey()])
     594                    childrenConfigsBySDK[strippedConfig.toKey()] = [];
     595                childrenConfigsBySDK[strippedConfig.toKey()].push(new Configuration(pair.configuration));
    358596            });
    359         });
    360         if (minDisplayedUuid === now)
    361             minDisplayedUuid = maxLimitedUuid;
    362         else
    363             minDisplayedUuid = Math.max(minDisplayedUuid, maxLimitedUuid);
    364 
    365         let commits = [];
    366         let repositories = new Set();
    367         let currentCommitIndex = CommitBank.commits.length - 1;
    368         this.configurations.forEach(configuration => {
    369             this.results[configuration.toKey()].forEach(pair => {
    370                 pair.results.forEach(result => {
    371                     if (result.uuid < minDisplayedUuid)
    372                         return;
    373                     let candidateCommits = [];
    374 
    375                     if (!this.displayAllCommits)
    376                         currentCommitIndex = CommitBank.commits.length - 1;
    377                     while (currentCommitIndex >= 0) {
    378                         if (CommitBank.commits[currentCommitIndex].uuid() < result.uuid)
    379                             break;
    380                         if (this.displayAllCommits || CommitBank.commits[currentCommitIndex].uuid() == result.uuid)
    381                             candidateCommits.push(CommitBank.commits[currentCommitIndex]);
    382                         --currentCommitIndex;
    383                     }
    384                     if (candidateCommits.length === 0 || candidateCommits[candidateCommits.length - 1].uuid() !== result.uuid)
    385                         candidateCommits.push({
    386                             'id': '?',
    387                             'uuid': () => {return result.uuid;}
    388                         });
    389 
    390                     let index = 0;
    391                     candidateCommits.forEach(commit => {
    392                         if (commit.repository_id)
    393                             repositories.add(commit.repository_id);
    394                         while (index < commits.length) {
    395                             if (commit.uuid() === commits[index].uuid())
    396                                 return;
    397                             if (commit.uuid() > commits[index].uuid()) {
    398                                 commits.splice(index, 0, commit);
    399                                 return;
    400                             }
    401                             ++index;
    402                         }
    403                         commits.push(commit);
     597            let childrenConfigs = [];
     598            Object.keys(mappedChildrenConfigs).forEach(key => {
     599                childrenConfigs.push(mappedChildrenConfigs[key]);
     600            });
     601            childrenConfigs.sort(function(a, b) {return a.compare(b);});
     602
     603            // Create the collapsed timelines, cobine results
     604            let allResults = [];
     605            let collapsedTimelines = [];
     606            childrenConfigs.forEach(config => {
     607                childrenConfigsBySDK[config.toKey()].sort(function(a, b) {return a.compareSDKs(b);});
     608
     609                let resultsForConfig = [];
     610                childrenConfigsBySDK[config.toKey()].forEach(sdkConfig => {
     611                    resultsForConfig = combineResults(resultsForConfig, resultsByKey[sdkConfig.toKey()]);
     612                });
     613                allResults = combineResults(allResults, resultsForConfig);
     614
     615                let queueParams = config.toParams();
     616                queueParams['suite'] = [this.suite];
     617                if (branch)
     618                    queueParams['branch'];
     619                let myTimeline = Timeline.SeriesWithHeaderComponent(
     620                    `${childrenConfigsBySDK[config.toKey()].length > 1 ? ' | ' : ''}<a href="/urls/queue?${paramsToQuery(queueParams)}" target="_blank">${config}</a>`,
     621                    Timeline.CanvasSeriesComponent(resultsForConfig, scale, {
     622                        getScaleFunc: options.getScaleFunc,
     623                        compareFunc: options.compareFunc,
     624                        renderFactory: options.renderFactory,
     625                        exporter: options.exporter,
     626                        onDotClick: onDotClickFactory(config),
     627                        exporter: exporterFactory(resultsForConfig),
     628                    }));
     629
     630                if (childrenConfigsBySDK[config.toKey()].length > 1) {
     631                    let timelinesBySDK = [];
     632                    childrenConfigsBySDK[config.toKey()].forEach(sdkConfig => {
     633                        timelinesBySDK.push(
     634                            Timeline.SeriesWithHeaderComponent(`${sdkConfig.sdk}`,
     635                                Timeline.CanvasSeriesComponent(resultsByKey[sdkConfig.toKey()], scale, {
     636                                    getScaleFunc: options.getScaleFunc,
     637                                    compareFunc: options.compareFunc,
     638                                    renderFactory: options.renderFactory,
     639                                    exporter: options.exporter,
     640                                    onDotClick: onDotClickFactory(sdkConfig),
     641                                    exporter: exporterFactory(resultsByKey[sdkConfig.toKey()]),
     642                                })));
    404643                    });
     644                    myTimeline = Timeline.ExpandableSeriesWithHeaderExpanderComponent(myTimeline, ...timelinesBySDK);
     645                }
     646                collapsedTimelines.push(myTimeline);
     647            });
     648
     649            if (collapsedTimelines.length === 0)
     650                return;
     651            if (collapsedTimelines.length === 1) {
     652                if (!collapsedTimelines[0].header.includes('class="series"'))
     653                    collapsedTimelines[0].header = Timeline.HeaderComponent(collapsedTimelines[0].header);
     654                children.push(collapsedTimelines[0]);
     655                return;
     656            }
     657
     658            children.push(
     659                Timeline.ExpandableSeriesWithHeaderExpanderComponent(
     660                Timeline.SeriesWithHeaderComponent(` ${configuration}`,
     661                    Timeline.CanvasSeriesComponent(allResults, scale, {
     662                        getScaleFunc: options.getScaleFunc,
     663                        compareFunc: options.compareFunc,
     664                        renderFactory: options.renderFactory,
     665                        onDotClick: onDotClickFactory(configuration),
     666                        exporter: exporterFactory(allResults),
     667                    })),
     668                ...collapsedTimelines
     669            ));
     670        });
     671
     672        let top = true;
     673        self.xaxisUpdates = [];
     674        this.repositories = repositoriesForCommits(commits);
     675        this.repositories.forEach(repository => {
     676            const xAxisComponent = xAxisFromScale(scale, repository, self.xaxisUpdates, top);
     677            if (top)
     678                children.unshift(xAxisComponent);
     679            else
     680                children.push(xAxisComponent);
     681            top = false;
     682        });
     683
     684        const composer = FP.composer((updateTimeline) => {
     685            self.timelineUpdate = (xAxises) => {
     686                children.splice(0, 1);
     687                if (self.repositories.length > 1)
     688                    children.splice(children.length - self.repositories.length, self.repositories.length);
     689
     690                let top = true;
     691                xAxises.forEach(component => {
     692                    if (top)
     693                        children.unshift(component);
     694                    else
     695                        children.push(component);
     696                    top = false;
    405697                });
    406             });
    407         });
    408         if (currentCommitIndex >= 0 && commits.length) {
    409             let trailingRepositories = new Set(repositories);
    410             trailingRepositories.delete(commits[commits.length - 1].repository_id);
    411             while (currentCommitIndex >= 0 && trailingRepositories.size) {
    412                 const commit = CommitBank.commits[currentCommitIndex];
    413                 if (trailingRepositories.has(commit.repository_id)) {
    414                     commits.push(commit);
    415                     trailingRepositories.delete(commit.repository_id);
    416                 }
    417                 --currentCommitIndex;
    418             }
    419 
    420         }
    421 
    422         repositories = [...repositories];
    423         repositories.sort();
    424 
    425         return `<div class="timeline">
    426                 <div ${repositories.length > 1 ? `class="header with-top-x-axis"` : `class="header"`}>
    427                     ${this.configurations.map(configuration => {
    428                         if (Object.keys(this.results[configuration.toKey()]).length === 0)
    429                             return '';
    430                         const self = this;
    431                         const expander = REF.createRef({
    432                             onElementMount: element => {
    433                                 element.onclick = () => {
    434                                     self.collapsed[configuration.toKey()] = !self.collapsed[configuration.toKey()];
    435                                     self.rerender();
    436                                 }
    437                             },
    438                         });
    439                         let result = `<div class="series">
    440                                 <a ref="${expander}" style="cursor: pointer;">
    441                                     ${function() {return self.collapsed[configuration.toKey()] ? '+' : '-'}()}
    442                                 </a>
    443                                 ${configuration}
    444                             </div>`;
    445                         if (!this.collapsed[configuration.toKey()]) {
    446                             const seriesHeaderForSDKLessConfig = config => {
    447                                 if (typeof config === 'string')
    448                                     return `<div class="series">${config}</div>`;
    449 
    450                                 let queueParams = config.toParams();
    451                                 queueParams['suite'] = [this.suite];
    452                                 const configLink = `<a class="text tiny" href="/urls/queue?${paramsToQuery(queueParams)}" target="_blank">${config}</a>`;
    453                                 const key = config.toKey();
    454                                 const expander = REF.createRef({
    455                                     onElementMount: element => {
    456                                         element.onclick = () => {
    457                                             self.expandedSDKs[key] = !self.expandedSDKs[key];
    458                                             self.rerender();
    459                                         }
    460                                     },
    461                                 });
    462                                 if (this.expandedSDKs[key] === undefined)
    463                                     return `<div class="series">${configLink}</div>`;
    464                                 return `<div class="series text tiny">
    465                                         <a ref="${expander}" style="cursor: pointer;">
    466                                             ${function() {return self.expandedSDKs[key] ? '-sdk' : '+sdk'}()}
    467                                         </a>
    468                                          | ${configLink}
    469                                     </div>`;
    470                             }
    471                             let lastConfig = null;
    472 
    473                             this.results[configuration.toKey()].forEach(pair => {
    474                                 if (pair.results.length <= 0 || pair.results[pair.results.length - 1].uuid < minDisplayedUuid)
    475                                     return '';
    476 
    477                                 let config = new Configuration(pair.configuration);
    478                                 const sdk = config.sdk;
    479                                 delete config.sdk;  // Strip out sdk information.
    480 
    481                                 const key = config.toKey();
    482                                 const compareIndex = config.compare(lastConfig);
    483                                 if (lastConfig !== null && compareIndex != 0 && !this.expandedSDKs[lastConfig.toKey()])
    484                                     result += seriesHeaderForSDKLessConfig(lastConfig);
    485                                 else if (compareIndex === 0 && this.expandedSDKs[key] === undefined)
    486                                     this.expandedSDKs[key] = false;  // Populate this dictionary on the fly.
    487                                 if (this.expandedSDKs[key]) {
    488                                     if (compareIndex)
    489                                         result += seriesHeaderForSDKLessConfig(config);
    490                                     result += `<div class="series">${sdk}</div>`;
    491                                 }
    492                                 lastConfig = config;
    493                             });
    494                             if (lastConfig !== null && !this.expandedSDKs[lastConfig.toKey()])
    495                                 result += seriesHeaderForSDKLessConfig(lastConfig);
    496                         }
    497                         return result;
    498                     }).join('')}
    499                 </div>
    500                 <div class="content">
    501                     ${renderTimeline(commits, repositories, true)}
    502                     ${this.configurations.map(configuration => {
    503                         if (Object.keys(this.results[configuration.toKey()]).length === 0)
    504                             return '';
    505 
    506                         let currentArrayIndex = new Array(this.results[configuration.toKey()].length).fill(1);
    507                         let dots = {};
    508                         for (let i = 0; i < this.results[configuration.toKey()].length; ++i) {
    509                             const pairForIndex = this.results[configuration.toKey()][i];
    510                             if (pairForIndex.results.length <= 0 || pairForIndex.results[pairForIndex.results.length - 1].uuid < minDisplayedUuid)
    511                                 continue;
    512                             dots[new Configuration(pairForIndex.configuration).toKey()] = [];
    513                         }
    514                         commits.slice(0, commits.length - Math.max(repositories.length - 1, 0)).forEach(commit => {
    515                             for (let i = 0; i < currentArrayIndex.length; ++i) {
    516                                 const pairForIndex = this.results[configuration.toKey()][i];
    517                                 if (pairForIndex.results.length <= 0 || pairForIndex.results[pairForIndex.results.length - 1].uuid < minDisplayedUuid)
    518                                     continue;
    519 
    520                                 const config = new Configuration(pairForIndex.configuration);
    521                                 const configurationKey = config.toKey();
    522                                 let resultIndex = pairForIndex.results.length - currentArrayIndex[i];
    523                                 if (resultIndex < 0) {
    524                                     dots[configurationKey].push(new Dot());
    525                                     continue;
    526                                 }
    527 
    528                                 if (commit.uuid() === pairForIndex.results[resultIndex].uuid) {
    529                                     let buildParams = config.toParams();
    530                                     buildParams['suite'] = [this.suite];
    531                                     buildParams['uuid'] = [commit.uuid()];
    532                                     const buildLink = `/urls/build?${paramsToQuery(buildParams)}`;
    533 
    534                                     if (pairForIndex.results[resultIndex].stats)
    535                                         dots[configurationKey].push(new Dot(
    536                                             pairForIndex.results[resultIndex].stats.tests_run,
    537                                             pairForIndex.results[resultIndex].stats[`tests${willFilterExpected ? '_unexpected_' : '_'}failed`],
    538                                             pairForIndex.results[resultIndex].stats[`tests${willFilterExpected ? '_unexpected_' : '_'}timedout`],
    539                                             pairForIndex.results[resultIndex].stats[`tests${willFilterExpected ? '_unexpected_' : '_'}crashed`],
    540                                             false,
    541                                             buildLink,
    542                                         ));
    543                                     else {
    544                                         let resultId = Expectations.stringToStateId(pairForIndex.results[resultIndex].actual);
    545                                         if (willFilterExpected)
    546                                             resultId = Expectations.stringToStateId(Expectations.unexpectedResults(
    547                                                 pairForIndex.results[resultIndex].actual,
    548                                                 pairForIndex.results[resultIndex].expected,
    549                                             ));
    550 
    551                                         dots[configurationKey].push(new Dot(
    552                                             1,
    553                                             resultId <= Expectations.stringToStateId('ERROR') ? 1 : 0,
    554                                             resultId <= Expectations.stringToStateId('TIMEOUT') ? 1 : 0,
    555                                             resultId <= Expectations.stringToStateId('CRASH') ? 1 : 0,
    556                                             false,
    557                                             buildLink,
    558                                         ));
    559                                     }
    560                                 } else
    561                                     dots[configurationKey].push(new Dot());
    562 
    563                                 while (resultIndex >= 0 && commit.uuid() <= pairForIndex.results[resultIndex].uuid) {
    564                                     ++currentArrayIndex[i];
    565                                     resultIndex = pairForIndex.results.length - currentArrayIndex[i];
    566                                 }
    567                             }
    568                         });
    569 
    570                         if (this.collapsed[configuration.toKey()])
    571                             return `<div class="series">${Dot.merge(dots).map(dot => {return dot.toString();}).join('')}</div>`;
    572 
    573                         let result = '';
    574                         let currentDots = {};
    575                         let lastConfig = null;
    576 
    577                         this.results[configuration.toKey()].forEach(pair => {
    578                             let config = new Configuration(pair.configuration);
    579                             const key = config.toKey();
    580                             if (!dots[key])
    581                                 return;
    582                             delete config.sdk;  // Strip out sdk information
    583 
    584                             const compareIndex = config.compare(lastConfig);
    585                             const isSDKExpanded = this.expandedSDKs[config.toKey()];
    586                             if (compareIndex || isSDKExpanded) {
    587                                 result += `<div class="series">${Dot.merge(currentDots).map(dot => {return dot.toString();}).join('')}</div>`;
    588                                 currentDots = {};
    589                             }
    590                             if (compareIndex && isSDKExpanded)
    591                                 result += '<div class="series"></div>';
    592                             currentDots[key] = dots[key];
    593                             lastConfig = config;
    594                         });
    595                         if (currentDots)
    596                             result += `<div class="series">${Dot.merge(currentDots).map(dot => {return dot.toString();}).join('')}</div>`;
    597 
    598                         return result;
    599                     }).join('')}
    600                     ${renderTimeline(commits, repositories)}
    601                 </div>
    602             </div>`;
    603     }
    604 }
     698                updateTimeline(children);
     699            };
     700        });
     701        return Timeline.CanvasContainer(composer, ...children);
     702    }
     703}
     704
    605705
    606706function LegendLabel(eventStream, filterExpectedText, filterUnexpectedText) {
     
    681781}
    682782
    683 export {Legend, Timeline, Expectations};
     783export {Legend, TimelineFromEndpoint, Expectations};
  • trunk/Tools/resultsdbpy/resultsdbpy/view/templates/search.html

    r247877 r248425  
    3737import {DOM, REF} from '/library/js/Ref.js';
    3838import {SearchBar} from '/assets/js/search.js';
    39 import {Legend, Timeline} from '/assets/js/timeline.js';
     39import {Legend, TimelineFromEndpoint} from '/assets/js/timeline.js';
    4040
    4141const DEFAULT_LIMIT = 100;
     
    5656                    suite: this.currentParams.suite[i],
    5757                    test: this.currentParams.test[i],
    58                     timeline: new Timeline(`api/results/${this.currentParams.suite[i]}/${this.currentParams.test[i]}`, this.currentParams.suite[i]),
     58                    timeline: new TimelineFromEndpoint(`api/results/${this.currentParams.suite[i]}/${this.currentParams.test[i]}`, this.currentParams.suite[i]),
    5959                });
    6060            }
     
    130130                    DOM.inject(element, Legend(() => {
    131131                        self.ref.state.children.forEach((child) => {
    132                             child.timeline.rerender();
     132                            child.timeline.update();
    133133                        });
    134134                    }, false) + diff.children.map(renderChild).join(''));
     
    161161                        this.ref.state.children.splice(search, 1);
    162162                        break;
    163                     }
     163                    } else
     164                        this.ref.state.children[search].timeline.teardown();
    164165                    needReRender = true;
    165166                }
     
    170171                        suite: params.suite[i],
    171172                        test: params.test[i],
    172                         timeline: new Timeline(`api/results/${params.suite[i]}/${params.test[i]}`),
     173                        timeline: new TimelineFromEndpoint(`api/results/${params.suite[i]}/${params.test[i]}`),
    173174                    });
    174175                }
     
    240241                suite: arguments[i].suite,
    241242                test: arguments[i].test,
    242                 timeline: new Timeline(`api/results/${arguments[i].suite}/${arguments[i].test}`),
     243                timeline: new TimelineFromEndpoint(`api/results/${arguments[i].suite}/${arguments[i].test}`),
    243244            }
    244245
  • trunk/Tools/resultsdbpy/resultsdbpy/view/templates/suite_results.html

    r247877 r248425  
    3636import {Drawer, BranchSelector, ConfigurationSelectors, LimitSlider} from '/assets/js/drawer.js';
    3737import {DOM, REF} from '/library/js/Ref.js';
    38 import {Legend, Timeline} from '/assets/js/timeline.js';
     38import {Legend, TimelineFromEndpoint} from '/assets/js/timeline.js';
    3939
    4040const DEFAULT_LIMIT = 100;
     
    6767
    6868        SUITES.forEach((suite) => {
    69             this.children[suite] = new Timeline('api/results/' + suite, suite);
     69            this.children[suite] = new TimelineFromEndpoint('api/results/' + suite, suite);
    7070        });
    7171    }
     
    8282
    8383        let sortedSuites = [...suites].sort();
     84        Object.keys(this.children).forEach(key => {this.children[key].teardown();});
    8485        this.children = {};
    8586        sortedSuites.forEach((suite) => {
    86             this.children[suite] = new Timeline('api/results/' + suite);
     87            this.children[suite] = new TimelineFromEndpoint('api/results/' + suite);
    8788        });
    8889        return sortedSuites;
     
    144145        return Legend(() => {
    145146            for (let suite in children) {
    146                 children[suite].rerender();
     147                children[suite].update();
    147148            }
    148149        }, true) + suites.map(suite => {
Note: See TracChangeset for help on using the changeset viewer.