All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ru.primetalk.synapse.concurrent.ComputationalGraph.scala Maven / Gradle / Ivy

///////////////////////////////////////////////////////////////
// © ООО «Праймтолк», 2011-2013                              //
// Все права принадлежат компании ООО «Праймтолк».           //
///////////////////////////////////////////////////////////////
/**
 * SynapseGrid
 * © Primetalk Ltd., 2013.
 * All rights reserved.
 * Authors: A.Zhizhelev, A.Nehaev, P. Popov
 *
 * Created: 21.09.13, zhizhelev
 */
package ru.primetalk.synapse.concurrent

import ru.primetalk.synapse.core._

import scala.annotation.tailrec
import scala.collection.mutable
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, ExecutionContext, Future}
import scala.language.existentials


/** The prerequisites for the UnitOfComputation
  * Requires that the states have got their values for AtTime moment.
  * The computation unit will softly block over this state. */
case class StateRequirement(stateHandles: List[Contact[_]], time: HTime)

//extends Requirement


/**
 * The unit of computation may have constraints:
 * - list of states @time
 *
 * Also it can have properties
 * - update state@time
 *
 * Units are interconnected with dependencies:
 * - incoming units of computation
 * - dependent units of computation
 *
 * interconnection is given outside
 */
case class UnitOfComputation(signalAtTime: AtTime[Signal[_]], // the signal that triggered the computation.
                             rc: RuntimeComponent) {
  val stateRequirement = rc match {
    case RuntimeComponentFlatMap(name, _, _, f) =>
      StateRequirement(List(), signalAtTime.time)
    case RuntimeComponentStateFlatMap(name, _, _, sh, f) =>
      StateRequirement(List(sh), signalAtTime.time)
    case RuntimeComponentMultiState(name, stateHandles, f) =>
      StateRequirement(stateHandles, signalAtTime.time)
  }
}

case class ComputationCompleted(
                                 computation: UnitOfComputation,
                                 newSignals: SignalCollection[AtTime[Signal[_]]],
                                 statesToRelease: SignalCollection[AtTime[Contact[_]]])

case class RunningUnitOfComputation(u: UnitOfComputation, future: Future[ComputationCompleted])


/** A shared state that is used by planner between threads.
  *
  * Computational units and time moments are connected
  * with Happens-before primitive
  *
  * If a function has side effects it should declare the dependency on
  * a state handle "GlobalState". Then the planner will avoid running
  * the function in parallel with other side-effect functions. All other
  * simple functions are considered as side-effect free.
  *
  * The signals are promoted to have `time moment` in order to ensure
  * trellis properties.
  *
  */
