
org.opalj.fpcf.AnalysisScenario.scala Maven / Gradle / Ivy
The newest version!
/* BSD 2-Clause License - see OPAL/LICENSE for details. */
package org.opalj
package fpcf
import org.opalj.log.LogContext
import org.opalj.log.OPALLogger
import org.opalj.graphs.Graph
/**
* Provides functionality to determine whether a set of analyses is compatible and to compute
* a schedule to execute a set of analyses.
*
* @author Michael Eichberg
*/
class AnalysisScenario[A](val ps: PropertyStore) {
private[this] var scheduleComputed: Boolean = false
private[this] var allCS: Set[ComputationSpecification[A]] = Set.empty
private[this] var derivedProperties: Set[PropertyBounds] = Set.empty
private[this] var eagerlyDerivedProperties: Set[PropertyBounds] = Set.empty
private[this] var collaborativelyDerivedProperties: Set[PropertyBounds] = Set.empty
private[this] var lazilyDerivedProperties: Set[PropertyBounds] = Set.empty
private[this] var initializationData: Map[ComputationSpecification[A], Any] = Map.empty
private[this] var usedProperties: Set[PropertyBounds] = Set.empty
private[this] var eagerCS: Set[ComputationSpecification[A]] = Set.empty
private[this] var lazyCS: Set[ComputationSpecification[A]] = Set.empty
private[this] var triggeredCS: Set[ComputationSpecification[A]] = Set.empty
private[this] var transformersCS: Set[ComputationSpecification[A]] = Set.empty
private[this] var derivedBy: Map[PropertyKind, (PropertyBounds, Set[ComputationSpecification[A]])] = {
Map.empty
}
def allProperties: Set[PropertyBounds] = derivedProperties ++ usedProperties
/**
* Adds the given computation specification (`cs`) to the set of computation specifications
* that should be scheduled.
*/
def +=(cs: ComputationSpecification[A]): this.type = {
if (scheduleComputed) {
throw new IllegalStateException("process was already computed");
}
cs.computationType match {
case EagerComputation => eagerCS += cs
case TriggeredComputation => triggeredCS += cs
case LazyComputation => lazyCS += cs
case Transformer => transformersCS += cs
}
allCS += cs
initializationData += cs -> cs.init(ps)
this
}
/**
* Returns the graph which depicts the dependencies between the computed properties
* based on the current computation specifications.
* I.e., a property `d` depends on another property `p` if the algorithm which computes
* `d` uses the property `p`.
*
* @note Can only be called after the schedule method was called! If no schedule could be
* computed the result of this method is undefined.
*/
def propertyComputationsDependencies: Graph[PropertyBounds] = {
if (!scheduleComputed) {
throw new IllegalStateException("initialization incomplete; schedule not computed");
}
val psDeps = Graph.empty[PropertyBounds]
allCS foreach { cs =>
// all derived properties depend on all used properties
cs.derives foreach { derived =>
psDeps addVertice derived
cs.uses(ps) foreach { use => psDeps addEdge (derived, use) }
}
}
psDeps
}
/**
* Returns the dependencies between the computations.
*
* @note Can only be called after the schedule method was called! If no schedule could be
* computed the result of this method is undefined.
*/
def computationDependencies: Graph[ComputationSpecification[A]] = {
if (!scheduleComputed) {
throw new IllegalStateException("initialization incomplete; schedule not computed");
}
val compDeps = Graph.empty[ComputationSpecification[A]]
val derivedBy: Map[PropertyBounds, Set[ComputationSpecification[A]]] = {
var derivedBy: Map[PropertyBounds, Set[ComputationSpecification[A]]] = Map.empty
allCS foreach { cs =>
cs.derives foreach { derives =>
derivedBy += derives -> (derivedBy.getOrElse(derives, Set.empty) + cs)
}
}
derivedBy
}
allCS foreach { cs =>
compDeps addVertice cs
cs.uses(ps) foreach { usedPK =>
derivedBy.get(usedPK).iterator.flatten.foreach { providerCS =>
if (providerCS ne cs) {
compDeps addEdge (cs, providerCS)
}
}
}
}
// let's handle the case that multiple analyses derive a property collaboratively
derivedBy.valuesIterator.filter(_.size > 1) foreach { css =>
val cssIt = css.iterator
val headCS = cssIt.next()
var lastCS = headCS
do {
val nextCS = cssIt.next()
compDeps addEdge (lastCS -> nextCS)
lastCS = nextCS
} while (cssIt.hasNext)
compDeps addEdge (lastCS -> headCS)
}
compDeps
}
private[this] def processCS(cs: ComputationSpecification[A]): Unit = {
// 1. check the most basic constraints
cs.derivesLazily foreach { lazilyDerivedProperty =>
if (derivedProperties.contains(lazilyDerivedProperty)) {
val pkName = PropertyKey.name(lazilyDerivedProperty.pk.id)
val m = s"can not register $cs: $pkName is already computed by another analysis"
throw new SpecificationViolation(m)
}
}
cs.derivesCollaboratively foreach { collaborativelyDerivedProperty =>
if (eagerlyDerivedProperties.contains(collaborativelyDerivedProperty) ||
lazilyDerivedProperties.contains(collaborativelyDerivedProperty)) {
val pkName = PropertyKey.name(collaborativelyDerivedProperty.pk.id)
val m =
s"can not register $cs: "+
s"$pkName is not computed collaboratively by all analyses"
throw new SpecificationViolation(m)
}
}
cs.derivesEagerly foreach { eagerlyDerivedProperty =>
if (derivedProperties.contains(eagerlyDerivedProperty)) {
val pkName = PropertyKey.name(eagerlyDerivedProperty.pk.id)
val m = s"can not register $cs: $pkName is already computed by another analysis"
throw new SpecificationViolation(m)
}
}
// TODO Check inner consistency: that is, if an analysis derives multiple properties, they have to be compatible
// 2. register the analysis
def handleDerivedProperties(derivedProperties: Set[PropertyBounds]): Unit = {
this.derivedProperties ++= derivedProperties
derivedProperties foreach { derivedProperty =>
val pk = derivedProperty.pk
derivedBy.get(pk) match {
case None =>
derivedBy += ((pk, (derivedProperty, Set(cs))))
case Some((`derivedProperty`, css)) =>
derivedBy += ((pk, (derivedProperty, css + cs)))
case Some((deviatingPropertyBounds, css)) =>
val propertyName = PropertyKey.name(pk)
throw new IllegalArgumentException(
s"different bounds ($deviatingPropertyBounds vs. $derivedProperty) "+
s"are computed by $css vs. $cs "+
s"for the collaboratively computed property $propertyName"
)
}
}
}
usedProperties ++= cs.uses(ps)
eagerlyDerivedProperties ++= cs.derivesEagerly
handleDerivedProperties(cs.derivesEagerly)
collaborativelyDerivedProperties ++= cs.derivesCollaboratively
handleDerivedProperties(cs.derivesCollaboratively)
lazilyDerivedProperties ++= cs.derivesLazily.toList
handleDerivedProperties(cs.derivesLazily.toSet)
}
/**
* Computes an executable schedule.
*
* When executing a schedule, the following steps will be performed:
* 1. all analyses' init method will be called; this may lead to the initialization
* of properties
*
* 1. setupPhase is called
* 1. all lazy analyses and all transformers are registered;
* (Immediately before registration, beforeSchedule is called.)
* 1. all triggered computations are registered; this may trigger computations related
* to initial properties
* (Immediately before registration, beforeSchedule is called.)
* 1. all eager analyses are started
* (Immediately before registration, beforeSchedule is called.)
* 1. when the phase has finished, all analyses' afterPhaseCompletion methods are called.
*
* @param propertyStore required to determine which properties are already computed!
*/
def computeSchedule(
propertyStore: PropertyStore
)(
implicit
logContext: LogContext
): Schedule[A] = {
if (scheduleComputed) {
throw new IllegalStateException("schedule already computed");
} else {
scheduleComputed = true
}
allCS.foreach(processCS)
val alreadyComputedPropertyKinds = propertyStore.alreadyComputedPropertyKindIds.toSet
// 0. check that a property was not already derived
allCS.foreach { cs =>
cs.derives foreach { derivedProperty =>
if (alreadyComputedPropertyKinds.contains(derivedProperty.pk.id)) {
val pkName = PropertyKey.name(derivedProperty.pk.id)
val m = s"can not register $cs: $pkName was computed in a previous phase"
throw new SpecificationViolation(m)
}
}
}
// 1. check for properties that are not derived (and which require an analysis)
val underivedProperties = usedProperties -- derivedProperties
underivedProperties
.filterNot { underivedProperty =>
alreadyComputedPropertyKinds.contains(underivedProperty.pk.id)
}
.foreach { underivedProperty =>
val propertyName = PropertyKey.name(underivedProperty.pk.id)
if (PropertyKey.hasFallback(underivedProperty.pk)) {
val message = s"no analyses scheduled for: $propertyName; using fallback"
OPALLogger.warn("analysis configuration", message)
} else {
throw new IllegalStateException(s"no analysis scheduled for $propertyName")
}
}
// TODO check all further constraints (in particular those related to cyclic dependencies between analysis...)
// 2. assign analyses to different batches if an analysis can only process
// final properties (unless it is a transformer, the latter have special paths and
// constraints and can always be scheduled in the same batch!)
// TODO ....
Schedule(
if (allCS.isEmpty) List.empty else List(computePhase(propertyStore)),
initializationData
)
}
/**
* Computes the configuration for a specific batch; this method can only handle the situation
* where all analyses can be executed in the same phase.
*/
private def computePhase(propertyStore: PropertyStore): PhaseConfiguration[A] = {
// 1. compute the phase configuration; i.e., find those properties for which we must
// suppress interim updates.
var suppressInterimUpdates: Map[PropertyKind, Set[PropertyKind]] = Map.empty
// Interim updates have to be suppressed when an analysis uses a property for which
// the wrong bounds/not enough bounds are computed.
transformersCS foreach { cs =>
suppressInterimUpdates += (cs.derivesLazily.get.pk -> cs.uses(ps).map(_.pk))
}
// 3. create the batch
val batchBuilder = List.newBuilder[ComputationSpecification[A]]
batchBuilder ++= lazyCS
batchBuilder ++= transformersCS
batchBuilder ++= triggeredCS
batchBuilder ++= eagerCS
// FIXME...
// Interim updates can be suppressed when the depender and dependee are not in a cyclic
// relation; however, this could have a negative impact on the effect of deep laziness -
// once we are actually implementing it. For the time being, suppress notifications is always
// advantageous.
val phase1Configuration = PropertyKindsConfiguration(
propertyKindsComputedInThisPhase = derivedProperties.map(_.pk),
suppressInterimUpdates = suppressInterimUpdates
)
PhaseConfiguration(phase1Configuration, batchBuilder.result())
}
}
/**
* Factory to create an [[AnalysisScenario]].
*/
object AnalysisScenario {
/**
* @param analyses The set of analyses that should be executed as part of this analysis scenario.
*/
def apply[A](
analyses: Iterable[ComputationSpecification[A]],
propertyStore: PropertyStore
): AnalysisScenario[A] = {
val as = new AnalysisScenario[A](propertyStore)
analyses.foreach(as.+=)
as
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy