Changeset 248425 in webkit
- Timestamp:
- Aug 8, 2019 10:53:51 AM (5 years ago)
- Location:
- trunk/Tools
- Files:
-
- 5 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/Tools/ChangeLog
r248410 r248425 1 2019-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 1 39 2019-08-08 Brady Eidson <beidson@apple.com> 2 40 -
trunk/Tools/resultsdbpy/resultsdbpy/view/static/js/commit.js
r247628 r248425 128 128 this.repository_id = json.repository_id; 129 129 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; 133 131 } 134 132 compare(commit) { 135 return this.uuid () - commit.uuid();133 return this.uuid - commit.uuid; 136 134 } 137 135 }; … … 156 154 const mid = Math.ceil((begin + end) / 2); 157 155 const candidate = this.commits[mid]; 158 if (candidate.uuid ()=== uuid)156 if (candidate.uuid === uuid) 159 157 return candidate; 160 if (candidate.uuid ()< uuid)158 if (candidate.uuid < uuid) 161 159 begin = mid + 1; 162 160 else … … 184 182 const commit = new Commit(commitJson); 185 183 while (index >= 0) { 186 if (self.commits[index].uuid () < commit.uuid()) {184 if (self.commits[index].uuid < commit.uuid) { 187 185 self.commits.splice(index, 0, commit); 188 186 --index; 189 187 break; 190 188 } 191 if (self.commits[index].uuid () === commit.uuid())189 if (self.commits[index].uuid === commit.uuid) 192 190 break; 193 191 --index; … … 241 239 242 240 while (commitsIndex < self.commits.length) { 243 if (self.commits[commitsIndex].uuid () > commit.uuid()) {241 if (self.commits[commitsIndex].uuid > commit.uuid) { 244 242 self.commits.splice(commitsIndex, 0, commit); 245 243 ++commitsIndex; 246 244 break; 247 245 } 248 if (self.commits[commitsIndex].uuid () === commit.uuid())246 if (self.commits[commitsIndex].uuid === commit.uuid) 249 247 break; 250 248 ++commitsIndex; … … 261 259 if (count === limit) { 262 260 let commit = new Commit(json[firstIndexForRepository.get(repo)]); 263 minFound = Math.max(minFound, commit.uuid ());261 minFound = Math.max(minFound, commit.uuid); 264 262 } 265 263 }); -
trunk/Tools/resultsdbpy/resultsdbpy/view/static/js/timeline.js
r247877 r248425 25 25 import {Configuration} from '/assets/js/configuration.js'; 26 26 import {deepCompare, ErrorDisplay, paramsToQuery, queryToParams} from '/assets/js/common.js'; 27 import {DOM, EventStream, REF} from '/library/js/Ref.js'; 27 import {DOM, EventStream, REF, FP} from '/library/js/Ref.js'; 28 import {Timeline} from '/library/js/components/TimelineComponents.js'; 28 29 29 30 … … 72 73 let willFilterExpected = false; 73 74 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 ''; 75 function 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 98 function 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; 130 137 } 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 }); 167 140 }); 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 161 function 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 181 function 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 194 function 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 231 const testsRegex = /tests_([a-z])+/; 232 const failureTypeOrder = ['failed', 'timedout', 'crashed']; 233 const failureTypeMapping = { 234 failed: 'ERROR', 235 timedout: 'TIMEOUT', 236 crashed: 'CRASH', 237 } 238 239 function 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 284 function 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 299 function 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 342 class TimelineFromEndpoint { 218 343 constructor(endpoint, suite = null) { 219 344 this.endpoint = endpoint; … … 222 347 this.configurations = Configuration.fromQuery(); 223 348 this.results = {}; 224 this.collapsed = {};225 this.expandedSDKs = {};226 349 this.suite = suite; // Suite is often implied by the endpoint, but trying to determine suite from endpoint is not trivial. 227 350 351 this.updates = []; 352 this.xaxisUpdates = []; 353 this.timelineUpdate = null; 354 this.repositories = []; 355 228 356 const self = this; 229 this.configurations.forEach(configuration => {230 this.results[configuration.toKey()] = [];231 this.collapsed[configuration.toKey()] = true;232 });233 357 234 358 this.latestDispatch = Date.now(); … … 245 369 }); 246 370 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); 251 375 252 376 this.reload(); 253 377 } 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 } 254 409 rerender() { 255 let params = queryToParams(document.URL.split('?')[1]);410 const params = queryToParams(document.URL.split('?')[1]); 256 411 this.ref.setState(params.limit ? parseInt(params.limit[params.limit.length - 1]) : DEFAULT_LIMIT); 257 412 } … … 274 429 this.configurations = newConfigs; 275 430 this.results = {}; 276 this.collapsed = {};277 this.expandedSDKs = {};278 431 this.configurations.forEach(configuration => { 279 432 this.results[configuration.toKey()] = []; 280 this.collapsed[configuration.toKey()] = true;281 433 }); 282 434 } … … 332 484 onStateUpdate: (element, state) => { 333 485 if (state.error) 334 element.innerHTML = ErrorDisplay(state);486 DOM.inject(element, ErrorDisplay(state)); 335 487 else if (state > 0) 336 488 DOM.inject(element, this.render(state)); 337 489 else 338 element.innerHTML = this.placeholder();490 DOM.inject(element, this.placeholder()); 339 491 } 340 492 }); … … 342 494 return `<div class="content" ref="${this.ref}"></div>`; 343 495 } 496 344 497 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 = []; 348 580 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 = {}; 349 588 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)); 358 596 }); 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 }))); 404 643 }); 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; 405 697 }); 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 605 705 606 706 function LegendLabel(eventStream, filterExpectedText, filterUnexpectedText) { … … 681 781 } 682 782 683 export {Legend, Timeline , Expectations};783 export {Legend, TimelineFromEndpoint, Expectations}; -
trunk/Tools/resultsdbpy/resultsdbpy/view/templates/search.html
r247877 r248425 37 37 import {DOM, REF} from '/library/js/Ref.js'; 38 38 import {SearchBar} from '/assets/js/search.js'; 39 import {Legend, Timeline } from '/assets/js/timeline.js';39 import {Legend, TimelineFromEndpoint} from '/assets/js/timeline.js'; 40 40 41 41 const DEFAULT_LIMIT = 100; … … 56 56 suite: this.currentParams.suite[i], 57 57 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]), 59 59 }); 60 60 } … … 130 130 DOM.inject(element, Legend(() => { 131 131 self.ref.state.children.forEach((child) => { 132 child.timeline. rerender();132 child.timeline.update(); 133 133 }); 134 134 }, false) + diff.children.map(renderChild).join('')); … … 161 161 this.ref.state.children.splice(search, 1); 162 162 break; 163 } 163 } else 164 this.ref.state.children[search].timeline.teardown(); 164 165 needReRender = true; 165 166 } … … 170 171 suite: params.suite[i], 171 172 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]}`), 173 174 }); 174 175 } … … 240 241 suite: arguments[i].suite, 241 242 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}`), 243 244 } 244 245 -
trunk/Tools/resultsdbpy/resultsdbpy/view/templates/suite_results.html
r247877 r248425 36 36 import {Drawer, BranchSelector, ConfigurationSelectors, LimitSlider} from '/assets/js/drawer.js'; 37 37 import {DOM, REF} from '/library/js/Ref.js'; 38 import {Legend, Timeline } from '/assets/js/timeline.js';38 import {Legend, TimelineFromEndpoint} from '/assets/js/timeline.js'; 39 39 40 40 const DEFAULT_LIMIT = 100; … … 67 67 68 68 SUITES.forEach((suite) => { 69 this.children[suite] = new Timeline ('api/results/' + suite, suite);69 this.children[suite] = new TimelineFromEndpoint('api/results/' + suite, suite); 70 70 }); 71 71 } … … 82 82 83 83 let sortedSuites = [...suites].sort(); 84 Object.keys(this.children).forEach(key => {this.children[key].teardown();}); 84 85 this.children = {}; 85 86 sortedSuites.forEach((suite) => { 86 this.children[suite] = new Timeline ('api/results/' + suite);87 this.children[suite] = new TimelineFromEndpoint('api/results/' + suite); 87 88 }); 88 89 return sortedSuites; … … 144 145 return Legend(() => { 145 146 for (let suite in children) { 146 children[suite]. rerender();147 children[suite].update(); 147 148 } 148 149 }, true) + suites.map(suite => {
Note: See TracChangeset
for help on using the changeset viewer.