class ComputationState(rs: RuntimeSystem,
                       state0: Map[Contact[_], _])(implicit val executionContext: ExecutionContext) {

  private var runningCalculationsSorted = List[RunningUnitOfComputation]()
  private var computationQueueSorted = List[UnitOfComputation]()

  /** Absolute past. No signal can appear before this time. */
  private var pastTimeBoundary = 0
  private var outputs = List[AtTime[Signal[_]]]()

  private var failure: Option[Throwable] = None

  private
  def addComputations(units: List[UnitOfComputation]) {
    this.synchronized {
      computationQueueSorted = units ::: computationQueueSorted
      computationQueueSorted = computationQueueSorted.sortBy(_.signalAtTime.time)
    }
  }

  private
  def addRunningComputation(r: RunningUnitOfComputation) {
    runningCalculationsSorted = r +: runningCalculationsSorted
    runningCalculationsSorted = runningCalculationsSorted.sortBy(_.u.signalAtTime.time)
  }

  private[concurrent] val variables: Map[Contact[_], SafeVariable[_]] =
    state0.map { case (sh, initialValue) => (sh, new SafeVariable(initialValue))}.toMap


  /** Changes context to the given value */
  def resetContext(context: Context) {
    this.synchronized {
      for {sh <- state0.keys} {
        val v = variables(sh).asInstanceOf[SafeVariable[Any]]
        v.update(old => context(sh))
      }
    }
  }

  def getContext: Context =
    this.synchronized {
      state0.keys.
        map(sh =>
        (sh,
          variables(sh).asInstanceOf[SafeVariable[Any]].get)).
        toMap[Contact[_], Any]
    }

  /** Add a single signal.
    *
    * Starts immediate computations if requirements are met.
    * Adds them to running. Those computations that cannot be started immediately
    * will be put to the computation plan. */
  def addSignal(signalAtTime: AtTime[Signal[_]]) {
    val AtTime(time, Signal(contact, _)) = signalAtTime
    if (rs.stopContacts.contains(contact))
      this.synchronized {
        outputs = signalAtTime :: outputs
      }
    else {
      val components = rs.signalProcessors(contact)
      val computationUnits = components.zipWithIndex.map { case (c, i) =>
        val signalForComponent = //AtTime.placeAfter(signalAtTime.time,  signalAtTime.value)//
          signalAtTime.copy(time = time.next(i))
        UnitOfComputation(signalForComponent, c)
      }
      val (stateful, stateless) = computationUnits.partition(_.rc.isStateful)
      stateless.foreach(start)
      addComputations(stateful)
    }
  }

  def addSignals(signalsAtTime: SignalCollection[AtTime[Signal[_]]]) {
    signalsAtTime.foreach(addSignal)
  }

  private
  def start(t: UnitOfComputation) {
    this.synchronized {
      if (failure.isDefined)
        return
      import t._
      t.stateRequirement.stateHandles.foreach { stateHandle =>
        variables(stateHandle).acquire()
      }
      val signal = signalAtTime.value
      val time = signalAtTime.time

      val future = rc match {
        case RuntimeComponentFlatMap(name, _, _, f) =>
          Future {
            val signals = f(signal)
            ComputationCompleted(t, AtTime.placeAfter(time, signals), List())
          }
        case RuntimeComponentStateFlatMap(name, _, _, sh, f) =>
          val v = variables(sh).asInstanceOf[SafeVariable[Any]]
          val f2 = f.asInstanceOf[((Any, Signal[_]) => (Any, List[Signal[_]]))] //(state, signal)=>(state, signals)
          Future {
            val signals = v.update2 {
              oldValue => f2(oldValue, signal)
            }
            ComputationCompleted(t, AtTime.placeAfter(time, signals), List(AtTime(signalAtTime.time.next(-1), sh)))
          }
        case RuntimeComponentMultiState(name, stateHandles, f) =>
          val f2 = f.asInstanceOf[((Context, Signal[_]) => (Context, List[Signal[_]]))] //(state, signal)=>(state, signals)
          Future {
            val context = stateHandles.map(sh =>
              (sh.asInstanceOf[Contact[Any]], variables(sh).get)).toMap.asInstanceOf[Context] //[Contact[_], _]
            val (newContext, signals) = f2(context, signal)
            for ((sh, v) <- newContext.asInstanceOf[Context])
              variables(sh).asInstanceOf[SafeVariable[Any]].update(t => v)
            ComputationCompleted(t, AtTime.placeAfter(time, signals), AtTime.placeAfter(time, stateHandles))
          }
      }
      future.onSuccess {
        case cc => computationCompleted(cc)
      }
      future.onFailure {
        case exception => computationFailed(t, exception)
      }
      addRunningComputation(RunningUnitOfComputation(t, future))
    }
  }

  private
  def computationCompleted(computationCompleted: ComputationCompleted) {
    import computationCompleted._
    addSignals(newSignals) // can be non blocking.
    this.synchronized {
      // TODO O(n)->O(1) (map)
      runningCalculationsSorted = runningCalculationsSorted.filterNot(_.u eq computation)
      statesToRelease.foreach(stateHandleAtTime => variables(stateHandleAtTime.value).release())
    }
    if (statesToRelease.nonEmpty)
      fastCheckPlan() // check plan only if there were states.
  }

  private
  def computationFailed(u: UnitOfComputation, e: Throwable) {
    this.synchronized {
      val rt = new RuntimeException(s"Exception during $u processing.", e)
      rt.fillInStackTrace()
      if (failure.isEmpty)
        failure = Some(rt)
    }
  }

  /**
  Runs computations for which requirements are met.

    During execution we select earlier signals that can be
    processed without violating computation constraints.

    The planner plans simple sequences that can be calculated in a row.

    The planning is performed whenever an executor has
    nothing to do independently. Before planning next steps the independently
    generated trellis is merged into the trunk one.

    Every time when planning is performed, signals that can be processed
    independently are removed from trellis and corresponding tasks are created.

    For links that depend on a state the state is moved to a soft lock list
    with the time moment.
    The state processing is not started until all previous time moments
    have been handled.

    */
  private
  def checkPlan() {
    this.synchronized {
      val times = Int.MaxValue ::
        runningCalculationsSorted.headOption.toList.map(_.u.signalAtTime.time.trellisTime) :::
        computationQueueSorted.headOption.toList.map(_.signalAtTime.time.trellisTime)
      pastTimeBoundary = times.min
      fastCheckPlan()
    }
  }

  /** Do not sort computations and update past time boundary. Just tries to find easy jobs. */
  private
  def fastCheckPlan() {
    this.synchronized {
      val delayed = mutable.ListBuffer[UnitOfComputation]()
      var alreadyRequiredStates = runningCalculationsSorted.flatMap(_.u.stateRequirement.stateHandles).toSet
      for {unit <- computationQueueSorted} {
        import unit._
        import stateRequirement._
        if (stateHandles.isEmpty)
          start(unit)
        else {
          if (time.trellisTime != pastTimeBoundary)
            delayed += unit
          else {
            if (
              stateHandles.forall(sh =>
                !alreadyRequiredStates.contains(sh) &&
                  variables(sh).isLockAvailable)
            )
              start(unit)
            else {
              alreadyRequiredStates = alreadyRequiredStates ++ stateHandles
              delayed += unit
            }
          }
        }
      }
      computationQueueSorted = delayed.toList
    }
  }


  private
  def waitUntilCompleted(atMostPerAwaitableFuture: Duration = Duration.Inf) {
    @tailrec
    def waitUntilCompleted0(): Unit = {
      val (failureIsDefined, runningHeadOpt, computationQueueIsEmpty) = this.synchronized {
        (failure.isDefined, runningCalculationsSorted.headOption, computationQueueSorted.isEmpty)
      }
      if (failureIsDefined || (runningHeadOpt.isEmpty && computationQueueIsEmpty))
        ()
      else {
        if (runningHeadOpt.isDefined)
          Await.result(runningHeadOpt.get.future, atMostPerAwaitableFuture)
        else
          checkPlan()
        waitUntilCompleted0()
      }
    }
    waitUntilCompleted0()
    this.synchronized {
      if (failure.isDefined)
        throw new RuntimeException("Exception in parallel processor", failure.get)
    }
  }

  def runUntilAllOutputSignals: List[Signal[_]] = {
    waitUntilCompleted()
    this.synchronized {
      // access to internals should be synchronized anyway
      outputs.sorted.map(_.value)
    }
  }
}


