1*6dbdd20aSAndroid Build Coastguard Worker/** 2*6dbdd20aSAndroid Build Coastguard Worker * Copyright (c) 2019 The Android Open Source Project 3*6dbdd20aSAndroid Build Coastguard Worker * 4*6dbdd20aSAndroid Build Coastguard Worker * Licensed under the Apache License, Version 2.0 (the "License"); you 5*6dbdd20aSAndroid Build Coastguard Worker * may not use this file except in compliance with the License. You may 6*6dbdd20aSAndroid Build Coastguard Worker * obtain a copy of the License at 7*6dbdd20aSAndroid Build Coastguard Worker * 8*6dbdd20aSAndroid Build Coastguard Worker * http://www.apache.org/licenses/LICENSE-2.0 9*6dbdd20aSAndroid Build Coastguard Worker * 10*6dbdd20aSAndroid Build Coastguard Worker * Unless required by applicable law or agreed to in writing, software 11*6dbdd20aSAndroid Build Coastguard Worker * distributed under the License is distributed on an "AS IS" BASIS, 12*6dbdd20aSAndroid Build Coastguard Worker * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 13*6dbdd20aSAndroid Build Coastguard Worker * implied. See the License for the specific language governing 14*6dbdd20aSAndroid Build Coastguard Worker * permissions and limitations under the License. 15*6dbdd20aSAndroid Build Coastguard Worker */ 16*6dbdd20aSAndroid Build Coastguard Worker 17*6dbdd20aSAndroid Build Coastguard Worker'use strict'; 18*6dbdd20aSAndroid Build Coastguard Worker 19*6dbdd20aSAndroid Build Coastguard Worker// If you add or remove job types, do not forget to fix the colspans below. 20*6dbdd20aSAndroid Build Coastguard Workerconst JOB_TYPES = [ 21*6dbdd20aSAndroid Build Coastguard Worker { id: 'linux-gcc8-x86_64-release', label: 'rel' }, 22*6dbdd20aSAndroid Build Coastguard Worker { id: 'linux-clang-x86_64-debug', label: 'dbg' }, 23*6dbdd20aSAndroid Build Coastguard Worker { id: 'linux-clang-x86_64-tsan', label: 'tsan' }, 24*6dbdd20aSAndroid Build Coastguard Worker { id: 'linux-clang-x86_64-msan', label: 'msan' }, 25*6dbdd20aSAndroid Build Coastguard Worker { id: 'linux-clang-x86_64-asan_lsan', label: '{a,l}san' }, 26*6dbdd20aSAndroid Build Coastguard Worker { id: 'linux-clang-x86-release', label: 'x86 rel' }, 27*6dbdd20aSAndroid Build Coastguard Worker { id: 'linux-clang-x86_64-libfuzzer', label: 'fuzzer' }, 28*6dbdd20aSAndroid Build Coastguard Worker { id: 'linux-clang-x86_64-bazel', label: 'bazel' }, 29*6dbdd20aSAndroid Build Coastguard Worker { id: 'ui-clang-x86_64-release', label: 'rel' }, 30*6dbdd20aSAndroid Build Coastguard Worker { id: 'android-clang-arm-release', label: 'rel' }, 31*6dbdd20aSAndroid Build Coastguard Worker]; 32*6dbdd20aSAndroid Build Coastguard Worker 33*6dbdd20aSAndroid Build Coastguard Workerconst STATS_LINK = 34*6dbdd20aSAndroid Build Coastguard Worker 'https://app.google.stackdriver.com/dashboards/5008687313278081798?project=perfetto-ci'; 35*6dbdd20aSAndroid Build Coastguard Worker 36*6dbdd20aSAndroid Build Coastguard Workerconst state = { 37*6dbdd20aSAndroid Build Coastguard Worker // An array of recent CL objects retrieved from Gerrit. 38*6dbdd20aSAndroid Build Coastguard Worker gerritCls: [], 39*6dbdd20aSAndroid Build Coastguard Worker 40*6dbdd20aSAndroid Build Coastguard Worker // A map of sha1 -> Gerrit commit object. 41*6dbdd20aSAndroid Build Coastguard Worker // See 42*6dbdd20aSAndroid Build Coastguard Worker // https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-commit 43*6dbdd20aSAndroid Build Coastguard Worker gerritCommits: {}, 44*6dbdd20aSAndroid Build Coastguard Worker 45*6dbdd20aSAndroid Build Coastguard Worker // A map of git-log ranges to commit objects: 46*6dbdd20aSAndroid Build Coastguard Worker // 'dead..beef' -> [commit1, 2] 47*6dbdd20aSAndroid Build Coastguard Worker gerritLogs: {}, 48*6dbdd20aSAndroid Build Coastguard Worker 49*6dbdd20aSAndroid Build Coastguard Worker // Maps 'cls/1234-1' or 'branches/xxxx' -> array of job ids. 50*6dbdd20aSAndroid Build Coastguard Worker dbJobSets: {}, 51*6dbdd20aSAndroid Build Coastguard Worker 52*6dbdd20aSAndroid Build Coastguard Worker // Maps 'jobId' -> DB job object, as perf /ci/jobs/jobID. 53*6dbdd20aSAndroid Build Coastguard Worker // A jobId looks like 20190702143507-1008614-9-android-clang-arm. 54*6dbdd20aSAndroid Build Coastguard Worker dbJobs: {}, 55*6dbdd20aSAndroid Build Coastguard Worker 56*6dbdd20aSAndroid Build Coastguard Worker // Maps 'worker id' -> DB wokrker object, as per /ci/workers. 57*6dbdd20aSAndroid Build Coastguard Worker dbWorker: {}, 58*6dbdd20aSAndroid Build Coastguard Worker 59*6dbdd20aSAndroid Build Coastguard Worker // Maps 'main-YYMMDD' -> DB branch object, as per /ci/branches/xxx. 60*6dbdd20aSAndroid Build Coastguard Worker dbBranches: {}, 61*6dbdd20aSAndroid Build Coastguard Worker getBranchKeys: () => Object.keys(state.dbBranches).sort().reverse(), 62*6dbdd20aSAndroid Build Coastguard Worker 63*6dbdd20aSAndroid Build Coastguard Worker // Maps 'CL number' -> true|false. Retains the collapsed/expanded information 64*6dbdd20aSAndroid Build Coastguard Worker // for each row in the CLs table. 65*6dbdd20aSAndroid Build Coastguard Worker expandCl: {}, 66*6dbdd20aSAndroid Build Coastguard Worker 67*6dbdd20aSAndroid Build Coastguard Worker postsubmitShown: 3, 68*6dbdd20aSAndroid Build Coastguard Worker 69*6dbdd20aSAndroid Build Coastguard Worker // Lines that will be appended to the terminal on the next redraw() cycle. 70*6dbdd20aSAndroid Build Coastguard Worker termLines: [ 71*6dbdd20aSAndroid Build Coastguard Worker 'Hover a CL icon to see the log tail.', 'Click on it to load the full log.' 72*6dbdd20aSAndroid Build Coastguard Worker ], 73*6dbdd20aSAndroid Build Coastguard Worker termJobId: undefined, // The job id currently being shown by the terminal. 74*6dbdd20aSAndroid Build Coastguard Worker termClear: false, // If true the next redraw will clear the terminal. 75*6dbdd20aSAndroid Build Coastguard Worker redrawPending: false, 76*6dbdd20aSAndroid Build Coastguard Worker 77*6dbdd20aSAndroid Build Coastguard Worker // State for the Jobs page. These are arrays of job ids. 78*6dbdd20aSAndroid Build Coastguard Worker jobsQueued: [], 79*6dbdd20aSAndroid Build Coastguard Worker jobsRunning: [], 80*6dbdd20aSAndroid Build Coastguard Worker jobsRecent: [], 81*6dbdd20aSAndroid Build Coastguard Worker 82*6dbdd20aSAndroid Build Coastguard Worker // Firebase DB listeners (the objects returned by the .ref() operator). 83*6dbdd20aSAndroid Build Coastguard Worker realTimeLogRef: undefined, // Ref for the real-time log streaming. 84*6dbdd20aSAndroid Build Coastguard Worker workersRef: undefined, 85*6dbdd20aSAndroid Build Coastguard Worker jobsRunningRef: undefined, 86*6dbdd20aSAndroid Build Coastguard Worker jobsQueuedRef: undefined, 87*6dbdd20aSAndroid Build Coastguard Worker jobsRecentRef: undefined, 88*6dbdd20aSAndroid Build Coastguard Worker clRefs: {}, // '1234-1' -> Ref subscribed to updates on the given cl. 89*6dbdd20aSAndroid Build Coastguard Worker jobRefs: {}, // '....-arm-asan' -> Ref subscribed updates on the given job. 90*6dbdd20aSAndroid Build Coastguard Worker branchRefs: {} // 'main' -> Ref subscribed updates on the given branch. 91*6dbdd20aSAndroid Build Coastguard Worker}; 92*6dbdd20aSAndroid Build Coastguard Worker 93*6dbdd20aSAndroid Build Coastguard Workerlet term = undefined; 94*6dbdd20aSAndroid Build Coastguard Workerlet fitAddon = undefined; 95*6dbdd20aSAndroid Build Coastguard Workerlet searchAddon = undefined; 96*6dbdd20aSAndroid Build Coastguard Worker 97*6dbdd20aSAndroid Build Coastguard Workerfunction main() { 98*6dbdd20aSAndroid Build Coastguard Worker firebase.initializeApp({ databaseURL: cfg.DB_ROOT }); 99*6dbdd20aSAndroid Build Coastguard Worker 100*6dbdd20aSAndroid Build Coastguard Worker m.route(document.body, '/cls', { 101*6dbdd20aSAndroid Build Coastguard Worker '/cls': CLsPageRenderer, 102*6dbdd20aSAndroid Build Coastguard Worker '/cls/:cl': CLsPageRenderer, 103*6dbdd20aSAndroid Build Coastguard Worker '/logs/:jobId': LogsPageRenderer, 104*6dbdd20aSAndroid Build Coastguard Worker '/jobs': JobsPageRenderer, 105*6dbdd20aSAndroid Build Coastguard Worker '/jobs/:jobId': JobsPageRenderer, 106*6dbdd20aSAndroid Build Coastguard Worker }); 107*6dbdd20aSAndroid Build Coastguard Worker 108*6dbdd20aSAndroid Build Coastguard Worker setInterval(fetchGerritCLs, 15000); 109*6dbdd20aSAndroid Build Coastguard Worker fetchGerritCLs(); 110*6dbdd20aSAndroid Build Coastguard Worker fetchCIStatusForBranch('main'); 111*6dbdd20aSAndroid Build Coastguard Worker} 112*6dbdd20aSAndroid Build Coastguard Worker 113*6dbdd20aSAndroid Build Coastguard Worker// ----------------------------------------------------------------------------- 114*6dbdd20aSAndroid Build Coastguard Worker// Rendering functions 115*6dbdd20aSAndroid Build Coastguard Worker// ----------------------------------------------------------------------------- 116*6dbdd20aSAndroid Build Coastguard Worker 117*6dbdd20aSAndroid Build Coastguard Workerfunction renderHeader() { 118*6dbdd20aSAndroid Build Coastguard Worker const active = id => m.route.get().startsWith(`/${id}`) ? '.active' : ''; 119*6dbdd20aSAndroid Build Coastguard Worker const logUrl = 'https://goto.google.com/perfetto-ci-logs-'; 120*6dbdd20aSAndroid Build Coastguard Worker const docsUrl = 121*6dbdd20aSAndroid Build Coastguard Worker 'https://perfetto.dev/docs/design-docs/continuous-integration'; 122*6dbdd20aSAndroid Build Coastguard Worker return m( 123*6dbdd20aSAndroid Build Coastguard Worker 'header', m('a[href=/#!/cls]', m('h1', 'Perfetto ', m('span', 'CI'))), 124*6dbdd20aSAndroid Build Coastguard Worker m( 125*6dbdd20aSAndroid Build Coastguard Worker 'nav', 126*6dbdd20aSAndroid Build Coastguard Worker m(`div${active('cls')}`, m('a[href=/#!/cls]', 'CLs')), 127*6dbdd20aSAndroid Build Coastguard Worker m(`div${active('jobs')}`, m('a[href=/#!/jobs]', 'Jobs')), 128*6dbdd20aSAndroid Build Coastguard Worker m(`div${active('stats')}`, 129*6dbdd20aSAndroid Build Coastguard Worker m(`a[href=${STATS_LINK}][target=_blank]`, 'Stats')), 130*6dbdd20aSAndroid Build Coastguard Worker m(`div`, m(`a[href=${docsUrl}][target=_blank]`, 'Docs')), 131*6dbdd20aSAndroid Build Coastguard Worker m( 132*6dbdd20aSAndroid Build Coastguard Worker `div.logs`, 133*6dbdd20aSAndroid Build Coastguard Worker 'Logs', 134*6dbdd20aSAndroid Build Coastguard Worker m('div', 135*6dbdd20aSAndroid Build Coastguard Worker m(`a[href=${logUrl}controller][target=_blank]`, 'Controller')), 136*6dbdd20aSAndroid Build Coastguard Worker m('div', m(`a[href=${logUrl}workers][target=_blank]`, 'Workers')), 137*6dbdd20aSAndroid Build Coastguard Worker m('div', 138*6dbdd20aSAndroid Build Coastguard Worker m(`a[href=${logUrl}frontend][target=_blank]`, 'Frontend')), 139*6dbdd20aSAndroid Build Coastguard Worker ), 140*6dbdd20aSAndroid Build Coastguard Worker )); 141*6dbdd20aSAndroid Build Coastguard Worker} 142*6dbdd20aSAndroid Build Coastguard Worker 143*6dbdd20aSAndroid Build Coastguard Workervar CLsPageRenderer = { 144*6dbdd20aSAndroid Build Coastguard Worker view: function (vnode) { 145*6dbdd20aSAndroid Build Coastguard Worker const allCols = 4 + JOB_TYPES.length; 146*6dbdd20aSAndroid Build Coastguard Worker const postsubmitHeader = m('tr', 147*6dbdd20aSAndroid Build Coastguard Worker m(`td.header[colspan=${allCols}]`, 'Post-submit') 148*6dbdd20aSAndroid Build Coastguard Worker ); 149*6dbdd20aSAndroid Build Coastguard Worker 150*6dbdd20aSAndroid Build Coastguard Worker const postsubmitLoadMore = m('tr', 151*6dbdd20aSAndroid Build Coastguard Worker m(`td[colspan=${allCols}]`, 152*6dbdd20aSAndroid Build Coastguard Worker m('a[href=#]', 153*6dbdd20aSAndroid Build Coastguard Worker { onclick: () => state.postsubmitShown += 10 }, 154*6dbdd20aSAndroid Build Coastguard Worker 'Load more' 155*6dbdd20aSAndroid Build Coastguard Worker ) 156*6dbdd20aSAndroid Build Coastguard Worker ) 157*6dbdd20aSAndroid Build Coastguard Worker ); 158*6dbdd20aSAndroid Build Coastguard Worker 159*6dbdd20aSAndroid Build Coastguard Worker const presubmitHeader = m('tr', 160*6dbdd20aSAndroid Build Coastguard Worker m(`td.header[colspan=${allCols}]`, 'Pre-submit') 161*6dbdd20aSAndroid Build Coastguard Worker ); 162*6dbdd20aSAndroid Build Coastguard Worker 163*6dbdd20aSAndroid Build Coastguard Worker let branchRows = []; 164*6dbdd20aSAndroid Build Coastguard Worker const branchKeys = state.getBranchKeys(); 165*6dbdd20aSAndroid Build Coastguard Worker for (let i = 0; i < branchKeys.length && i < state.postsubmitShown; i++) { 166*6dbdd20aSAndroid Build Coastguard Worker const rowsForBranch = renderPostsubmitRow(branchKeys[i]); 167*6dbdd20aSAndroid Build Coastguard Worker branchRows = branchRows.concat(rowsForBranch); 168*6dbdd20aSAndroid Build Coastguard Worker } 169*6dbdd20aSAndroid Build Coastguard Worker 170*6dbdd20aSAndroid Build Coastguard Worker let clRows = []; 171*6dbdd20aSAndroid Build Coastguard Worker for (const gerritCl of state.gerritCls) { 172*6dbdd20aSAndroid Build Coastguard Worker if (vnode.attrs.cl && gerritCl.num != vnode.attrs.cl) continue; 173*6dbdd20aSAndroid Build Coastguard Worker clRows = clRows.concat(renderCLRow(gerritCl)); 174*6dbdd20aSAndroid Build Coastguard Worker } 175*6dbdd20aSAndroid Build Coastguard Worker 176*6dbdd20aSAndroid Build Coastguard Worker let footer = []; 177*6dbdd20aSAndroid Build Coastguard Worker if (vnode.attrs.cl) { 178*6dbdd20aSAndroid Build Coastguard Worker footer = m('footer', 179*6dbdd20aSAndroid Build Coastguard Worker `Showing only CL ${vnode.attrs.cl} - `, 180*6dbdd20aSAndroid Build Coastguard Worker m(`a[href=#!/cls]`, 'Click here to see all CLs') 181*6dbdd20aSAndroid Build Coastguard Worker ); 182*6dbdd20aSAndroid Build Coastguard Worker } 183*6dbdd20aSAndroid Build Coastguard Worker 184*6dbdd20aSAndroid Build Coastguard Worker return [ 185*6dbdd20aSAndroid Build Coastguard Worker renderHeader(), 186*6dbdd20aSAndroid Build Coastguard Worker m('main#cls', 187*6dbdd20aSAndroid Build Coastguard Worker m('div.table-scrolling-container', 188*6dbdd20aSAndroid Build Coastguard Worker m('table.main-table', 189*6dbdd20aSAndroid Build Coastguard Worker m('thead', 190*6dbdd20aSAndroid Build Coastguard Worker m('tr', 191*6dbdd20aSAndroid Build Coastguard Worker m('td[rowspan=4]', 'Subject'), 192*6dbdd20aSAndroid Build Coastguard Worker m('td[rowspan=4]', 'Status'), 193*6dbdd20aSAndroid Build Coastguard Worker m('td[rowspan=4]', 'Owner'), 194*6dbdd20aSAndroid Build Coastguard Worker m('td[rowspan=4]', 'Updated'), 195*6dbdd20aSAndroid Build Coastguard Worker m('td[colspan=10]', 'Bots'), 196*6dbdd20aSAndroid Build Coastguard Worker ), 197*6dbdd20aSAndroid Build Coastguard Worker m('tr', 198*6dbdd20aSAndroid Build Coastguard Worker m('td[colspan=9]', 'linux'), 199*6dbdd20aSAndroid Build Coastguard Worker m('td[colspan=2]', 'android'), 200*6dbdd20aSAndroid Build Coastguard Worker ), 201*6dbdd20aSAndroid Build Coastguard Worker m('tr', 202*6dbdd20aSAndroid Build Coastguard Worker m('td', 'gcc8'), 203*6dbdd20aSAndroid Build Coastguard Worker m('td[colspan=7]', 'clang'), 204*6dbdd20aSAndroid Build Coastguard Worker m('td[colspan=1]', 'ui'), 205*6dbdd20aSAndroid Build Coastguard Worker m('td[colspan=1]', 'clang-arm'), 206*6dbdd20aSAndroid Build Coastguard Worker ), 207*6dbdd20aSAndroid Build Coastguard Worker m('tr#cls_header', 208*6dbdd20aSAndroid Build Coastguard Worker JOB_TYPES.map(job => m(`td#${job.id}`, job.label)) 209*6dbdd20aSAndroid Build Coastguard Worker ), 210*6dbdd20aSAndroid Build Coastguard Worker ), 211*6dbdd20aSAndroid Build Coastguard Worker m('tbody', 212*6dbdd20aSAndroid Build Coastguard Worker postsubmitHeader, 213*6dbdd20aSAndroid Build Coastguard Worker branchRows, 214*6dbdd20aSAndroid Build Coastguard Worker postsubmitLoadMore, 215*6dbdd20aSAndroid Build Coastguard Worker presubmitHeader, 216*6dbdd20aSAndroid Build Coastguard Worker clRows, 217*6dbdd20aSAndroid Build Coastguard Worker ) 218*6dbdd20aSAndroid Build Coastguard Worker ), 219*6dbdd20aSAndroid Build Coastguard Worker footer, 220*6dbdd20aSAndroid Build Coastguard Worker ), 221*6dbdd20aSAndroid Build Coastguard Worker m(TermRenderer), 222*6dbdd20aSAndroid Build Coastguard Worker ), 223*6dbdd20aSAndroid Build Coastguard Worker ]; 224*6dbdd20aSAndroid Build Coastguard Worker } 225*6dbdd20aSAndroid Build Coastguard Worker}; 226*6dbdd20aSAndroid Build Coastguard Worker 227*6dbdd20aSAndroid Build Coastguard Worker 228*6dbdd20aSAndroid Build Coastguard Workerfunction getLastUpdate(lastUpdate) { 229*6dbdd20aSAndroid Build Coastguard Worker const lastUpdateMins = Math.ceil((Date.now() - lastUpdate) / 60000); 230*6dbdd20aSAndroid Build Coastguard Worker if (lastUpdateMins < 60) 231*6dbdd20aSAndroid Build Coastguard Worker return lastUpdateMins + ' mins ago'; 232*6dbdd20aSAndroid Build Coastguard Worker if (lastUpdateMins < 60 * 24) 233*6dbdd20aSAndroid Build Coastguard Worker return Math.ceil(lastUpdateMins / 60) + ' hours ago'; 234*6dbdd20aSAndroid Build Coastguard Worker return lastUpdate.toISOString().substr(0, 10); 235*6dbdd20aSAndroid Build Coastguard Worker} 236*6dbdd20aSAndroid Build Coastguard Worker 237*6dbdd20aSAndroid Build Coastguard Workerfunction renderCLRow(cl) { 238*6dbdd20aSAndroid Build Coastguard Worker const expanded = !!state.expandCl[cl.num]; 239*6dbdd20aSAndroid Build Coastguard Worker const toggleExpand = () => { 240*6dbdd20aSAndroid Build Coastguard Worker state.expandCl[cl.num] ^= 1; 241*6dbdd20aSAndroid Build Coastguard Worker fetchCIJobsForAllPatchsetOfCL(cl.num); 242*6dbdd20aSAndroid Build Coastguard Worker } 243*6dbdd20aSAndroid Build Coastguard Worker const rows = []; 244*6dbdd20aSAndroid Build Coastguard Worker 245*6dbdd20aSAndroid Build Coastguard Worker // Create the row for the latest patchset (as fetched by Gerrit). 246*6dbdd20aSAndroid Build Coastguard Worker rows.push(m(`tr.${cl.status}`, 247*6dbdd20aSAndroid Build Coastguard Worker m('td', 248*6dbdd20aSAndroid Build Coastguard Worker m(`i.material-icons.expand${expanded ? '.expanded' : ''}`, 249*6dbdd20aSAndroid Build Coastguard Worker { onclick: toggleExpand }, 250*6dbdd20aSAndroid Build Coastguard Worker 'arrow_right' 251*6dbdd20aSAndroid Build Coastguard Worker ), 252*6dbdd20aSAndroid Build Coastguard Worker m(`a[href=${cfg.GERRIT_REVIEW_URL}/+/${cl.num}/${cl.psNum}]`, 253*6dbdd20aSAndroid Build Coastguard Worker `${cl.subject}`, m('span.ps', `#${cl.psNum}`)) 254*6dbdd20aSAndroid Build Coastguard Worker ), 255*6dbdd20aSAndroid Build Coastguard Worker m('td', cl.status), 256*6dbdd20aSAndroid Build Coastguard Worker m('td', stripEmail(cl.owner || '')), 257*6dbdd20aSAndroid Build Coastguard Worker m('td', getLastUpdate(cl.lastUpdate)), 258*6dbdd20aSAndroid Build Coastguard Worker JOB_TYPES.map(x => renderClJobCell(`cls/${cl.num}-${cl.psNum}`, x.id)) 259*6dbdd20aSAndroid Build Coastguard Worker )); 260*6dbdd20aSAndroid Build Coastguard Worker 261*6dbdd20aSAndroid Build Coastguard Worker // If the usere clicked on the expand button, show also the other patchsets 262*6dbdd20aSAndroid Build Coastguard Worker // present in the CI DB. 263*6dbdd20aSAndroid Build Coastguard Worker for (let psNum = cl.psNum; expanded && psNum > 0; psNum--) { 264*6dbdd20aSAndroid Build Coastguard Worker const src = `cls/${cl.num}-${psNum}`; 265*6dbdd20aSAndroid Build Coastguard Worker const jobs = state.dbJobSets[src]; 266*6dbdd20aSAndroid Build Coastguard Worker if (!jobs) continue; 267*6dbdd20aSAndroid Build Coastguard Worker rows.push(m(`tr.nested`, 268*6dbdd20aSAndroid Build Coastguard Worker m('td', 269*6dbdd20aSAndroid Build Coastguard Worker m(`a[href=${cfg.GERRIT_REVIEW_URL}/+/${cl.num}/${psNum}]`, 270*6dbdd20aSAndroid Build Coastguard Worker ' Patchset', m('span.ps', `#${psNum}`)) 271*6dbdd20aSAndroid Build Coastguard Worker ), 272*6dbdd20aSAndroid Build Coastguard Worker m('td', ''), 273*6dbdd20aSAndroid Build Coastguard Worker m('td', ''), 274*6dbdd20aSAndroid Build Coastguard Worker m('td', ''), 275*6dbdd20aSAndroid Build Coastguard Worker JOB_TYPES.map(x => renderClJobCell(src, x.id)) 276*6dbdd20aSAndroid Build Coastguard Worker )); 277*6dbdd20aSAndroid Build Coastguard Worker } 278*6dbdd20aSAndroid Build Coastguard Worker 279*6dbdd20aSAndroid Build Coastguard Worker return rows; 280*6dbdd20aSAndroid Build Coastguard Worker} 281*6dbdd20aSAndroid Build Coastguard Worker 282*6dbdd20aSAndroid Build Coastguard Workerfunction renderPostsubmitRow(key) { 283*6dbdd20aSAndroid Build Coastguard Worker const branch = state.dbBranches[key]; 284*6dbdd20aSAndroid Build Coastguard Worker console.assert(branch !== undefined); 285*6dbdd20aSAndroid Build Coastguard Worker const subject = branch.subject; 286*6dbdd20aSAndroid Build Coastguard Worker let rows = []; 287*6dbdd20aSAndroid Build Coastguard Worker rows.push(m(`tr`, 288*6dbdd20aSAndroid Build Coastguard Worker m('td', 289*6dbdd20aSAndroid Build Coastguard Worker m(`a[href=${cfg.REPO_URL}/+/${branch.rev}]`, 290*6dbdd20aSAndroid Build Coastguard Worker subject, m('span.ps', `#${branch.rev.substr(0, 8)}`) 291*6dbdd20aSAndroid Build Coastguard Worker ) 292*6dbdd20aSAndroid Build Coastguard Worker ), 293*6dbdd20aSAndroid Build Coastguard Worker m('td', ''), 294*6dbdd20aSAndroid Build Coastguard Worker m('td', stripEmail(branch.author)), 295*6dbdd20aSAndroid Build Coastguard Worker m('td', getLastUpdate(new Date(branch.time_committed))), 296*6dbdd20aSAndroid Build Coastguard Worker JOB_TYPES.map(x => renderClJobCell(`branches/${key}`, x.id)) 297*6dbdd20aSAndroid Build Coastguard Worker )); 298*6dbdd20aSAndroid Build Coastguard Worker 299*6dbdd20aSAndroid Build Coastguard Worker 300*6dbdd20aSAndroid Build Coastguard Worker const allKeys = state.getBranchKeys(); 301*6dbdd20aSAndroid Build Coastguard Worker const curIdx = allKeys.indexOf(key); 302*6dbdd20aSAndroid Build Coastguard Worker if (curIdx >= 0 && curIdx < allKeys.length - 1) { 303*6dbdd20aSAndroid Build Coastguard Worker const nextKey = allKeys[curIdx + 1]; 304*6dbdd20aSAndroid Build Coastguard Worker const range = `${state.dbBranches[nextKey].rev}..${branch.rev}`; 305*6dbdd20aSAndroid Build Coastguard Worker const logs = (state.gerritLogs[range] || []).slice(1); 306*6dbdd20aSAndroid Build Coastguard Worker for (const log of logs) { 307*6dbdd20aSAndroid Build Coastguard Worker if (log.parents.length < 2) 308*6dbdd20aSAndroid Build Coastguard Worker continue; // Show only merge commits. 309*6dbdd20aSAndroid Build Coastguard Worker rows.push( 310*6dbdd20aSAndroid Build Coastguard Worker m('tr.nested', 311*6dbdd20aSAndroid Build Coastguard Worker m('td', 312*6dbdd20aSAndroid Build Coastguard Worker m(`a[href=${cfg.REPO_URL}/+/${log.commit}]`, 313*6dbdd20aSAndroid Build Coastguard Worker log.message.split('\n')[0], 314*6dbdd20aSAndroid Build Coastguard Worker m('span.ps', `#${log.commit.substr(0, 8)}`) 315*6dbdd20aSAndroid Build Coastguard Worker ) 316*6dbdd20aSAndroid Build Coastguard Worker ), 317*6dbdd20aSAndroid Build Coastguard Worker m('td', ''), 318*6dbdd20aSAndroid Build Coastguard Worker m('td', stripEmail(log.author.email)), 319*6dbdd20aSAndroid Build Coastguard Worker m('td', getLastUpdate(parseGerritTime(log.committer.time))), 320*6dbdd20aSAndroid Build Coastguard Worker m(`td[colspan=${JOB_TYPES.length}]`, 321*6dbdd20aSAndroid Build Coastguard Worker 'No post-submit was run for this revision' 322*6dbdd20aSAndroid Build Coastguard Worker ), 323*6dbdd20aSAndroid Build Coastguard Worker ) 324*6dbdd20aSAndroid Build Coastguard Worker ); 325*6dbdd20aSAndroid Build Coastguard Worker } 326*6dbdd20aSAndroid Build Coastguard Worker } 327*6dbdd20aSAndroid Build Coastguard Worker 328*6dbdd20aSAndroid Build Coastguard Worker return rows; 329*6dbdd20aSAndroid Build Coastguard Worker} 330*6dbdd20aSAndroid Build Coastguard Worker 331*6dbdd20aSAndroid Build Coastguard Workerfunction renderJobLink(jobId, jobStatus) { 332*6dbdd20aSAndroid Build Coastguard Worker const ICON_MAP = { 333*6dbdd20aSAndroid Build Coastguard Worker 'COMPLETED': 'check_circle', 334*6dbdd20aSAndroid Build Coastguard Worker 'STARTED': 'hourglass_full', 335*6dbdd20aSAndroid Build Coastguard Worker 'QUEUED': 'schedule', 336*6dbdd20aSAndroid Build Coastguard Worker 'FAILED': 'bug_report', 337*6dbdd20aSAndroid Build Coastguard Worker 'CANCELLED': 'cancel', 338*6dbdd20aSAndroid Build Coastguard Worker 'INTERRUPTED': 'cancel', 339*6dbdd20aSAndroid Build Coastguard Worker 'TIMED_OUT': 'notification_important', 340*6dbdd20aSAndroid Build Coastguard Worker }; 341*6dbdd20aSAndroid Build Coastguard Worker const icon = ICON_MAP[jobStatus] || 'clear'; 342*6dbdd20aSAndroid Build Coastguard Worker const eventHandlers = jobId ? { onmouseover: () => showLogTail(jobId) } : {}; 343*6dbdd20aSAndroid Build Coastguard Worker const logUrl = jobId ? `#!/logs/${jobId}` : '#'; 344*6dbdd20aSAndroid Build Coastguard Worker return m(`a.${jobStatus}[href=${logUrl}][title=${jobStatus}]`, 345*6dbdd20aSAndroid Build Coastguard Worker eventHandlers, 346*6dbdd20aSAndroid Build Coastguard Worker m(`i.material-icons`, icon) 347*6dbdd20aSAndroid Build Coastguard Worker ); 348*6dbdd20aSAndroid Build Coastguard Worker} 349*6dbdd20aSAndroid Build Coastguard Worker 350*6dbdd20aSAndroid Build Coastguard Workerfunction renderClJobCell(src, jobType) { 351*6dbdd20aSAndroid Build Coastguard Worker let jobStatus = 'UNKNOWN'; 352*6dbdd20aSAndroid Build Coastguard Worker let jobId = undefined; 353*6dbdd20aSAndroid Build Coastguard Worker 354*6dbdd20aSAndroid Build Coastguard Worker // To begin with check that the given CL/PS is present in the DB (the 355*6dbdd20aSAndroid Build Coastguard Worker // AppEngine cron job might have not seen that at all yet). 356*6dbdd20aSAndroid Build Coastguard Worker // If it is, find the global job id for the given jobType for the passed CL. 357*6dbdd20aSAndroid Build Coastguard Worker for (const id of (state.dbJobSets[src] || [])) { 358*6dbdd20aSAndroid Build Coastguard Worker const job = state.dbJobs[id]; 359*6dbdd20aSAndroid Build Coastguard Worker if (job !== undefined && job.type == jobType) { 360*6dbdd20aSAndroid Build Coastguard Worker // We found the job object that corresponds to jobType for the given CL. 361*6dbdd20aSAndroid Build Coastguard Worker jobStatus = job.status; 362*6dbdd20aSAndroid Build Coastguard Worker jobId = id; 363*6dbdd20aSAndroid Build Coastguard Worker } 364*6dbdd20aSAndroid Build Coastguard Worker } 365*6dbdd20aSAndroid Build Coastguard Worker return m('td.job', renderJobLink(jobId, jobStatus)); 366*6dbdd20aSAndroid Build Coastguard Worker} 367*6dbdd20aSAndroid Build Coastguard Worker 368*6dbdd20aSAndroid Build Coastguard Workerconst TermRenderer = { 369*6dbdd20aSAndroid Build Coastguard Worker oncreate: function(vnode) { 370*6dbdd20aSAndroid Build Coastguard Worker console.log('Creating terminal object'); 371*6dbdd20aSAndroid Build Coastguard Worker fitAddon = new FitAddon.FitAddon(); 372*6dbdd20aSAndroid Build Coastguard Worker searchAddon = new SearchAddon.SearchAddon(); 373*6dbdd20aSAndroid Build Coastguard Worker term = new Terminal({ 374*6dbdd20aSAndroid Build Coastguard Worker rows: 6, 375*6dbdd20aSAndroid Build Coastguard Worker fontFamily: 'monospace', 376*6dbdd20aSAndroid Build Coastguard Worker fontSize: 12, 377*6dbdd20aSAndroid Build Coastguard Worker scrollback: 100000, 378*6dbdd20aSAndroid Build Coastguard Worker disableStdin: true, 379*6dbdd20aSAndroid Build Coastguard Worker }); 380*6dbdd20aSAndroid Build Coastguard Worker term.loadAddon(fitAddon); 381*6dbdd20aSAndroid Build Coastguard Worker term.loadAddon(searchAddon); 382*6dbdd20aSAndroid Build Coastguard Worker term.open(vnode.dom); 383*6dbdd20aSAndroid Build Coastguard Worker fitAddon.fit(); 384*6dbdd20aSAndroid Build Coastguard Worker if (vnode.attrs.focused) 385*6dbdd20aSAndroid Build Coastguard Worker term.focus(); 386*6dbdd20aSAndroid Build Coastguard Worker }, 387*6dbdd20aSAndroid Build Coastguard Worker onremove: function(vnode) { 388*6dbdd20aSAndroid Build Coastguard Worker term.dispose(); 389*6dbdd20aSAndroid Build Coastguard Worker fitAddon.dispose(); 390*6dbdd20aSAndroid Build Coastguard Worker searchAddon.dispose(); 391*6dbdd20aSAndroid Build Coastguard Worker }, 392*6dbdd20aSAndroid Build Coastguard Worker onupdate: function(vnode) { 393*6dbdd20aSAndroid Build Coastguard Worker fitAddon.fit(); 394*6dbdd20aSAndroid Build Coastguard Worker if (state.termClear) { 395*6dbdd20aSAndroid Build Coastguard Worker term.clear(); 396*6dbdd20aSAndroid Build Coastguard Worker state.termClear = false; 397*6dbdd20aSAndroid Build Coastguard Worker } 398*6dbdd20aSAndroid Build Coastguard Worker for (const line of state.termLines) { 399*6dbdd20aSAndroid Build Coastguard Worker term.write(line + '\r\n'); 400*6dbdd20aSAndroid Build Coastguard Worker } 401*6dbdd20aSAndroid Build Coastguard Worker state.termLines = []; 402*6dbdd20aSAndroid Build Coastguard Worker }, 403*6dbdd20aSAndroid Build Coastguard Worker view: function() { 404*6dbdd20aSAndroid Build Coastguard Worker return m('.term-container', 405*6dbdd20aSAndroid Build Coastguard Worker { 406*6dbdd20aSAndroid Build Coastguard Worker onkeydown: (e) => { 407*6dbdd20aSAndroid Build Coastguard Worker if (e.key === 'f' && (e.ctrlKey || e.metaKey)) { 408*6dbdd20aSAndroid Build Coastguard Worker document.querySelector('.term-search').select(); 409*6dbdd20aSAndroid Build Coastguard Worker e.preventDefault(); 410*6dbdd20aSAndroid Build Coastguard Worker } 411*6dbdd20aSAndroid Build Coastguard Worker } 412*6dbdd20aSAndroid Build Coastguard Worker }, 413*6dbdd20aSAndroid Build Coastguard Worker m('input[type=text][placeholder=search and press Enter].term-search', { 414*6dbdd20aSAndroid Build Coastguard Worker onkeydown: (e) => { 415*6dbdd20aSAndroid Build Coastguard Worker if (e.key !== 'Enter') return; 416*6dbdd20aSAndroid Build Coastguard Worker if (e.shiftKey) { 417*6dbdd20aSAndroid Build Coastguard Worker searchAddon.findNext(e.target.value); 418*6dbdd20aSAndroid Build Coastguard Worker } else { 419*6dbdd20aSAndroid Build Coastguard Worker searchAddon.findPrevious(e.target.value); 420*6dbdd20aSAndroid Build Coastguard Worker } 421*6dbdd20aSAndroid Build Coastguard Worker e.stopPropagation(); 422*6dbdd20aSAndroid Build Coastguard Worker e.preventDefault(); 423*6dbdd20aSAndroid Build Coastguard Worker } 424*6dbdd20aSAndroid Build Coastguard Worker }) 425*6dbdd20aSAndroid Build Coastguard Worker ); 426*6dbdd20aSAndroid Build Coastguard Worker } 427*6dbdd20aSAndroid Build Coastguard Worker}; 428*6dbdd20aSAndroid Build Coastguard Worker 429*6dbdd20aSAndroid Build Coastguard Workerconst LogsPageRenderer = { 430*6dbdd20aSAndroid Build Coastguard Worker oncreate: function (vnode) { 431*6dbdd20aSAndroid Build Coastguard Worker showFullLog(vnode.attrs.jobId); 432*6dbdd20aSAndroid Build Coastguard Worker }, 433*6dbdd20aSAndroid Build Coastguard Worker view: function () { 434*6dbdd20aSAndroid Build Coastguard Worker return [ 435*6dbdd20aSAndroid Build Coastguard Worker renderHeader(), 436*6dbdd20aSAndroid Build Coastguard Worker m(TermRenderer, { focused: true }) 437*6dbdd20aSAndroid Build Coastguard Worker ]; 438*6dbdd20aSAndroid Build Coastguard Worker } 439*6dbdd20aSAndroid Build Coastguard Worker} 440*6dbdd20aSAndroid Build Coastguard Worker 441*6dbdd20aSAndroid Build Coastguard Workerconst JobsPageRenderer = { 442*6dbdd20aSAndroid Build Coastguard Worker oncreate: function (vnode) { 443*6dbdd20aSAndroid Build Coastguard Worker fetchRecentJobsStatus(); 444*6dbdd20aSAndroid Build Coastguard Worker fetchWorkers(); 445*6dbdd20aSAndroid Build Coastguard Worker }, 446*6dbdd20aSAndroid Build Coastguard Worker 447*6dbdd20aSAndroid Build Coastguard Worker createWorkerTable: function () { 448*6dbdd20aSAndroid Build Coastguard Worker const makeWokerRow = workerId => { 449*6dbdd20aSAndroid Build Coastguard Worker const worker = state.dbWorker[workerId]; 450*6dbdd20aSAndroid Build Coastguard Worker if (worker.status === 'TERMINATED') return []; 451*6dbdd20aSAndroid Build Coastguard Worker return m('tr', 452*6dbdd20aSAndroid Build Coastguard Worker m('td', worker.host), 453*6dbdd20aSAndroid Build Coastguard Worker m('td', workerId), 454*6dbdd20aSAndroid Build Coastguard Worker m('td', worker.status), 455*6dbdd20aSAndroid Build Coastguard Worker m('td', getLastUpdate(new Date(worker.last_update))), 456*6dbdd20aSAndroid Build Coastguard Worker m('td', m(`a[href=#!/jobs/${worker.job_id}]`, worker.job_id)), 457*6dbdd20aSAndroid Build Coastguard Worker ); 458*6dbdd20aSAndroid Build Coastguard Worker }; 459*6dbdd20aSAndroid Build Coastguard Worker return m('table.main-table', 460*6dbdd20aSAndroid Build Coastguard Worker m('thead', 461*6dbdd20aSAndroid Build Coastguard Worker m('tr', m('td[colspan=5]', 'Workers')), 462*6dbdd20aSAndroid Build Coastguard Worker m('tr', 463*6dbdd20aSAndroid Build Coastguard Worker m('td', 'Host'), 464*6dbdd20aSAndroid Build Coastguard Worker m('td', 'Worker'), 465*6dbdd20aSAndroid Build Coastguard Worker m('td', 'Status'), 466*6dbdd20aSAndroid Build Coastguard Worker m('td', 'Last ping'), 467*6dbdd20aSAndroid Build Coastguard Worker m('td', 'Job'), 468*6dbdd20aSAndroid Build Coastguard Worker ) 469*6dbdd20aSAndroid Build Coastguard Worker ), 470*6dbdd20aSAndroid Build Coastguard Worker m('tbody', Object.keys(state.dbWorker).map(makeWokerRow)) 471*6dbdd20aSAndroid Build Coastguard Worker ); 472*6dbdd20aSAndroid Build Coastguard Worker }, 473*6dbdd20aSAndroid Build Coastguard Worker 474*6dbdd20aSAndroid Build Coastguard Worker createJobsTable: function (vnode, title, jobIds) { 475*6dbdd20aSAndroid Build Coastguard Worker const tStr = function (tStart, tEnd) { 476*6dbdd20aSAndroid Build Coastguard Worker return new Date(tEnd - tStart).toUTCString().substr(17, 9); 477*6dbdd20aSAndroid Build Coastguard Worker }; 478*6dbdd20aSAndroid Build Coastguard Worker 479*6dbdd20aSAndroid Build Coastguard Worker const makeJobRow = function (jobId) { 480*6dbdd20aSAndroid Build Coastguard Worker const job = state.dbJobs[jobId] || {}; 481*6dbdd20aSAndroid Build Coastguard Worker let cols = [ 482*6dbdd20aSAndroid Build Coastguard Worker m('td.job.align-left', 483*6dbdd20aSAndroid Build Coastguard Worker renderJobLink(jobId, job ? job.status : undefined), 484*6dbdd20aSAndroid Build Coastguard Worker m(`span.status.${job.status}`, job.status) 485*6dbdd20aSAndroid Build Coastguard Worker ) 486*6dbdd20aSAndroid Build Coastguard Worker ]; 487*6dbdd20aSAndroid Build Coastguard Worker if (job) { 488*6dbdd20aSAndroid Build Coastguard Worker const tQ = Date.parse(job.time_queued); 489*6dbdd20aSAndroid Build Coastguard Worker const tS = Date.parse(job.time_started); 490*6dbdd20aSAndroid Build Coastguard Worker const tE = Date.parse(job.time_ended) || Date.now(); 491*6dbdd20aSAndroid Build Coastguard Worker let cell = m(''); 492*6dbdd20aSAndroid Build Coastguard Worker if (job.src === undefined) { 493*6dbdd20aSAndroid Build Coastguard Worker cell = '?'; 494*6dbdd20aSAndroid Build Coastguard Worker } else if (job.src.startsWith('cls/')) { 495*6dbdd20aSAndroid Build Coastguard Worker const cl_and_ps = job.src.substr(4).replace('-', '/'); 496*6dbdd20aSAndroid Build Coastguard Worker const href = `${cfg.GERRIT_REVIEW_URL}/+/${cl_and_ps}`; 497*6dbdd20aSAndroid Build Coastguard Worker cell = m(`a[href=${href}][target=_blank]`, cl_and_ps); 498*6dbdd20aSAndroid Build Coastguard Worker } else if (job.src.startsWith('branches/')) { 499*6dbdd20aSAndroid Build Coastguard Worker cell = job.src.substr(9).split('-')[0] 500*6dbdd20aSAndroid Build Coastguard Worker } 501*6dbdd20aSAndroid Build Coastguard Worker cols.push(m('td', cell)); 502*6dbdd20aSAndroid Build Coastguard Worker cols.push(m('td', `${job.type}`)); 503*6dbdd20aSAndroid Build Coastguard Worker cols.push(m('td', `${job.worker || ''}`)); 504*6dbdd20aSAndroid Build Coastguard Worker cols.push(m('td', `${job.time_queued}`)); 505*6dbdd20aSAndroid Build Coastguard Worker cols.push(m(`td[title=Start ${job.time_started}]`, `${tStr(tQ, tS)}`)); 506*6dbdd20aSAndroid Build Coastguard Worker cols.push(m(`td[title=End ${job.time_ended}]`, `${tStr(tS, tE)}`)); 507*6dbdd20aSAndroid Build Coastguard Worker } else { 508*6dbdd20aSAndroid Build Coastguard Worker cols.push(m('td[colspan=6]', jobId)); 509*6dbdd20aSAndroid Build Coastguard Worker } 510*6dbdd20aSAndroid Build Coastguard Worker return m(`tr${vnode.attrs.jobId === jobId ? '.selected' : ''}`, cols) 511*6dbdd20aSAndroid Build Coastguard Worker }; 512*6dbdd20aSAndroid Build Coastguard Worker 513*6dbdd20aSAndroid Build Coastguard Worker return m('table.main-table', 514*6dbdd20aSAndroid Build Coastguard Worker m('thead', 515*6dbdd20aSAndroid Build Coastguard Worker m('tr', m('td[colspan=7]', title)), 516*6dbdd20aSAndroid Build Coastguard Worker 517*6dbdd20aSAndroid Build Coastguard Worker m('tr', 518*6dbdd20aSAndroid Build Coastguard Worker m('td', 'Status'), 519*6dbdd20aSAndroid Build Coastguard Worker m('td', 'CL'), 520*6dbdd20aSAndroid Build Coastguard Worker m('td', 'Type'), 521*6dbdd20aSAndroid Build Coastguard Worker m('td', 'Worker'), 522*6dbdd20aSAndroid Build Coastguard Worker m('td', 'T queued'), 523*6dbdd20aSAndroid Build Coastguard Worker m('td', 'Queue time'), 524*6dbdd20aSAndroid Build Coastguard Worker m('td', 'Run time'), 525*6dbdd20aSAndroid Build Coastguard Worker ) 526*6dbdd20aSAndroid Build Coastguard Worker ), 527*6dbdd20aSAndroid Build Coastguard Worker m('tbody', jobIds.map(makeJobRow)) 528*6dbdd20aSAndroid Build Coastguard Worker ); 529*6dbdd20aSAndroid Build Coastguard Worker }, 530*6dbdd20aSAndroid Build Coastguard Worker 531*6dbdd20aSAndroid Build Coastguard Worker view: function (vnode) { 532*6dbdd20aSAndroid Build Coastguard Worker return [ 533*6dbdd20aSAndroid Build Coastguard Worker renderHeader(), 534*6dbdd20aSAndroid Build Coastguard Worker m('main', 535*6dbdd20aSAndroid Build Coastguard Worker m('.jobs-list', 536*6dbdd20aSAndroid Build Coastguard Worker this.createWorkerTable(), 537*6dbdd20aSAndroid Build Coastguard Worker this.createJobsTable(vnode, 'Queued + Running jobs', 538*6dbdd20aSAndroid Build Coastguard Worker state.jobsRunning.concat(state.jobsQueued)), 539*6dbdd20aSAndroid Build Coastguard Worker this.createJobsTable(vnode, 'Last 100 jobs', state.jobsRecent), 540*6dbdd20aSAndroid Build Coastguard Worker ), 541*6dbdd20aSAndroid Build Coastguard Worker ) 542*6dbdd20aSAndroid Build Coastguard Worker ]; 543*6dbdd20aSAndroid Build Coastguard Worker } 544*6dbdd20aSAndroid Build Coastguard Worker}; 545*6dbdd20aSAndroid Build Coastguard Worker 546*6dbdd20aSAndroid Build Coastguard Worker// ----------------------------------------------------------------------------- 547*6dbdd20aSAndroid Build Coastguard Worker// Business logic (handles fetching from Gerrit and Firebase DB). 548*6dbdd20aSAndroid Build Coastguard Worker// ----------------------------------------------------------------------------- 549*6dbdd20aSAndroid Build Coastguard Worker 550*6dbdd20aSAndroid Build Coastguard Workerfunction parseGerritTime(str) { 551*6dbdd20aSAndroid Build Coastguard Worker // Gerrit timestamps are UTC (as per public docs) but obviously they are not 552*6dbdd20aSAndroid Build Coastguard Worker // encoded in ISO format. 553*6dbdd20aSAndroid Build Coastguard Worker return new Date(`${str} UTC`); 554*6dbdd20aSAndroid Build Coastguard Worker} 555*6dbdd20aSAndroid Build Coastguard Worker 556*6dbdd20aSAndroid Build Coastguard Workerfunction stripEmail(email) { 557*6dbdd20aSAndroid Build Coastguard Worker return email.replace('@google.com', '@'); 558*6dbdd20aSAndroid Build Coastguard Worker} 559*6dbdd20aSAndroid Build Coastguard Worker 560*6dbdd20aSAndroid Build Coastguard Worker// Fetches the list of CLs from gerrit and updates the state. 561*6dbdd20aSAndroid Build Coastguard Workerasync function fetchGerritCLs() { 562*6dbdd20aSAndroid Build Coastguard Worker console.log('Fetching CL list from Gerrit'); 563*6dbdd20aSAndroid Build Coastguard Worker let uri = '/gerrit/changes/?-age:7days'; 564*6dbdd20aSAndroid Build Coastguard Worker uri += '+-is:abandoned+branch:main&o=DETAILED_ACCOUNTS&o=CURRENT_REVISION'; 565*6dbdd20aSAndroid Build Coastguard Worker const response = await fetch(uri); 566*6dbdd20aSAndroid Build Coastguard Worker state.gerritCls = []; 567*6dbdd20aSAndroid Build Coastguard Worker if (response.status !== 200) { 568*6dbdd20aSAndroid Build Coastguard Worker setTimeout(fetchGerritCLs, 3000); // Retry. 569*6dbdd20aSAndroid Build Coastguard Worker return; 570*6dbdd20aSAndroid Build Coastguard Worker } 571*6dbdd20aSAndroid Build Coastguard Worker 572*6dbdd20aSAndroid Build Coastguard Worker const json = (await response.text()); 573*6dbdd20aSAndroid Build Coastguard Worker const cls = []; 574*6dbdd20aSAndroid Build Coastguard Worker for (const e of JSON.parse(json)) { 575*6dbdd20aSAndroid Build Coastguard Worker const revHash = Object.keys(e.revisions)[0]; 576*6dbdd20aSAndroid Build Coastguard Worker const cl = { 577*6dbdd20aSAndroid Build Coastguard Worker subject: e.subject, 578*6dbdd20aSAndroid Build Coastguard Worker status: e.status, 579*6dbdd20aSAndroid Build Coastguard Worker num: e._number, 580*6dbdd20aSAndroid Build Coastguard Worker revHash: revHash, 581*6dbdd20aSAndroid Build Coastguard Worker psNum: e.revisions[revHash]._number, 582*6dbdd20aSAndroid Build Coastguard Worker lastUpdate: parseGerritTime(e.updated), 583*6dbdd20aSAndroid Build Coastguard Worker owner: e.owner.email, 584*6dbdd20aSAndroid Build Coastguard Worker }; 585*6dbdd20aSAndroid Build Coastguard Worker cls.push(cl); 586*6dbdd20aSAndroid Build Coastguard Worker fetchCIJobsForCLOrBranch(`cls/${cl.num}-${cl.psNum}`); 587*6dbdd20aSAndroid Build Coastguard Worker } 588*6dbdd20aSAndroid Build Coastguard Worker state.gerritCls = cls; 589*6dbdd20aSAndroid Build Coastguard Worker scheduleRedraw(); 590*6dbdd20aSAndroid Build Coastguard Worker} 591*6dbdd20aSAndroid Build Coastguard Worker 592*6dbdd20aSAndroid Build Coastguard Workerasync function fetchGerritCommit(sha1) { 593*6dbdd20aSAndroid Build Coastguard Worker const response = await fetch(`/gerrit/commits/${sha1}`); 594*6dbdd20aSAndroid Build Coastguard Worker console.assert(response.status === 200); 595*6dbdd20aSAndroid Build Coastguard Worker const json = (await response.text()); 596*6dbdd20aSAndroid Build Coastguard Worker state.gerritCommits[sha1] = JSON.parse(json); 597*6dbdd20aSAndroid Build Coastguard Worker scheduleRedraw(); 598*6dbdd20aSAndroid Build Coastguard Worker} 599*6dbdd20aSAndroid Build Coastguard Worker 600*6dbdd20aSAndroid Build Coastguard Workerasync function fetchGerritLog(first, second) { 601*6dbdd20aSAndroid Build Coastguard Worker const range = `${first}..${second}`; 602*6dbdd20aSAndroid Build Coastguard Worker const response = await fetch(`/gerrit/log/${range}`); 603*6dbdd20aSAndroid Build Coastguard Worker if (response.status !== 200) return; 604*6dbdd20aSAndroid Build Coastguard Worker const json = await response.text(); 605*6dbdd20aSAndroid Build Coastguard Worker state.gerritLogs[range] = JSON.parse(json).log; 606*6dbdd20aSAndroid Build Coastguard Worker scheduleRedraw(); 607*6dbdd20aSAndroid Build Coastguard Worker} 608*6dbdd20aSAndroid Build Coastguard Worker 609*6dbdd20aSAndroid Build Coastguard Worker// Retrieves the status of a given (CL, PS) in the DB. 610*6dbdd20aSAndroid Build Coastguard Workerfunction fetchCIJobsForCLOrBranch(src) { 611*6dbdd20aSAndroid Build Coastguard Worker if (src in state.clRefs) return; // Aslready have a listener for this key. 612*6dbdd20aSAndroid Build Coastguard Worker const ref = firebase.database().ref(`/ci/${src}`); 613*6dbdd20aSAndroid Build Coastguard Worker state.clRefs[src] = ref; 614*6dbdd20aSAndroid Build Coastguard Worker ref.on('value', (e) => { 615*6dbdd20aSAndroid Build Coastguard Worker const obj = e.val(); 616*6dbdd20aSAndroid Build Coastguard Worker if (!obj) return; 617*6dbdd20aSAndroid Build Coastguard Worker state.dbJobSets[src] = Object.keys(obj.jobs); 618*6dbdd20aSAndroid Build Coastguard Worker for (var jobId of state.dbJobSets[src]) { 619*6dbdd20aSAndroid Build Coastguard Worker fetchCIStatusForJob(jobId); 620*6dbdd20aSAndroid Build Coastguard Worker } 621*6dbdd20aSAndroid Build Coastguard Worker scheduleRedraw(); 622*6dbdd20aSAndroid Build Coastguard Worker }); 623*6dbdd20aSAndroid Build Coastguard Worker} 624*6dbdd20aSAndroid Build Coastguard Worker 625*6dbdd20aSAndroid Build Coastguard Workerfunction fetchCIJobsForAllPatchsetOfCL(cl) { 626*6dbdd20aSAndroid Build Coastguard Worker let ref = firebase.database().ref('/ci/cls').orderByKey(); 627*6dbdd20aSAndroid Build Coastguard Worker ref = ref.startAt(`${cl}-0`).endAt(`${cl}-~`); 628*6dbdd20aSAndroid Build Coastguard Worker ref.once('value', (e) => { 629*6dbdd20aSAndroid Build Coastguard Worker const patchsets = e.val() || {}; 630*6dbdd20aSAndroid Build Coastguard Worker for (const clAndPs in patchsets) { 631*6dbdd20aSAndroid Build Coastguard Worker const jobs = Object.keys(patchsets[clAndPs].jobs); 632*6dbdd20aSAndroid Build Coastguard Worker state.dbJobSets[`cls/${clAndPs}`] = jobs; 633*6dbdd20aSAndroid Build Coastguard Worker for (var jobId of jobs) { 634*6dbdd20aSAndroid Build Coastguard Worker fetchCIStatusForJob(jobId); 635*6dbdd20aSAndroid Build Coastguard Worker } 636*6dbdd20aSAndroid Build Coastguard Worker } 637*6dbdd20aSAndroid Build Coastguard Worker scheduleRedraw(); 638*6dbdd20aSAndroid Build Coastguard Worker }); 639*6dbdd20aSAndroid Build Coastguard Worker} 640*6dbdd20aSAndroid Build Coastguard Worker 641*6dbdd20aSAndroid Build Coastguard Workerfunction fetchCIStatusForJob(jobId) { 642*6dbdd20aSAndroid Build Coastguard Worker if (jobId in state.jobRefs) return; // Already have a listener for this key. 643*6dbdd20aSAndroid Build Coastguard Worker const ref = firebase.database().ref(`/ci/jobs/${jobId}`); 644*6dbdd20aSAndroid Build Coastguard Worker state.jobRefs[jobId] = ref; 645*6dbdd20aSAndroid Build Coastguard Worker ref.on('value', (e) => { 646*6dbdd20aSAndroid Build Coastguard Worker if (e.val()) state.dbJobs[jobId] = e.val(); 647*6dbdd20aSAndroid Build Coastguard Worker scheduleRedraw(); 648*6dbdd20aSAndroid Build Coastguard Worker }); 649*6dbdd20aSAndroid Build Coastguard Worker} 650*6dbdd20aSAndroid Build Coastguard Worker 651*6dbdd20aSAndroid Build Coastguard Workerfunction fetchCIStatusForBranch(branch) { 652*6dbdd20aSAndroid Build Coastguard Worker if (branch in state.branchRefs) return; // Already have a listener. 653*6dbdd20aSAndroid Build Coastguard Worker const db = firebase.database(); 654*6dbdd20aSAndroid Build Coastguard Worker const ref = db.ref('/ci/branches') 655*6dbdd20aSAndroid Build Coastguard Worker .orderByKey() 656*6dbdd20aSAndroid Build Coastguard Worker .startAt('main') 657*6dbdd20aSAndroid Build Coastguard Worker .endAt('maio') 658*6dbdd20aSAndroid Build Coastguard Worker .limitToLast(20); 659*6dbdd20aSAndroid Build Coastguard Worker state.branchRefs[branch] = ref; 660*6dbdd20aSAndroid Build Coastguard Worker ref.on('value', (e) => { 661*6dbdd20aSAndroid Build Coastguard Worker const resp = e.val(); 662*6dbdd20aSAndroid Build Coastguard Worker if (!resp) return; 663*6dbdd20aSAndroid Build Coastguard Worker // key looks like 'main-YYYYMMDDHHMMSS', where YMD is the commit datetime. 664*6dbdd20aSAndroid Build Coastguard Worker // Iterate in most-recent-first order. 665*6dbdd20aSAndroid Build Coastguard Worker const keys = Object.keys(resp).sort().reverse(); 666*6dbdd20aSAndroid Build Coastguard Worker for (let i = 0; i < keys.length; i++) { 667*6dbdd20aSAndroid Build Coastguard Worker const key = keys[i]; 668*6dbdd20aSAndroid Build Coastguard Worker const branchInfo = resp[key]; 669*6dbdd20aSAndroid Build Coastguard Worker state.dbBranches[key] = branchInfo; 670*6dbdd20aSAndroid Build Coastguard Worker fetchCIJobsForCLOrBranch(`branches/${key}`); 671*6dbdd20aSAndroid Build Coastguard Worker if (i < keys.length - 1) { 672*6dbdd20aSAndroid Build Coastguard Worker fetchGerritLog(resp[keys[i + 1]].rev, branchInfo.rev); 673*6dbdd20aSAndroid Build Coastguard Worker } 674*6dbdd20aSAndroid Build Coastguard Worker } 675*6dbdd20aSAndroid Build Coastguard Worker scheduleRedraw(); 676*6dbdd20aSAndroid Build Coastguard Worker }); 677*6dbdd20aSAndroid Build Coastguard Worker} 678*6dbdd20aSAndroid Build Coastguard Worker 679*6dbdd20aSAndroid Build Coastguard Workerfunction fetchWorkers() { 680*6dbdd20aSAndroid Build Coastguard Worker if (state.workersRef !== undefined) return; // Aslready have a listener. 681*6dbdd20aSAndroid Build Coastguard Worker const ref = firebase.database().ref('/ci/workers'); 682*6dbdd20aSAndroid Build Coastguard Worker state.workersRef = ref; 683*6dbdd20aSAndroid Build Coastguard Worker ref.on('value', (e) => { 684*6dbdd20aSAndroid Build Coastguard Worker state.dbWorker = e.val() || {}; 685*6dbdd20aSAndroid Build Coastguard Worker scheduleRedraw(); 686*6dbdd20aSAndroid Build Coastguard Worker }); 687*6dbdd20aSAndroid Build Coastguard Worker} 688*6dbdd20aSAndroid Build Coastguard Worker 689*6dbdd20aSAndroid Build Coastguard Workerasync function showLogTail(jobId) { 690*6dbdd20aSAndroid Build Coastguard Worker if (state.termJobId === jobId) return; // Already on it. 691*6dbdd20aSAndroid Build Coastguard Worker const TAIL = 20; 692*6dbdd20aSAndroid Build Coastguard Worker state.termClear = true; 693*6dbdd20aSAndroid Build Coastguard Worker state.termLines = [ 694*6dbdd20aSAndroid Build Coastguard Worker `Fetching last ${TAIL} lines for ${jobId}.`, 695*6dbdd20aSAndroid Build Coastguard Worker `Click on the CI icon to see the full log.` 696*6dbdd20aSAndroid Build Coastguard Worker ]; 697*6dbdd20aSAndroid Build Coastguard Worker state.termJobId = jobId; 698*6dbdd20aSAndroid Build Coastguard Worker scheduleRedraw(); 699*6dbdd20aSAndroid Build Coastguard Worker const ref = firebase.database().ref(`/ci/logs/${jobId}`); 700*6dbdd20aSAndroid Build Coastguard Worker const lines = (await ref.orderByKey().limitToLast(TAIL).once('value')).val(); 701*6dbdd20aSAndroid Build Coastguard Worker if (state.termJobId !== jobId || !lines) return; 702*6dbdd20aSAndroid Build Coastguard Worker const lastKey = appendLogLinesAndRedraw(lines); 703*6dbdd20aSAndroid Build Coastguard Worker startRealTimeLogs(jobId, lastKey); 704*6dbdd20aSAndroid Build Coastguard Worker} 705*6dbdd20aSAndroid Build Coastguard Worker 706*6dbdd20aSAndroid Build Coastguard Workerasync function showFullLog(jobId) { 707*6dbdd20aSAndroid Build Coastguard Worker state.termClear = true; 708*6dbdd20aSAndroid Build Coastguard Worker state.termLines = [`Fetching full for ${jobId} ...`]; 709*6dbdd20aSAndroid Build Coastguard Worker state.termJobId = jobId; 710*6dbdd20aSAndroid Build Coastguard Worker scheduleRedraw(); 711*6dbdd20aSAndroid Build Coastguard Worker 712*6dbdd20aSAndroid Build Coastguard Worker // Suspend any other real-time logging in progress. 713*6dbdd20aSAndroid Build Coastguard Worker stopRealTimeLogs(); 714*6dbdd20aSAndroid Build Coastguard Worker 715*6dbdd20aSAndroid Build Coastguard Worker // Starts a chain of async tasks that fetch the current log lines in batches. 716*6dbdd20aSAndroid Build Coastguard Worker state.termJobId = jobId; 717*6dbdd20aSAndroid Build Coastguard Worker const ref = firebase.database().ref(`/ci/logs/${jobId}`).orderByKey(); 718*6dbdd20aSAndroid Build Coastguard Worker let lastKey = ''; 719*6dbdd20aSAndroid Build Coastguard Worker const BATCH = 1000; 720*6dbdd20aSAndroid Build Coastguard Worker for (; ;) { 721*6dbdd20aSAndroid Build Coastguard Worker const batchRef = ref.startAt(`${lastKey}!`).limitToFirst(BATCH); 722*6dbdd20aSAndroid Build Coastguard Worker const logs = (await batchRef.once('value')).val(); 723*6dbdd20aSAndroid Build Coastguard Worker if (!logs) 724*6dbdd20aSAndroid Build Coastguard Worker break; 725*6dbdd20aSAndroid Build Coastguard Worker lastKey = appendLogLinesAndRedraw(logs); 726*6dbdd20aSAndroid Build Coastguard Worker } 727*6dbdd20aSAndroid Build Coastguard Worker 728*6dbdd20aSAndroid Build Coastguard Worker startRealTimeLogs(jobId, lastKey) 729*6dbdd20aSAndroid Build Coastguard Worker} 730*6dbdd20aSAndroid Build Coastguard Worker 731*6dbdd20aSAndroid Build Coastguard Workerfunction startRealTimeLogs(jobId, lastLineKey) { 732*6dbdd20aSAndroid Build Coastguard Worker stopRealTimeLogs(); 733*6dbdd20aSAndroid Build Coastguard Worker console.log('Starting real-time logs for ', jobId); 734*6dbdd20aSAndroid Build Coastguard Worker state.termJobId = jobId; 735*6dbdd20aSAndroid Build Coastguard Worker let ref = firebase.database().ref(`/ci/logs/${jobId}`); 736*6dbdd20aSAndroid Build Coastguard Worker ref = ref.orderByKey().startAt(`${lastLineKey}!`); 737*6dbdd20aSAndroid Build Coastguard Worker state.realTimeLogRef = ref; 738*6dbdd20aSAndroid Build Coastguard Worker state.realTimeLogRef.on('child_added', res => { 739*6dbdd20aSAndroid Build Coastguard Worker const line = res.val(); 740*6dbdd20aSAndroid Build Coastguard Worker if (state.termJobId !== jobId || !line) return; 741*6dbdd20aSAndroid Build Coastguard Worker const lines = {}; 742*6dbdd20aSAndroid Build Coastguard Worker lines[res.key] = line; 743*6dbdd20aSAndroid Build Coastguard Worker appendLogLinesAndRedraw(lines); 744*6dbdd20aSAndroid Build Coastguard Worker }); 745*6dbdd20aSAndroid Build Coastguard Worker} 746*6dbdd20aSAndroid Build Coastguard Worker 747*6dbdd20aSAndroid Build Coastguard Workerfunction stopRealTimeLogs() { 748*6dbdd20aSAndroid Build Coastguard Worker if (state.realTimeLogRef !== undefined) { 749*6dbdd20aSAndroid Build Coastguard Worker state.realTimeLogRef.off(); 750*6dbdd20aSAndroid Build Coastguard Worker state.realTimeLogRef = undefined; 751*6dbdd20aSAndroid Build Coastguard Worker } 752*6dbdd20aSAndroid Build Coastguard Worker} 753*6dbdd20aSAndroid Build Coastguard Worker 754*6dbdd20aSAndroid Build Coastguard Workerfunction appendLogLinesAndRedraw(lines) { 755*6dbdd20aSAndroid Build Coastguard Worker const keys = Object.keys(lines).sort(); 756*6dbdd20aSAndroid Build Coastguard Worker for (var key of keys) { 757*6dbdd20aSAndroid Build Coastguard Worker const date = new Date(null); 758*6dbdd20aSAndroid Build Coastguard Worker date.setSeconds(parseInt(key.substr(0, 6), 16) / 1000); 759*6dbdd20aSAndroid Build Coastguard Worker const timeString = date.toISOString().substr(11, 8); 760*6dbdd20aSAndroid Build Coastguard Worker const isErr = lines[key].indexOf('FAILED:') >= 0; 761*6dbdd20aSAndroid Build Coastguard Worker let line = `[${timeString}] ${lines[key]}`; 762*6dbdd20aSAndroid Build Coastguard Worker if (isErr) line = `\u001b[33m${line}\u001b[0m`; 763*6dbdd20aSAndroid Build Coastguard Worker state.termLines.push(line); 764*6dbdd20aSAndroid Build Coastguard Worker } 765*6dbdd20aSAndroid Build Coastguard Worker scheduleRedraw(); 766*6dbdd20aSAndroid Build Coastguard Worker return keys[keys.length - 1]; 767*6dbdd20aSAndroid Build Coastguard Worker} 768*6dbdd20aSAndroid Build Coastguard Worker 769*6dbdd20aSAndroid Build Coastguard Workerasync function fetchRecentJobsStatus() { 770*6dbdd20aSAndroid Build Coastguard Worker const db = firebase.database(); 771*6dbdd20aSAndroid Build Coastguard Worker if (state.jobsQueuedRef === undefined) { 772*6dbdd20aSAndroid Build Coastguard Worker state.jobsQueuedRef = db.ref(`/ci/jobs_queued`).on('value', e => { 773*6dbdd20aSAndroid Build Coastguard Worker state.jobsQueued = Object.keys(e.val() || {}).sort().reverse(); 774*6dbdd20aSAndroid Build Coastguard Worker for (const jobId of state.jobsQueued) 775*6dbdd20aSAndroid Build Coastguard Worker fetchCIStatusForJob(jobId); 776*6dbdd20aSAndroid Build Coastguard Worker scheduleRedraw(); 777*6dbdd20aSAndroid Build Coastguard Worker }); 778*6dbdd20aSAndroid Build Coastguard Worker } 779*6dbdd20aSAndroid Build Coastguard Worker 780*6dbdd20aSAndroid Build Coastguard Worker if (state.jobsRunningRef === undefined) { 781*6dbdd20aSAndroid Build Coastguard Worker state.jobsRunningRef = db.ref(`/ci/jobs_running`).on('value', e => { 782*6dbdd20aSAndroid Build Coastguard Worker state.jobsRunning = Object.keys(e.val() || {}).sort().reverse(); 783*6dbdd20aSAndroid Build Coastguard Worker for (const jobId of state.jobsRunning) 784*6dbdd20aSAndroid Build Coastguard Worker fetchCIStatusForJob(jobId); 785*6dbdd20aSAndroid Build Coastguard Worker scheduleRedraw(); 786*6dbdd20aSAndroid Build Coastguard Worker }); 787*6dbdd20aSAndroid Build Coastguard Worker } 788*6dbdd20aSAndroid Build Coastguard Worker 789*6dbdd20aSAndroid Build Coastguard Worker if (state.jobsRecentRef === undefined) { 790*6dbdd20aSAndroid Build Coastguard Worker state.jobsRecentRef = db.ref(`/ci/jobs`).orderByKey().limitToLast(100); 791*6dbdd20aSAndroid Build Coastguard Worker state.jobsRecentRef.on('value', e => { 792*6dbdd20aSAndroid Build Coastguard Worker state.jobsRecent = Object.keys(e.val() || {}).sort().reverse(); 793*6dbdd20aSAndroid Build Coastguard Worker for (const jobId of state.jobsRecent) 794*6dbdd20aSAndroid Build Coastguard Worker fetchCIStatusForJob(jobId); 795*6dbdd20aSAndroid Build Coastguard Worker scheduleRedraw(); 796*6dbdd20aSAndroid Build Coastguard Worker }); 797*6dbdd20aSAndroid Build Coastguard Worker } 798*6dbdd20aSAndroid Build Coastguard Worker} 799*6dbdd20aSAndroid Build Coastguard Worker 800*6dbdd20aSAndroid Build Coastguard Worker 801*6dbdd20aSAndroid Build Coastguard Workerfunction scheduleRedraw() { 802*6dbdd20aSAndroid Build Coastguard Worker if (state.redrawPending) return; 803*6dbdd20aSAndroid Build Coastguard Worker state.redrawPending = true; 804*6dbdd20aSAndroid Build Coastguard Worker window.requestAnimationFrame(() => { 805*6dbdd20aSAndroid Build Coastguard Worker state.redrawPending = false; 806*6dbdd20aSAndroid Build Coastguard Worker m.redraw(); 807*6dbdd20aSAndroid Build Coastguard Worker }); 808*6dbdd20aSAndroid Build Coastguard Worker} 809*6dbdd20aSAndroid Build Coastguard Worker 810*6dbdd20aSAndroid Build Coastguard Workermain(); 811