AnalysisController.java

package de.uka.ipd.sdq.beagle.core;

import de.uka.ipd.sdq.beagle.core.analysis.MeasurementResultAnalyser;
import de.uka.ipd.sdq.beagle.core.analysis.MeasurementResultAnalyserBlackboardView;
import de.uka.ipd.sdq.beagle.core.analysis.ProposedExpressionAnalyser;
import de.uka.ipd.sdq.beagle.core.analysis.ProposedExpressionAnalyserBlackboardView;
import de.uka.ipd.sdq.beagle.core.analysis.ReadOnlyMeasurementResultAnalyserBlackboardView;
import de.uka.ipd.sdq.beagle.core.analysis.ReadOnlyProposedExpressionAnalyserBlackboardView;
import de.uka.ipd.sdq.beagle.core.judge.FinalJudge;
import de.uka.ipd.sdq.beagle.core.measurement.MeasurementController;
import de.uka.ipd.sdq.beagle.core.measurement.MeasurementControllerBlackboardView;
import de.uka.ipd.sdq.beagle.core.measurement.MeasurementTool;
import de.uka.ipd.sdq.beagle.core.measurement.ReadOnlyMeasurementControllerBlackboardView;

import org.apache.commons.lang3.Validate;

import java.util.HashSet;
import java.util.Random;
import java.util.Set;

/**
 * Conducts a complete analysis of elements on a blackboard. Controls the
 * {@link MeasurementController}, {@link ProposedExpressionAnalyser ResultAnalysers} and
 * the {@link FinalJudge} to measure and analyse parametric dependencies.
 *
 * <h3>The Analysis</h3> During the analysis, the controller will ask participants whether
 * they can contribute (except for the {@linkplain FinalJudge}, which must always be able
 * to judge). The participants may only make these assumptions of the control flow:
 *
 * <ul>
 *
 * <li>There is always only the {@link MeasurementController}, only the {@link FinalJudge}
 * or only one {@link ProposedExpressionAnalyser} (or none of the previous) running. They
 * may use parallelisation as they wish but are self-responsible for synchronisation.
 *
 * <li>A {@link ProposedExpressionAnalyser} will only be called if its
 * {@link ProposedExpressionAnalyser#canContribute} method returns {@code true}. The
 * {@link MeasurementController} will only be called if its
 * {@link MeasurementController#canMeasure canMeasure} method returns {@code true}.
 *
 * <li>When picking the next participant, the {@link MeasurementController} will be always
 * be called next if its {@link MeasurementController#canMeasure canMeasure} method
 * returns {@code true}.
 *
 * <li>Any {@link ProposedExpressionAnalyser} whose
 * {@link ProposedExpressionAnalyser#canContribute canContribute} method returns
 * {@code true} will be called.
 *
 * <li>The {@linkplain FinalJudge} will only be called if no
 * {@link ProposedExpressionAnalyser} can contribute.
 *
 * </ul>
 *
 * @author Roman Langrehr
 * @author Joshua Gleitze
 * @author Christoph Michelbach
 *
 * @see MeasurementController
 * @see Blackboard
 */
public class AnalysisController {

	/**
	 * The {@link Blackboard} this {@link AnalysisController} knows and uses.
	 */
	private final Blackboard blackboard;

	/**
	 * The {@link MeasurementController} this {@link AnalysisController} knows and uses.
	 */
	private final MeasurementController measurementController;

	/**
	 * The {@link MeasurementResultAnalyser}s this {@link AnalysisController} knows and
	 * uses.
	 */
	private final Set<MeasurementResultAnalyser> measurementResultAnalysers;

	/**
	 * The {@link ProposedExpressionAnalyser}s this {@link AnalysisController} knows and
	 * uses.
	 */
	private final Set<ProposedExpressionAnalyser> proposedExpressionAnalysers;

	/**
	 * The current state of the analysis.
	 */
	private volatile AnalysisState analysisState;

