| /* |
| * Copyright 2014 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| * use this file except in compliance with the License. You may obtain a copy of |
| * the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| package com.google.gwt.dev.codeserver; |
| |
| import com.google.gwt.core.ext.TreeLogger; |
| import com.google.gwt.core.ext.TreeLogger.Type; |
| import com.google.gwt.dev.cfg.ModuleDef; |
| import com.google.gwt.dev.codeserver.JobEvent.CompileStrategy; |
| import com.google.gwt.dev.codeserver.JobEvent.Status; |
| import com.google.gwt.dev.util.log.AbstractTreeLogger; |
| import com.google.gwt.thirdparty.guava.common.base.Preconditions; |
| import com.google.gwt.thirdparty.guava.common.collect.ImmutableList; |
| import com.google.gwt.thirdparty.guava.common.collect.ImmutableMap; |
| import com.google.gwt.thirdparty.guava.common.collect.ImmutableSortedMap; |
| import com.google.gwt.thirdparty.guava.common.util.concurrent.Futures; |
| import com.google.gwt.thirdparty.guava.common.util.concurrent.ListenableFuture; |
| import com.google.gwt.thirdparty.guava.common.util.concurrent.SettableFuture; |
| |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.ConcurrentMap; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| /** |
| * A request for Super Dev Mode to compile something. |
| * |
| * <p>Each job has a lifecycle where it goes through up to four states. See |
| * {@link JobEvent.Status}. |
| * |
| * <p>Jobs are thread-safe. |
| */ |
| class Job { |
| private static final ConcurrentMap<String, AtomicInteger> prefixToNextId = |
| new ConcurrentHashMap<String, AtomicInteger>(); |
| |
| // Primary key |
| |
| private final String id; |
| |
| // Input |
| |
| private final String inputModuleName; |
| |
| private final ImmutableSortedMap<String, String> bindingProperties; |
| |
| // Output |
| |
| private final SettableFuture<Result> result = SettableFuture.create(); |
| |
| // Listeners |
| |
| private final Outbox outbox; |
| private final RecompileListener recompileListener; |
| private final JobChangeListener jobChangeListener; |
| private final LogSupplier logSupplier; |
| |
| private JobEventTable table; // non-null when submitted |
| |
| // Miscellaneous |
| |
| private final ImmutableList<String> args; |
| private final Set<String> tags; |
| |
| /** |
| * The id to report to the recompile listener. |
| */ |
| private int compileId = -1; // non-negative after the compile has started |
| |
| private CompileDir compileDir; // non-null after the compile has started |
| private CompileStrategy compileStrategy; // non-null after the compile has started |
| private String outputModuleName; // non-null after successful compile |
| |
| private Exception listenerFailure; |
| |
| /** |
| * Creates a job to update an outbox. |
| * @param bindingProperties Properties that uniquely identify a permutation. |
| * (Otherwise, more than one permutation will be compiled.) |
| * @param parentLogger The parent of the logger that will be used for this job. |
| */ |
| Job(Outbox box, Map<String, String> bindingProperties, |
| TreeLogger parentLogger, Options options) { |
| this.id = chooseNextId(box); |
| this.outbox = box; |
| this.inputModuleName = box.getInputModuleName(); |
| // TODO: we will use the binding properties to find or create the outbox, |
| // then take binding properties from the outbox here. |
| this.bindingProperties = ImmutableSortedMap.copyOf(bindingProperties); |
| this.recompileListener = Preconditions.checkNotNull(options.getRecompileListener()); |
| this.jobChangeListener = Preconditions.checkNotNull(options.getJobChangeListener()); |
| this.args = Preconditions.checkNotNull(options.getArgs()); |
| this.tags = Preconditions.checkNotNull(options.getTags()); |
| this.logSupplier = new LogSupplier(parentLogger, id); |
| } |
| |
| static boolean isValidJobId(String id) { |
| return ModuleDef.isValidModuleName(id); |
| } |
| |
| private static String chooseNextId(Outbox box) { |
| String prefix = box.getId(); |
| prefixToNextId.putIfAbsent(prefix, new AtomicInteger(0)); |
| return prefix + "_" + prefixToNextId.get(prefix).getAndIncrement(); |
| } |
| |
| /** |
| * A string uniquely identifying this job (within this process). |
| * |
| * <p>Note that the number doesn't have any particular relationship |
| * with the output directory's name since jobs can be submitted out of order. |
| */ |
| String getId() { |
| return id; |
| } |
| |
| /** |
| * The module name that will be sent to the compiler. |
| */ |
| String getInputModuleName() { |
| return inputModuleName; |
| } |
| |
| /** |
| * The binding properties to use for this recompile. |
| */ |
| ImmutableSortedMap<String, String> getBindingProperties() { |
| return bindingProperties; |
| } |
| |
| /** |
| * The outbox that will serve the job's result (if successful). |
| */ |
| Outbox getOutbox() { |
| return outbox; |
| } |
| |
| /** |
| * Returns the logger for this job. (Creates it on first use.) |
| */ |
| TreeLogger getLogger() { |
| return logSupplier.get(); |
| } |
| |
| /** |
| * Blocks until we have the result of this recompile. |
| */ |
| Result waitForResult() { |
| return Futures.getUnchecked(getFutureResult()); |
| } |
| |
| /** |
| * Returns a Future that will contain the result of this recompile. |
| */ |
| ListenableFuture<Result> getFutureResult() { |
| return result; |
| } |
| |
| Exception getListenerFailure() { |
| return listenerFailure; |
| } |
| |
| // === state transitions === |
| |
| /** |
| * Returns true if this job has been submitted to the JobRunner. |
| * (That is, if {@link #onSubmitted} has ever been called.) |
| */ |
| synchronized boolean wasSubmitted() { |
| return table != null; |
| } |
| |
| boolean isDone() { |
| return result.isDone(); |
| } |
| |
| /** |
| * Reports that this job has been submitted to the JobRunner. |
| * Starts sending updates to the JobTable. |
| * @throws IllegalStateException if the job was already started. |
| */ |
| synchronized void onSubmitted(JobEventTable table) { |
| if (wasSubmitted()) { |
| throw new IllegalStateException("compile job has already started: " + id); |
| } |
| this.table = table; |
| table.publish(makeEvent(Status.WAITING), getLogger()); |
| } |
| |
| /** |
| * Reports that we started to compile the job. |
| */ |
| synchronized void onStarted(int compileId, CompileDir compileDir) { |
| if (table == null || !table.isActive(this)) { |
| throw new IllegalStateException("compile job is not active: " + id); |
| } |
| this.compileId = compileId; |
| this.compileDir = compileDir; |
| |
| try { |
| recompileListener.startedCompile(inputModuleName, compileId, compileDir); |
| } catch (Exception e) { |
| getLogger().log(TreeLogger.Type.WARN, "recompile listener threw exception", e); |
| listenerFailure = e; |
| } |
| |
| publish(makeEvent(Status.COMPILING)); |
| } |
| |
| /** |
| * Reports that this job has made progress while compiling. |
| * @throws IllegalStateException if the job is not running. |
| */ |
| synchronized void onProgress(String stepMessage) { |
| checkIsCompiling("onProgress"); |
| publish(makeEvent(Status.COMPILING, stepMessage)); |
| } |
| |
| synchronized void setCompileStrategy(CompileStrategy strategy) { |
| checkIsCompiling("setCompileStrategy"); |
| if (compileStrategy != null) { |
| throw new IllegalStateException("setCompileStrategy can only be set once per job"); |
| } |
| this.compileStrategy = strategy; |
| // Not bothering to send an event just for this change, so it will be included |
| // in the next event. |
| } |
| |
| /** |
| * Reports that this job has finished. |
| * @throws IllegalStateException if the job is not running. |
| */ |
| synchronized void onFinished(Result newResult) { |
| if (table == null || !table.isActive(this)) { |
| throw new IllegalStateException("compile job is not active: " + id); |
| } |
| |
| // Report that we finished unless the listener messed up already. |
| if (listenerFailure == null) { |
| try { |
| recompileListener.finishedCompile(inputModuleName, compileId, newResult.isOk()); |
| } catch (Exception e) { |
| getLogger().log(TreeLogger.Type.WARN, "recompile listener threw exception", e); |
| listenerFailure = e; |
| } |
| } |
| |
| result.set(newResult); |
| outputModuleName = newResult.outputModuleName; |
| if (newResult.isOk()) { |
| publish(makeEvent(Status.SERVING)); |
| } else { |
| publish(makeEvent(Status.ERROR)); |
| } |
| } |
| |
| /** |
| * Reports that this job's output is no longer available. |
| */ |
| synchronized void onGone() { |
| if (table == null || !table.isActive(this)) { |
| throw new IllegalStateException("compile job is not active: " + id); |
| } |
| publish(makeEvent(Status.GONE)); |
| } |
| |
| private JobEvent makeEvent(Status status) { |
| return makeEvent(status, null); |
| } |
| |
| private JobEvent makeEvent(Status status, String message) { |
| JobEvent.Builder out = new JobEvent.Builder(); |
| out.setJobId(getId()); |
| out.setInputModuleName(getInputModuleName()); |
| out.setBindings(getBindingProperties()); |
| out.setStatus(status); |
| out.setMessage(message); |
| out.setOutputModuleName(outputModuleName); |
| out.setCompileDir(compileDir); |
| out.setCompileStrategy(compileStrategy); |
| out.setArguments(args); |
| out.setTags(tags); |
| out.setMetricMap(getMetricMapSnapshot()); |
| return out.build(); |
| } |
| |
| private Map<String, Long> getMetricMapSnapshot() { |
| TreeLogger logger = getLogger(); |
| if (logger instanceof AbstractTreeLogger) { |
| return ((AbstractTreeLogger)logger).getMetricMap().getSnapshot(); |
| } |
| return ImmutableMap.of(); // not found |
| } |
| |
| /** |
| * Makes an event visible externally. |
| */ |
| private void publish(JobEvent event) { |
| if (listenerFailure == null) { |
| try { |
| jobChangeListener.onJobChange(event); |
| } catch (Exception e) { |
| getLogger().log(Type.WARN, "JobChangeListener threw exception", e); |
| listenerFailure = e; |
| } |
| } |
| table.publish(event, getLogger()); |
| } |
| |
| private void checkIsCompiling(String methodName) { |
| if (table == null || table.getPublishedEvent(this).getStatus() != Status.COMPILING) { |
| throw new IllegalStateException(methodName + " called for a job that isn't compiling: " + id); |
| } |
| } |
| |
| /** |
| * Creates a child logger on first use. |
| */ |
| static class LogSupplier { |
| private final TreeLogger parent; |
| private final String jobId; |
| private TreeLogger child; |
| |
| LogSupplier(TreeLogger parent, String jobId) { |
| this.parent = parent; |
| this.jobId = jobId; |
| } |
| |
| synchronized TreeLogger get() { |
| if (child == null) { |
| child = parent.branch(Type.INFO, "Job " + jobId); |
| if (child instanceof AbstractTreeLogger) { |
| ((AbstractTreeLogger)child).resetMetricMap(); |
| } |
| } |
| return child; |
| } |
| } |
| |
| /** |
| * The result of a recompile. |
| */ |
| static class Result { |
| |
| /** |
| * non-null if successful |
| */ |
| final CompileDir outputDir; |
| |
| /** |
| * non-null if successful. |
| */ |
| final String outputModuleName; |
| |
| /** |
| * non-null for an error |
| */ |
| final Throwable error; |
| |
| Result(CompileDir outputDir, String outputModuleName, Throwable error) { |
| assert (outputDir == null) != (error == null); |
| this.outputDir = outputDir; |
| this.outputModuleName = outputModuleName; |
| this.error = error; |
| } |
| |
| boolean isOk() { |
| return error == null; |
| } |
| } |
| } |