xref: /aosp_15_r20/external/perfetto/infra/ci/frontend/static/script.js (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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