	/**
	 * Interrupts the analysis if its state is set to {@link AnalysisState#ENDING} or
	 * {@link AnalysisState#ABORTING}.
	 */
	private Runnable analysisInterruptor;

	/**
	 * Creates a controller to analyse all elements written on {@code blackboard}.
	 *
	 * @param blackboard A blackboard having everything to be analysed written on it. Must
	 *            not be {@code null}.
	 * @param measurementTools The {@link MeasurementTool}s to use. Must not be
	 *            {@code null} and must not contain {@code null}.
	 * @param measurementResultAnalysers The {@link MeasurementResultAnalyser}s to use.
	 *            Must not be {@code null} and must not contain {@code null}.
	 * @param proposedExpressionAnalysers The {@link ProposedExpressionAnalyser}s to use.
	 *            Must not be {@code null} and must not contain {@code null}.
	 */
	public AnalysisController(final Blackboard blackboard, final Set<MeasurementTool> measurementTools,
		final Set<MeasurementResultAnalyser> measurementResultAnalysers,
		final Set<ProposedExpressionAnalyser> proposedExpressionAnalysers) {
		Validate.notNull(blackboard);
		Validate.notNull(measurementTools);
		Validate.notNull(measurementResultAnalysers);
		Validate.notNull(proposedExpressionAnalysers);
		Validate.noNullElements(measurementTools);
		Validate.noNullElements(measurementResultAnalysers);
		Validate.noNullElements(proposedExpressionAnalysers);

		this.blackboard = blackboard;
		this.measurementController = new MeasurementController(measurementTools);
		this.measurementResultAnalysers = new HashSet<>(measurementResultAnalysers);
		this.proposedExpressionAnalysers = new HashSet<>(proposedExpressionAnalysers);
	}

	/**
	 * Runs the complete analysis, including measurements, result analysis and the final
	 * judging. See the class description for more details on the analysis process.
	 */
	public void performAnalysis() {

		this.analysisInterruptor = new Interruptor(Thread.currentThread());
		this.blackboard.getProjectInformation()
			.getTimeout()
			.registerCallback(() -> this.setAnalysisState(AnalysisState.ABORTING));

		final ReadOnlyMeasurementControllerBlackboardView readOnlyMeasurementControllerBlackboardView =
			new ReadOnlyMeasurementControllerBlackboardView(this.blackboard);
		final MeasurementControllerBlackboardView measurementControllerBlackboardView =
			new MeasurementControllerBlackboardView(this.blackboard);

		this.addAllSeffElementsAsToBeMeasured();

		final FinalJudge finalJudge = new FinalJudge();
		finalJudge.init(this.blackboard);
		this.analysisState = AnalysisState.RUNNING;
		boolean shouldContinue = true;

		this.waitForPauseEnd();

		while (this.analysisState != AnalysisState.ABORTING && shouldContinue) {
			if (this.measurementController.canMeasure(readOnlyMeasurementControllerBlackboardView)) {
				this.measurementController.measure(measurementControllerBlackboardView);

				if (this.analysisState == AnalysisState.RUNNING) {
					// After the measurements completed, clear the seff elements to be
					// measured on the blackboard so they won't be measured again in the
					// next iteration.
					this.clearSeffElementsToBeMeasuredFromBlackboard();
				}
			} else if (!this.chooseRandomMeasurementResultAnalyserToContribute()) {
				this.chooseRandomProposedExpressionAnalyserToContribute();
			}

			shouldContinue = !finalJudge.judge(this.blackboard);
			this.waitForPauseEnd();
		}

		this.analysisState = AnalysisState.TERMINATED;
	}

	/**
	 * Clears the "to be measured" lists of seff elements on the blackboard.
	 *
	 */
	private void clearSeffElementsToBeMeasuredFromBlackboard() {
		this.blackboard.clearToBeMeasuredBranches();
		this.blackboard.clearToBeMeasuredLoops();
		this.blackboard.clearToBeMeasuredRdias();
		this.blackboard.clearToBeMeasuredExternalCalls();
	}

