Skip to content

Commit

Permalink
[FIXED JENKINS-20892] /{view,computer,user}/*/builds & /job/*/buildTi…
Browse files Browse the repository at this point in the history
…meTrend block HTTP response on build record loading.

Merge branch 'lazy-build-lists-JENKINS-20892'
  • Loading branch information
jglick committed Dec 10, 2013
2 parents 0c2b722 + 207d676 commit 67b1633
Show file tree
Hide file tree
Showing 12 changed files with 273 additions and 72 deletions.
4 changes: 3 additions & 1 deletion core/src/main/java/hudson/Functions.java
Expand Up @@ -150,6 +150,7 @@

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import hudson.util.RunList;
import java.util.concurrent.atomic.AtomicLong;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
Expand Down Expand Up @@ -1431,7 +1432,8 @@ public static String runScript(Script script) throws JellyTagException {
}

/**
* Returns a sub-list if the given list is bigger than the specified 'maxSize'
* Returns a sub-list if the given list is bigger than the specified {@code maxSize}.
* <strong>Warning:</strong> do not call this with a {@link RunList}, or you will break lazy loading!
*/
public static <T> List<T> subList(List<T> base, int maxSize) {
if(maxSize<base.size())
Expand Down
4 changes: 3 additions & 1 deletion core/src/main/java/hudson/model/BuildTimelineWidget.java
Expand Up @@ -45,13 +45,15 @@ public class BuildTimelineWidget {
protected final RunList<?> builds;

public BuildTimelineWidget(RunList<?> builds) {
this.builds = builds;
this.builds = builds.limit(20); // TODO instead render lazily
}

@Deprecated
public Run<?, ?> getFirstBuild() {
return builds.getFirstBuild();
}

@Deprecated
public Run<?, ?> getLastBuild() {
return builds.getLastBuild();
}
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/hudson/model/Job.java
Expand Up @@ -1269,7 +1269,7 @@ public String toString() {
}

DataSetBuilder<String, ChartLabel> data = new DataSetBuilder<String, ChartLabel>();
for (Run r : getBuilds()) {
for (Run r : getNewBuilds()) {
if (r.isBuilding())
continue;
data.add(((double) r.getDuration()) / (1000 * 60), "min",
Expand Down
14 changes: 5 additions & 9 deletions core/src/main/java/hudson/model/User.java
Expand Up @@ -24,6 +24,7 @@
*/
package hudson.model;

import com.google.common.base.Predicate;
import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
import hudson.*;
import hudson.model.Descriptor.FormException;
Expand Down Expand Up @@ -451,19 +452,14 @@ private boolean relatedTo(AbstractBuild<?,?> b) {
/**
* Gets the list of {@link Build}s that include changes by this user,
* by the timestamp order.
*
* TODO: do we need some index for this?
*/
@WithBridgeMethods(List.class)
public RunList getBuilds() {
List<AbstractBuild> r = new ArrayList<AbstractBuild>();
for (AbstractProject<?,?> p : Jenkins.getInstance().getAllItems(AbstractProject.class))
for (AbstractBuild<?,?> b : p.getBuilds().newBuilds()){
if (relatedTo(b)) {
r.add(b);
}
return new RunList<Run<?,?>>(Jenkins.getInstance().getAllItems(Job.class)).filter(new Predicate<Run<?,?>>() {
@Override public boolean apply(Run<?,?> r) {
return r instanceof AbstractBuild && relatedTo((AbstractBuild<?,?>) r);
}
return RunList.fromRuns(r);
});
}

/**
Expand Down
5 changes: 3 additions & 2 deletions core/src/main/java/hudson/util/RunList.java
Expand Up @@ -186,9 +186,10 @@ RunList<R> fromRuns(Collection<? extends R> runs) {

/**
* Returns elements that satisfy the given predicate.
* <em>Warning:</em> this method mutates the original list and then returns it.
* @since TODO
*/
// for compatibility reasons, this method doesn't create a new list but updates the current one
private RunList<R> filter(Predicate<R> predicate) {
public RunList<R> filter(Predicate<R> predicate) {
size = null;
first = null;
base = Iterables.filter(base,predicate);
Expand Down
53 changes: 53 additions & 0 deletions core/src/main/java/jenkins/widgets/BuildListTable.java
@@ -0,0 +1,53 @@
/*
* The MIT License
*
* Copyright 2013 Jesse Glick.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package jenkins.widgets;

import hudson.Functions;
import hudson.model.BallColor;
import hudson.model.Run;
import net.sf.json.JSONObject;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;

@Restricted(DoNotUse.class) // only for buildListTable.jelly
public class BuildListTable extends RunListProgressiveRendering {

@Override protected void calculate(Run<?,?> build, JSONObject element) {
BallColor iconColor = build.getIconColor();
element.put("iconColorOrdinal", iconColor.ordinal());
element.put("iconColorDescription", iconColor.getDescription());
element.put("url", build.getUrl());
element.put("buildStatusUrl", build.getBuildStatusUrl());
element.put("parentUrl", build.getParent().getUrl());
element.put("parentFullDisplayName", Functions.breakableString(Functions.escape(build.getParent().getFullDisplayName())));
element.put("displayName", build.getDisplayName());
element.put("timestampString", build.getTimestampString());
element.put("timestampString2", build.getTimestampString2());
Run.Summary buildStatusSummary = build.getBuildStatusSummary();
element.put("buildStatusSummaryWorse", buildStatusSummary.isWorse);
element.put("buildStatusSummaryMessage", buildStatusSummary.message);
}

}
65 changes: 65 additions & 0 deletions core/src/main/java/jenkins/widgets/BuildTimeTrend.java
@@ -0,0 +1,65 @@
/*
* The MIT License
*
* Copyright 2013 Jesse Glick.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package jenkins.widgets;

import hudson.model.AbstractBuild;
import hudson.model.BallColor;
import hudson.model.Node;
import hudson.model.Run;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;

@Restricted(DoNotUse.class) // only for buildTimeTrend.jelly
public class BuildTimeTrend extends RunListProgressiveRendering {

@Override protected void calculate(Run<?,?> build, JSONObject element) {
BallColor iconColor = build.getIconColor();
element.put("iconColorOrdinal", iconColor.ordinal());
element.put("iconColorDescription", iconColor.getDescription());
element.put("buildStatusUrl", build.getBuildStatusUrl());
element.put("number", build.getNumber());
element.put("displayName", build.getDisplayName());
element.put("duration", build.getDuration());
element.put("durationString", build.getDurationString());
if (build instanceof AbstractBuild) {
AbstractBuild<?,?> b = (AbstractBuild) build;
Node n = b.getBuiltOn();
if (n == null) {
String ns = b.getBuiltOnStr();
if (ns != null && !ns.isEmpty()) {
element.put("builtOnStr", ns);
}
} else if (n != Jenkins.getInstance()) {
element.put("builtOn", n.getNodeName());
element.put("builtOnStr", n.getDisplayName());
} else {
element.put("builtOnStr", hudson.model.Messages.Hudson_Computer_DisplayName());
}
}
}

}
@@ -0,0 +1,81 @@
/*
* The MIT License
*
* Copyright 2013 Jesse Glick.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package jenkins.widgets;

import hudson.model.Run;
import hudson.util.RunList;
import java.util.ArrayList;
import java.util.List;
import jenkins.util.ProgressiveRendering;
import net.sf.json.JSON;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

/**
* Makes it possible to incrementally render some information from a {@link RunList}.
*/
@Restricted(NoExternalUse.class)
public abstract class RunListProgressiveRendering extends ProgressiveRendering {

/**
* Since we cannot predict how many runs there will be, just show an ever-growing progress bar.
* The first increment will be sized as if this many runs will be in the total,
* but then like Zeno’s paradox we will never seem to finish until we actually do.
*/
private static final double MAX_LIKELY_RUNS = 20;
private final List<JSONObject> results = new ArrayList<JSONObject>();
private Iterable<? extends Run<?,?>> builds;

/** Jelly cannot call a constructor with arguments. */
public void setBuilds(Iterable<? extends Run<?,?>> builds) {
this.builds = builds;
}

@Override protected void compute() throws Exception {
double decay = 1;
for (Run<?,?> build : builds) {
if (canceled()) {
return;
}
JSONObject element = new JSONObject();
calculate(build, element);
synchronized (results) {
results.add(element);
}
decay *= (1 - 1 / MAX_LIKELY_RUNS);
progress(1 - decay);
}
}

@Override protected synchronized JSON data() {
JSONArray d = JSONArray.fromObject(results);
results.clear();
return d;
}

protected abstract void calculate(Run<?,?> build, JSONObject element);
}
Expand Up @@ -77,10 +77,7 @@ THE SOFTWARE.
// theme1.autoWidth = true; // Set the Timeline's "width" automatically.
// Set autoWidth on the Timeline's first band's theme,
// will affect all bands.
theme1.timeline_start = new Date(${it.firstBuild.timeInMillis-24*60*60*1000});
theme1.timeline_stop = new Date(${it.lastBuild.timeInMillis+24*60*60*1000});
var d = new Date(${it.lastBuild.timeInMillis});
var bandInfos = [
// the bar that shows outline
Timeline.createBandInfo({
Expand Down
55 changes: 27 additions & 28 deletions core/src/main/resources/hudson/model/Job/buildTimeTrend.jelly
Expand Up @@ -34,15 +34,39 @@ THE SOFTWARE.
<div style="height:2em"/><!-- spacer -->

<h1>${%Build Time Trend}</h1>
<j:choose>
<j:when test="${it.builds.size()&gt;1}">
<div align="center">
<img src="buildTimeGraph/png" width="500" height="400" lazymap="buildTimeGraph/map" alt="[Build time graph]" style="float:right"/>
</div>

<j:set var="isMasterSlaveEnabled" value="${!empty(app.slaves)}"/>
<div align="center">
<table class="sortable" style="margin-top:1em;">
<script>
function displayBuilds(data) {
var p = $$('trend');
for (var x = 0; data.length > x; x++) {
var e = data[x];
var tr = new Element('tr');
tr.insert(new Element('td', {data: e.iconColorOrdinal}).
insert(new Element('img', {width: 16, height: 16, src: '${imagesURL}/16x16/' + e.buildStatusUrl, alt: e.iconColorDescription})));
tr.insert(new Element('td', {data: e.number}).
insert(new Element('a', {href: e.number + '/', 'class': 'model-link inside'}).
update(e.displayName.escapeHTML())));
tr.insert(new Element('td', {data: e.duration}).
update(e.durationString.escapeHTML()));
<j:if test="${isMasterSlaveEnabled}">
tr.insert(new Element('td').
update(e.builtOn ? new Element('a', {href: '${rootURL}/computer/' + e.builtOn, 'class': 'model-link inside'}).update(e.builtOnStr) : e.builtOnStr));
</j:if>
p.insert(tr);
Behaviour.applySubtree(tr);
}
ts_refresh(p);
}
</script>
<j:new var="handler" className="jenkins.widgets.BuildTimeTrend"/>
${handler.setBuilds(it.builds)}
<l:progressiveRendering handler="${handler}" callback="displayBuilds"/>
<table class="sortable" style="margin-top:1em;" id="trend">
<tr>
<th><st:nbsp/></th>
<th initialSortDir="up">${%Build}</th>
Expand All @@ -51,33 +75,8 @@ THE SOFTWARE.
<th>${%Slave}</th>
</j:if>
</tr>
<j:forEach var="r" items="${it.builds}">
<tr>
<td data="${r.iconColor.ordinal()}">
<img width="16" height="16" src="${imagesURL}/16x16/${r.buildStatusUrl}" alt="${r.iconColor.description}" />
</td>
<td data="${r.number}">
<a href="${r.number}/" class="model-link inside">
${r.displayName}
</a>
</td>
<td data="${r.duration}">
${r.durationString}
</td>
<j:if test="${isMasterSlaveEnabled}">
<td>
<t:node value="${r.builtOn}" valueStr="${r.builtOnStr}"/>
</td>
</j:if>
</tr>
</j:forEach>
</table>
</div>
</j:when>
<j:otherwise>
${%More than 1 builds are needed for the trend report.}
</j:otherwise>
</j:choose>
</l:main-panel>
</l:layout>
</j:jelly>
1 change: 1 addition & 0 deletions core/src/main/resources/hudson/model/User/builds.jelly
Expand Up @@ -30,6 +30,7 @@ THE SOFTWARE.
<img src="${h.getUserAvatar(it,'48x48')}" alt="" height="48" width="48" />
${%title(it)}
</h1>
<!-- TODO consider adding a BuildTimelineWidget (cf. Job, View, Computer) -->
<t:buildListTable builds="${it.builds}" jobBaseUrl="${rootURL}/" />
</l:main-panel>
</l:layout>
Expand Down

0 comments on commit 67b1633

Please sign in to comment.