Search code examples
scalaakkaakka-streamakka-http

How does one measure throughput of Akka WebSocket stream?


I am new to Akka and developed a sample Akka WebSocket server that streams a file's contents to clients using BroadcastHub (based on a sample from the Akka docs).

How can I measure the throughput (messages/second), assuming the clients are consuming as fast as the server?

// file source
val fileSource = FileIO.fromPath(Paths.get(path)

// Akka file source
val theFileSource = fileSource
  .toMat(BroadcastHub.sink)(Keep.right)
  .run
//Akka kafka file source
lazy val kafkaSourceActorStream = {

val (kafkaSourceActorRef, kafkaSource) = Source.actorRef[String](Int.MaxValue, OverflowStrategy.fail)
  .toMat(BroadcastHub.sink)(Keep.both).run()

Consumer.plainSource(consumerSettings, Subscriptions.topics("perf-test-topic"))
  .runForeach(record => kafkaSourceActorRef ! record.value().toString)
}

def logicFlow: Flow[String, String, NotUsed] = Flow.fromSinkAndSource(Sink.ignore, theFileSource)

val websocketFlow: Flow[Message, Message, Any] = {
  Flow[Message]
    .collect {
      case TextMessage.Strict(msg) => Future.successful(msg)
      case _ => println("ignore streamed message")
    }
    .mapAsync(parallelism = 2)(identity)
    .via(logicFlow)
    .map { msg: String => TextMessage.Strict(msg) }
  }

val fileRoute =
  path("file") {
    handleWebSocketMessages(websocketFlow)
  }
}

def startServer(): Unit = {
  bindingFuture = Http().bindAndHandle(wsRoutes, HOST, PORT)
  log.info(s"Server online at http://localhost:9000/")
}

def stopServer(): Unit = {
  bindingFuture
   .flatMap(_.unbind())
   .onComplete{
    _ => system.terminate()
      log.info("terminated")
  }
}
//ws client
def connectToWebSocket(url: String) = {
 println("Connecting to websocket: " + url)

 val (upgradeResponse, closed) = Http().singleWebSocketRequest(WebSocketRequest(url), websocketFlow)

 val connected = upgradeResponse.flatMap{ upgrade =>

   if(upgrade.response.status == StatusCodes.SwitchingProtocols )
  {
    println("Web socket connection success")
    Future.successful(Done)

  }else {
     println("Web socket connection failed with error: {}", upgrade.response.status)
     throw new RuntimeException(s"Web socket connection failed: ${upgrade.response.status}")
   }
}

connected.onComplete { msg =>
    println(msg)
 }         
}
def websocketFlow: Flow[Message, Message, _] = { 
 Flow.fromSinkAndSource(printFlowRate, Source.maybe)
}

lazy val printFlowRate  =
 Flow[Message]    
  .alsoTo(fileSink("output.txt"))
  .via(flowRate(1.seconds))
  .to(Sink.foreach(rate => println(s"$rate")))

def flowRate(sampleTime: FiniteDuration) =
 Flow[Message]
  .conflateWithSeed(_ ⇒ 1){ case (acc, _) ⇒ acc + 1 }
  .zip(Source.tick(sampleTime, sampleTime, NotUsed))
  .map(_._1.toDouble / sampleTime.toUnit(SECONDS))

def fileSink(file: String): Sink[Message, Future[IOResult]] = {
 Flow[Message]
  .map{
    case TextMessage.Strict(msg) => msg
    case TextMessage.Streamed(stream) => stream.runFold("")(_ + _).flatMap(msg => Future.successful(msg))
  }
  .map(s => ByteString(s + "\n"))
  .toMat(FileIO.toFile(new File(file)))(Keep.right)
}

Solution

  • You could attach a throughput-measuring stream to your existing stream. Here is an example, inspired by this answer, that prints the number of integers that are emitted from the upstream source every second:

    val rateSink = Flow[Int]
      .conflateWithSeed(_ => 0){ case (acc, _) => acc + 1 }
      .zip(Source.tick(1.second, 1.second, NotUsed))
      .map(_._1)
      .toMat(Sink.foreach(i => println(s"$i elements/second")))(Keep.right)
    

    In the following example, we attach the above sink to a source that emits the integers 1 to 10 million. To prevent the rate-measuring stream from interfering with the main stream (which, in this case, simply converts every integer to a string and returns the last string processed as part of the materialized value), we use wireTapMat:

    val (rateFut, mainFut) = Source(1 to 10000000)
      .wireTapMat(rateSink)(Keep.right)
      .map(_.toString)
      .toMat(Sink.last[String])(Keep.both)
      .run() // (Future[Done], Future[String])
    
    rateFut onComplete {
      case Success(x) => println(s"rateFut completed: $x")
      case Failure(_) =>
    }
    
    mainFut onComplete {
      case Success(s) => println(s"mainFut completed: $s")
      case Failure(_) =>
    }
    

    Running the above sample prints something like the following:

    0 elements/second
    2597548 elements/second
    3279052 elements/second
    mainFut completed: 10000000
    3516141 elements/second
    607254 elements/second
    rateFut completed: Done
    

    If you don't need a reference to the materialized value of rateSink, use wireTap instead of wireTapMat. For example, attaching rateSink to your WebSocket flow could look like the following:

    val websocketFlow: Flow[Message, Message, Any] = {
      Flow[Message]
        .wireTap(rateSink) // <---
        .collect {
          case TextMessage.Strict(msg) => Future.successful(msg)
          case _ => println("ignore streamed message")
        }
        .mapAsync(parallelism = 2)(identity)
        .via(logicFlow)
        .map { msg: String => TextMessage.Strict(msg) }
      }
    

    wireTap is defined on both Source and Flow.