	/**
	 * Causes the analysis thread to sleep and wait for the analysis state to be set to
	 * {@link AnalysisState#RUNNING} or {@link AnalysisState#ABORTING}.
	 */
	private synchronized void waitForPauseEnd() {
		while (this.analysisState == AnalysisState.ENDING) {
			try {
				this.wait();
			} catch (final InterruptedException exception) {
				// Retry on interrupt. No handling is needed because the loop just
				// tries again.
			}
		}
	}

	/**
	 * Adds all seff elements on the blackboard to the "to be measured" sets.
	 *
	 */
	private void addAllSeffElementsAsToBeMeasured() {
		final Set<SeffBranch> seffBranches = this.blackboard.getAllSeffBranches();
		final Set<SeffLoop> seffLoops = this.blackboard.getAllSeffLoops();
		final Set<ResourceDemandingInternalAction> rdias = this.blackboard.getAllRdias();
		final Set<ExternalCallParameter> externalCallParameters = this.blackboard.getAllExternalCallParameters();

		this.blackboard.addToBeMeasuredSeffBranches(seffBranches);
		this.blackboard.addToBeMeasuredSeffLoops(seffLoops);
		this.blackboard.addToBeMeasuredRdias(rdias);
		this.blackboard.addToBeMeasuredExternalCallParameters(externalCallParameters);
	}

	/**
	 * Chooses a {@link MeasurementResultAnalyser} able to contribute at random and lets
	 * it contribute.
	 *
	 * @return {@code true} if the task was executed successfully; {@code false} if there
	 *         was no {@link MeasurementResultAnalyser} able to contribute.
	 */
	private boolean chooseRandomMeasurementResultAnalyserToContribute() {
		final ReadOnlyMeasurementResultAnalyserBlackboardView readOnlyMeasurementResultAnalyserBlackboardView =
			new ReadOnlyMeasurementResultAnalyserBlackboardView(this.blackboard);
		final MeasurementResultAnalyserBlackboardView measurementResultAnalyserBlackboardView =
			new MeasurementResultAnalyserBlackboardView(this.blackboard);

		final Set<MeasurementResultAnalyser> measurementResultAnalysersAbleToContribute =
			new HashSet<MeasurementResultAnalyser>();

		for (final MeasurementResultAnalyser measurementResultAnalyser : this.measurementResultAnalysers) {
			if (measurementResultAnalyser.canContribute(readOnlyMeasurementResultAnalyserBlackboardView)) {
				measurementResultAnalysersAbleToContribute.add(measurementResultAnalyser);
			}
		}

		if (measurementResultAnalysersAbleToContribute.size() != 0) {
			// Choose a measurement result analyser at random.
			final int minimum = 1;
			final int maximum = measurementResultAnalysersAbleToContribute.size();
			final int chosenResultAnalyser = new Random().nextInt(maximum - minimum + 1) + minimum;

			int count = 1;
			// @formatter:off
			for (final MeasurementResultAnalyser measurementResultAnalyserAbleToContribute
				: measurementResultAnalysersAbleToContribute) {
				// @formatter:on
				if (count == chosenResultAnalyser) {
					measurementResultAnalyserAbleToContribute.contribute(measurementResultAnalyserBlackboardView);
					return true;
				}

				count++;
			}

		}

		return false;
	}

