├── .gitignore ├── README.md ├── erlang ├── client.erl ├── plists.erl ├── server.erl └── server2.erl └── scala ├── build.sbt ├── project └── plugins.sbt ├── sbt └── src └── main └── scala ├── AkkaActor.scala ├── Benchmark.scala ├── LiftActor.scala └── Wactor.scala /.gitignore: -------------------------------------------------------------------------------- 1 | # swap files 2 | *~ 3 | *# 4 | *.sw[op] 5 | *.beam 6 | scala/sbt-launch.jar* 7 | 8 | # sbt 9 | src_managed 10 | project/plugins/project 11 | project/boot/* 12 | */project/build/target 13 | */project/boot 14 | lib_managed 15 | 16 | 17 | # OSX 18 | .DS_Store 19 | 20 | 21 | # Intellij IDEA 22 | *.iws 23 | *.ipr 24 | *.iml 25 | 26 | 27 | # Eclipse & Netbeans 28 | .project 29 | .settings 30 | .classpath 31 | .idea 32 | .scala_dependencies 33 | 34 | 35 | # ensime 36 | ensime_port 37 | .ensime.* 38 | 39 | 40 | # stolen from akka's .gitignore ;) 41 | activemq-data 42 | etags 43 | tags 44 | TAGS 45 | akka.tmproj 46 | reports 47 | dist 48 | build 49 | target 50 | deploy/*.jar 51 | data 52 | out 53 | logs 54 | .#* 55 | .codefellow 56 | storage 57 | .codefellow 58 | _dump 59 | .manager 60 | manifest.mf 61 | semantic.cache 62 | tm*.log 63 | tm*.lck 64 | tm.out 65 | *.tm.epoch 66 | multiverse.log 67 | 68 | src/main/resources/props/*.default.props 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![wasted.io](http://wasted.io/images/soon/wasted.png) 2 | 3 | ## Scala vs Erlang Actors 4 | 5 | Thanks to [Paul Keeble](https://github.com/PaulKeeble) for getting this benchmark project started a few years back. 6 | 7 | Benchmark for testing Actor implementations on Scala and Erlang. 8 | 9 | The Actor has 2 responsibilities: 10 | 11 | - Add a count X to the Actors current tally 12 | - Output the current count and reset the agent count to zero 13 | 14 | ## Usability of these benchmarks 15 | 16 | We know that these benchmarks are not representative for 99% of actor use cases, the simple idea is to get number of msgs/sec put thru the actor without having it do too much. 17 | 18 | ## Erlang OTP 19 | 20 | Compile the 3 files then run ```client:runTest(3000000).``` 21 | 22 | ## Erlang bare receive version 23 | 24 | Compile server2 as well as the 3 files for OTP and then run ```client:runTest2(3000000).``` 25 | 26 | ## Scala 27 | 28 | ### Actors being used are Lift Actor, Akka and our own Wactor. 29 | 30 | Simply run ```./sbt run``` and select one of the benchmarks. 31 | 32 | 33 | ## 2011 results on Scala 2.9.1 vs Erlang 5.8.5 34 | 35 | I wrote [Scala, Akka and Erlang Actor Benchmarks](http://uberblo.gs/2011/12/scala-akka-and-erlang-actor-benchmarks) when i was really starting out with Scala. 36 | 37 | 38 | -------------------------------------------------------------------------------- /erlang/client.erl: -------------------------------------------------------------------------------- 1 | -module(client). 2 | 3 | -export([runTest/1,runTest2/1,runTest25/1,runTest3/1]). 4 | -export([runTest1a/1, 5 | runTest1b/1, 6 | runTest2a/1, 7 | runTest2b/1]). 8 | 9 | runTest(Size) -> 10 | server:start_link(), 11 | Start=now(), 12 | Count=test(Size), 13 | Finish=now(), 14 | server:stop(), 15 | print_results(Count,Size,Start,Finish). 16 | 17 | %% move the list generation outside the measurement 18 | runTest1a(Size) -> 19 | server:start_link(), 20 | Input = lists:seq(1,Size), 21 | Start=now(), 22 | Count=test1a(Input), 23 | Finish=now(), 24 | server:stop(), 25 | print_results(Count,Size,Start,Finish). 26 | 27 | %% remove usage of plists 28 | runTest1b(Size) -> 29 | server:start_link(), 30 | Input = lists:seq(1,Size), 31 | Start=now(), 32 | Count=test1b(Input), 33 | Finish=now(), 34 | server:stop(), 35 | print_results(Count,Size,Start,Finish). 36 | 37 | runTest2(Size) -> 38 | server2:start_link(), 39 | Start=now(), 40 | Count=test2(Size), 41 | Finish=now(), 42 | server2:stop(), 43 | print_results(Count,Size,Start,Finish). 44 | 45 | %% move list generation outside measurement 46 | runTest2a(Size) -> 47 | server2:start_link(), 48 | Input = lists:seq(1,Size), 49 | Start=now(), 50 | Count=test2a(Input), 51 | Finish=now(), 52 | server2:stop(), 53 | print_results(Count,Size,Start,Finish). 54 | 55 | %% remove usage of plists 56 | runTest2b(Size) -> 57 | server2:start_link(), 58 | Input = lists:seq(1,Size), 59 | Start=now(), 60 | Count=test2b(Input), 61 | Finish=now(), 62 | server2:stop(), 63 | print_results(Count,Size,Start,Finish). 64 | 65 | runTest25(Size) -> 66 | P=server2:start_link(), 67 | Start=now(), 68 | Count=test2(P,Size), 69 | Finish=now(), 70 | server2:stop(), 71 | print_results(Count,Size,Start,Finish). 72 | 73 | runTest3(Size) -> 74 | P = server2:start_link(), 75 | Start=now(), 76 | Count=test3(P,Size), 77 | Finish=now(), 78 | server2:stop(), 79 | print_results(Count,Size,Start,Finish). 80 | 81 | test(Size) -> 82 | plists:foreach(fun (_X)-> server:bytes(100) end,lists:seq(1,Size)), 83 | server:get_count(). 84 | 85 | test1a(Input) -> 86 | plists:foreach(fun (_X)-> server:bytes(100) end,Input), 87 | server:get_count(). 88 | 89 | test1b(Input) -> 90 | lists:foreach(fun(_X) -> server:bytes(100) end, Input), 91 | server:get_count(). 92 | 93 | 94 | test2(PID,Size) -> 95 | plists:foreach(fun (_X) -> server2:bytes(PID,100) end,lists:seq(1,Size)), 96 | server2:get_count(PID). 97 | 98 | test2(Size) -> 99 | plists:foreach(fun (_X)-> server2:bytes(100) end,lists:seq(1,Size)), 100 | server2:get_count(). 101 | 102 | test2a(Input) -> 103 | plists:foreach(fun (_X)-> server2:bytes(100) end, Input), 104 | server2:get_count(). 105 | 106 | test2b(Input) -> 107 | lists:foreach(fun(_X) -> server2:bytes(100) end, 108 | Input), 109 | server2:get_count(). 110 | 111 | 112 | test3(Pid,Size) -> 113 | NProcs = erlang:system_info(logical_processors), 114 | SMsgs = round(Size/NProcs), 115 | Pids = test3_launch(NProcs,SMsgs,Pid), 116 | lists:foreach(fun(CPid) -> receive {CPid,done} -> ok end end, Pids), 117 | server2:get_count(Pid). 118 | 119 | test3_launch(0,_,_) -> []; 120 | test3_launch(N,SMsgs,Pid) -> 121 | Self = self(), 122 | [spawn(fun() -> test3_broadcast(SMsgs,Pid,Self) end) | 123 | test3_launch(N-1,SMsgs,Pid)]. 124 | 125 | test3_broadcast(0,_,ParentPid) -> 126 | ParentPid ! {self(),done}; 127 | test3_broadcast(SMsgs,Pid,ParentPid) -> 128 | server2:bytes(Pid,100), 129 | test3_broadcast(SMsgs-1,Pid,ParentPid). 130 | 131 | print_results(Count,Size,Start,Finish) -> 132 | io:format("Count is ~p~n",[Count]), 133 | io:format("Test took ~p seconds~n",[elapsedTime(Start,Finish)]), 134 | io:format("Throughput=~p per sec~n",[throughput(Size,Start,Finish)]). 135 | 136 | elapsedTime(Start,Finish) -> 137 | timer:now_diff(Finish, Start) / 1000000. 138 | 139 | throughput(Size,Start,Finish) -> Size / elapsedTime(Start,Finish). 140 | -------------------------------------------------------------------------------- /erlang/plists.erl: -------------------------------------------------------------------------------- 1 | -module(plists). 2 | 3 | -export([foreach/2]). 4 | 5 | 6 | % Algorithm - Split into 4 lists, have each list and the fun run in a separate spawned process 7 | foreach(Fun,List) -> 8 | Splits = split(8,List), 9 | Pids = execute(Splits,Fun,self()), 10 | waitCompletion(Pids). 11 | 12 | split(1,List) -> [List]; 13 | split(Pieces,List) -> 14 | Length = length(List), 15 | N = round(Length/Pieces), 16 | {List1,Remainder} = lists:split(N,List), 17 | [List1|split(Pieces-1,Remainder)]. 18 | 19 | execute([],_Fun,_EndPid) -> []; 20 | execute([L|Lists],Fun,EndPid) -> 21 | Pid = spawn( fun()->runFun(Fun,L,EndPid) end ), 22 | [Pid|execute(Lists,Fun,EndPid)]. 23 | 24 | runFun(Fun,List,EndPid) -> lists:foreach(Fun,List), 25 | EndPid ! finished. 26 | 27 | waitCompletion(Pids) -> 28 | Times = lists:seq(1,length(Pids)), 29 | lists:foreach(fun (_X) -> 30 | receive finished -> ok 31 | end 32 | end, Times). -------------------------------------------------------------------------------- /erlang/server.erl: -------------------------------------------------------------------------------- 1 | %%% 2 | %%% @author Paul 3 | %%% @doc RPC over TCP sever. 4 | %%% This module defines that listens on TCP and executes RCP commands 5 | %%% @end 6 | -module(server). 7 | 8 | -behaviour(gen_server). 9 | 10 | %% API 11 | -export([start_link/0,bytes/1,get_count/0,stop/0]). 12 | 13 | %% gen_server callabacks 14 | -export([init/1,handle_call/3,handle_cast/2,handle_info/2,terminate/2,code_change/3]). 15 | 16 | -define(SERVER,?MODULE). 17 | 18 | -record(state,{count=0}). 19 | 20 | %% API 21 | 22 | %% @doc Starts the server. 23 | %% @spec start_link(Port::integer()) -> {ok,Pid} 24 | %% where 25 | %% Pid = pid() 26 | %% @end 27 | start_link() -> 28 | gen_server:start_link({local,?SERVER},?MODULE,[],[]). 29 | 30 | %% @doc Fetches the number of requests made 31 | %% @spec get_count() -> {ok,Count} 32 | %% where 33 | %% Count = integer() 34 | %% @end 35 | get_count() -> gen_server:call(?SERVER,get_count). 36 | 37 | bytes(Count) -> gen_server:cast(?SERVER,{bytes,Count}). 38 | 39 | %% @doc stops the server 40 | %% @spec stop() -> ok 41 | %% @end 42 | stop() -> gen_server:cast(?SERVER,stop). 43 | 44 | %% gen_server callbacks 45 | init(_Params) -> 46 | {ok,#state{count=0}}. 47 | 48 | handle_call(_Request,_From,State) -> 49 | {reply,{ok,State#state.count},State}. 50 | 51 | handle_cast(stop,State) -> {stop,normal,State}; 52 | handle_cast({bytes,Count},State) -> 53 | UpdatedCount = State#state.count+Count, 54 | {noreply,State#state{count=UpdatedCount}}. 55 | 56 | handle_info(_Msg,State) -> 57 | {noreply,State}. 58 | 59 | terminate(_Reason,_State) -> ok. 60 | 61 | code_change(_OLdVersion,State,_Extra) -> 62 | {ok,State}. 63 | -------------------------------------------------------------------------------- /erlang/server2.erl: -------------------------------------------------------------------------------- 1 | -module(server2). 2 | 3 | -export([start_link/0,bytes/1,bytes/2,get_count/0,get_count/1,stop/0]). 4 | 5 | -define(SERVER,?MODULE). 6 | 7 | start_link() -> 8 | PID = spawn(fun()-> serve_request(0) end), 9 | register(?SERVER,PID), 10 | PID. 11 | 12 | stop() -> 13 | ?SERVER ! exit, 14 | unregister(?SERVER). 15 | 16 | get_count(PID) -> 17 | PID ! {get_count,self()}, 18 | receive X -> X end. 19 | 20 | get_count() -> 21 | ?SERVER ! {get_count,self()}, 22 | receive 23 | X -> X 24 | end. 25 | 26 | bytes(PID,Count) -> 27 | PID ! {bytes,Count}. 28 | 29 | bytes(Count) -> 30 | ?SERVER ! {bytes,Count}, 31 | ok. 32 | 33 | serve_request(State) -> 34 | receive 35 | {bytes,Count} -> 36 | UpdatedState = State + Count, 37 | serve_request(UpdatedState); 38 | {get_count,PID} -> 39 | PID ! State, 40 | serve_request(State); 41 | exit -> ok 42 | end. 43 | -------------------------------------------------------------------------------- /scala/build.sbt: -------------------------------------------------------------------------------- 1 | import AssemblyKeys._ 2 | 3 | name := "scalavserlang" 4 | 5 | organization := "io.wasted.bench" 6 | 7 | version := "2.0" 8 | 9 | scalaVersion := "2.10.0" 10 | 11 | assemblySettings 12 | 13 | resolvers ++= Seq( 14 | "wasted.io/repo" at "http://repo.wasted.io/mvn", 15 | "Sonatype Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots/", 16 | "Repo Maven" at "http://repo1.maven.org/maven2/", 17 | "Java.net Maven2 Repository" at "http://download.java.net/maven/2/", 18 | "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/" 19 | ) 20 | 21 | // if you have issues pulling dependencies from the scala-tools repositories (checksums don't match), you can disable checksums 22 | //checksums := Nil 23 | 24 | scalacOptions ++= Seq("-encoding", "UTF-8", "-deprecation", "-Xcheckinit", "-unchecked", "-feature", "-optimise") 25 | 26 | // Base dependencies 27 | libraryDependencies ++= Seq( 28 | "io.wasted" %% "wasted-util" % "0.5.0-SNAPSHOT", 29 | "net.liftweb" %% "lift-actor" % "2.5-SNAPSHOT", 30 | "com.typesafe.akka" %% "akka-actor" % "2.1.0", 31 | "ch.qos.logback" % "logback-classic" % "0.9.26" % "compile->default" 32 | ) 33 | 34 | 35 | -------------------------------------------------------------------------------- /scala/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += Resolver.url("artifactory", url("http://scalasbt.artifactoryonline.com/scalasbt/sbt-plugin-releases"))(Resolver.ivyStylePatterns) 2 | 3 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.8.3") 4 | -------------------------------------------------------------------------------- /scala/sbt: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | root=$( 4 | cd $(dirname $(readlink $0 || echo $0))/.. 5 | pwd 6 | ) 7 | 8 | sbtver=0.12.2-RC2 9 | sbtjar=sbt-launch.jar 10 | sbtsum=db591e4ab57657591ae3ccc666f77b77 11 | 12 | download () 13 | { 14 | echo "downloading ${sbtjar}" 1>&2 15 | curl -O "http://repo.typesafe.com/typesafe/ivy-releases/org.scala-sbt/sbt-launch/${sbtver}/${sbtjar}" 16 | } 17 | 18 | sbtjar_md5 () 19 | { 20 | openssl md5 < ${sbtjar} | cut -f2 -d'=' | awk '{print $1}' 21 | } 22 | 23 | if [ ! -f "${sbtjar}" ]; then 24 | download 25 | fi 26 | 27 | test -f "${sbtjar}" || exit 1 28 | 29 | jarmd5=$(sbtjar_md5) 30 | if [ "${jarmd5}" != "${sbtsum}" ]; then 31 | echo "Bad MD5 checksum on ${sbtjar}!" 1>&2 32 | echo "Moving current sbt-launch.jar to sbt-launch.jar.old!" 1>&2 33 | mv "${sbtjar}" "${sbtjar}.old" 34 | download 35 | 36 | jarmd5=$(sbtjar_md5) 37 | if [ "${jarmd5}" != "${sbtsum}" ]; then 38 | echo "Bad MD5 checksum *AGAIN*!" 1>&2 39 | exit 1 40 | fi 41 | fi 42 | 43 | test -f ~/.sbtconfig && . ~/.sbtconfig 44 | 45 | java -ea -server $SBT_OPTS $JAVA_OPTS \ 46 | -XX:+AggressiveOpts \ 47 | -XX:+OptimizeStringConcat \ 48 | -XX:+UseConcMarkSweepGC \ 49 | -Xms1G \ 50 | -Xmx2G \ 51 | -jar $sbtjar "$@" 52 | 53 | -------------------------------------------------------------------------------- /scala/src/main/scala/AkkaActor.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.bench.scalavserlang.akka 2 | 3 | import _root_.scala.compat.Platform 4 | import io.wasted.bench.scalavserlang._ 5 | 6 | import _root_.akka.actor._ 7 | import _root_.akka.util._ 8 | import scala.concurrent.duration._ 9 | 10 | 11 | object Application { 12 | val system = ActorSystem("AkkaTest") 13 | val testActor = system.actorOf(Props[CounterActor], "test") 14 | val runs = 12000000 15 | 16 | def main(args: Array[String]) { 17 | start() 18 | stop() 19 | sys.exit(0) 20 | } 21 | 22 | def start() { runTest(testActor, runs) } 23 | def stop() { system.shutdown() } 24 | 25 | def runTest(counter: ActorRef, msgCount: Long) { 26 | val start = Platform.currentTime 27 | theTest(counter, msgCount) 28 | val finish = Platform.currentTime 29 | val elapsedTime = (finish - start) / 1000.0 30 | 31 | printf("%n") 32 | printf("[akka] Count is %s%n", msgCount) 33 | printf("[akka] Test took %s seconds%n", elapsedTime) 34 | printf("[akka] Throughput=%s per sec%n", msgCount / elapsedTime) 35 | printf("%n") 36 | } 37 | 38 | def theTest(counter: ActorRef, msgCount: Long) = { 39 | val bytesPerMsg = 100 40 | val updates = (1L to msgCount).par.foreach((x: Long) => counter ! new AddCount(bytesPerMsg)) 41 | 42 | counter ! Reset 43 | } 44 | 45 | } 46 | 47 | class CounterActor extends Actor { 48 | var count: Long = 0 49 | 50 | def receive = { 51 | case Reset => 52 | printf("[akka] Count is %s%n",count) 53 | count = 0 54 | case AddCount(extraCount) => 55 | count=count+extraCount 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /scala/src/main/scala/Benchmark.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.bench.scalavserlang 2 | 3 | 4 | case object Reset 5 | case class AddCount(number:Long) 6 | 7 | 8 | object BenchmarkAll extends App { 9 | val runs = 12000000 10 | 11 | override def main(args: Array[String]) { 12 | 13 | // lift 14 | println("Warmup run!") 15 | lift.Application.start(false) 16 | lift.Application.stop 17 | println("Warmup run finished!") 18 | val runtime = Runtime.getRuntime 19 | println 20 | 21 | println("Garbage Collection") 22 | runtime.gc 23 | println("Garbage Collection finished") 24 | println 25 | Thread.sleep(1000) 26 | 27 | // lift 28 | lift.Application.start() 29 | lift.Application.stop() 30 | 31 | println("Garbage Collection") 32 | runtime.gc 33 | println("Garbage Collection finished") 34 | println 35 | Thread.sleep(1000) 36 | 37 | // akka 38 | akka.Application.start() 39 | akka.Application.stop() 40 | 41 | println("Garbage Collection") 42 | runtime.gc 43 | println("Garbage Collection finished") 44 | println 45 | Thread.sleep(1000) 46 | 47 | //wactor 48 | wactor.Application.start() 49 | wactor.Application.stop() 50 | 51 | println("Garbage Collection") 52 | runtime.gc 53 | println 54 | 55 | 56 | sys.exit(0) 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /scala/src/main/scala/LiftActor.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.bench.scalavserlang.lift 2 | 3 | import _root_.scala.compat.Platform 4 | import io.wasted.bench.scalavserlang._ 5 | 6 | import net.liftweb.actor._ 7 | import java.util.concurrent.atomic._ 8 | 9 | 10 | object Application { 11 | val runs = 12000000 12 | val counter = new CounterActor 13 | 14 | def main(args: Array[String]) { 15 | start() 16 | stop() 17 | sys.exit(0) 18 | } 19 | 20 | def start(print: Boolean = true) { runTest(runs, print) } 21 | def stop() { LAScheduler.shutdown() } 22 | 23 | 24 | def runTest(msgCount: Long, print: Boolean) { 25 | val start = Platform.currentTime 26 | theTest(msgCount) 27 | val finish = Platform.currentTime 28 | val elapsedTime = (finish - start) / 1000.0 29 | 30 | // disable output on warmup run! 31 | if (print) { 32 | printf("%n") 33 | printf("[lift] Count is %s%n", msgCount) 34 | printf("[lift] Test took %s seconds%n", elapsedTime) 35 | printf("[lift] Throughput=%s per sec%n", msgCount / elapsedTime) 36 | printf("%n") 37 | } 38 | } 39 | 40 | def theTest(msgCount: Long): Any = { 41 | val bytesPerMsg = 100 42 | val updates = (1L to msgCount).par.foreach((x: Long) => counter ! new AddCount(bytesPerMsg)) 43 | 44 | counter ! Reset 45 | } 46 | 47 | } 48 | 49 | 50 | class CounterActor extends LiftActor { 51 | var count = 0L 52 | 53 | def messageHandler = { 54 | case Reset => 55 | printf("[lift] Count is %s%n",count) 56 | count = 0 57 | case AddCount(extraCount) => 58 | count += extraCount 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /scala/src/main/scala/Wactor.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.bench.scalavserlang.wactor 2 | 3 | import _root_.scala.compat.Platform 4 | import io.wasted.bench.scalavserlang._ 5 | 6 | import io.wasted.util._ 7 | import java.util.concurrent.atomic._ 8 | 9 | 10 | object Application { 11 | val runs = 12000000 12 | val counter = new CounterWactor 13 | 14 | def main(args: Array[String]) { 15 | start() 16 | stop() 17 | sys.exit(0) 18 | } 19 | 20 | def start(print: Boolean = true) { runTest(runs, print) } 21 | def stop() { counter ! Wactor.Die } 22 | 23 | 24 | def runTest(msgCount: Long, print: Boolean) { 25 | val start = Platform.currentTime 26 | theTest(msgCount) 27 | val finish = Platform.currentTime 28 | val elapsedTime = (finish - start) / 1000.0 29 | 30 | // disable output on warmup run! 31 | if (print) { 32 | printf("%n") 33 | printf("[wactor] Count is %s%n", msgCount) 34 | printf("[wactor] Test took %s seconds%n", elapsedTime) 35 | printf("[wactor] Throughput=%s per sec%n", msgCount / elapsedTime) 36 | printf("%n") 37 | } 38 | } 39 | 40 | def theTest(msgCount: Long): Any = { 41 | val bytesPerMsg = 100 42 | val updates = (1L to msgCount).par.foreach((x: Long) => counter !! new AddCount(bytesPerMsg)) 43 | counter ! Reset 44 | } 45 | 46 | } 47 | 48 | class CounterWactor extends Wactor { 49 | override val loggerName = "CounterWactor" 50 | var count = 0L 51 | 52 | def receive = { 53 | case Reset => 54 | printf("[wactor] Count is %s%n",count) 55 | count = 0 56 | case AddCount(extraCount) => 57 | count += extraCount 58 | } 59 | } 60 | 61 | --------------------------------------------------------------------------------