diff --git a/.gitignore b/.gitignore index b91e59a..e520917 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,5 @@ project/plugins/project project/sbt-ui.sbt target/ credentials.sbt + +*.csv diff --git a/README.md b/README.md index b75a895..2d18bfd 100644 --- a/README.md +++ b/README.md @@ -5,27 +5,17 @@ [![image](https://upload.wikimedia.org/wikipedia/en/a/a7/MassiveAttackBlueLines.jpg)](https://en.wikipedia.org/wiki/Blue_Lines) -`massive-attack` is a simple and configurable load generator test tool written in Scala originally to test Apache Thrift endpoints, but it can be -used to benchmark any method that returns a Scala or Twitter `Future`. -

Why?

+Proprietor: RMS -Because I needed to load test a Thrift endpoint in one of my APIs, and could not find an easy to use tool after looking around for days, at least -not one that did not require me to develop JMeter plugins, or one that has been updated in the past few years. - -

How?

- -So I decided to create one which is easy to use: - -1. It can easily be added as a dependency to your API or application -2. It is configurable as much or as little as you need it to be -3. It is extensible +Load generator test tool written in Scala used to benchmark any method that returns a Scala or Twitter `Future`. +Project forked from [massive-attack](https://github.com/delprks/massive-attack) project created and mantained by [Daniel Parks](https://github.com/delprks) -

Usage

+## Usage 1. Add it as a dependency to `build.sbt`: -`libraryDependencies ++= Seq("com.delprks" %% "massive-attack" % "1.0.0" % "test")` +`libraryDependencies += "bbc.rms" %% "massive-attack" % % "test")` 2. Create your test in [ScalaTest](http://www.scalatest.org) or [Specs2](https://etorreborre.github.io/specs2) (this library might change to be a testing framework in future) @@ -58,36 +48,36 @@ And (if enabled) generate a report containing the test results: ![image](https://user-images.githubusercontent.com/8627976/41814268-92468eac-773f-11e8-8076-88b4ef9e17e1.png) -

Test properties

+## Test properties Following properties are available and configurable through `MassiveAttackProperties`: -

invocations

+### invocations Specifies how many times the method should be invoked. -

threads

+### threads You can set how many threads you want to run the load test on - beware that Thrift clients can run only on single threads. -

duration

+### duration Specifies how long the method should be tested for in seconds - whichever comes first (duration or invocations) determines the length of the test. -

warmUp

+### warmUp This is by default set to `true` to avoid cold start times affecting the test results - set it to false if you want to test cold starts. -

warmUpInvocations

+### warmUpInvocations If `warmUp` is set to true, `warmUpInvocations` determines how many times the method should be invoked before the load test starts. -

spikeFactor

+### spikeFactor This is used to decide which response times should be considered as spikes, by multiplying the average response time and `spikeFactor`. It has the default value of `3.0`. -

verbose

+### verbose Set this to true if you want to see invocation times when the load test is in progress. @@ -95,10 +85,10 @@ Set this to true if you want to see invocation times when the load test is in pr Set this to true if you want to save test results in a CSV file. -

reportName

+### reportName If `report` is set to true, results will be saved to this file. If no `reportName` is specified, one will be generated. # License -`massive-attack` is open source software released under the [Apache 2 License](http://www.apache.org/licenses/LICENSE-2.0). +original [massive-attack](https://github.com/delprks/massive-attack) is an open source software released under the [Apache 2 License](http://www.apache.org/licenses/LICENSE-2.0). diff --git a/build.sbt b/build.sbt index 40cb515..5bee151 100644 --- a/build.sbt +++ b/build.sbt @@ -1,74 +1,78 @@ -name := "massive-attack" +import sbt.Keys.pomExtra +import sbt.{Classpaths, Test} -crossScalaVersions := Seq("2.12.6", "2.11.12") - -organization := "com.delprks" +ThisBuild / organization := "bbc.rms" +ThisBuild / scalaVersion := "2.13.2" +ThisBuild / name := "massive-attack" scalacOptions := Seq("-unchecked", "-deprecation", "-feature", "-encoding", "utf8") javacOptions ++= Seq("-source", "1.8", "-target", "1.8") -publishTo := { - val isSnapshotValue = isSnapshot.value - val nexus = "https://oss.sonatype.org/" - if (isSnapshotValue) Some("snapshots" at nexus + "content/repositories/snapshots") - else Some("releases" at nexus + "service/local/staging/deploy/maven2") -} - -publishMavenStyle := true - -publishArtifact in Test := false - -parallelExecution in Test := false - -releaseCrossBuild := true - -sbtrelease.ReleasePlugin.autoImport.releasePublishArtifactsAction := PgpKeys.publishSigned.value - -sbtrelease.ReleasePlugin.autoImport.releaseCrossBuild := false - -SbtPgp.autoImport.useGpg := true +resolvers ++= Seq("BBC Artifactory" at "https://artifactory.dev.bbc.co.uk/artifactory/repo/") :+ Classpaths.typesafeReleases + +lazy val mavenStyleSettings = Seq( + publishMavenStyle := true, + pomIncludeRepository := { + _ => false + }, + pomExtra := { + https://github.com/delprks/massive-attack + + + Apache 2 + http://www.apache.org/licenses/LICENSE-2.0 + repo + + + + git@github.com:delprks/massive-attack.git + scm:git@github.com:delprks/massive-attack.git + + + + delprks + Daniel Parks + http://github.com/delprks + + + } +) SbtPgp.autoImport.useGpgAgent := true -libraryDependencies ++= Seq( - "com.twitter" %% "util-core" % "18.5.0", - "com.typesafe.akka" %% "akka-actor" % "2.5.13", - "com.typesafe.akka" %% "akka-testkit" % "2.5.13" % Test, - "org.scalatest" %% "scalatest" % "3.0.5" % Test +lazy val publishSettings = Seq( + version := scala.util.Properties.envOrElse("BUILD_VERSION", "0.1-SNAPSHOT"), + publishArtifact in (Test, packageBin) := true, + publishMavenStyle := true, + publishTo := Some("BBC Repository" at "https://artifactory.dev.bbc.co.uk/artifactory/int-bbc-releases"), + credentials += Credentials(Path.userHome / ".ivy2" / ".credentials") ) -releaseTagComment := s"Releasing ${(version in ThisBuild).value}" -releaseCommitMessage := s"= Setting version to ${(version in ThisBuild).value}" - -pomIncludeRepository := { - _ => false -} - -pomExtra := { - https://github.com/delprks/massive-attack - - - Apache 2 - http://www.apache.org/licenses/LICENSE-2.0 - repo - - - - git@github.com:delprks/massive-attack.git - scm:git@github.com:delprks/massive-attack.git - - - - delprks - Daniel Parks - http://github.com/delprks - - -} - -connectInput in run := true +lazy val testSettings = Seq( + Test / publishArtifact := false, + Test / parallelExecution := false +) -fork in run := true -lazy val root = project in file(".") +libraryDependencies ++= Seq( + "com.twitter" %% "util-core" % "20.5.0", + "com.typesafe.akka" %% "akka-actor" % "2.6.5", + "com.typesafe.akka" %% "akka-testkit" % "2.6.5" % Test, + "org.scalatest" %% "scalatest" % "3.1.2" % Test +) ++ (CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, major)) if major <= 12 => + Seq() + case _ => + Seq("org.scala-lang.modules" %% "scala-parallel-collections" % "0.2.0") +}) + + +run /connectInput := true + +run / fork := true + +lazy val `massive-attack` = (project in file(".")) + .settings(publishSettings) + .settings(testSettings) + .settings(mavenStyleSettings) diff --git a/project/build.properties b/project/build.properties index 16eecf5..f37b0aa 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version = 1.1.6 \ No newline at end of file +sbt.version = 1.3.10 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt index e5e6894..1883c16 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,3 @@ resolvers += Classpaths.sbtPluginReleases -addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0") -addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.7") +addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.0.0") diff --git a/src/main/scala/com/delprks/massiveattack/MassiveAttack.scala b/src/main/scala/bbc/rms/massiveattack/MassiveAttack.scala similarity index 91% rename from src/main/scala/com/delprks/massiveattack/MassiveAttack.scala rename to src/main/scala/bbc/rms/massiveattack/MassiveAttack.scala index 6cb8c37..a06bc6f 100644 --- a/src/main/scala/com/delprks/massiveattack/MassiveAttack.scala +++ b/src/main/scala/bbc/rms/massiveattack/MassiveAttack.scala @@ -1,4 +1,4 @@ -package com.delprks.massiveattack +package bbc.rms.massiveattack import com.twitter.util.{Future => TwitterFuture} diff --git a/src/main/scala/bbc/rms/massiveattack/MassiveAttackProps.scala b/src/main/scala/bbc/rms/massiveattack/MassiveAttackProps.scala new file mode 100644 index 0000000..7a61b2e --- /dev/null +++ b/src/main/scala/bbc/rms/massiveattack/MassiveAttackProps.scala @@ -0,0 +1,3 @@ +package bbc.rms.massiveattack + +trait MassiveAttackProps diff --git a/src/main/scala/com/delprks/massiveattack/MassiveAttackResult.scala b/src/main/scala/bbc/rms/massiveattack/MassiveAttackResult.scala similarity index 61% rename from src/main/scala/com/delprks/massiveattack/MassiveAttackResult.scala rename to src/main/scala/bbc/rms/massiveattack/MassiveAttackResult.scala index f1f2a5b..7c243ce 100644 --- a/src/main/scala/com/delprks/massiveattack/MassiveAttackResult.scala +++ b/src/main/scala/bbc/rms/massiveattack/MassiveAttackResult.scala @@ -1,4 +1,4 @@ -package com.delprks.massiveattack +package bbc.rms.massiveattack trait MassiveAttackResult { def toString: String diff --git a/src/main/scala/com/delprks/massiveattack/method/MethodPerformance.scala b/src/main/scala/bbc/rms/massiveattack/method/MethodPerformance.scala similarity index 87% rename from src/main/scala/com/delprks/massiveattack/method/MethodPerformance.scala rename to src/main/scala/bbc/rms/massiveattack/method/MethodPerformance.scala index 53af278..5f0d5f2 100644 --- a/src/main/scala/com/delprks/massiveattack/method/MethodPerformance.scala +++ b/src/main/scala/bbc/rms/massiveattack/method/MethodPerformance.scala @@ -1,19 +1,20 @@ -package com.delprks.massiveattack.method +package bbc.rms.massiveattack.method import java.util.concurrent.Executors import akka.util.Timeout -import com.delprks.massiveattack.MassiveAttack -import com.delprks.massiveattack.method.result.{MethodDurationResult, MethodPerformanceResult} -import com.delprks.massiveattack.method.util.{MethodOps, ResultOps} +import bbc.rms.massiveattack.MassiveAttack +import bbc.rms.massiveattack.method.result.{MethodDurationResult, MethodPerformanceResult} +import bbc.rms.massiveattack.method.util.{MethodOps, ResultOps} +import scala.collection.parallel.CollectionConverters._ + import scala.collection.mutable.ListBuffer -import scala.collection.parallel.ForkJoinTaskSupport -import scala.collection.parallel.mutable.ParArray import com.twitter.util.{Future => TwitterFuture} +import scala.collection.parallel.ForkJoinTaskSupport +import scala.collection.parallel.mutable.ParArray import scala.concurrent.duration._ -import scala.concurrent.forkjoin.ForkJoinPool import scala.concurrent.{ExecutionContext, Future => ScalaFuture} import scala.reflect.ClassTag @@ -37,8 +38,8 @@ class MethodPerformance(props: MethodPerformanceProps = MethodPerformanceProps() val testStartTime = System.currentTimeMillis() val testEndTime = testStartTime + props.duration * 1000 - val parallelInvocation: ParArray[Int] = (1 to props.invocations).toParArray - val forkJoinPool = new ForkJoinPool(props.threads) + val parallelInvocation: ParArray[Int] = (1 to props.invocations).toArray.par + val forkJoinPool = new java.util.concurrent.ForkJoinPool(props.threads) parallelInvocation.tasksupport = new ForkJoinTaskSupport(forkJoinPool) @@ -62,10 +63,11 @@ class MethodPerformance(props: MethodPerformanceProps = MethodPerformanceProps() println(Console.RED + s"Invoking method ${props.invocations} times - or maximum ${props.duration} seconds - on ${props.threads} threads" + Console.RESET) + val testStartTime = System.currentTimeMillis() val testEndTime = testStartTime + props.duration * 1000 - val parallelInvocation: ParArray[Int] = (1 to props.invocations).toParArray - val forkJoinPool = new scala.concurrent.forkjoin.ForkJoinPool(props.threads) + val parallelInvocation: ParArray[Int] = (1 to props.invocations).toArray.par + val forkJoinPool = new java.util.concurrent.ForkJoinPool(props.threads) parallelInvocation.tasksupport = new ForkJoinTaskSupport(forkJoinPool) diff --git a/src/main/scala/com/delprks/massiveattack/method/MethodPerformanceProps.scala b/src/main/scala/bbc/rms/massiveattack/method/MethodPerformanceProps.scala similarity index 77% rename from src/main/scala/com/delprks/massiveattack/method/MethodPerformanceProps.scala rename to src/main/scala/bbc/rms/massiveattack/method/MethodPerformanceProps.scala index 8f0f91f..734e3f2 100644 --- a/src/main/scala/com/delprks/massiveattack/method/MethodPerformanceProps.scala +++ b/src/main/scala/bbc/rms/massiveattack/method/MethodPerformanceProps.scala @@ -1,6 +1,6 @@ -package com.delprks.massiveattack.method +package bbc.rms.massiveattack.method -import com.delprks.massiveattack.MassiveAttackProps +import bbc.rms.massiveattack.MassiveAttackProps case class MethodPerformanceProps( invocations: Int = 1000, diff --git a/src/main/scala/com/delprks/massiveattack/method/result/MethodDurationResult.scala b/src/main/scala/bbc/rms/massiveattack/method/result/MethodDurationResult.scala similarity index 57% rename from src/main/scala/com/delprks/massiveattack/method/result/MethodDurationResult.scala rename to src/main/scala/bbc/rms/massiveattack/method/result/MethodDurationResult.scala index fcd1cb2..7d4d7f1 100644 --- a/src/main/scala/com/delprks/massiveattack/method/result/MethodDurationResult.scala +++ b/src/main/scala/bbc/rms/massiveattack/method/result/MethodDurationResult.scala @@ -1,3 +1,3 @@ -package com.delprks.massiveattack.method.result +package bbc.rms.massiveattack.method.result case class MethodDurationResult(duration: Long, endTime: Long) diff --git a/src/main/scala/com/delprks/massiveattack/method/result/MethodPerformanceResult.scala b/src/main/scala/bbc/rms/massiveattack/method/result/MethodPerformanceResult.scala similarity index 93% rename from src/main/scala/com/delprks/massiveattack/method/result/MethodPerformanceResult.scala rename to src/main/scala/bbc/rms/massiveattack/method/result/MethodPerformanceResult.scala index ba5289f..d5b936e 100644 --- a/src/main/scala/com/delprks/massiveattack/method/result/MethodPerformanceResult.scala +++ b/src/main/scala/bbc/rms/massiveattack/method/result/MethodPerformanceResult.scala @@ -1,6 +1,6 @@ -package com.delprks.massiveattack.method.result +package bbc.rms.massiveattack.method.result -import com.delprks.massiveattack.MassiveAttackResult +import bbc.rms.massiveattack.MassiveAttackResult case class MethodPerformanceResult( responseTimeMin: Int, diff --git a/src/main/scala/com/delprks/massiveattack/method/result/MethodStatsRecorder.scala b/src/main/scala/bbc/rms/massiveattack/method/result/MethodStatsRecorder.scala similarity index 88% rename from src/main/scala/com/delprks/massiveattack/method/result/MethodStatsRecorder.scala rename to src/main/scala/bbc/rms/massiveattack/method/result/MethodStatsRecorder.scala index e825228..eb4d6c3 100644 --- a/src/main/scala/com/delprks/massiveattack/method/result/MethodStatsRecorder.scala +++ b/src/main/scala/bbc/rms/massiveattack/method/result/MethodStatsRecorder.scala @@ -1,4 +1,4 @@ -package com.delprks.massiveattack.method.result +package bbc.rms.massiveattack.method.result import akka.actor.Actor diff --git a/src/main/scala/com/delprks/massiveattack/method/util/MethodOps.scala b/src/main/scala/bbc/rms/massiveattack/method/util/MethodOps.scala similarity index 94% rename from src/main/scala/com/delprks/massiveattack/method/util/MethodOps.scala rename to src/main/scala/bbc/rms/massiveattack/method/util/MethodOps.scala index 477579f..e88fe07 100644 --- a/src/main/scala/com/delprks/massiveattack/method/util/MethodOps.scala +++ b/src/main/scala/bbc/rms/massiveattack/method/util/MethodOps.scala @@ -1,7 +1,6 @@ -package com.delprks.massiveattack.method.util +package bbc.rms.massiveattack.method.util import akka.actor.{ActorSystem, Props} -import com.delprks.massiveattack.method.result.{GetStats, MethodDurationResult, MethodStatsRecorder} import com.twitter.util.{Future => TwitterFuture} import scala.collection.mutable.ListBuffer @@ -11,6 +10,7 @@ import scala.reflect.ClassTag import akka.actor._ import akka.pattern.ask import akka.util.Timeout +import bbc.rms.massiveattack.method.result.{GetStats, MethodDurationResult, MethodStatsRecorder} import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ diff --git a/src/main/scala/com/delprks/massiveattack/method/util/ResultOps.scala b/src/main/scala/bbc/rms/massiveattack/method/util/ResultOps.scala similarity index 88% rename from src/main/scala/com/delprks/massiveattack/method/util/ResultOps.scala rename to src/main/scala/bbc/rms/massiveattack/method/util/ResultOps.scala index f3f46b7..7500422 100644 --- a/src/main/scala/com/delprks/massiveattack/method/util/ResultOps.scala +++ b/src/main/scala/bbc/rms/massiveattack/method/util/ResultOps.scala @@ -1,12 +1,12 @@ -package com.delprks.massiveattack.method.util +package bbc.rms.massiveattack.method.util import java.nio.charset.StandardCharsets import java.nio.file.{Files, Paths} import java.text.SimpleDateFormat import java.util.Calendar -import com.delprks.massiveattack.method.MethodPerformanceProps -import com.delprks.massiveattack.method.result.{MethodDurationResult, MethodPerformanceResult} +import bbc.rms.massiveattack.method.MethodPerformanceProps +import bbc.rms.massiveattack.method.result.{MethodDurationResult, MethodPerformanceResult} import scala.concurrent.{ExecutionContext, Future} import scala.collection.mutable.ListBuffer @@ -15,7 +15,8 @@ class ResultOps()(implicit ec: ExecutionContext) { def testResults(results: Future[ListBuffer[MethodDurationResult]], testProps: MethodPerformanceProps): Future[MethodPerformanceResult] = results map { response => val responseDuration = response.map(_.duration.toInt) - val average = avg(responseDuration) + val responseDurationSeq = responseDuration.toSeq + val average = avg(responseDurationSeq) val invocationSeconds = response.map(_.endTime / 1000) val requestTimesPerSecond = invocationSeconds.groupBy(identity).map(_._2.size) val spikeBoundary = (average * testProps.spikeFactor).toInt @@ -27,8 +28,8 @@ class ResultOps()(implicit ec: ExecutionContext) { val testResult = MethodPerformanceResult( responseTimeMin = responseDuration.min, responseTimeMax = responseDuration.max, - responseTime95tile = percentile(95)(responseDuration), - responseTime99tile = percentile(99)(responseDuration), + responseTime95tile = percentile(95)(responseDurationSeq), + responseTime99tile = percentile(99)(responseDurationSeq), responseTimeAvg = average, rpsMin = requestTimesPerSecond.min, rpsMax = requestTimesPerSecond.max, diff --git a/src/main/scala/com/delprks/massiveattack/MassiveAttackProps.scala b/src/main/scala/com/delprks/massiveattack/MassiveAttackProps.scala deleted file mode 100644 index 705cc21..0000000 --- a/src/main/scala/com/delprks/massiveattack/MassiveAttackProps.scala +++ /dev/null @@ -1,3 +0,0 @@ -package com.delprks.massiveattack - -trait MassiveAttackProps diff --git a/src/test/scala/com/delprks/massiveattack/method/MethodPerformanceSpec.scala b/src/test/scala/bbc/rms/massiveattack/method/MethodPerformanceSpec.scala similarity index 87% rename from src/test/scala/com/delprks/massiveattack/method/MethodPerformanceSpec.scala rename to src/test/scala/bbc/rms/massiveattack/method/MethodPerformanceSpec.scala index 3dc9599..3b36944 100644 --- a/src/test/scala/com/delprks/massiveattack/method/MethodPerformanceSpec.scala +++ b/src/test/scala/bbc/rms/massiveattack/method/MethodPerformanceSpec.scala @@ -1,16 +1,18 @@ -package com.delprks.massiveattack.method +package bbc.rms.massiveattack.method -import com.delprks.massiveattack.method.result.MethodPerformanceResult -import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike} +import org.scalatest.BeforeAndAfterAll +import org.scalatest.matchers.should.Matchers import scala.concurrent.duration._ import scala.concurrent.{Await, Future => ScalaFuture} import com.twitter.util.{Future => TwitterFuture} import akka.actor.ActorSystem import akka.testkit.{ImplicitSender, TestKit} +import bbc.rms.massiveattack.method.result.MethodPerformanceResult +import org.scalatest.wordspec.AnyWordSpecLike class MethodPerformanceSpec extends TestKit(ActorSystem("MassiveAttackSpec")) with ImplicitSender - with WordSpecLike with Matchers with BeforeAndAfterAll { + with AnyWordSpecLike with Matchers with BeforeAndAfterAll { protected lazy val futureSupportTimeout: Duration = 30.seconds diff --git a/version.sbt b/version.sbt deleted file mode 100644 index e14138d..0000000 --- a/version.sbt +++ /dev/null @@ -1 +0,0 @@ -version in ThisBuild := "1.0.8-SNAPSHOT"