object ComputationState {
  def runtimeSystemToTotalTrellisProducer(implicit ec: ExecutionContext) =
    (rs: RuntimeSystem) =>
      (context: Context, signal: Signal[_]) => {
        val cs = new ComputationState(rs, context)
        cs.addSignal(AtTime(HTime(None, 0), signal))
        val signals = cs.runUntilAllOutputSignals
        val newContext = cs.getContext
        (newContext, signals)
      }

  implicit class ParRichStaticSystem(system: StaticSystem)(implicit ec: ExecutionContext) {
    /** Converts a system to a parallel RuntimeSystem.
      * Actually, converts inner subsystems to ParallelRuntimeSystem.
      */
    def toParallelRuntimeSystem: RuntimeSystem = {
      SystemConverting.toRuntimeSystem(system, system.outputContacts, runtimeSystemToTotalTrellisProducer)
    }

    def toParallelSimpleSignalProcessor: SimpleSignalProcessor =
      parallel

    def parallel: SimpleSignalProcessor =
      runtimeSystemToTotalTrellisProducer(ec)(toParallelRuntimeSystem).toSimpleSignalProcessor(system.s0)

    /**
     * The resulting Dynamic system will work in background. However,
     * it will block until all computations have been completed.
     */
    def toParallelDynamicSystem: DynamicSystem =
      DynamicSystem(system.inputContacts, system.outputContacts, system.name,
        toParallelSimpleSignalProcessor, system.index)

  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy