Skip to content

Instantly share code, notes, and snippets.

@ldaley
Created March 28, 2019 04:10
Show Gist options
  • Save ldaley/c8538870b40a01328dc01af899ed2fb2 to your computer and use it in GitHub Desktop.
Save ldaley/c8538870b40a01328dc01af899ed2fb2 to your computer and use it in GitHub Desktop.
Gradle Enterprise Export API - Build configuration time
<!DOCTYPE html>
<html>
<head>
<title>Build configuration time</title>
</head>
<body>
<script>
// The address of your Gradle Enterprise server
const GRADLE_ENTERPRISE_SERVER_URL = 'http://localhost:5050';
// The point in time from which builds should be processed.
// Values can be 'now', or a number of milliseconds since the UNIX epoch.
// The time is the point in time that the build was published to the server.
const PROCESS_FROM = '0';
// How many builds to process at one time.
// If running with very fast network connection to the server,
// this number can be increased for better throughput.
const MAX_CONCURRENT_BUILDS_TO_PROCESS = 6;
// A build event handler that counts how many tasks of the build were cacheable.
class ConfigurationTimeCounter {
constructor(build) {
this.buildId = build.buildId;
this.loadProjectStarted = {};
this.time = {};
this.currentStartedAt = 0;
this.buildPath = null;
}
onLoadProjectsStarted(e) {
this.loadProjectStarted[e.data.id] = e;
}
onLoadProjectsFinished(e) {
const started = this.loadProjectStarted[e.data.id];
if (started.data.buildPath === ':' || started.data.buildPath.indexOf(':buildSrc') >= 0) {
this.buildPath = started.data.buildPath;
this.currentStartedAt = e.timestamp;
}
}
onTaskStarted(e) {
if (!this.time[this.buildPath]) {
this.finish(e);
}
}
onBuildFinished(e) {
if (!this.time[this.buildPath]) {
this.finish(e);
}
}
finish(e) {
this.time[this.buildPath] = (this.time[this.buildPath] || 0) + e.timestamp - this.currentStartedAt;
}
complete() {
document.write(`<p>Build ${this.buildId} ${JSON.stringify(this.time)}</p>`,);
}
}
// The event handlers to use to process builds.
const BUILD_EVENT_HANDLERS = [ConfigurationTimeCounter];
// Code below is a generic utility for interacting with the Export API.
class BuildProcessor {
constructor(gradleEnterpriseServerUrl, maxConcurrentBuildsToProcess, eventHandlerClasses) {
this.gradleEnterpriseServerUrl = gradleEnterpriseServerUrl;
this.eventHandlerClasses = eventHandlerClasses;
this.allHandledEventTypes = this.getAllHandledEventTypes();
this.pendingBuilds = [];
this.numBuildsInProcess = 0;
this.maxConcurrentBuildsToProcess = maxConcurrentBuildsToProcess;
}
start(startTime) {
const buildStreamUrl = this.createBuildStreamUrl(startTime);
const buildStream = new EventSource(buildStreamUrl);
buildStream.onopen = event => console.log(`Build stream '${buildStreamUrl}' open`);
buildStream.onerror = event => console.error('Build stream error', event);
buildStream.addEventListener('Build', event => {
this.enqueue(JSON.parse(event.data));
});
}
enqueue(build) {
this.pendingBuilds.push(build);
this.processPendingBuilds();
}
processPendingBuilds() {
if (this.pendingBuilds.length > 0 && this.numBuildsInProcess < this.maxConcurrentBuildsToProcess) {
this.processBuild(this.pendingBuilds.shift());
}
}
createBuildStreamUrl(startTime) {
return `${this.gradleEnterpriseServerUrl}/build-export/v1/builds/since/${startTime}?stream`;
}
// Inspect the methods on the handler class to find any event handlers that start with 'on' followed by the event type like 'onBuildStarted'.
// Then take the part of the method name after the 'on' to get the event type.
getHandledEventTypesForHandlerClass(handlerClass) {
return Object.getOwnPropertyNames(handlerClass.prototype)
.filter(methodName => methodName.startsWith('on'))
.map(methodName => methodName.substring(2));
}
getAllHandledEventTypes() {
return new Set(this.eventHandlerClasses.reduce((eventTypes, handlerClass) => eventTypes.concat(this.getHandledEventTypesForHandlerClass(handlerClass)), []));
}
createBuildEventStreamUrl(buildId) {
const types = [...this.allHandledEventTypes].join(',');
return `${this.gradleEnterpriseServerUrl}/build-export/v1/build/${buildId}/events?eventTypes=${types}`;
}
// Creates a map of event type -> handler instance for each event type supported by one or more handlers.
createBuildEventHandlers(build) {
return this.eventHandlerClasses.reduce((handlers, handlerClass) => {
const addHandler = (type, handler) => handlers[type] ? handlers[type].push(handler) : handlers[type] = [handler];
const handler = new handlerClass.prototype.constructor(build);
this.getHandledEventTypesForHandlerClass(handlerClass).forEach(eventType => addHandler(eventType, handler));
if (Object.getOwnPropertyNames(handlerClass.prototype).includes('complete')) {
addHandler('complete', handler);
}
return handlers;
}, {});
}
processBuild(build) {
this.numBuildsInProcess++;
const buildEventHandlers = this.createBuildEventHandlers(build);
const buildEventStream = new EventSource(this.createBuildEventStreamUrl(build.buildId));
let buildEventStreamOpen = true;
buildEventStream.addEventListener('BuildEvent', event => {
const buildEventPayload = JSON.parse(event.data);
const { eventType } = buildEventPayload.type;
if (this.allHandledEventTypes.has(eventType)) {
buildEventHandlers[eventType].forEach(handler => handler[`on${eventType}`](buildEventPayload));
}
});
// there isn't an onclose event that we can listen to, but an error event does get triggered
// when the end of the stream is reached so use that as a rough proxy for the end of the stream
buildEventStream.onerror = event => {
if (buildEventStreamOpen) {
this.finishedProcessingBuild();
// Call the 'complete()' method on any handler that has it.
if (buildEventHandlers.complete) {
buildEventHandlers.complete.forEach(handler => handler.complete());
}
buildEventStream.close();
buildEventStreamOpen = false;
}
}
}
finishedProcessingBuild() {
this.numBuildsInProcess--;
setTimeout(() => this.processPendingBuilds(), 0); // process the next set of pending builds, if any
}
}
new BuildProcessor(
GRADLE_ENTERPRISE_SERVER_URL,
MAX_CONCURRENT_BUILDS_TO_PROCESS,
BUILD_EVENT_HANDLERS
).start(PROCESS_FROM);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment