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

de.sciss.fscape.stream.Control.scala Maven / Gradle / Ivy

There is a newer version: 2.28.0
Show newest version
/*
 *  Control.scala
 *  (FScape)
 *
 *  Copyright (c) 2001-2016 Hanns Holger Rutz. All rights reserved.
 *
 *  This software is published under the GNU General Public License v2+
 *
 *
 *  For further information, please contact Hanns Holger Rutz at
 *  [email protected]
 */

package de.sciss.fscape
package stream

import java.util.concurrent.ConcurrentLinkedQueue

import akka.actor.{Actor, ActorRef, ActorSystem, Props}
import akka.stream.{ActorMaterializer, Materializer}
import de.sciss.file.File

import scala.collection.mutable
import scala.concurrent.{ExecutionContext, Future, Promise}
import scala.language.implicitConversions
import scala.util.{Random, Success}

object Control {
  trait ConfigLike {
    /** The block size for buffers send between nodes. */
    def blockSize: Int

    /** Whether to isolate nodes through an asynchronous barrier. */
    def useAsync: Boolean

    /** The recommended maximum number of frames internally
      * stored in memory by a single node (UGen). Some UGen
      * may allow for ad-hoc disk buffering and thus can check
      * this user-defined value to decide whether to keep a buffer
      * in memory or swap it to disk.
      *
      * Note that because a frame is considered to be a `Double`,
      * the actual memory size will be eight times this value.
      */
    def nodeBufferSize: Int

    /** Random number generator seed. */
    def seed: Long

    def actorSystem: ActorSystem

    /** Whether to terminate the `actorSystem` when the rendering
      * completes. When using the default `actorSystem`, this will
      * be `true`, when setting a custom actor system, this will
      * default to `false`.
      */
    def terminateActors: Boolean

    def materializer: Materializer

    def executionContext: ExecutionContext

    def progressReporter: ProgressReporter
  }

  object Config {
    def apply() = new ConfigBuilder
    implicit def build(b: ConfigBuilder): Config = b.build
  }

  final case class Config(blockSize: Int, nodeBufferSize: Int,
                          useAsync: Boolean, seed: Long, actorSystem: ActorSystem,
                          materializer0: Materializer,
                          executionContext0: ExecutionContext,
                          progressReporter: ProgressReporter,
                          terminateActors: Boolean
                         )
    extends ConfigLike {

    implicit def materializer    : Materializer     = materializer0
    implicit def executionContext: ExecutionContext = executionContext0
  }

  final class ConfigBuilder extends ConfigLike {
    /** The default block size is 1024. */
    var blockSize: Int = 1024

    /** The default internal node buffer size is 65536. */
    var nodeBufferSize: Int = 65536

    /** The default behavior is to isolate blocking nodes
      * into a separate graph. This should usually be the case.
      * It can be disabled for debugging purposes, for example
      * in order to allow the debug printer to create an entire
      * GraphViz representation.
      */
    var useAsync: Boolean = true

    private[this] var _seed = Option.empty[Long]
    private[this] lazy val defaultSeed: Long = System.currentTimeMillis()

    def seed: Long = _seed.getOrElse {
      _seed = Some(defaultSeed)
      defaultSeed
    }

    def seed_=(value: Long): Unit = _seed = Some(value)

    private[this] var _actor: ActorSystem = _
    private[this] var _actorSet           = false
    private[this] lazy val defaultActor: ActorSystem = ActorSystem("fscape")

    def actorSystem: ActorSystem = if (_actorSet) _actor else defaultActor

    def actorSystem_=(value: ActorSystem): Unit = {
      _actor    = value
      _actorSet = true
    }

    private[this] var _terminateActors: Boolean = _
    private[this] var _terminateSet             = false

    def terminateActors: Boolean = if (_terminateSet) _terminateActors else !_actorSet

    def terminateActors_=(value: Boolean): Unit = {
      _terminateActors  = value
      _terminateSet     = true
    }

    private[this] var _mat: Materializer = _
    private[this] lazy val defaultMat: Materializer = ActorMaterializer()(actorSystem)

    def materializer: Materializer = {
      if (_mat == null) _mat = defaultMat
      _mat
    }
    def materializer_=(value: Materializer): Unit =
      _mat = value

    private[this] var _exec: ExecutionContext = _

    def executionContext: ExecutionContext = {
      if (_exec == null) _exec = ExecutionContext.Implicits.global
      _exec
    }
    def executionContext_=(value: ExecutionContext): Unit =
      _exec = value

    var progressReporter: ProgressReporter = NoReport

    def build = Config(
      blockSize         = blockSize,
      nodeBufferSize    = nodeBufferSize,
      useAsync          = useAsync,
      seed              = seed,
      actorSystem       = actorSystem,
      terminateActors   = terminateActors,
      materializer0     = materializer,
      executionContext0 = executionContext,
      progressReporter  = progressReporter
    )
  }

  def apply(config: Config = Config()): Control = new Impl(config)

  implicit def fromBuilder(implicit b: Builder): Control = b.control

//  final case class ProgressPart(label: String, frac: Double)

  final case class ProgressReport(total: Double, partLabel: String, part: Double)

  type ProgressReporter = ProgressReport => Unit

  final val NoReport: ProgressReporter = { _ => () }

  final case class Stats(numBufD: Int, numBufI: Int, numBufL: Int, numNodes: Int)

  // ------------------------

  private final class Impl(val config: Config) extends AbstractImpl {
    protected def expand(graph: Graph): UGenGraph = UGenGraph.build(graph)(this)
  }

  private[fscape] trait AbstractImpl extends Control {
    // ---- abstract ----

    protected def expand(graph: Graph): UGenGraph

    // ---- impl ----

    override def toString = s"Control@${hashCode().toHexString}"

    final def blockSize     : Int = config.blockSize
    final def nodeBufferSize: Int = config.nodeBufferSize

    private[this] val queueD      = new ConcurrentLinkedQueue[BufD]
    private[this] val queueI      = new ConcurrentLinkedQueue[BufI]
    private[this] val queueL      = new ConcurrentLinkedQueue[BufL]
    private[this] val nodes       = mutable.Set.empty[Node]
    private[this] val sync        = new AnyRef
    private[this] val statusP     = Promise[Unit]()
    private[this] var _actor      = null : ActorRef
    private[this] val metaRand    = new Random(config.seed)
    private[this] var _progLabels = Array.empty[String]
    private[this] var _progParts  = Array.empty[Double]
    private[this] val _progHasRep = config.progressReporter ne NoReport

    final def debugDotGraph(): Unit = akka.stream.sciss.Util.debugDotGraph()(config.materializer)

    final def run(graph: Graph): UGenGraph = {
      val ugens = expand(graph)
      runExpanded(ugens)
      ugens
    }

    private final class ActorImpl extends Actor {
      def receive = {
        case SetProgress(key, frac) =>
          val pf    = _progParts
          val clip  = if (frac < 0.0) 0.0 else if (frac > 1.0) 1.0 else frac
          if (pf(key) != clip) {
            pf(key)     = clip
            if (_progHasRep) {
              var i     = 0
              var total = 0.0
              while (i < pf.length) {
                total += pf(i)
                i += 1
              }
              if (total > 1.0) total = 1.0
              val report = ProgressReport(total = total, partLabel = _progLabels(key), part = clip)
              config.progressReporter(report)
            }
          }

        case RemoveNode(n) =>
          if (nodes.remove(n)) {
            // XXX TODO --- check for error other than `Cancelled` --- how?
            if (nodes.isEmpty) {
              statusP.tryComplete(Success(()))
              context.stop(self)
              if (config.terminateActors) config.actorSystem.terminate()
            }
          } else {
            Console.err.println(s"Warning: node $n was not registered with Control")
          }

        case Cancel =>
          val ex = Cancelled()
          statusP.tryFailure(ex)
          nodes.foreach { n =>
            n.failAsync(ex)
          }
      }
    }

    private def mkActor(): Unit = sync.synchronized {
      require(_actor == null)
      _actor          = config.actorSystem.actorOf(Props(new ActorImpl))
    }

    final def runExpanded(ugens: UGenGraph): Unit = {
      val r = ugens.runnable
      mkActor()
      r.run()(config.materializer)
    }

    final def mkRandom(): Random = /* sync.synchronized */ {
      new Random(metaRand.nextLong())
    }

    final def mkProgress(label: String): Int = /* sync.synchronized */ {
      val res = _progLabels.length
      _progLabels :+= label
      _progParts  :+= 0.0
      res
    }

    final def setProgress(key: Int, frac: Double): Unit =
      if (!frac.isNaN) _actor ! SetProgress(key = key, frac = frac)

    final def borrowBufD(): BufD = {
      val res0 = queueD.poll()
      val res  = if (res0 == null) BufD.alloc(blockSize) else {
        res0.acquire()
        res0.size = res0.buf.length
        res0
      }
      // println(s"Control.borrowBufD(): $res / ${res.allocCount()}")
      // assert(res.allocCount() == 1, res.allocCount())
      res
    }

    final def returnBufD(buf: BufD): Unit = {
      require(buf.allocCount() == 0)
      // println(s"control: ${buf.hashCode.toHexString} - ${buf.buf.toVector.hashCode.toHexString}")
      queueD.offer(buf) // XXX TODO -- limit size?
    }

    final def borrowBufI(): BufI = {
      val res0 = queueI.poll()
      if (res0 == null) BufI.alloc(blockSize) else {
        res0.acquire()
        res0.size = res0.buf.length
        res0
      }
    }

    final def returnBufI(buf: BufI): Unit = {
      require(buf.allocCount() == 0)
      queueI.offer(buf) // XXX TODO -- limit size?
    }

    final def borrowBufL(): BufL = {
      val res0 = queueL.poll()
      if (res0 == null) BufL.alloc(blockSize) else {
        res0.acquire()
        res0.size = res0.buf.length
        res0
      }
    }

    final def returnBufL(buf: BufL): Unit = {
      require(buf.allocCount() == 0)
      queueL.offer(buf) // XXX TODO -- limit size?
    }

    // called during materialization, no sync needed
    final private[stream] def addNode   (n: Node): Unit = nodes += n
    // called during run, have to relay using actor
    final private[stream] def removeNode(n: Node): Unit = _actor ! RemoveNode(n)

    final def status: Future[Unit] = statusP.future

    final def cancel(): Unit = sync.synchronized {
      if (_actor != null) _actor ! Cancel
    }

    // XXX TODO --- should be in the actor body
    final def stats = {
      val res = Stats(numBufD = queueD.size(), numBufI = queueI.size(), numBufL = queueL.size(),
        numNodes = nodes.size)
      println("--- NODES: ---")
      nodes.foreach { n =>
        println(n)
//        val gs   = n.asInstanceOf[GraphStageLogic]
//        val conn = akka.stream.sciss.Util.portToConn(gs)
//        val ni   = n.asInstanceOf[NodeImpl[Shape]]
//        val ins  = ni.shape.inlets
//        val numIns = ins.size
//        val outs = ni.shape.outlets
//        val numOuts = outs.size
//        println("  ins:")
//        (ins zip conn.take(numIns)).foreach { case (in, con) =>
//          println(s"   $in - $con")
//        }
//        println("  outs:")
//        (outs zip conn.takeRight(numOuts)).foreach { case (out, con) =>
//            println(s"   $out - $con")
//        }
      }
      res
    }

    final def createTempFile(): File = File.createTemp()
  }

