blob: d0c0bb5f113ce260c8f840d0e94d87d30b1ee52c [file] [log] [blame]
/*
* Copyright (C) 2012 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.caliper.runner;
import com.google.caliper.bridge.CommandLineSerializer;
import com.google.caliper.bridge.OpenedSocket;
import com.google.caliper.bridge.WorkerSpec;
import com.google.caliper.config.VmConfig;
import com.google.caliper.model.BenchmarkSpec;
import com.google.caliper.runner.Instrument.Instrumentation;
import com.google.caliper.worker.WorkerMain;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.logging.Logger;
import javax.annotation.concurrent.GuardedBy;
import javax.inject.Inject;
/**
* A representation of an unstarted worker.
*
* <p>A worker is a sub process that runs a benchmark trial. Specifically it is a JVM running
* {@link com.google.caliper.worker.WorkerMain}. Because of this we can make certain assumptions
* about its behavior, including but not limited to:
*
* <ul>
* <li>The worker will connect back to us over a socket connection and send us UTF-8 json
* messages in a line oriented protocol.
* <li>TODO(lukes,gak): This is probably as good a place as any to specify the entire protocol.
* </ul>
*/
@TrialScoped final class WorkerProcess {
private static final Logger logger = Logger.getLogger(WorkerProcess.class.getName());
@GuardedBy("this")
private Process worker;
private final ProcessBuilder workerBuilder;
private final ShutdownHookRegistrar shutdownHookRegistrar;
private final ListenableFuture<OpenedSocket> openedSocket;
private final UUID trialId;
@VisibleForTesting WorkerProcess(ProcessBuilder workerBuilder,
UUID trialId,
ListenableFuture<OpenedSocket> openedSocket,
ShutdownHookRegistrar shutdownHookRegistrar) {
this.trialId = trialId;
this.workerBuilder = workerBuilder;
this.openedSocket = openedSocket;
this.shutdownHookRegistrar = shutdownHookRegistrar;
}
@Inject WorkerProcess(@TrialId UUID trialId,
ListenableFuture<OpenedSocket> openedSocket,
Experiment experiment,
BenchmarkSpec benchmarkSpec,
@LocalPort int localPort,
BenchmarkClass benchmarkClass,
ShutdownHookRegistrar shutdownHookRegistrar) {
this.trialId = trialId;
this.workerBuilder =
buildProcess(trialId, experiment, benchmarkSpec, localPort, benchmarkClass);
this.openedSocket = openedSocket;
this.shutdownHookRegistrar = shutdownHookRegistrar;
}
ListenableFuture<OpenedSocket> socketFuture() {
return openedSocket;
}
/**
* Returns a {@link Process} representing this worker. The process will be started if it hasn't
* already.
*/
synchronized Process startWorker() throws IOException {
if (worker == null) {
final Process delegate = workerBuilder.start();
final Thread shutdownHook = new Thread("worker-shutdown-hook-" + trialId) {
@Override public void run() {
delegate.destroy();
}
};
shutdownHookRegistrar.addShutdownHook(shutdownHook);
worker = new Process() {
@Override public OutputStream getOutputStream() {
return delegate.getOutputStream();
}
@Override public InputStream getInputStream() {
return delegate.getInputStream();
}
@Override public InputStream getErrorStream() {
return delegate.getErrorStream();
}
@Override public int waitFor() throws InterruptedException {
int waitFor = delegate.waitFor();
shutdownHookRegistrar.removeShutdownHook(shutdownHook);
return waitFor;
}
@Override public int exitValue() {
int exitValue = delegate.exitValue();
// if it hasn't thrown, the process is done
shutdownHookRegistrar.removeShutdownHook(shutdownHook);
return exitValue;
}
@Override public void destroy() {
delegate.destroy();
shutdownHookRegistrar.removeShutdownHook(shutdownHook);
}
};
}
return worker;
}
@VisibleForTesting static ProcessBuilder buildProcess(
UUID trialId,
Experiment experiment,
BenchmarkSpec benchmarkSpec,
int localPort,
BenchmarkClass benchmarkClass) {
// TODO(lukes): it would be nice to split this method into a few smaller more targeted methods
Instrumentation instrumentation = experiment.instrumentation();
Instrument instrument = instrumentation.instrument();
WorkerSpec request = new WorkerSpec(
trialId,
instrumentation.workerClass(),
instrumentation.workerOptions(),
benchmarkSpec,
ImmutableList.copyOf(instrumentation.benchmarkMethod.getParameterTypes()),
localPort);
ProcessBuilder processBuilder = new ProcessBuilder().redirectErrorStream(false);
List<String> args = processBuilder.command();
VirtualMachine vm = experiment.vm();
VmConfig vmConfig = vm.config;
args.addAll(getJvmArgs(vm, benchmarkClass));
Iterable<String> instrumentJvmOptions = instrument.getExtraCommandLineArgs(vmConfig);
logger.fine(String.format("Instrument(%s) Java args: %s", instrument.getClass().getName(),
instrumentJvmOptions));
Iterables.addAll(args, instrumentJvmOptions);
// last to ensure that they're always applied
args.addAll(vmConfig.workerProcessArgs());
args.add(WorkerMain.class.getName());
args.add(CommandLineSerializer.render(request));
logger.finest(String.format("Full JVM (%s) args: %s", vm.name, args));
return processBuilder;
}
@VisibleForTesting static List<String> getJvmArgs(
VirtualMachine vm,
BenchmarkClass benchmarkClass) {
VmConfig vmConfig = vm.config;
String platformName = vmConfig.platformName();
List<String> args = Lists.newArrayList();
String jdkPath = vmConfig.vmExecutable().getAbsolutePath();
args.add(jdkPath);
logger.fine(String.format("%s(%s) Path: %s", platformName, vm.name, jdkPath));
ImmutableList<String> jvmOptions = vmConfig.options();
args.addAll(jvmOptions);
logger.fine(String.format("%s(%s) args: %s", platformName, vm.name, jvmOptions));
ImmutableSet<String> benchmarkJvmOptions = benchmarkClass.vmOptions();
args.addAll(benchmarkJvmOptions);
logger.fine(String.format("Benchmark(%s) %s args: %s", benchmarkClass.name(), platformName,
benchmarkJvmOptions));
ImmutableList<String> classPathArgs = vmConfig.workerClassPathArgs();
args.addAll(classPathArgs);
logger.finer(String.format("Class path args: %s", classPathArgs));
// TODO(iam): consider forwarding -Djava.library.path= for JNI library support.
return args;
}
}