	/**
	 * Chooses a {@link ProposedExpressionAnalyser} able to contribute at random and lets
	 * it contribute.
	 *
	 * @return {@code true} if the task was executed successfully; {@code false} if there
	 *         was no {@link ProposedExpressionAnalyser} able to contribute.
	 */
	private boolean chooseRandomProposedExpressionAnalyserToContribute() {
		final ReadOnlyProposedExpressionAnalyserBlackboardView readOnlyProposedExpressionAnalyserBlackboardView =
			new ReadOnlyProposedExpressionAnalyserBlackboardView(this.blackboard);
		final ProposedExpressionAnalyserBlackboardView proposedExpressionAnalyserBlackboardView =
			new ProposedExpressionAnalyserBlackboardView(this.blackboard);

		final Set<ProposedExpressionAnalyser> proposedExpressionAnalysersAbleToContribute =
			new HashSet<ProposedExpressionAnalyser>();

		for (final ProposedExpressionAnalyser proposedExpressionAnalyser : this.proposedExpressionAnalysers) {
			if (proposedExpressionAnalyser.canContribute(readOnlyProposedExpressionAnalyserBlackboardView)) {
				proposedExpressionAnalysersAbleToContribute.add(proposedExpressionAnalyser);
			}
		}

		if (proposedExpressionAnalysersAbleToContribute.size() != 0) {
			// Choose a proposed expression analyser at random.
			final int minimum = 1;
			final int maximum = proposedExpressionAnalysersAbleToContribute.size();
			final int chosenProposedExpressionAnalyser = new Random().nextInt(maximum - minimum + 1) + minimum;

			int count = 1;
			// @formatter:off
			for (final ProposedExpressionAnalyser proposedExpressionAnalyserAbleToContribute
				: proposedExpressionAnalysersAbleToContribute) {
				// @formatter:on
				if (count == chosenProposedExpressionAnalyser) {
					proposedExpressionAnalyserAbleToContribute.contribute(proposedExpressionAnalyserBlackboardView);
					return true;
				}

				count++;
			}

		}

		return false;
	}

	/**
	 * Returns the current state of the analysis.
	 *
	 * @return The current state of the analysis.
	 */
	public AnalysisState getAnalysisState() {
		return this.analysisState;
	}

	/**
	 * Sets the current state of the analysis to {@code analysisState}. The analysis must
	 * be started before this method is called. Otherwise an {@link IllegalStateException}
	 * will be thrown.
	 *
	 * @param analysisState The state the analysis will be in after this method has been
	 *            called.
	 */
	public void setAnalysisState(final AnalysisState analysisState) {
		Validate.validState(this.analysisState != null);

		/*
		 * Ignore this method call if the new state is equal to the old state.
		 */
		if (this.analysisState.equals(analysisState)) {
			return;
		}

		switch (analysisState) {
			case RUNNING:
				Validate.validState(this.analysisState == AnalysisState.ENDING,
					"Can't switch from %s to AnalysisState.RUNNING.", this.analysisState);
				break;
			case ABORTING:
				Validate.validState(this.analysisState != AnalysisState.TERMINATED,
					"Can't switch from %s to AnalysisState.ABORTING.", this.analysisState);
				break;
			case ENDING:
				Validate.validState(this.analysisState == AnalysisState.RUNNING,
					"Can't switch from %s to AnalysisState.ENDING.", this.analysisState);
				break;
			case TERMINATED:
				// no restrictions
				break;
			default:
				assert false;
				break;
		}

		if (analysisState == AnalysisState.ENDING || analysisState == AnalysisState.ABORTING) {
			this.analysisInterruptor.run();
		}

		synchronized (this) {
			this.analysisState = analysisState;
			this.notifyAll();
		}
	}

	/**
	 * The callback the {@link AnalysisController} uses to interrupt the working thread if
	 * the user is pausing or aborting the analysis.
	 *
	 * @author Christoph Michelbach
	 */
	private class Interruptor implements Runnable {

		/**
		 * The main thread. (The working thread.)
		 */
		private final Thread mainTread;

		/**
		 * Constructs a new callback for {@link AnalysisController}.
		 *
		 * @param mainThread The main thread. (The working thread.)
		 */
		Interruptor(final Thread mainThread) {
			this.mainTread = mainThread;
		}

		@Override
		public void run() {
			this.mainTread.interrupt();
		}

	}
}