io.hyperfoil.core.impl.PhaseInstanceImpl Maven / Gradle / Ivy
package io.hyperfoil.core.impl;
import io.hyperfoil.api.BenchmarkExecutionException;
import io.netty.util.concurrent.EventExecutorGroup;
import io.hyperfoil.api.config.BenchmarkDefinitionException;
import io.hyperfoil.api.collection.ElasticPool;
import io.hyperfoil.api.config.Phase;
import io.hyperfoil.api.session.PhaseChangeHandler;
import io.hyperfoil.api.session.Session;
import io.hyperfoil.api.session.PhaseInstance;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiFunction;
import java.util.stream.IntStream;
public abstract class PhaseInstanceImpl implements PhaseInstance {
protected static final Logger log = LoggerFactory.getLogger(PhaseInstanceImpl.class);
protected static final boolean trace = log.isTraceEnabled();
private static Map, BiFunction extends Phase, Integer, PhaseInstance>> constructors = new HashMap<>();
protected final D def;
private final int agentThreads;
private final int agentFirstThreadId;
protected ElasticPool sessionPool;
protected List sessionList;
private PhaseChangeHandler phaseChangeHandler;
// Reads are done without locks
protected volatile Status status = Status.NOT_STARTED;
protected long absoluteStartTime;
protected AtomicInteger activeSessions = new AtomicInteger(0);
private volatile Throwable error;
private volatile boolean sessionLimitExceeded;
public static PhaseInstance newInstance(Phase def, int agentId) {
@SuppressWarnings("unchecked")
BiFunction ctor = (BiFunction) constructors.get(def.getClass());
if (ctor == null) throw new BenchmarkDefinitionException("Unknown phase type: " + def);
return ctor.apply(def, agentId);
}
static {
constructors.put(Phase.AtOnce.class, (BiFunction) AtOnce::new);
constructors.put(Phase.Always.class, (BiFunction) Always::new);
constructors.put(Phase.RampRate.class, (BiFunction) RampRate::new);
constructors.put(Phase.ConstantRate.class, (BiFunction) ConstantRate::new);
constructors.put(Phase.Sequentially.class, (BiFunction) Sequentially::new);
constructors.put(Phase.Noop.class, (BiFunction) Noop::new);
}
protected PhaseInstanceImpl(D def, int agentId) {
this.def = def;
this.agentThreads = def.benchmark().threads(agentId);
this.agentFirstThreadId = IntStream.range(0, agentId).map(id -> def.benchmark().threads(id)).sum();
}
@Override
public D definition() {
return def;
}
@Override
public Status status() {
return status;
}
@Override
public long absoluteStartTime() {
return absoluteStartTime;
}
@Override
public void start(EventExecutorGroup executorGroup) {
assert status == Status.NOT_STARTED : "Status is " + status;
status = Status.RUNNING;
absoluteStartTime = System.currentTimeMillis();
log.debug("{} changing status to RUNNING", def.name);
phaseChangeHandler.onChange(def, Status.RUNNING, false, error).thenRun(() -> proceed(executorGroup));
}
@Override
public void finish() {
assert status == Status.RUNNING : "Status is " + status;
status = Status.FINISHED;
log.debug("{} changing status to FINISHED", def.name);
BenchmarkExecutionException error = null;
phaseChangeHandler.onChange(def, Status.FINISHED, sessionLimitExceeded, error);
}
@Override
public void tryTerminate() {
assert status.isFinished();
if (activeSessions.compareAndSet(0, Integer.MIN_VALUE)) {
setTerminated();
} else if (sessionList != null && status == Status.TERMINATING) {
// We need to force blocked sessions to check the termination status
synchronized (sessionList) {
for (int i = 0; i < sessionList.size(); i++) {
Session session = sessionList.get(i);
if (session.isActive()) {
session.proceed();
}
}
}
}
}
@Override
public void terminate() {
if (status != Status.TERMINATED) {
status = Status.TERMINATING;
}
log.debug("{} changing status to TERMINATING", def.name);
tryTerminate();
}
// TODO better name
@Override
public void setComponents(ElasticPool sessionPool, List sessionList, PhaseChangeHandler phaseChangeHandler) {
this.sessionPool = sessionPool;
this.sessionList = sessionList;
this.phaseChangeHandler = phaseChangeHandler;
}
@Override
public void notifyFinished(Session session) {
if (session != null) {
sessionPool.release(session);
}
int numActive = activeSessions.decrementAndGet();
if (trace) {
log.trace("#{} NotifyFinished, {} has {} active sessions", session == null ? -1 : session.uniqueId(), def.name, numActive);
}
if (numActive < 0) {
throw new IllegalStateException(def.name + " has " + numActive + " active sessions");
}
if (numActive == 0 && status.isFinished() && activeSessions.compareAndSet(0, Integer.MIN_VALUE)) {
setTerminated();
}
}
@Override
public void setTerminated() {
status = Status.TERMINATED;
log.debug("{} changing status to TERMINATED", def.name);
phaseChangeHandler.onChange(def, status, false, error);
}
@Override
public void fail(Throwable error) {
this.error = error;
terminate();
}
@Override
public void setSessionLimitExceeded() {
sessionLimitExceeded = true;
}
@Override
public Throwable getError() {
return error;
}
@Override
public int agentThreads() {
return agentThreads;
}
@Override
public int agentFirstThreadId() {
return agentFirstThreadId;
}
protected boolean startNewSession() {
int numActive = activeSessions.incrementAndGet();
if (numActive < 0) {
// finished
return true;
}
if (trace) {
log.trace("{} has {} active sessions", def.name, numActive);
}
Session session;
try {
session = sessionPool.acquire();
} catch (Throwable t) {
log.error("Error during session acquisition", t);
notifyFinished(null);
return true;
}
if (session == null) {
notifyFinished(null);
return true;
}
session.start(this);
return false;
}
public static class AtOnce extends PhaseInstanceImpl {
private final int users;
public AtOnce(Phase.AtOnce def, int agentId) {
super(def, agentId);
this.users = def.benchmark().slice(def.users, agentId);
}
@Override
public void proceed(EventExecutorGroup executorGroup) {
assert activeSessions.get() == 0;
for (int i = 0; i < users; ++i) {
startNewSession();
}
}
@Override
public void reserveSessions() {
if (users > 0) {
sessionPool.reserve(users);
}
}
}
public static class Always extends PhaseInstanceImpl {
int users;
public Always(Phase.Always def, int agentId) {
super(def, agentId);
users = def.benchmark().slice(def.users, agentId);
}
@Override
public void proceed(EventExecutorGroup executorGroup) {
assert activeSessions.get() == 0;
for (int i = 0; i < users; ++i) {
startNewSession();
}
}
@Override
public void reserveSessions() {
if (users > 0) {
sessionPool.reserve(users);
}
}
@Override
public void notifyFinished(Session session) {
if (status.isFinished() || session == null) {
super.notifyFinished(session);
} else {
session.start(this);
}
}
}
protected abstract static class OpenModelPhase extends PhaseInstanceImpl
{
protected final int maxSessions;
protected final Random random = new Random();
protected double nextScheduled;
protected AtomicLong throttledUsers = new AtomicLong(0);
protected long startedOrThrottledUsers = 0;
protected OpenModelPhase(P def, int agentId) {
super(def, agentId);
maxSessions = def.benchmark().slice(def.maxSessions, agentId);
}
@Override
public void proceed(EventExecutorGroup executorGroup) {
if (status.isFinished()) {
return;
}
long now = System.currentTimeMillis();
long delta = now - absoluteStartTime;
long nextDelta;
if (def.variance) {
while (delta > nextScheduled) {
if (startNewSession()) {
throttledUsers.incrementAndGet();
}
startedOrThrottledUsers++;
// TODO: after many iterations there will be some skew due to imprecise double calculations
// Maybe we could restart from the expected rate every 1000th session?
nextScheduled = nextSessionRandomized();
}
} else {
long required = nextSessionMetronome(delta);
for (long i = required - startedOrThrottledUsers; i > 0; --i) {
if (startNewSession()) {
throttledUsers.addAndGet(i);
break;
}
}
startedOrThrottledUsers = Math.max(required, startedOrThrottledUsers);
}
nextDelta = (long) Math.ceil(nextScheduled);
if (trace) {
log.trace("{}: {} after start, {} started ({} throttled), next user in {} ms", def.name, delta,
startedOrThrottledUsers, throttledUsers.get(), nextDelta - delta);
}
executorGroup.schedule(() -> proceed(executorGroup), nextDelta - delta, TimeUnit.MILLISECONDS);
}
protected abstract long nextSessionMetronome(long delta);
protected abstract double nextSessionRandomized();
@Override
public void reserveSessions() {
if (maxSessions > 0) {
sessionPool.reserve(maxSessions);
}
}
@Override
public void notifyFinished(Session session) {
if (session != null && !status.isFinished()) {
long throttled = throttledUsers.get();
while (throttled != 0) {
if (throttledUsers.compareAndSet(throttled, throttled - 1)) {
// TODO: it would be nice to compensate response times
// in these invocations for the fact that we're applying
// SUT feedback, but that would be imprecise anyway.
session.start(this);
return;
} else {
throttled = throttledUsers.get();
}
}
}
super.notifyFinished(session);
}
}
public static class RampRate extends OpenModelPhase {
private final double initialUsersPerSec;
private final double targetUsersPerSec;
public RampRate(Phase.RampRate def, int agentId) {
super(def, agentId);
initialUsersPerSec = def.benchmark().slice(def.initialUsersPerSec, agentId);
targetUsersPerSec = def.benchmark().slice(def.targetUsersPerSec, agentId);
nextScheduled = def.variance ? nextSessionRandomized() : 0;
}
@Override
protected long nextSessionMetronome(long delta) {
double progress = (targetUsersPerSec - initialUsersPerSec) / (def.duration * 1000);
long required = (long) (((progress * (delta + 1)) / 2 + initialUsersPerSec / 1000) * delta);
// Next time is the root of quadratic equation
double bCoef = progress + initialUsersPerSec / 500;
nextScheduled = Math.ceil((-bCoef + Math.sqrt(bCoef * bCoef + 8 * progress * (startedOrThrottledUsers + 1))) / (2 * progress));
return required;
}
@Override
protected double nextSessionRandomized() {
// we're solving quadratic equation coming from t = (duration * -log(rand))/(((t + now) * (target - initial)) + initial * duration)
double aCoef = (targetUsersPerSec - initialUsersPerSec);
if (aCoef < 0.000001) {
// prevent division 0f/0f
return 1000 * -Math.log(Math.max(1e-20, random.nextDouble())) / initialUsersPerSec;
}
double bCoef = nextScheduled * (targetUsersPerSec - initialUsersPerSec) + initialUsersPerSec * def.duration;
double cCoef = def.duration * 1000 * Math.log(random.nextDouble());
return nextScheduled + (-bCoef + Math.sqrt(bCoef * bCoef - 4 * aCoef * cCoef)) / (2 * aCoef);
}
}
public static class ConstantRate extends OpenModelPhase {
private final double usersPerSec;
public ConstantRate(Phase.ConstantRate def, int agentId) {
super(def, agentId);
usersPerSec = def.benchmark().slice(def.usersPerSec, agentId);
nextScheduled = def.variance ? nextSessionRandomized() : 0;
}
@Override
protected long nextSessionMetronome(long delta) {
long required = (long) (delta * usersPerSec / 1000);
nextScheduled = (1000 * (startedOrThrottledUsers + 1) + usersPerSec) / usersPerSec;
return required;
}
@Override
protected double nextSessionRandomized() {
return nextScheduled + (1000 * -Math.log(Math.max(1e-20, random.nextDouble())) / usersPerSec);
}
}
public static class Sequentially extends PhaseInstanceImpl {
private int counter = 0;
public Sequentially(Phase.Sequentially def, int agentId) {
super(def, agentId);
}
@Override
public void proceed(EventExecutorGroup executorGroup) {
assert activeSessions.get() == 0;
startNewSession();
}
@Override
public void reserveSessions() {
sessionPool.reserve(1);
}
@Override
public void notifyFinished(Session session) {
if (++counter >= def.repeats) {
status = Status.TERMINATING;
log.debug("{} changing status to TERMINATING", def.name);
super.notifyFinished(session);
} else {
session.start(this);
}
}
}
public static class Noop extends PhaseInstanceImpl {
protected Noop(Phase.Noop def, int agentId) {
super(def, agentId);
}
@Override
public void proceed(EventExecutorGroup executorGroup) {
}
@Override
public void reserveSessions() {
}
}
}