  // ---- actor messages

  private final case class RemoveNode(node: Node)
  private case object Cancel
  private final case class SetProgress(key: Int, frac: Double)
}
trait Control {
  /** Global buffer size. The guaranteed size of the double and integer arrays.
    * A shortcut for `config.bufSize`.
    */
  def blockSize: Int

  /** A shortcut for `config.nodeBufferSize`.
    */
  def nodeBufferSize: Int

  /** Borrows a double buffer. Its size is reset to `bufSize`. */
  def borrowBufD(): BufD

  /** Borrows an integer buffer. Its size is reset to `bufSize`. */
  def borrowBufI(): BufI

  /** Borrows a long buffer. Its size is reset to `bufSize`. */
  def borrowBufL(): BufL

  /** Returns a double buffer. When `buf.borrowed` is `false`, this is a no-op.
    * This should never be called directly but only by the buffer itself
    * through `buf.release()`.
    */
  def returnBufD(buf: BufD): Unit

  /** Returns an integer buffer. When `buf.borrowed` is `false`, this is a no-op.
    * This should never be called directly but only by the buffer itself
    * through `buf.release()`.
    */
  def returnBufI(buf: BufI): Unit

  /** Returns an integer buffer. When `buf.borrowed` is `false`, this is a no-op.
    * This should never be called directly but only by the buffer itself
    * through `buf.release()`.
    */
  def returnBufL(buf: BufL): Unit

  /** Adds a node of a stage logic. Must be called during materialization. */
  private[stream] def addNode(n: Node): Unit

  /** Removes a finished node of a stage logic. */
  private[stream] def removeNode(n: Node): Unit

  /** Registers a progress reporter. */
  private[stream] def mkProgress(label: String): Int

  /** Reports the progress for a particular instance. */
  private[stream] def setProgress(key: Int, frac: Double): Unit

  /** Creates a temporary file. The caller is responsible to deleting the file
    * after it is not needed any longer. (The file will still be marked `deleteOnExit`)
    */
  def createTempFile(): File

  /** Cancels the process. This works by cancelling all registered leaves. If the graph
    * is correctly constructed, this should shut down all connected trees from there automatically.
    */
  def cancel(): Unit

  /** Creates an aggregated `Future` over the state of the graph.
    * In the case of cancelling the graph, the result will be `Failure(Cancelled())`.
    */
  def status: Future[Unit]

  def stats: Control.Stats

  val config: Control.Config

  def debugDotGraph(): Unit

  /** Expands with default builder and then runs the graph. */
  def run(graph: Graph): UGenGraph

  /** Runs an already expanded graph. */
  def runExpanded(ugens: UGenGraph): Unit

  def mkRandom(): Random
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy