waterfall: don't offer a 30 second reload, just 1/5/10 minutes
[buildbot.git] / buildbot / status / web / waterfall.py
bloba5ed259282baeed0566ba73f4d2ee1cb79028244
1 # -*- test-case-name: buildbot.test.test_web -*-
3 from zope.interface import implements
4 from twisted.python import log, components
5 from twisted.web import html
6 import urllib
8 import time
10 from buildbot import interfaces, util
11 from buildbot import version
12 from buildbot.status import builder
14 from buildbot.status.web.base import Box, HtmlResource, IBox, ICurrentBox, \
15 ITopBox, td, build_get_class
19 class CurrentBox(components.Adapter):
20 # this provides the "current activity" box, just above the builder name
21 implements(ICurrentBox)
23 def formatETA(self, eta):
24 if eta is None:
25 return []
26 if eta < 0:
27 return ["Soon"]
28 abstime = time.strftime("%H:%M:%S", time.localtime(util.now()+eta))
29 return ["ETA in", "%d secs" % eta, "at %s" % abstime]
31 def getBox(self, status):
32 # getState() returns offline, idle, or building
33 state, builds = self.original.getState()
35 # look for upcoming builds. We say the state is "waiting" if the
36 # builder is otherwise idle and there is a scheduler which tells us a
37 # build will be performed some time in the near future. TODO: this
38 # functionality used to be in BuilderStatus.. maybe this code should
39 # be merged back into it.
40 upcoming = []
41 builderName = self.original.getName()
42 for s in status.getSchedulers():
43 if builderName in s.listBuilderNames():
44 upcoming.extend(s.getPendingBuildTimes())
45 if state == "idle" and upcoming:
46 state = "waiting"
48 if state == "building":
49 color = "yellow"
50 text = ["building"]
51 if builds:
52 for b in builds:
53 eta = b.getETA()
54 if eta:
55 text.extend(self.formatETA(eta))
56 elif state == "offline":
57 color = "red"
58 text = ["offline"]
59 elif state == "idle":
60 color = "white"
61 text = ["idle"]
62 elif state == "waiting":
63 color = "yellow"
64 text = ["waiting"]
65 else:
66 # just in case I add a state and forget to update this
67 color = "white"
68 text = [state]
70 # TODO: for now, this pending/upcoming stuff is in the "current
71 # activity" box, but really it should go into a "next activity" row
72 # instead. The only times it should show up in "current activity" is
73 # when the builder is otherwise idle.
75 # are any builds pending? (waiting for a slave to be free)
76 pbs = self.original.getPendingBuilds()
77 if pbs:
78 text.append("%d pending" % len(pbs))
79 for t in upcoming:
80 text.extend(["next at",
81 time.strftime("%H:%M:%S", time.localtime(t)),
82 "[%d secs]" % (t - util.now()),
84 # TODO: the upcoming-builds box looks like:
85 # ['waiting', 'next at', '22:14:15', '[86 secs]']
86 # while the currently-building box is reversed:
87 # ['building', 'ETA in', '2 secs', 'at 22:12:50']
88 # consider swapping one of these to make them look the same. also
89 # consider leaving them reversed to make them look different.
90 return Box(text, color=color, class_="Activity " + state)
92 components.registerAdapter(CurrentBox, builder.BuilderStatus, ICurrentBox)
95 class BuildTopBox(components.Adapter):
96 # this provides a per-builder box at the very top of the display,
97 # showing the results of the most recent build
98 implements(IBox)
100 def getBox(self):
101 assert interfaces.IBuilderStatus(self.original)
102 b = self.original.getLastFinishedBuild()
103 if not b:
104 return Box(["none"], "white", class_="LastBuild")
105 name = b.getBuilder().getName()
106 number = b.getNumber()
107 url = "%s/builds/%d" % (name, number)
108 text = b.getText()
109 # TODO: add logs?
110 # TODO: add link to the per-build page at 'url'
111 c = b.getColor()
112 class_ = build_get_class(b)
113 return Box(text, c, class_="LastBuild %s" % class_)
114 components.registerAdapter(BuildTopBox, builder.BuilderStatus, ITopBox)
116 class BuildBox(components.Adapter):
117 # this provides the yellow "starting line" box for each build
118 implements(IBox)
120 def getBox(self):
121 b = self.original
122 name = b.getBuilder().getName()
123 number = b.getNumber()
124 url = "builders/%s/builds/%d" % (urllib.quote(name, safe=''), number)
125 reason = b.getReason()
126 text = ('<a title="Reason: %s" href="%s">Build %d</a>'
127 % (html.escape(reason), url, number))
128 color = "yellow"
129 class_ = "start"
130 if b.isFinished() and not b.getSteps():
131 # the steps have been pruned, so there won't be any indication
132 # of whether it succeeded or failed. Color the box red or green
133 # to show its status
134 color = b.getColor()
135 class_ = build_get_class(b)
136 return Box([text], color=color, class_="BuildStep " + class_)
137 components.registerAdapter(BuildBox, builder.BuildStatus, IBox)
139 class StepBox(components.Adapter):
140 implements(IBox)
142 def getBox(self):
143 b = self.original.getBuild()
144 urlbase = "builders/%s/builds/%d/steps/%s" % (
145 urllib.quote(b.getBuilder().getName(), safe=''),
146 b.getNumber(),
147 urllib.quote(self.original.getName(), safe=''))
148 text = self.original.getText()
149 if text is None:
150 log.msg("getText() gave None", urlbase)
151 text = []
152 text = text[:]
153 logs = self.original.getLogs()
154 for num in range(len(logs)):
155 name = logs[num].getName()
156 if logs[num].hasContents():
157 url = urlbase + "/logs/%s" % urllib.quote(name)
158 text.append("<a href=\"%s\">%s</a>" % (url, html.escape(name)))
159 else:
160 text.append(html.escape(name))
161 urls = self.original.getURLs()
162 ex_url_class = "BuildStep external"
163 for name, target in urls.items():
164 text.append('[<a href="%s" class="%s">%s</a>]' %
165 (target, ex_url_class, html.escape(name)))
166 color = self.original.getColor()
167 class_ = "BuildStep " + build_get_class(self.original)
168 return Box(text, color, class_=class_)
169 components.registerAdapter(StepBox, builder.BuildStepStatus, IBox)
172 class EventBox(components.Adapter):
173 implements(IBox)
175 def getBox(self):
176 text = self.original.getText()
177 color = self.original.getColor()
178 class_ = "Event"
179 if color:
180 class_ += " " + color
181 return Box(text, color, class_=class_)
182 components.registerAdapter(EventBox, builder.Event, IBox)
185 class Spacer(builder.Event):
186 def __init__(self, start, finish):
187 self.started = start
188 self.finished = finish
190 class SpacerBox(components.Adapter):
191 implements(IBox)
193 def getBox(self):
194 #b = Box(["spacer"], "white")
195 b = Box([])
196 b.spacer = True
197 return b
198 components.registerAdapter(SpacerBox, Spacer, IBox)
200 def insertGaps(g, lastEventTime, idleGap=2):
201 debug = False
203 e = g.next()
204 starts, finishes = e.getTimes()
205 if debug: log.msg("E0", starts, finishes)
206 if finishes == 0:
207 finishes = starts
208 if debug: log.msg("E1 finishes=%s, gap=%s, lET=%s" % \
209 (finishes, idleGap, lastEventTime))
210 if finishes is not None and finishes + idleGap < lastEventTime:
211 if debug: log.msg(" spacer0")
212 yield Spacer(finishes, lastEventTime)
214 followingEventStarts = starts
215 if debug: log.msg(" fES0", starts)
216 yield e
218 while 1:
219 e = g.next()
220 starts, finishes = e.getTimes()
221 if debug: log.msg("E2", starts, finishes)
222 if finishes == 0:
223 finishes = starts
224 if finishes is not None and finishes + idleGap < followingEventStarts:
225 # there is a gap between the end of this event and the beginning
226 # of the next one. Insert an idle event so the waterfall display
227 # shows a gap here.
228 if debug:
229 log.msg(" finishes=%s, gap=%s, fES=%s" % \
230 (finishes, idleGap, followingEventStarts))
231 yield Spacer(finishes, followingEventStarts)
232 yield e
233 followingEventStarts = starts
234 if debug: log.msg(" fES1", starts)
236 HELP = '''
237 <form action="../waterfall" method="GET">
239 <h1>The Waterfall Display</h1>
241 <p>The Waterfall display can be controlled by adding query arguments to the
242 URL. For example, if your Waterfall is accessed via the URL
243 <tt>http://buildbot.example.org:8080</tt>, then you could add a
244 <tt>branch=</tt> argument (described below) by going to
245 <tt>http://buildbot.example.org:8080?branch=beta4</tt> instead. Remember that
246 query arguments are separated from each other with ampersands, but they are
247 separated from the main URL with a question mark, so to add a
248 <tt>branch=</tt> and two <tt>builder=</tt> arguments, you would use
249 <tt>http://buildbot.example.org:8080?branch=beta4&amp;builder=unix&amp;builder=macos</tt>.</p>
251 <h2>Limiting the Displayed Interval</h2>
253 <p>The <tt>last_time=</tt> argument is a unix timestamp (seconds since the
254 start of 1970) that will be used as an upper bound on the interval of events
255 displayed: nothing will be shown that is more recent than the given time.
256 When no argument is provided, all events up to and including the most recent
257 steps are included.</p>
259 <p>The <tt>first_time=</tt> argument provides the lower bound. No events will
260 be displayed that occurred <b>before</b> this timestamp. Instead of providing
261 <tt>first_time=</tt>, you can provide <tt>show_time=</tt>: in this case,
262 <tt>first_time</tt> will be set equal to <tt>last_time</tt> minus
263 <tt>show_time</tt>. <tt>show_time</tt> overrides <tt>first_time</tt>.</p>
265 <p>The display normally shows the latest 200 events that occurred in the
266 given interval, where each timestamp on the left hand edge counts as a single
267 event. You can add a <tt>num_events=</tt> argument to override this this.</p>
269 <h2>Hiding non-Build events</h2>
271 <p>By passing <tt>show_events=false</tt>, you can remove the "buildslave
272 attached", "buildslave detached", and "builder reconfigured" events that
273 appear in-between the actual builds.</p>
275 %(show_events_input)s
277 <h2>Showing only Certain Branches</h2>
279 <p>If you provide one or more <tt>branch=</tt> arguments, the display will be
280 limited to builds that used one of the given branches. If no <tt>branch=</tt>
281 arguments are given, builds from all branches will be displayed.</p>
283 Erase the text from these "Show Branch:" boxes to remove that branch filter.
285 %(show_branches_input)s
287 <h2>Limiting the Builders that are Displayed</h2>
289 <p>By adding one or more <tt>builder=</tt> arguments, the display will be
290 limited to showing builds that ran on the given builders. This serves to
291 limit the display to the specific named columns. If no <tt>builder=</tt>
292 arguments are provided, all Builders will be displayed.</p>
294 <p>To view a Waterfall page with only a subset of Builders displayed, select
295 the Builders you are interested in here.</p>
297 %(show_builders_input)s
300 <h2>Auto-reloading the Page</h2>
302 <p>Adding a <tt>reload=</tt> argument will cause the page to automatically
303 reload itself after that many seconds.</p>
305 %(show_reload_input)s
308 <input type="submit" value="View Waterfall" />
309 </form>
312 class WaterfallHelp(HtmlResource):
313 title = "Waterfall Help"
315 def __init__(self, categories=None):
316 HtmlResource.__init__(self)
317 self.categories = categories
319 def body(self, request):
320 data = ''
321 status = self.getStatus(request)
323 showEvents_checked = 'checked="checked"'
324 if request.args.get("show_events", ["true"])[0].lower() == "true":
325 showEvents_checked = ''
326 show_events_input = ('<p>'
327 '<input type="checkbox" name="show_events" '
328 'value="false" %s>'
329 'Hide non-Build events'
330 '</p>\n'
331 ) % showEvents_checked
333 branches = [b
334 for b in request.args.get("branch", [])
335 if b]
336 branches.append('')
337 show_branches_input = '<table>\n'
338 for b in branches:
339 show_branches_input += ('<tr>'
340 '<td>Show Branch: '
341 '<input type="text" name="branch" '
342 'value="%s">'
343 '</td></tr>\n'
344 ) % (b,)
345 show_branches_input += '</table>\n'
347 # this has a set of toggle-buttons to let the user choose the
348 # builders
349 showBuilders = request.args.get("show", [])
350 showBuilders.extend(request.args.get("builder", []))
351 allBuilders = status.getBuilderNames(categories=self.categories)
353 show_builders_input = '<table>\n'
354 for bn in allBuilders:
355 checked = ""
356 if bn in showBuilders:
357 checked = 'checked="checked"'
358 show_builders_input += ('<tr>'
359 '<td><input type="checkbox"'
360 ' name="builder" '
361 'value="%s" %s></td> '
362 '<td>%s</td></tr>\n'
363 ) % (bn, checked, bn)
364 show_builders_input += '</table>\n'
366 # a couple of radio-button selectors for refresh time will appear
367 # just after that text
368 show_reload_input = '<table>\n'
369 times = [("none", "None"),
370 ("60", "60 seconds"),
371 ("300", "5 minutes"),
372 ("600", "10 minutes"),
374 current_reload_time = request.args.get("reload", ["none"])
375 if current_reload_time:
376 current_reload_time = current_reload_time[0]
377 if current_reload_time not in [t[0] for t in times]:
378 times.insert(0, (current_reload_time, current_reload_time) )
379 for value, name in times:
380 checked = ""
381 if value == current_reload_time:
382 checked = 'checked="checked"'
383 show_reload_input += ('<tr>'
384 '<td><input type="radio" name="reload" '
385 'value="%s" %s></td> '
386 '<td>%s</td></tr>\n'
387 ) % (value, checked, name)
388 show_reload_input += '</table>\n'
390 fields = {"show_events_input": show_events_input,
391 "show_branches_input": show_branches_input,
392 "show_builders_input": show_builders_input,
393 "show_reload_input": show_reload_input,
395 data += HELP % fields
396 return data
398 class WaterfallStatusResource(HtmlResource):
399 """This builds the main status page, with the waterfall display, and
400 all child pages."""
402 def __init__(self, categories=None):
403 HtmlResource.__init__(self)
404 self.categories = categories
405 self.putChild("help", WaterfallHelp(categories))
407 def getTitle(self, request):
408 status = self.getStatus(request)
409 p = status.getProjectName()
410 if p:
411 return "BuildBot: %s" % p
412 else:
413 return "BuildBot"
415 def getChangemaster(self, request):
416 # TODO: this wants to go away, access it through IStatus
417 return request.site.buildbot_service.parent.change_svc
419 def get_reload_time(self, request):
420 if "reload" in request.args:
421 try:
422 reload_time = int(request.args["reload"][0])
423 if reload_time > 15:
424 return reload_time
425 except ValueError:
426 pass
427 return None
429 def head(self, request):
430 head = ''
431 reload_time = self.get_reload_time(request)
432 if reload_time is not None:
433 head += '<meta http-equiv="refresh" content="%d">\n' % reload_time
434 return head
436 def body(self, request):
437 "This method builds the main waterfall display."
439 status = self.getStatus(request)
440 data = ''
442 projectName = status.getProjectName()
443 projectURL = status.getProjectURL()
445 phase = request.args.get("phase",["2"])
446 phase = int(phase[0])
448 showBuilders = request.args.get("show", [])
449 showBuilders.extend(request.args.get("builder", []))
450 allBuilders = status.getBuilderNames(categories=self.categories)
451 if showBuilders:
452 builderNames = []
453 for b in showBuilders:
454 if b not in allBuilders:
455 continue
456 if b in builderNames:
457 continue
458 builderNames.append(b)
459 else:
460 builderNames = allBuilders
461 builders = map(lambda name: status.getBuilder(name),
462 builderNames)
464 if phase == -1:
465 return self.body0(request, builders)
466 (changeNames, builderNames, timestamps, eventGrid, sourceEvents) = \
467 self.buildGrid(request, builders)
468 if phase == 0:
469 return self.phase0(request, (changeNames + builderNames),
470 timestamps, eventGrid)
471 # start the table: top-header material
472 data += '<table border="0" cellspacing="0">\n'
474 if projectName and projectURL:
475 # TODO: this is going to look really ugly
476 topleft = "<a href=\"%s\">%s</a><br />last build" % \
477 (projectURL, projectName)
478 else:
479 topleft = "last build"
480 data += ' <tr class="LastBuild">\n'
481 data += td(topleft, align="right", colspan=2, class_="Project")
482 for b in builders:
483 box = ITopBox(b).getBox()
484 data += box.td(align="center")
485 data += " </tr>\n"
487 data += ' <tr class="Activity">\n'
488 data += td('current activity', align='right', colspan=2)
489 for b in builders:
490 box = ICurrentBox(b).getBox(status)
491 data += box.td(align="center")
492 data += " </tr>\n"
494 data += " <tr>\n"
495 TZ = time.tzname[time.daylight]
496 data += td("time (%s)" % TZ, align="center", class_="Time")
497 name = changeNames[0]
498 data += td(
499 "<a href=\"%s\">%s</a>" % (urllib.quote(name, safe=''), name),
500 align="center", class_="Change")
501 for name in builderNames:
502 safename = urllib.quote(name, safe='')
503 data += td( "<a href=\"builders/%s\">%s</a>" % (safename, name),
504 align="center", class_="Builder")
505 data += " </tr>\n"
507 if phase == 1:
508 f = self.phase1
509 else:
510 f = self.phase2
511 data += f(request, changeNames + builderNames, timestamps, eventGrid,
512 sourceEvents)
514 data += "</table>\n"
516 data += "<hr />\n"
518 def with_args(req, remove_args=[], new_args=[], new_path=None):
519 # sigh, nevow makes this sort of manipulation easier
520 newargs = req.args.copy()
521 for argname in remove_args:
522 newargs[argname] = []
523 newargs["branch"] = [b for b in newargs["branch"] if b]
524 for k,v in new_args:
525 if k in newargs:
526 newargs[k].append(v)
527 else:
528 newargs[k] = [v]
529 newquery = "&".join(["%s=%s" % (k, v)
530 for k in newargs
531 for v in newargs[k]
533 new_url = req.URLPath()
534 if new_path:
535 new_url.path = new_path
536 new_url.query = newquery
537 #if newquery:
538 # new_url += "?" + newquery
539 return str(new_url)
541 if timestamps:
542 bottom = timestamps[-1]
543 nextpage = with_args(request, ["last_time"],
544 [("last_time", str(int(bottom)))])
545 data += '<a href="%s">next page</a>\n' % nextpage
547 helppage = with_args(request, new_path="waterfall/help")
548 data += '<a href="%s">help</a>\n' % helppage
550 if self.get_reload_time(request) is not None:
551 no_reload_page = with_args(request, remove_args=["reload"])
552 data += '<a href="%s">Stop Reloading</a>\n' % no_reload_page
554 data += "<br />\n"
557 bburl = "http://buildbot.net/?bb-ver=%s" % urllib.quote(version)
558 data += "<a href=\"%s\">Buildbot-%s</a> " % (bburl, version)
559 if projectName:
560 data += "working for the "
561 if projectURL:
562 data += "<a href=\"%s\">%s</a> project." % (projectURL,
563 projectName)
564 else:
565 data += "%s project." % projectName
566 data += "<br />\n"
567 # TODO: push this to the right edge, if possible
568 data += ("Page built: " +
569 time.strftime("%a %d %b %Y %H:%M:%S",
570 time.localtime(util.now()))
571 + "\n")
572 return data
574 def body0(self, request, builders):
575 # build the waterfall display
576 data = ""
577 data += "<h2>Basic display</h2>\n"
578 data += "<p>See <a href=\"%s\">here</a>" % \
579 urllib.quote(request.childLink("waterfall"))
580 data += " for the waterfall display</p>\n"
582 data += '<table border="0" cellspacing="0">\n'
583 names = map(lambda builder: builder.name, builders)
585 # the top row is two blank spaces, then the top-level status boxes
586 data += " <tr>\n"
587 data += td("", colspan=2)
588 for b in builders:
589 text = ""
590 color = "#ca88f7"
591 state, builds = b.getState()
592 if state != "offline":
593 text += "%s<br />\n" % state #b.getCurrentBig().text[0]
594 else:
595 text += "OFFLINE<br />\n"
596 color = "#ffe0e0"
597 data += td(text, align="center", bgcolor=color)
599 # the next row has the column headers: time, changes, builder names
600 data += " <tr>\n"
601 data += td("Time", align="center")
602 data += td("Changes", align="center")
603 for name in names:
604 data += td(
605 "<a href=\"%s\">%s</a>" % (urllib.quote(request.childLink(name)), name),
606 align="center")
607 data += " </tr>\n"
609 # all further rows involve timestamps, commit events, and build events
610 data += " <tr>\n"
611 data += td("04:00", align="bottom")
612 data += td("fred", align="center")
613 for name in names:
614 data += td("stuff", align="center", bgcolor="red")
615 data += " </tr>\n"
617 data += "</table>\n"
618 return data
620 def buildGrid(self, request, builders):
621 debug = False
622 # TODO: see if we can use a cached copy
624 showEvents = False
625 if request.args.get("show_events", ["true"])[0].lower() == "true":
626 showEvents = True
627 filterBranches = [b for b in request.args.get("branch", []) if b]
628 maxTime = int(request.args.get("last_time", [util.now()])[0])
629 if "show_time" in request.args:
630 minTime = maxTime - int(request.args["show_time"][0])
631 elif "first_time" in request.args:
632 minTime = int(request.args["first_time"][0])
633 else:
634 minTime = None
635 spanLength = 10 # ten-second chunks
636 maxPageLen = int(request.args.get("num_events", [200])[0])
638 # first step is to walk backwards in time, asking each column
639 # (commit, all builders) if they have any events there. Build up the
640 # array of events, and stop when we have a reasonable number.
642 commit_source = self.getChangemaster(request)
644 lastEventTime = util.now()
645 sources = [commit_source] + builders
646 changeNames = ["changes"]
647 builderNames = map(lambda builder: builder.getName(), builders)
648 sourceNames = changeNames + builderNames
649 sourceEvents = []
650 sourceGenerators = []
652 def get_event_from(g):
653 try:
654 while True:
655 e = g.next()
656 if not showEvents and isinstance(e, builder.Event):
657 continue
658 break
659 event = interfaces.IStatusEvent(e)
660 if debug:
661 log.msg("gen %s gave1 %s" % (g, event.getText()))
662 except StopIteration:
663 event = None
664 return event
666 for s in sources:
667 gen = insertGaps(s.eventGenerator(filterBranches), lastEventTime)
668 sourceGenerators.append(gen)
669 # get the first event
670 sourceEvents.append(get_event_from(gen))
671 eventGrid = []
672 timestamps = []
674 lastEventTime = 0
675 for e in sourceEvents:
676 if e and e.getTimes()[0] > lastEventTime:
677 lastEventTime = e.getTimes()[0]
678 if lastEventTime == 0:
679 lastEventTime = util.now()
681 spanStart = lastEventTime - spanLength
682 debugGather = 0
684 while 1:
685 if debugGather: log.msg("checking (%s,]" % spanStart)
686 # the tableau of potential events is in sourceEvents[]. The
687 # window crawls backwards, and we examine one source at a time.
688 # If the source's top-most event is in the window, is it pushed
689 # onto the events[] array and the tableau is refilled. This
690 # continues until the tableau event is not in the window (or is
691 # missing).
693 spanEvents = [] # for all sources, in this span. row of eventGrid
694 firstTimestamp = None # timestamp of first event in the span
695 lastTimestamp = None # last pre-span event, for next span
697 for c in range(len(sourceGenerators)):
698 events = [] # for this source, in this span. cell of eventGrid
699 event = sourceEvents[c]
700 while event and spanStart < event.getTimes()[0]:
701 # to look at windows that don't end with the present,
702 # condition the .append on event.time <= spanFinish
703 if not IBox(event, None):
704 log.msg("BAD EVENT", event, event.getText())
705 assert 0
706 if debug:
707 log.msg("pushing", event.getText(), event)
708 events.append(event)
709 starts, finishes = event.getTimes()
710 firstTimestamp = util.earlier(firstTimestamp, starts)
711 event = get_event_from(sourceGenerators[c])
712 if debug:
713 log.msg("finished span")
715 if event:
716 # this is the last pre-span event for this source
717 lastTimestamp = util.later(lastTimestamp,
718 event.getTimes()[0])
719 if debugGather:
720 log.msg(" got %s from %s" % (events, sourceNames[c]))
721 sourceEvents[c] = event # refill the tableau
722 spanEvents.append(events)
724 # only show events older than maxTime. This makes it possible to
725 # visit a page that shows what it would be like to scroll off the
726 # bottom of this one.
727 if firstTimestamp is not None and firstTimestamp <= maxTime:
728 eventGrid.append(spanEvents)
729 timestamps.append(firstTimestamp)
731 if lastTimestamp:
732 spanStart = lastTimestamp - spanLength
733 else:
734 # no more events
735 break
736 if minTime is not None and lastTimestamp < minTime:
737 break
739 if len(timestamps) > maxPageLen:
740 break
743 # now loop
745 # loop is finished. now we have eventGrid[] and timestamps[]
746 if debugGather: log.msg("finished loop")
747 assert(len(timestamps) == len(eventGrid))
748 return (changeNames, builderNames, timestamps, eventGrid, sourceEvents)
750 def phase0(self, request, sourceNames, timestamps, eventGrid):
751 # phase0 rendering
752 if not timestamps:
753 return "no events"
754 data = ""
755 for r in range(0, len(timestamps)):
756 data += "<p>\n"
757 data += "[%s]<br />" % timestamps[r]
758 row = eventGrid[r]
759 assert(len(row) == len(sourceNames))
760 for c in range(0, len(row)):
761 if row[c]:
762 data += "<b>%s</b><br />\n" % sourceNames[c]
763 for e in row[c]:
764 log.msg("Event", r, c, sourceNames[c], e.getText())
765 lognames = [loog.getName() for loog in e.getLogs()]
766 data += "%s: %s: %s %s<br />" % (e.getText(),
767 e.getTimes()[0],
768 e.getColor(),
769 lognames)
770 else:
771 data += "<b>%s</b> [none]<br />\n" % sourceNames[c]
772 return data
774 def phase1(self, request, sourceNames, timestamps, eventGrid,
775 sourceEvents):
776 # phase1 rendering: table, but boxes do not overlap
777 data = ""
778 if not timestamps:
779 return data
780 lastDate = None
781 for r in range(0, len(timestamps)):
782 chunkstrip = eventGrid[r]
783 # chunkstrip is a horizontal strip of event blocks. Each block
784 # is a vertical list of events, all for the same source.
785 assert(len(chunkstrip) == len(sourceNames))
786 maxRows = reduce(lambda x,y: max(x,y),
787 map(lambda x: len(x), chunkstrip))
788 for i in range(maxRows):
789 data += " <tr>\n";
790 if i == 0:
791 stuff = []
792 # add the date at the beginning, and each time it changes
793 today = time.strftime("<b>%d %b %Y</b>",
794 time.localtime(timestamps[r]))
795 todayday = time.strftime("<b>%a</b>",
796 time.localtime(timestamps[r]))
797 if today != lastDate:
798 stuff.append(todayday)
799 stuff.append(today)
800 lastDate = today
801 stuff.append(
802 time.strftime("%H:%M:%S",
803 time.localtime(timestamps[r])))
804 data += td(stuff, valign="bottom", align="center",
805 rowspan=maxRows, class_="Time")
806 for c in range(0, len(chunkstrip)):
807 block = chunkstrip[c]
808 assert(block != None) # should be [] instead
809 # bottom-justify
810 offset = maxRows - len(block)
811 if i < offset:
812 data += td("")
813 else:
814 e = block[i-offset]
815 box = IBox(e).getBox()
816 box.parms["show_idle"] = 1
817 data += box.td(valign="top", align="center")
818 data += " </tr>\n"
820 return data
822 def phase2(self, request, sourceNames, timestamps, eventGrid,
823 sourceEvents):
824 data = ""
825 if not timestamps:
826 return data
827 # first pass: figure out the height of the chunks, populate grid
828 grid = []
829 for i in range(1+len(sourceNames)):
830 grid.append([])
831 # grid is a list of columns, one for the timestamps, and one per
832 # event source. Each column is exactly the same height. Each element
833 # of the list is a single <td> box.
834 lastDate = time.strftime("<b>%d %b %Y</b>",
835 time.localtime(util.now()))
836 for r in range(0, len(timestamps)):
837 chunkstrip = eventGrid[r]
838 # chunkstrip is a horizontal strip of event blocks. Each block
839 # is a vertical list of events, all for the same source.
840 assert(len(chunkstrip) == len(sourceNames))
841 maxRows = reduce(lambda x,y: max(x,y),
842 map(lambda x: len(x), chunkstrip))
843 for i in range(maxRows):
844 if i != maxRows-1:
845 grid[0].append(None)
846 else:
847 # timestamp goes at the bottom of the chunk
848 stuff = []
849 # add the date at the beginning (if it is not the same as
850 # today's date), and each time it changes
851 todayday = time.strftime("<b>%a</b>",
852 time.localtime(timestamps[r]))
853 today = time.strftime("<b>%d %b %Y</b>",
854 time.localtime(timestamps[r]))
855 if today != lastDate:
856 stuff.append(todayday)
857 stuff.append(today)
858 lastDate = today
859 stuff.append(
860 time.strftime("%H:%M:%S",
861 time.localtime(timestamps[r])))
862 grid[0].append(Box(text=stuff, class_="Time",
863 valign="bottom", align="center"))
865 # at this point the timestamp column has been populated with
866 # maxRows boxes, most None but the last one has the time string
867 for c in range(0, len(chunkstrip)):
868 block = chunkstrip[c]
869 assert(block != None) # should be [] instead
870 for i in range(maxRows - len(block)):
871 # fill top of chunk with blank space
872 grid[c+1].append(None)
873 for i in range(len(block)):
874 # so the events are bottom-justified
875 b = IBox(block[i]).getBox()
876 b.parms['valign'] = "top"
877 b.parms['align'] = "center"
878 grid[c+1].append(b)
879 # now all the other columns have maxRows new boxes too
880 # populate the last row, if empty
881 gridlen = len(grid[0])
882 for i in range(len(grid)):
883 strip = grid[i]
884 assert(len(strip) == gridlen)
885 if strip[-1] == None:
886 if sourceEvents[i-1]:
887 filler = IBox(sourceEvents[i-1]).getBox()
888 else:
889 # this can happen if you delete part of the build history
890 filler = Box(text=["?"], align="center")
891 strip[-1] = filler
892 strip[-1].parms['rowspan'] = 1
893 # second pass: bubble the events upwards to un-occupied locations
894 # Every square of the grid that has a None in it needs to have
895 # something else take its place.
896 noBubble = request.args.get("nobubble",['0'])
897 noBubble = int(noBubble[0])
898 if not noBubble:
899 for col in range(len(grid)):
900 strip = grid[col]
901 if col == 1: # changes are handled differently
902 for i in range(2, len(strip)+1):
903 # only merge empty boxes. Don't bubble commit boxes.
904 if strip[-i] == None:
905 next = strip[-i+1]
906 assert(next)
907 if next:
908 #if not next.event:
909 if next.spacer:
910 # bubble the empty box up
911 strip[-i] = next
912 strip[-i].parms['rowspan'] += 1
913 strip[-i+1] = None
914 else:
915 # we are above a commit box. Leave it
916 # be, and turn the current box into an
917 # empty one
918 strip[-i] = Box([], rowspan=1,
919 comment="commit bubble")
920 strip[-i].spacer = True
921 else:
922 # we are above another empty box, which
923 # somehow wasn't already converted.
924 # Shouldn't happen
925 pass
926 else:
927 for i in range(2, len(strip)+1):
928 # strip[-i] will go from next-to-last back to first
929 if strip[-i] == None:
930 # bubble previous item up
931 assert(strip[-i+1] != None)
932 strip[-i] = strip[-i+1]
933 strip[-i].parms['rowspan'] += 1
934 strip[-i+1] = None
935 else:
936 strip[-i].parms['rowspan'] = 1
937 # third pass: render the HTML table
938 for i in range(gridlen):
939 data += " <tr>\n";
940 for strip in grid:
941 b = strip[i]
942 if b:
943 data += b.td()
944 else:
945 if noBubble:
946 data += td([])
947 # Nones are left empty, rowspan should make it all fit
948 data += " </tr>\n"
949 return data