diff --git a/.gitignore b/.gitignore index 4085dcb..0c2585b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ pony.exe *.pdb */build/ .idea/ +/akka/target diff --git a/akka/.scalafmt.conf b/akka/.scalafmt.conf new file mode 100644 index 0000000..8aad45f --- /dev/null +++ b/akka/.scalafmt.conf @@ -0,0 +1,3 @@ +version = 2.6.4 +align.preset = more +maxColumn = 80 \ No newline at end of file diff --git a/akka/README.md b/akka/README.md new file mode 100644 index 0000000..26e0063 --- /dev/null +++ b/akka/README.md @@ -0,0 +1,9 @@ +# ChatApp Benchmaek Akka + +## Build from source +``` +./sbt update +./sbt compile +./sbt stage +./target/universal/stage/bin/chat-app +``` diff --git a/akka/build.sbt b/akka/build.sbt new file mode 100644 index 0000000..0fb6b1e --- /dev/null +++ b/akka/build.sbt @@ -0,0 +1,21 @@ +enablePlugins(JavaAppPackaging) + +name := "chat-app" +version := "1.0" +scalaVersion := "2.13.1" +organization := "chatapp" +val akkaVersion = "2.6.8" + +scalacOptions ++= Seq( + "-feature", + "-deprecation", + "-Xfatal-warnings" +) + +libraryDependencies ++= Seq( + "com.typesafe.akka" %% "akka-actor-typed" % akkaVersion, + "ch.qos.logback" % "logback-classic" % "1.2.3", +) + +mainClass in Compile := Some("chatapp.Main") +discoveredMainClasses in Compile := Seq() diff --git a/akka/project/build.properties b/akka/project/build.properties new file mode 100644 index 0000000..00b48d9 --- /dev/null +++ b/akka/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.3.6 diff --git a/akka/project/plugins.sbt b/akka/project/plugins.sbt new file mode 100644 index 0000000..7c51efc --- /dev/null +++ b/akka/project/plugins.sbt @@ -0,0 +1,2 @@ +addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.7.4") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.0") diff --git a/akka/sbt b/akka/sbt new file mode 100755 index 0000000..efdfda6 --- /dev/null +++ b/akka/sbt @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +./sbt-dist/bin/sbt "$@" \ No newline at end of file diff --git a/akka/sbt-dist/bin/sbt b/akka/sbt-dist/bin/sbt new file mode 100755 index 0000000..48d1ff9 --- /dev/null +++ b/akka/sbt-dist/bin/sbt @@ -0,0 +1,151 @@ +#!/usr/bin/env bash + + +### ------------------------------- ### +### Helper methods for BASH scripts ### +### ------------------------------- ### + +realpath () { +( + TARGET_FILE="$1" + FIX_CYGPATH="$2" + + cd "$(dirname "$TARGET_FILE")" + TARGET_FILE=$(basename "$TARGET_FILE") + + COUNT=0 + while [ -L "$TARGET_FILE" -a $COUNT -lt 100 ] + do + TARGET_FILE=$(readlink "$TARGET_FILE") + cd "$(dirname "$TARGET_FILE")" + TARGET_FILE=$(basename "$TARGET_FILE") + COUNT=$(($COUNT + 1)) + done + + # make sure we grab the actual windows path, instead of cygwin's path. + if [[ "x$FIX_CYGPATH" != "x" ]]; then + echo "$(cygwinpath "$(pwd -P)/$TARGET_FILE")" + else + echo "$(pwd -P)/$TARGET_FILE" + fi +) +} + + +# Uses uname to detect if we're in the odd cygwin environment. +is_cygwin() { + local os=$(uname -s) + case "$os" in + CYGWIN*) return 0 ;; + *) return 1 ;; + esac +} + +# TODO - Use nicer bash-isms here. +CYGWIN_FLAG=$(if is_cygwin; then echo true; else echo false; fi) + + +# This can fix cygwin style /cygdrive paths so we get the +# windows style paths. +cygwinpath() { + local file="$1" + if [[ "$CYGWIN_FLAG" == "true" ]]; then + echo $(cygpath -w $file) + else + echo $file + fi +} + +. "$(dirname "$(realpath "$0")")/sbt-launch-lib.bash" + + +declare -r noshare_opts="-Dsbt.global.base=project/.sbtboot -Dsbt.boot.directory=project/.boot -Dsbt.ivy.home=project/.ivy" +declare -r sbt_opts_file=".sbtopts" +declare -r etc_sbt_opts_file="${sbt_home}/conf/sbtopts" +declare -r win_sbt_opts_file="${sbt_home}/conf/sbtconfig.txt" + +usage() { + cat < path to global settings/plugins directory (default: ~/.sbt) + -sbt-boot path to shared boot directory (default: ~/.sbt/boot in 0.11 series) + -ivy path to local Ivy repository (default: ~/.ivy2) + -mem set memory options (default: $sbt_mem, which is $(get_mem_opts $sbt_mem)) + -no-share use all local caches; no sharing + -no-global uses global caches, but does not use global ~/.sbt directory. + -jvm-debug Turn on JVM debugging, open at the given port. + -batch Disable interactive mode + + # sbt version (default: from project/build.properties if present, else latest release) + -sbt-version use the specified version of sbt + -sbt-jar use the specified jar as the sbt launcher + -sbt-rc use an RC version of sbt + -sbt-snapshot use a snapshot version of sbt + + # java version (default: java from PATH, currently $(java -version 2>&1 | grep version)) + -java-home alternate JAVA_HOME + + # jvm options and output control + JAVA_OPTS environment variable, if unset uses "$java_opts" + SBT_OPTS environment variable, if unset uses "$default_sbt_opts" + .sbtopts if this file exists in the current directory, it is + prepended to the runner args + /etc/sbt/sbtopts if this file exists, it is prepended to the runner args + -Dkey=val pass -Dkey=val directly to the java runtime + -J-X pass option -X directly to the java runtime + (-J is stripped) + -S-X add -X to sbt's scalacOptions (-S is stripped) + +In the case of duplicated or conflicting options, the order above +shows precedence: JAVA_OPTS lowest, command line options highest. +EOM +} + + + +process_my_args () { + while [[ $# -gt 0 ]]; do + case "$1" in + -no-colors) addJava "-Dsbt.log.noformat=true" && shift ;; + -no-share) addJava "$noshare_opts" && shift ;; + -no-global) addJava "-Dsbt.global.base=$(pwd)/project/.sbtboot" && shift ;; + -sbt-boot) require_arg path "$1" "$2" && addJava "-Dsbt.boot.directory=$2" && shift 2 ;; + -sbt-dir) require_arg path "$1" "$2" && addJava "-Dsbt.global.base=$2" && shift 2 ;; + -debug-inc) addJava "-Dxsbt.inc.debug=true" && shift ;; + -batch) exec &2 "$@" +} +vlog () { + [[ $verbose || $debug ]] && echoerr "$@" +} +dlog () { + [[ $debug ]] && echoerr "$@" +} + +jar_file () { + echo "$(cygwinpath "${sbt_home}/bin/sbt-launch.jar")" +} + +acquire_sbt_jar () { + sbt_jar="$(jar_file)" + + if [[ ! -f "$sbt_jar" ]]; then + echoerr "Could not find launcher jar: $sbt_jar" + exit 2 + fi +} + +execRunner () { + # print the arguments one to a line, quoting any containing spaces + [[ $verbose || $debug ]] && echo "# Executing command line:" && { + for arg; do + if printf "%s\n" "$arg" | grep -q ' '; then + printf "\"%s\"\n" "$arg" + else + printf "%s\n" "$arg" + fi + done + echo "" + } + + # THis used to be exec, but we loose the ability to re-hook stty then + # for cygwin... Maybe we should flag the feature here... + "$@" +} + +addJava () { + dlog "[addJava] arg = '$1'" + java_args=( "${java_args[@]}" "$1" ) +} +addSbt () { + dlog "[addSbt] arg = '$1'" + sbt_commands=( "${sbt_commands[@]}" "$1" ) +} +addResidual () { + dlog "[residual] arg = '$1'" + residual_args=( "${residual_args[@]}" "$1" ) +} +addDebugger () { + addJava "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$1" +} + +get_mem_opts () { + # if we detect any of these settings in ${JAVA_OPTS} or ${JAVA_TOOL_OPTIONS} we need to NOT output our settings. + # The reason is the Xms/Xmx, if they don't line up, cause errors. + if [[ "${JAVA_OPTS}" == *-Xmx* ]] || [[ "${JAVA_OPTS}" == *-Xms* ]] || [[ "${JAVA_OPTS}" == *-XX:MaxPermSize* ]] || [[ "${JAVA_OPTS}" == *-XX:MaxMetaspaceSize* ]] || [[ "${JAVA_OPTS}" == *-XX:ReservedCodeCacheSize* ]]; then + echo "" + elif [[ "${JAVA_TOOL_OPTIONS}" == *-Xmx* ]] || [[ "${JAVA_TOOL_OPTIONS}" == *-Xms* ]] || [[ "${JAVA_TOOL_OPTIONS}" == *-XX:MaxPermSize* ]] || [[ "${JAVA_TOOL_OPTIONS}" == *-XX:MaxMetaspaceSize* ]] || [[ "${JAVA_TOOL_OPTIONS}" == *-XX:ReservedCodeCacheSize* ]]; then + echo "" + elif [[ "${SBT_OPTS}" == *-Xmx* ]] || [[ "${SBT_OPTS}" == *-Xms* ]] || [[ "${SBT_OPTS}" == *-XX:MaxPermSize* ]] || [[ "${SBT_OPTS}" == *-XX:MaxMetaspaceSize* ]] || [[ "${SBT_OPTS}" == *-XX:ReservedCodeCacheSize* ]]; then + echo "" + else + # a ham-fisted attempt to move some memory settings in concert + # so they need not be messed around with individually. + local mem=${1:-1024} + local codecache=$(( $mem / 8 )) + (( $codecache > 128 )) || codecache=128 + (( $codecache < 512 )) || codecache=512 + local class_metadata_size=$(( $codecache * 2 )) + local class_metadata_opt=$([[ "$java_version" < "1.8" ]] && echo "MaxPermSize" || echo "MaxMetaspaceSize") + + local arg_xms=$([[ "${java_args[@]}" == *-Xms* ]] && echo "" || echo "-Xms${mem}m") + local arg_xmx=$([[ "${java_args[@]}" == *-Xmx* ]] && echo "" || echo "-Xmx${mem}m") + local arg_rccs=$([[ "${java_args[@]}" == *-XX:ReservedCodeCacheSize* ]] && echo "" || echo "-XX:ReservedCodeCacheSize=${codecache}m") + local arg_meta=$([[ "${java_args[@]}" == *-XX:${class_metadata_opt}* ]] && echo "" || echo "-XX:${class_metadata_opt}=${class_metadata_size}m") + + echo "${arg_xms} ${arg_xmx} ${arg_rccs} ${arg_meta}" + fi +} + +require_arg () { + local type="$1" + local opt="$2" + local arg="$3" + if [[ -z "$arg" ]] || [[ "${arg:0:1}" == "-" ]]; then + echo "$opt requires <$type> argument" + exit 1 + fi +} + +is_function_defined() { + declare -f "$1" > /dev/null +} + +process_args () { + while [[ $# -gt 0 ]]; do + case "$1" in + -h|-help) usage; exit 1 ;; + -v|-verbose) verbose=1 && shift ;; + -d|-debug) debug=1 && shift ;; + + -ivy) require_arg path "$1" "$2" && addJava "-Dsbt.ivy.home=$2" && shift 2 ;; + -mem) require_arg integer "$1" "$2" && sbt_mem="$2" && shift 2 ;; + -jvm-debug) require_arg port "$1" "$2" && addDebugger $2 && shift 2 ;; + -batch) exec &1 | awk -F '"' '/version/ {print $2}') + vlog "[process_args] java_version = '$java_version'" +} + +# Detect that we have java installed. +checkJava() { + local required_version="$1" + # Now check to see if it's a good enough version + if [[ "$java_version" == "" ]]; then + echo + echo No java installations was detected. + echo Please go to http://www.java.com/getjava/ and download + echo + exit 1 + elif [[ ! "$java_version" > "$required_version" ]]; then + echo + echo The java installation you have is not up to date + echo $script_name requires at least version $required_version+, you have + echo version $java_version + echo + echo Please go to http://www.java.com/getjava/ and download + echo a valid Java Runtime and install before running $script_name. + echo + exit 1 + fi +} + + +run() { + # no jar? download it. + [[ -f "$sbt_jar" ]] || acquire_sbt_jar "$sbt_version" || { + # still no jar? uh-oh. + echo "Download failed. Obtain the sbt-launch.jar manually and place it at $sbt_jar" + exit 1 + } + + # process the combined args, then reset "$@" to the residuals + process_args "$@" + set -- "${residual_args[@]}" + argumentCount=$# + + # TODO - java check should be configurable... + checkJava "1.6" + + #If we're in cygwin, we should use the windows config, and terminal hacks + if [[ "$CYGWIN_FLAG" == "true" ]]; then + stty -icanon min 1 -echo > /dev/null 2>&1 + addJava "-Djline.terminal=jline.UnixTerminal" + addJava "-Dsbt.cygwin=true" + fi + + # run sbt + execRunner "$java_cmd" \ + $(get_mem_opts $sbt_mem) \ + ${JAVA_OPTS} \ + ${SBT_OPTS:-$default_sbt_opts} \ + ${java_args[@]} \ + -jar "$sbt_jar" \ + "${sbt_commands[@]}" \ + "${residual_args[@]}" + + exit_code=$? + + # Clean up the terminal from cygwin hacks. + if [[ "$CYGWIN_FLAG" == "true" ]]; then + stty icanon echo > /dev/null 2>&1 + fi + exit $exit_code +} diff --git a/akka/sbt-dist/bin/sbt-launch.jar b/akka/sbt-dist/bin/sbt-launch.jar new file mode 100644 index 0000000..307fc22 Binary files /dev/null and b/akka/sbt-dist/bin/sbt-launch.jar differ diff --git a/akka/sbt-dist/bin/sbt.bat b/akka/sbt-dist/bin/sbt.bat new file mode 100644 index 0000000..0b70781 --- /dev/null +++ b/akka/sbt-dist/bin/sbt.bat @@ -0,0 +1,53 @@ +@REM SBT launcher script +@REM +@REM Envioronment: +@REM JAVA_HOME - location of a JDK home dir (mandatory) +@REM SBT_OPTS - JVM options (optional) +@REM Configuration: +@REM sbtconfig.txt found in the SBT_HOME. + +@REM ZOMG! We need delayed expansion to build up CFG_OPTS later +@setlocal enabledelayedexpansion + +@echo off +set SBT_HOME=%~dp0 + +rem FIRST we load the config file of extra options. +set FN=%SBT_HOME%\..\conf\sbtconfig.txt +set CFG_OPTS= +FOR /F "tokens=* eol=# usebackq delims=" %%i IN ("%FN%") DO ( + set DO_NOT_REUSE_ME=%%i + rem ZOMG (Part #2) WE use !! here to delay the expansion of + rem CFG_OPTS, otherwise it remains "" for this loop. + set CFG_OPTS=!CFG_OPTS! !DO_NOT_REUSE_ME! +) + +rem We use the value of the JAVACMD environment variable if defined +set _JAVACMD=%JAVACMD% + +if "%_JAVACMD%"=="" ( + if not "%JAVA_HOME%"=="" ( + if exist "%JAVA_HOME%\bin\java.exe" set "_JAVACMD=%JAVA_HOME%\bin\java.exe" + ) +) + +if "%_JAVACMD%"=="" set _JAVACMD=java + +rem We use the value of the JAVA_OPTS environment variable if defined, rather than the config. +set _JAVA_OPTS=%JAVA_OPTS% +if "%_JAVA_OPTS%"=="" set _JAVA_OPTS=%CFG_OPTS% + +:run + +"%_JAVACMD%" %_JAVA_OPTS% %SBT_OPTS% -cp "%SBT_HOME%sbt-launch.jar" xsbt.boot.Boot %* +if ERRORLEVEL 1 goto error +goto end + +:error +@endlocal +exit /B 1 + + +:end +@endlocal +exit /B 0 diff --git a/akka/sbt-dist/conf/sbtconfig.txt b/akka/sbt-dist/conf/sbtconfig.txt new file mode 100644 index 0000000..a4da43e --- /dev/null +++ b/akka/sbt-dist/conf/sbtconfig.txt @@ -0,0 +1,14 @@ +# Set the java args to high + +-Xmx512M + +-XX:MaxPermSize=256m + +-XX:ReservedCodeCacheSize=128m + + + +# Set the extra SBT options + +-Dsbt.log.format=true + diff --git a/akka/sbt-dist/conf/sbtopts b/akka/sbt-dist/conf/sbtopts new file mode 100644 index 0000000..f018465 --- /dev/null +++ b/akka/sbt-dist/conf/sbtopts @@ -0,0 +1,49 @@ +# ------------------------------------------------ # +# The SBT Configuration file. # +# ------------------------------------------------ # + + +# Disable ANSI color codes +# +#-no-colors + +# Starts sbt even if the current directory contains no sbt project. +# +-sbt-create + +# Path to global settings/plugins directory (default: ~/.sbt) +# +#-sbt-dir /etc/sbt + +# Path to shared boot directory (default: ~/.sbt/boot in 0.11 series) +# +#-sbt-boot ~/.sbt/boot + +# Path to local Ivy repository (default: ~/.ivy2) +# +#-ivy ~/.ivy2 + +# set memory options +# +#-mem + +# Use local caches for projects, no sharing. +# +#-no-share + +# Put SBT in offline mode. +# +#-offline + +# Sets the SBT version to use. +#-sbt-version 0.11.3 + +# Scala version (default: latest release) +# +#-scala-home +#-scala-version + +# java version (default: java from PATH, currently $(java -version |& grep version)) +# +#-java-home + diff --git a/akka/sbt.bat b/akka/sbt.bat new file mode 100755 index 0000000..d199a1c --- /dev/null +++ b/akka/sbt.bat @@ -0,0 +1,3 @@ +@REM SBT launcher script + +.\sbt-dist\bin\sbt.bat %* diff --git a/akka/src/main/scala/chatapp/Chat.scala b/akka/src/main/scala/chatapp/Chat.scala new file mode 100644 index 0000000..5a27c01 --- /dev/null +++ b/akka/src/main/scala/chatapp/Chat.scala @@ -0,0 +1,621 @@ +package chatapp + +import akka.actor.typed.scaladsl.{ActorContext, Behaviors, LoggerOps} +import akka.actor.typed.{ActorRef, ActorSystem, Behavior} +import com.typesafe.config.ConfigFactory + +import scala.collection.mutable +import scala.concurrent.Promise +import scala.util.control.Breaks.{break, breakable} +import chatapp.utils.random._ + +import scala.jdk.CollectionConverters._ + +object Action extends Enumeration { + type Action = Value + val Post, PostDelivery, Leave, Invite, Compute, Ignore, Error, None = Value +} +import Action._ + +class BehaviorFactory(compute: Int, post: Int, leave: Int, invite: Int) { + val _compute: Int = compute + val _post: Int = _compute + post + val _leave: Int = _post + leave + val _invite: Int = _leave + invite + + def apply(dice: DiceRoll): Action = { + val pick: Int = dice() + var action: Action = None + + if (pick < _compute) { + action = Compute + } else if (pick < _post) { + action = Post + } else if (pick < _leave) { + action = Leave + } else if (pick < _invite) { + action = Invite + } + action + } +} + +sealed trait ChatMsg +final case class ChatPost(payload: Array[Byte], accumulator: ActorRef[AccMsg]) + extends ChatMsg +final case class ChatJoin( + client: ActorRef[ClientMsg], + accumulator: ActorRef[AccMsg] +) extends ChatMsg +final case class ChatLeave( + client: ActorRef[ClientMsg], + didLogout: Boolean, + accumulator: ActorRef[AccMsg] +) extends ChatMsg + +class Chat(initiator: ActorRef[ClientMsg]) { + + val members = new mutable.ArrayBuffer[ActorRef[ClientMsg]]() + var buffer = new mutable.ArrayBuffer[Array[Byte]]() + + members.append(initiator) + + def apply(): Behavior[ChatMsg] = + Behaviors.receive { (context, message) => + message match { + case ChatPost(payload, accumulator) => + post(context.self, payload, accumulator) + Behaviors.same + case ChatJoin(client, accumulator) => + join(context.self, client, accumulator) + Behaviors.same + case ChatLeave(client, didLogout, accumulator) => + leave(context.self, client, didLogout, accumulator) + Behaviors.same + } + } + + private def post( + chat: ActorRef[ChatMsg], + payload: Array[Byte], + accumulator: ActorRef[AccMsg] + ): Unit = { + buffer.append(payload) + if (members.nonEmpty) { + accumulator ! AccBump(Action.Post, members.length) + for (member <- members) { + member ! ClForward(chat, payload, accumulator) + } + } else { + accumulator ! AccStop(Post) + } + } + + private def join( + chat: ActorRef[ChatMsg], + client: ActorRef[ClientMsg], + accumulator: ActorRef[AccMsg] + ): Unit = { + members.append(client) + if (buffer.nonEmpty) { + accumulator ! AccBump(Ignore, buffer.length) + for (message <- buffer) { + client ! ClForward(chat, message, accumulator) + } + } + client ! ClAccepted(chat, accumulator) + } + + private def leave( + chat: ActorRef[ChatMsg], + client: ActorRef[ClientMsg], + didLogout: Boolean, + accumulator: ActorRef[AccMsg] + ): Unit = { + breakable { + for ((c, i) <- members.zipWithIndex) { + if (c == client) { + members.remove(i) + break + } + } + } + client ! ClLeft(chat, didLogout, accumulator) + } +} + +sealed trait ClientMsg +final case class ClBefriend(client: ActorRef[ClientMsg]) extends ClientMsg +final case class ClLogout() extends ClientMsg +final case class ClLeft( + chat: ActorRef[ChatMsg], + didLogout: Boolean, + accumulator: ActorRef[AccMsg] +) extends ClientMsg +final case class ClAccepted( + chat: ActorRef[ChatMsg], + accumulator: ActorRef[AccMsg] +) extends ClientMsg +final case class ClForward( + chat: ActorRef[ChatMsg], + payload: Array[Byte], + accumulator: ActorRef[AccMsg] +) extends ClientMsg +final case class ClAct(behavior: BehaviorFactory, accumulator: ActorRef[AccMsg]) + extends ClientMsg + +class Client(id: Long, directory: ActorRef[DirMsg], seed: Long) { + + val friends = new mutable.ArrayBuffer[ActorRef[ClientMsg]]() + val chats = new mutable.ArrayBuffer[ActorRef[ChatMsg]]() + val rand = new SimpleRand(seed) + val dice = new DiceRoll(rand) + var fibIndex = 35 + + def apply(): Behavior[ClientMsg] = active() + + private def active(): Behavior[ClientMsg] = { + Behaviors.receive { (context, message) => + message match { + case ClBefriend(client) => + befriend(client) + Behaviors.same + case ClLogout() => + logout() + case ClLeft(chat, didLogout, accumulator) => + left(chat, didLogout, accumulator) + case ClAccepted(chat, accumulator) => + accepted(chat, accumulator) + Behaviors.same + case ClForward(chat, payload, accumulator) => + forward(chat, payload, accumulator) + Behaviors.same + case ClAct(behavior, accumulator) => + act(behavior, accumulator) + } + } + } + + private def befriend(client: ActorRef[ClientMsg]) = + friends.append(client) + + private def logout(): Behavior[ClientMsg] = { + Behaviors.setup { context => + if (chats.nonEmpty) { + for (chat <- chats) { + chat ! ChatLeave(context.self, didLogout = true, null) + } + active() + } else { + directory ! DirLeft(context.self) + Behaviors.stopped + } + } + } + private def left( + chat: ActorRef[ChatMsg], + didLogout: Boolean, + accumulator: ActorRef[AccMsg] + ): Behavior[ClientMsg] = { + Behaviors.setup { context => + for ((c, i) <- chats.zipWithIndex) { + if (c == chat) { + chats.remove(i) + } + } + if (chats.isEmpty && didLogout) { + directory ! DirLeft(context.self) + Behaviors.stopped + } else if (accumulator != null) { + accumulator ! AccStop(Leave) + active() + } else { + active() + } + } + } + + private def accepted( + chat: ActorRef[ChatMsg], + accumulator: ActorRef[AccMsg] + ): Unit = { + chats.append(chat) + accumulator ! AccStop(Ignore) + } + + private def forward( + chat: ActorRef[ChatMsg], + payload: Array[Byte], + accumulator: ActorRef[AccMsg] + ): Unit = + accumulator ! AccStop(PostDelivery) + + private def act( + behavior: BehaviorFactory, + accumulator: ActorRef[AccMsg] + ): Behavior[ClientMsg] = { + Behaviors.setup { context => + val index = rand.nextInt(chats.length) + + behavior(dice) match { + case Post => chats(index) ! ChatPost(null, accumulator) + case Leave => + chats(index) ! ChatLeave(context.self, didLogout = false, accumulator) + case Compute => + for (i <- 1 to 10000) { + if (fibonacci(fibIndex) != 9_227_465) { + accumulator ! AccStop(Error) + fibIndex += 1 + } + } + accumulator ! AccStop(Compute) + case Invite => + val chat = new Chat(context.self) + val created: ActorRef[ChatMsg] = context.spawn(chat(), "Chat") + chats.append(created) + rand.shuffle[ActorRef[ClientMsg]](friends) + var invitations = rand.nextInt(friends.length) + if (invitations == 0) { + invitations = 1 + } + accumulator ! AccBump(Invite, invitations) + for (k <- 0 until invitations) { + created ! ChatJoin(friends(k), accumulator) + } + case None => accumulator ! AccStop(None) + case default => + throw new RuntimeException( + "This should never happen. Action was " + default + ) + } + active() + } + } + + private def fibonacci(n: Int): Int = { + if (n == 0) return 0 + if (n == 1) return 1 + + val j: Int = n / 2 + + val fibJ = fibonacci(j) + val fibI = fibonacci(j - 1) + + if (n % 2 == 0) { + return fibJ * (fibJ + (fibI * 2)) + } + if (n % 4 == 1) { + return ((fibJ * 2) + fibI) * ((fibJ * 2) - fibI) + 2 + } else { + return ((fibJ * 2) + fibI) * ((fibJ * 2) - fibI) - 2 + } + } +} + +sealed trait DirMsg +final case class DirLogin(id: Long) extends DirMsg +final case class DirBefriend() extends DirMsg +final case class DirLeft(client: ActorRef[ClientMsg]) extends DirMsg +final case class DirPoke( + factory: BehaviorFactory, + accumulator: ActorRef[AccMsg] +) extends DirMsg +final case class DirDisconnect(poker: ActorRef[PokerMsg]) extends DirMsg + +class Directory(seed: Long, befriend: Int) { + val clients = new mutable.ArrayBuffer[ActorRef[ClientMsg]]() + val random = new SimpleRand(seed) + var _poker: ActorRef[PokerMsg] = _ + + def apply(): Behavior[DirMsg] = active() + + private def active(): Behavior[DirMsg] = { + Behaviors.receive { (context, message) => + message match { + case DirLogin(id) => + login(id) + case DirBefriend() => + befriend() + Behaviors.same + case DirLeft(client) => + left(client) + case DirPoke(factory, accumulator) => + poke(factory, accumulator) + Behaviors.same + case DirDisconnect(poker) => + disconnect(poker) + Behaviors.same + } + } + } + + private def login(id: Long): Behavior[DirMsg] = { + Behaviors.setup { context => + val client = new Client(id, context.self, random.next()) + clients.append(context.spawn(client(), s"Client${id}")) + active() + } + } + + private def befriend(): Unit = { + for (fclients <- clients) { + var foundFriend = false + while (foundFriend) { + for (client <- clients) { + if (random.nextInt(100) < befriend && fclients != clients) { + client ! ClBefriend(fclients) + fclients ! ClBefriend(client) + foundFriend = true + } + } + } + } + } + + private def left(client: ActorRef[ClientMsg]): Behavior[DirMsg] = { + Behaviors.setup { context => + for ((c, i) <- clients.zipWithIndex) { + if (c == client) { + clients.remove(i) + } + } + if (clients.isEmpty) { + _poker ! PokFinished + Behaviors.stopped + } else { + active() + } + } + } + + private def poke( + factory: BehaviorFactory, + accumulator: ActorRef[AccMsg] + ): Unit = { + for (client <- clients) { + client ! ClAct(factory, accumulator) + } + } + + private def disconnect(poker: ActorRef[PokerMsg]): Unit = { + _poker = poker + for (client <- clients) { + client ! ClLogout() + } + } +} + +sealed trait AccMsg +final case class AccBump(action: Action, increase: Int) extends AccMsg +final case class AccStop(action: Action = Ignore) extends AccMsg +final case class AccPrint(collector: ActorRef[PokerMsg], i: Int, j: Int) + extends AccMsg + +class Accumulator(poker: ActorRef[PokerMsg], expected: Long) { + val startTime: Long = System.currentTimeMillis() + var endTime: Long = 0 + var duration: Double = 0 + var did_stop: Boolean = false + var actions: mutable.HashMap[Action.Value, Int] = + new mutable.HashMap[Action.Value, Int]() + var _expected: Long = expected + + def apply(): Behavior[AccMsg] = { + Behaviors.receive { (context, message) => + message match { + case AccBump(action, increase) => + count(action) + _expected = (_expected + increase) - 1 + Behaviors.same + case AccStop(action) => + stop(action) + Behaviors.same + case AccPrint(collector, i, j) => + collector ! PokCollect(i, j, duration, actions) + Behaviors.same + } + } + } + + private def count(action: Action): Unit = actions.apply(action) += 1 + + private def stop(action: Action): Unit = { + count(action) + _expected -= 1 + if (_expected == 0) { + endTime = System.currentTimeMillis() + duration = endTime - startTime + did_stop = true + poker ! PokConfirm + } + } + +} + +sealed trait PokerMsg +final case object PokFinished extends PokerMsg +final case object PokConfirm extends PokerMsg +final case class PokCollect( + i: Int, + j: Int, + duration: Double, + actions: mutable.HashMap[Action, Int] +) extends PokerMsg + +class Poker( + parsable: Boolean, + clients: Long, + turns: Long, + directories: Long, + befriend: Int, + factory: BehaviorFactory +) { + var actions = new mutable.HashMap[Action, Int]() + var logouts = 0 + var confirmations = 0 + var iterations = 0 + var directories = new mutable.ArrayBuffer[ActorRef[DirMsg]]() + var runtimes = new mutable.ArrayBuffer[ActorRef[AccMsg]]() + var accumulations = 0 + var bench: ActorRef[_] = _ + var last = false + var turnSeries = new mutable.ArrayBuffer[Double]() + + def apply(): Behavior[PokerMsg] = {} +} + +class Config(args: Array[String]) { + var clients = 1024 + var numDirs = 8 + var turns = 32 + var compute = 55 + var post = 25 + var leave = 10 + var invite = 10 + var befriend = 10 + + var parsable = false + + readConfig() + + private def readConfig(): Unit = { + var i = 0 + while (i < args.length) { + args(i) match { + case "-c" => { clients = args(i + 1).toInt; i += 1 } + case "-d" => { numDirs = args(i + 1).toInt; i += 1 } + case "-t" => { turns = args(i + 1).toInt; i += 1 } + case "-m" => { compute = args(i + 1).toInt; i += 1 } + case "-p" => { post = args(i + 1).toInt; i += 1 } + case "-l" => { leave = args(i + 1).toInt; i += 1 } + case "-i" => { invite = args(i + 1).toInt; i += 1 } + case "-b" => { befriend = args(i + 1).toInt; i += 1 } + case "-parse" => { parsable = true } + } + i += 1 + } + + if (numDirs > clients) { + println( + s"Invalid arguments! Cannot have more directories ($numDirs) than clients ($clients)" + ) + System.exit(1) + } + + if (clients < (numDirs * 2)) { + println( + s"Invalid arguments! Need to have 2x as many dictionaries ($numDirs) so that clients ($clients) have at least 1 friend" + ) + System.exit(1) + } + + val sum = compute + post + leave + invite + if (sum != 100) { + println("Invalid arguments! Sum of probabilities != 100.") + println(s"\tIt is: $sum") + System.exit(1) + } + } + + def print(): Unit = { + println(s"Configuration ChatApp Benchmark") + println(s"\tclients: $clients") + println(s"\tdirectories: $numDirs") + println(s"\tturns: $turns") + println(s"\tcompute: $compute") + println(s"\tpost: $post") + println(s"\tleave: $leave") + println(s"\tinvite: $invite") + println(s"\tbefriend: $befriend") + println(s"\tparsable: $parsable") + } +} + +class Runner(configuration: Config) { + var benchmark: ChatApp + val result = new Result(configuration.parsable) + + var iterations = 0 + + var running = false + var summarize = false + var startTime = 0L + var endTime = 0L + + var completion: Promise[Int] + + sealed trait RunnerMsg + final case class Start(iter: Int) extends RunnerMsg + final case class Complete(poker: Poker, last: Boolean) extends RunnerMsg + + def apply(): Behavior[RunnerMsg] = + Behaviors.setup { context => + Behaviors.receiveMessage { + case Start(iter) => + start(iter) + Behaviors.same + + case Complete(poker, last) => + complete(poker, last) + Behaviors.same + } + } + + private def start(iter: Int): Unit = { + // val promise = Promise[Int]() + // | pp | + // pp:: actors createPromisePair. + // completionR:: pp resolver. + + // benchmark:: ChatApp new: configuration. + // iterations:: iter. + // next: nil isLast: false. + // ^ pp promise + // return promise + } + + private def complete(poker: Poker, last: Boolean): Unit = { + endTime = System.nanoTime() / 1000L + running = false + + result.record((endTime - startTime) / 1000.0) + // context.log.info("Total: {}us", (endTime - startTime) / 1000.0) + next(poker, last) + } + + private def next(poker: Poker, last: Boolean): Unit = { + if (running) { + return + } + + if (summarize) { + // Runner.summarize' println + result.summarize(poker, last) + summarize = false + } + + if (iterations > 0) { + running = true + startTime = System.nanoTime() + + // context.log.info("Runner.run: {}", iterations) + + benchmark.run(this, iterations == 1) + iterations -= -1 + summarize = iterations == 0 + } else { + completion.complete(0) + } + } + /* + + + + */ +} + +object Main extends App { + val cfg = new Config(args) + // val runner: ActorSystem[Runner.Start] = ActorSystem(Runner(), "Runner") + // runner ! Start(100) +} diff --git a/akka/src/main/scala/chatapp/utils/random/random.scala b/akka/src/main/scala/chatapp/utils/random/random.scala new file mode 100644 index 0000000..a4e7cb0 --- /dev/null +++ b/akka/src/main/scala/chatapp/utils/random/random.scala @@ -0,0 +1,43 @@ +package chatapp.utils.random + +import scala.collection.mutable + +class SimpleRand(seed: Long) { + var value: Long = seed + + def next(): Long = { + nextLong() + } + + def nextLong(): Long = { + value = ((value * 1309) + 13849) & 65535 + value + } + + def nextInt(max: Int = 0) = { + if (max == 0) { + nextLong().asInstanceOf[Int] + } else { + nextLong().asInstanceOf[Int] % max + } + } + + def shuffle[A](array: mutable.ArrayBuffer[A]) = { + var i = array.length + + while (i > 1) { + i -= 1 + val ceil = i + val randI = nextInt(ceil) + + val tmp = array(ceil) + array(ceil) = array(randI) + array(randI) = tmp + } + } +} + +class DiceRoll(rand: SimpleRand) { + def apply(): Int = + rand.nextInt(100) +}