waterfall: make links more visually distinct, handle unused buildslaves
[buildbot.git] / buildbot / status / web / waterfall.py
blob58f683cdfc50caf54100fbda5cece873fbece5a2
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
307 <h2>Reload Waterfall Page</h2>
309 <input type="submit" value="View Waterfall" />
310 </form>
313 class WaterfallHelp(HtmlResource):
314 title = "Waterfall Help"
316 def __init__(self, categories=None):
317 HtmlResource.__init__(self)
318 self.categories = categories
320 def body(self, request):
321 data = ''
322 status = self.getStatus(request)
324 showEvents_checked = 'checked="checked"'
325 if request.args.get("show_events", ["true"])[0].lower() == "true":
326 showEvents_checked = ''
327 show_events_input = ('<p>'
328 '<input type="checkbox" name="show_events" '
329 'value="false" %s>'
330 'Hide non-Build events'
331 '</p>\n'
332 ) % showEvents_checked
334 branches = [b
335 for b in request.args.get("branch", [])
336 if b]
337 branches.append('')
338 show_branches_input = '<table>\n'
339 for b in branches:
340 show_branches_input += ('<tr>'
341 '<td>Show Branch: '
342 '<input type="text" name="branch" '
343 'value="%s">'
344 '</td></tr>\n'
345 ) % (b,)
346 show_branches_input += '</table>\n'
348 # this has a set of toggle-buttons to let the user choose the
349 # builders
350 showBuilders = request.args.get("show", [])
351 showBuilders.extend(request.args.get("builder", []))
352 allBuilders = status.getBuilderNames(categories=self.categories)
354 show_builders_input = '<table>\n'
355 for bn in allBuilders:
356 checked = ""
357 if bn in showBuilders:
358 checked = 'checked="checked"'
359 show_builders_input += ('<tr>'
360 '<td><input type="checkbox"'
361 ' name="builder" '
362 'value="%s" %s></td> '
363 '<td>%s</td></tr>\n'
364 ) % (bn, checked, bn)
365 show_builders_input += '</table>\n'
367 # a couple of radio-button selectors for refresh time will appear
368 # just after that text
369 show_reload_input = '<table>\n'
370 times = [("none", "None"),
371 ("60", "60 seconds"),
372 ("300", "5 minutes"),
373 ("600", "10 minutes"),
375 current_reload_time = request.args.get("reload", ["none"])
376 if current_reload_time:
377 current_reload_time = current_reload_time[0]
378 if current_reload_time not in [t[0] for t in times]:
379 times.insert(0, (current_reload_time, current_reload_time) )
380 for value, name in times:
381 checked = ""
382 if value == current_reload_time:
383 checked = 'checked="checked"'
384 show_reload_input += ('<tr>'
385 '<td><input type="radio" name="reload" '
386 'value="%s" %s></td> '
387 '<td>%s</td></tr>\n'
388 ) % (value, checked, name)
389 show_reload_input += '</table>\n'
391 fields = {"show_events_input": show_events_input,
392 "show_branches_input": show_branches_input,
393 "show_builders_input": show_builders_input,
394 "show_reload_input": show_reload_input,
396 data += HELP % fields
397 return data
399 class WaterfallStatusResource(HtmlResource):
400 """This builds the main status page, with the waterfall display, and
401 all child pages."""
403 def __init__(self, categories=None):
404 HtmlResource.__init__(self)
405 self.categories = categories
406 self.putChild("help", WaterfallHelp(categories))
408 def getTitle(self, request):
409 status = self.getStatus(request)
410 p = status.getProjectName()
411 if p:
412 return "BuildBot: %s" % p
413 else:
414 return "BuildBot"
416 def getChangemaster(self, request):
417 # TODO: this wants to go away, access it through IStatus
418 return request.site.buildbot_service.parent.change_svc
420 def get_reload_time(self, request):
421 if "reload" in request.args:
422 try:
423 reload_time = int(request.args["reload"][0])
424 if reload_time > 15:
425 return reload_time
426 except ValueError:
427 pass
428 return None
430 def head(self, request):
431 head = ''
432 reload_time = self.get_reload_time(request)
433 if reload_time is not None:
434 head += '<meta http-equiv="refresh" content="%d">\n' % reload_time
435 return head
437 def body(self, request):
438 "This method builds the main waterfall display."
440 status = self.getStatus(request)
441 data = ''
443 projectName = status.getProjectName()
444 projectURL = status.getProjectURL()
446 phase = request.args.get("phase",["2"])
447 phase = int(phase[0])
449 showBuilders = request.args.get("show", [])
450 showBuilders.extend(request.args.get("builder", []))
451 allBuilders = status.getBuilderNames(categories=self.categories)
452 if showBuilders:
453 builderNames = []
454 for b in showBuilders:
455 if b not in allBuilders:
456 continue
457 if b in builderNames:
458 continue
459 builderNames.append(b)
460 else:
461 builderNames = allBuilders
462 builders = map(lambda name: status.getBuilder(name),
463 builderNames)
465 if phase == -1:
466 return self.body0(request, builders)
467 (changeNames, builderNames, timestamps, eventGrid, sourceEvents) = \
468 self.buildGrid(request, builders)
469 if phase == 0:
470 return self.phase0(request, (changeNames + builderNames),
471 timestamps, eventGrid)
472 # start the table: top-header material
473 data += '<table border="0" cellspacing="0">\n'
475 if projectName and projectURL:
476 # TODO: this is going to look really ugly
477 topleft = "<a href=\"%s\">%s</a><br />last build" % \
478 (projectURL, projectName)
479 else:
480 topleft = "last build"
481 data += ' <tr class="LastBuild">\n'
482 data += td(topleft, align="right", colspan=2, class_="Project")
483 for b in builders:
484 box = ITopBox(b).getBox()
485 data += box.td(align="center")
486 data += " </tr>\n"
488 data += ' <tr class="Activity">\n'
489 data += td('current activity', align='right', colspan=2)
490 for b in builders:
491 box = ICurrentBox(b).getBox(status)
492 data += box.td(align="center")
493 data += " </tr>\n"
495 data += " <tr>\n"
496 TZ = time.tzname[time.daylight]
497 data += td("time (%s)" % TZ, align="center", class_="Time")
498 name = changeNames[0]
499 data += td(
500 "<a href=\"%s\">%s</a>" % (urllib.quote(name, safe=''), name),
501 align="center", class_="Change")
502 for name in builderNames:
503 safename = urllib.quote(name, safe='')
504 data += td( "<a href=\"builders/%s\">%s</a>" % (safename, name),
505 align="center", class_="Builder")
506 data += " </tr>\n"
508 if phase == 1:
509 f = self.phase1
510 else:
511 f = self.phase2
512 data += f(request, changeNames + builderNames, timestamps, eventGrid,
513 sourceEvents)
515 data += "</table>\n"
517 data += "<hr />\n"
519 def with_args(req, remove_args=[], new_args=[], new_path=None):
520 # sigh, nevow makes this sort of manipulation easier
521 newargs = req.args.copy()
522 for argname in remove_args:
523 newargs[argname] = []
524 if "branch" in newargs:
525 newargs["branch"] = [b for b in newargs["branch"] if b]
526 for k,v in new_args:
527 if k in newargs:
528 newargs[k].append(v)
529 else:
530 newargs[k] = [v]
531 newquery = "&".join(["%s=%s" % (k, v)
532 for k in newargs
533 for v in newargs[k]
535 if new_path:
536 new_url = new_path
537 elif req.prepath:
538 new_url = req.prepath[-1]
539 else:
540 new_url = ''
541 if newquery:
542 new_url += "?" + newquery
543 return new_url
545 if timestamps:
546 bottom = timestamps[-1]
547 nextpage = with_args(request, ["last_time"],
548 [("last_time", str(int(bottom)))])
549 data += '[<a href="%s">next page</a>]\n' % nextpage
551 helpurl = self.path_to_root(request) + "waterfall/help"
552 helppage = with_args(request, new_path=helpurl)
553 data += '[<a href="%s">help</a>]\n' % helppage
555 welcomeurl = self.path_to_root(request) + "."
556 data += '[<a href="%s">welcome</a>]\n' % welcomeurl
558 if self.get_reload_time(request) is not None:
559 no_reload_page = with_args(request, remove_args=["reload"])
560 data += '[<a href="%s">Stop Reloading</a>]\n' % no_reload_page
562 data += "<br />\n"
565 bburl = "http://buildbot.net/?bb-ver=%s" % urllib.quote(version)
566 data += "<a href=\"%s\">Buildbot-%s</a> " % (bburl, version)
567 if projectName:
568 data += "working for the "
569 if projectURL:
570 data += "<a href=\"%s\">%s</a> project." % (projectURL,
571 projectName)
572 else:
573 data += "%s project." % projectName
574 data += "<br />\n"
575 # TODO: push this to the right edge, if possible
576 data += ("Page built: " +
577 time.strftime("%a %d %b %Y %H:%M:%S",
578 time.localtime(util.now()))
579 + "\n")
580 return data
582 def body0(self, request, builders):
583 # build the waterfall display
584 data = ""
585 data += "<h2>Basic display</h2>\n"
586 data += "<p>See <a href=\"%s\">here</a>" % \
587 urllib.quote(request.childLink("waterfall"))
588 data += " for the waterfall display</p>\n"
590 data += '<table border="0" cellspacing="0">\n'
591 names = map(lambda builder: builder.name, builders)
593 # the top row is two blank spaces, then the top-level status boxes
594 data += " <tr>\n"
595 data += td("", colspan=2)
596 for b in builders:
597 text = ""
598 color = "#ca88f7"
599 state, builds = b.getState()
600 if state != "offline":
601 text += "%s<br />\n" % state #b.getCurrentBig().text[0]
602 else:
603 text += "OFFLINE<br />\n"
604 color = "#ffe0e0"
605 data += td(text, align="center", bgcolor=color)
607 # the next row has the column headers: time, changes, builder names
608 data += " <tr>\n"
609 data += td("Time", align="center")
610 data += td("Changes", align="center")
611 for name in names:
612 data += td(
613 "<a href=\"%s\">%s</a>" % (urllib.quote(request.childLink(name)), name),
614 align="center")
615 data += " </tr>\n"
617 # all further rows involve timestamps, commit events, and build events
618 data += " <tr>\n"
619 data += td("04:00", align="bottom")
620 data += td("fred", align="center")
621 for name in names:
622 data += td("stuff", align="center", bgcolor="red")
623 data += " </tr>\n"
625 data += "</table>\n"
626 return data
628 def buildGrid(self, request, builders):
629 debug = False
630 # TODO: see if we can use a cached copy
632 showEvents = False
633 if request.args.get("show_events", ["true"])[0].lower() == "true":
634 showEvents = True
635 filterBranches = [b for b in request.args.get("branch", []) if b]
636 maxTime = int(request.args.get("last_time", [util.now()])[0])
637 if "show_time" in request.args:
638 minTime = maxTime - int(request.args["show_time"][0])
639 elif "first_time" in request.args:
640 minTime = int(request.args["first_time"][0])
641 else:
642 minTime = None
643 spanLength = 10 # ten-second chunks
644 maxPageLen = int(request.args.get("num_events", [200])[0])
646 # first step is to walk backwards in time, asking each column
647 # (commit, all builders) if they have any events there. Build up the
648 # array of events, and stop when we have a reasonable number.
650 commit_source = self.getChangemaster(request)
652 lastEventTime = util.now()
653 sources = [commit_source] + builders
654 changeNames = ["changes"]
655 builderNames = map(lambda builder: builder.getName(), builders)
656 sourceNames = changeNames + builderNames
657 sourceEvents = []
658 sourceGenerators = []
660 def get_event_from(g):
661 try:
662 while True:
663 e = g.next()
664 if not showEvents and isinstance(e, builder.Event):
665 continue
666 break
667 event = interfaces.IStatusEvent(e)
668 if debug:
669 log.msg("gen %s gave1 %s" % (g, event.getText()))
670 except StopIteration:
671 event = None
672 return event
674 for s in sources:
675 gen = insertGaps(s.eventGenerator(filterBranches), lastEventTime)
676 sourceGenerators.append(gen)
677 # get the first event
678 sourceEvents.append(get_event_from(gen))
679 eventGrid = []
680 timestamps = []
682 lastEventTime = 0
683 for e in sourceEvents:
684 if e and e.getTimes()[0] > lastEventTime:
685 lastEventTime = e.getTimes()[0]
686 if lastEventTime == 0:
687 lastEventTime = util.now()
689 spanStart = lastEventTime - spanLength
690 debugGather = 0
692 while 1:
693 if debugGather: log.msg("checking (%s,]" % spanStart)
694 # the tableau of potential events is in sourceEvents[]. The
695 # window crawls backwards, and we examine one source at a time.
696 # If the source's top-most event is in the window, is it pushed
697 # onto the events[] array and the tableau is refilled. This
698 # continues until the tableau event is not in the window (or is
699 # missing).
701 spanEvents = [] # for all sources, in this span. row of eventGrid
702 firstTimestamp = None # timestamp of first event in the span
703 lastTimestamp = None # last pre-span event, for next span
705 for c in range(len(sourceGenerators)):
706 events = [] # for this source, in this span. cell of eventGrid
707 event = sourceEvents[c]
708 while event and spanStart < event.getTimes()[0]:
709 # to look at windows that don't end with the present,
710 # condition the .append on event.time <= spanFinish
711 if not IBox(event, None):
712 log.msg("BAD EVENT", event, event.getText())
713 assert 0
714 if debug:
715 log.msg("pushing", event.getText(), event)
716 events.append(event)
717 starts, finishes = event.getTimes()
718 firstTimestamp = util.earlier(firstTimestamp, starts)
719 event = get_event_from(sourceGenerators[c])
720 if debug:
721 log.msg("finished span")
723 if event:
724 # this is the last pre-span event for this source
725 lastTimestamp = util.later(lastTimestamp,
726 event.getTimes()[0])
727 if debugGather:
728 log.msg(" got %s from %s" % (events, sourceNames[c]))
729 sourceEvents[c] = event # refill the tableau
730 spanEvents.append(events)
732 # only show events older than maxTime. This makes it possible to
733 # visit a page that shows what it would be like to scroll off the
734 # bottom of this one.
735 if firstTimestamp is not None and firstTimestamp <= maxTime:
736 eventGrid.append(spanEvents)
737 timestamps.append(firstTimestamp)
739 if lastTimestamp:
740 spanStart = lastTimestamp - spanLength
741 else:
742 # no more events
743 break
744 if minTime is not None and lastTimestamp < minTime:
745 break
747 if len(timestamps) > maxPageLen:
748 break
751 # now loop
753 # loop is finished. now we have eventGrid[] and timestamps[]
754 if debugGather: log.msg("finished loop")
755 assert(len(timestamps) == len(eventGrid))
756 return (changeNames, builderNames, timestamps, eventGrid, sourceEvents)
758 def phase0(self, request, sourceNames, timestamps, eventGrid):
759 # phase0 rendering
760 if not timestamps:
761 return "no events"
762 data = ""
763 for r in range(0, len(timestamps)):
764 data += "<p>\n"
765 data += "[%s]<br />" % timestamps[r]
766 row = eventGrid[r]
767 assert(len(row) == len(sourceNames))
768 for c in range(0, len(row)):
769 if row[c]:
770 data += "<b>%s</b><br />\n" % sourceNames[c]
771 for e in row[c]:
772 log.msg("Event", r, c, sourceNames[c], e.getText())
773 lognames = [loog.getName() for loog in e.getLogs()]
774 data += "%s: %s: %s %s<br />" % (e.getText(),
775 e.getTimes()[0],
776 e.getColor(),
777 lognames)
778 else:
779 data += "<b>%s</b> [none]<br />\n" % sourceNames[c]
780 return data
782 def phase1(self, request, sourceNames, timestamps, eventGrid,
783 sourceEvents):
784 # phase1 rendering: table, but boxes do not overlap
785 data = ""
786 if not timestamps:
787 return data
788 lastDate = None
789 for r in range(0, len(timestamps)):
790 chunkstrip = eventGrid[r]
791 # chunkstrip is a horizontal strip of event blocks. Each block
792 # is a vertical list of events, all for the same source.
793 assert(len(chunkstrip) == len(sourceNames))
794 maxRows = reduce(lambda x,y: max(x,y),
795 map(lambda x: len(x), chunkstrip))
796 for i in range(maxRows):
797 data += " <tr>\n";
798 if i == 0:
799 stuff = []
800 # add the date at the beginning, and each time it changes
801 today = time.strftime("<b>%d %b %Y</b>",
802 time.localtime(timestamps[r]))
803 todayday = time.strftime("<b>%a</b>",
804 time.localtime(timestamps[r]))
805 if today != lastDate:
806 stuff.append(todayday)
807 stuff.append(today)
808 lastDate = today
809 stuff.append(
810 time.strftime("%H:%M:%S",
811 time.localtime(timestamps[r])))
812 data += td(stuff, valign="bottom", align="center",
813 rowspan=maxRows, class_="Time")
814 for c in range(0, len(chunkstrip)):
815 block = chunkstrip[c]
816 assert(block != None) # should be [] instead
817 # bottom-justify
818 offset = maxRows - len(block)
819 if i < offset:
820 data += td("")
821 else:
822 e = block[i-offset]
823 box = IBox(e).getBox()
824 box.parms["show_idle"] = 1
825 data += box.td(valign="top", align="center")
826 data += " </tr>\n"
828 return data
830 def phase2(self, request, sourceNames, timestamps, eventGrid,
831 sourceEvents):
832 data = ""
833 if not timestamps:
834 return data
835 # first pass: figure out the height of the chunks, populate grid
836 grid = []
837 for i in range(1+len(sourceNames)):
838 grid.append([])
839 # grid is a list of columns, one for the timestamps, and one per
840 # event source. Each column is exactly the same height. Each element
841 # of the list is a single <td> box.
842 lastDate = time.strftime("<b>%d %b %Y</b>",
843 time.localtime(util.now()))
844 for r in range(0, len(timestamps)):
845 chunkstrip = eventGrid[r]
846 # chunkstrip is a horizontal strip of event blocks. Each block
847 # is a vertical list of events, all for the same source.
848 assert(len(chunkstrip) == len(sourceNames))
849 maxRows = reduce(lambda x,y: max(x,y),
850 map(lambda x: len(x), chunkstrip))
851 for i in range(maxRows):
852 if i != maxRows-1:
853 grid[0].append(None)
854 else:
855 # timestamp goes at the bottom of the chunk
856 stuff = []
857 # add the date at the beginning (if it is not the same as
858 # today's date), and each time it changes
859 todayday = time.strftime("<b>%a</b>",
860 time.localtime(timestamps[r]))
861 today = time.strftime("<b>%d %b %Y</b>",
862 time.localtime(timestamps[r]))
863 if today != lastDate:
864 stuff.append(todayday)
865 stuff.append(today)
866 lastDate = today
867 stuff.append(
868 time.strftime("%H:%M:%S",
869 time.localtime(timestamps[r])))
870 grid[0].append(Box(text=stuff, class_="Time",
871 valign="bottom", align="center"))
873 # at this point the timestamp column has been populated with
874 # maxRows boxes, most None but the last one has the time string
875 for c in range(0, len(chunkstrip)):
876 block = chunkstrip[c]
877 assert(block != None) # should be [] instead
878 for i in range(maxRows - len(block)):
879 # fill top of chunk with blank space
880 grid[c+1].append(None)
881 for i in range(len(block)):
882 # so the events are bottom-justified
883 b = IBox(block[i]).getBox()
884 b.parms['valign'] = "top"
885 b.parms['align'] = "center"
886 grid[c+1].append(b)
887 # now all the other columns have maxRows new boxes too
888 # populate the last row, if empty
889 gridlen = len(grid[0])
890 for i in range(len(grid)):
891 strip = grid[i]
892 assert(len(strip) == gridlen)
893 if strip[-1] == None:
894 if sourceEvents[i-1]:
895 filler = IBox(sourceEvents[i-1]).getBox()
896 else:
897 # this can happen if you delete part of the build history
898 filler = Box(text=["?"], align="center")
899 strip[-1] = filler
900 strip[-1].parms['rowspan'] = 1
901 # second pass: bubble the events upwards to un-occupied locations
902 # Every square of the grid that has a None in it needs to have
903 # something else take its place.
904 noBubble = request.args.get("nobubble",['0'])
905 noBubble = int(noBubble[0])
906 if not noBubble:
907 for col in range(len(grid)):
908 strip = grid[col]
909 if col == 1: # changes are handled differently
910 for i in range(2, len(strip)+1):
911 # only merge empty boxes. Don't bubble commit boxes.
912 if strip[-i] == None:
913 next = strip[-i+1]
914 assert(next)
915 if next:
916 #if not next.event:
917 if next.spacer:
918 # bubble the empty box up
919 strip[-i] = next
920 strip[-i].parms['rowspan'] += 1
921 strip[-i+1] = None
922 else:
923 # we are above a commit box. Leave it
924 # be, and turn the current box into an
925 # empty one
926 strip[-i] = Box([], rowspan=1,
927 comment="commit bubble")
928 strip[-i].spacer = True
929 else:
930 # we are above another empty box, which
931 # somehow wasn't already converted.
932 # Shouldn't happen
933 pass
934 else:
935 for i in range(2, len(strip)+1):
936 # strip[-i] will go from next-to-last back to first
937 if strip[-i] == None:
938 # bubble previous item up
939 assert(strip[-i+1] != None)
940 strip[-i] = strip[-i+1]
941 strip[-i].parms['rowspan'] += 1
942 strip[-i+1] = None
943 else:
944 strip[-i].parms['rowspan'] = 1
945 # third pass: render the HTML table
946 for i in range(gridlen):
947 data += " <tr>\n";
948 for strip in grid:
949 b = strip[i]
950 if b:
951 data += b.td()
952 else:
953 if noBubble:
954 data += td([])
955 # Nones are left empty, rowspan should make it all fit
956 data += " </tr>\n"
957 return data