├── src ├── main │ ├── resources │ │ └── config.conf │ └── scala │ │ └── com │ │ └── twitter │ │ └── jackhammer │ │ └── LoggingLoadTest.scala └── test │ └── scala │ └── com │ └── twitter │ └── jackhammer │ └── LoggingLoadTestSpec.scala ├── .gitignore ├── project ├── build.properties └── build │ └── JackhammerProject.scala ├── LICENSE └── README.textile /src/main/resources/config.conf: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | dist 3 | #*# 4 | .#* 5 | *.log 6 | *.iml 7 | *.ipr 8 | *.iws 9 | *.swp 10 | lib_managed 11 | project/boot 12 | .DS_Store 13 | src_managed 14 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | #Project properties 2 | #Thu Mar 04 15:53:57 PST 2010 3 | project.organization=Twitter, Inc. 4 | project.name=Jackhammer 5 | sbt.version=0.7.1 6 | project.version=1.0 7 | def.scala.version=2.7.7 8 | build.scala.versions=2.7.7 9 | project.initialize=false 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2010 Alex Payne 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /src/test/scala/com/twitter/jackhammer/LoggingLoadTestSpec.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.jackhammer 2 | 3 | import org.specs._ 4 | 5 | 6 | class LoggingLoadTestSpec extends Specification with LoggingLoadTest { 7 | "LoggingLoadTest" should { 8 | var counter = 0 9 | 10 | doBefore { 11 | counter = 0 12 | counter mustEqual 0 13 | } 14 | 15 | "increment a counter once when running with timing" in { 16 | val result: Int = runWithTiming { 17 | counter += 1 18 | counter 19 | } 20 | 21 | counter mustEqual 1 22 | result mustEqual 1 23 | } 24 | 25 | "increment a counter for every one of ten runs of runWithTimingNTimes" in { 26 | runWithTimingNTimes(10) { 27 | counter += 1 28 | } 29 | 30 | counter mustEqual 10 31 | } 32 | 33 | "increment a counter in each parallel run via an Actor" in { 34 | runInActorNTimes(10) { 35 | counter += 1 36 | Thread.sleep(20) // simulate the delay of performing a non-trival computation 37 | } 38 | 39 | counter must be(10).eventually 40 | } 41 | 42 | "increment a counter for each parallel threaded run" in { 43 | runInNParallelThreadsMTimes(4, 10) { 44 | counter += 1 45 | Thread.sleep(20) 46 | } 47 | 48 | counter must be(40).eventually 49 | } 50 | 51 | "log runs and dump log output to a file" in { 52 | runWithTimingNTimes(10) { 53 | counter += 1 54 | } 55 | 56 | logOutput.isEmpty mustBe false 57 | 58 | val logfile = dumpLogOutput 59 | logfile.exists mustBe true 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | *THIS IS NOT USABLE YET UNLESS YOU'RE EXTREMELY PATIENT* 2 | 3 | h1. Jackhammer 4 | 5 | h2. About 6 | 7 | Jackhammer is a generic load testing framework, written in "Scala":http://scala-lang.org/. Without assuming what sort of system you're trying to load test, Jackhammer provides flexible higher-order functions to measurably, repeatedly, and concurrently execute operations. Scripts are provided to tally statistics gathered in test runs and produce basic reports. 8 | 9 | You might use Jackhammer to load test a new system you're building (ex: a domain-specific custom datastore), or you might use it to load test a commercial or open source system (ex: a web server, a database, a distributed caching layer) to ensure it meets your operational requirements. 10 | 11 | You can easily use Jackhammer to drive load tests of Java code, by virtue of Scala's ability to transparently call into Java. With a little more work, you could probably use it to load test code written in other JVM languages, such as Clojure or JRuby. 12 | 13 | Jackhammer was developed by members of the Infrastructure team at "Twitter, Inc.":http://twitter.com/jobs after writing a number of one-off load tests and observing what patterns worked well for us. The project is named in honor of Twitter's primary creator and Chairman of the Board, "Jack Dorsey":http://twitter.com/jack. 14 | 15 | h2. Requirements 16 | 17 | TODO 18 | 19 | h2. Usage 20 | 21 | TODO 22 | 23 | h2. Authors 24 | 25 | "Alex Payne":http://github.com/al3x is the primary committer. Jackhammer borrows heavily from load testing techniques used by his colleague at Twitter, "Steve Jenson":http://github.com/stevej. "Matt Knox":http://github.com/mattknox helped out, and pushed for a higher-order approach. 26 | 27 | h2. License 28 | 29 | Apache License, Version 2.0. See the LICENSE file for more information. 30 | -------------------------------------------------------------------------------- /project/build/JackhammerProject.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import Process._ 3 | 4 | 5 | class JackhammerProject(info: ProjectInfo) extends DefaultProject(info) { 6 | // Maven repositories 7 | val scalaToolsTesting = "testing.scala-tools.org" at "http://scala-tools.org/repo-releases/" 8 | val powerMock = "powermock-api" at "http://powermock.googlecode.com/svn/repo/" 9 | val mavenDotOrg = "repo1" at "http://repo1.maven.org/maven2/" 10 | val scalaToolsReleases = "scala-tools.org" at "http://scala-tools.org/repo-releases/" 11 | val reucon = "reucon" at "http://maven.reucon.com/public/" 12 | val lagDotNet = "lag.net" at "http://www.lag.net/repo/" 13 | val oauthDotNet = "oauth.net" at "http://oauth.googlecode.com/svn/code/maven" 14 | val javaDotNet = "download.java.net" at "http://download.java.net/maven/2/" 15 | val jBoss = "jboss-repo" at "http://repository.jboss.org/maven2/" 16 | val nest = "nest" at "http://www.lag.net/nest/" 17 | 18 | // dependencies 19 | val specs = "org.scala-tools.testing" % "specs" % "1.6.2" 20 | val vscaladoc = "org.scala-tools" % "vscaladoc" % "1.1-md-3" 21 | val markdownj = "markdownj" % "markdownj" % "1.0.2b4-0.3.0" 22 | val slf4jApi = "org.slf4j" % "slf4j-api" % "1.5.8" 23 | val slf4jLog = "org.slf4j" % "slf4j-log4j12" % "1.5.8" 24 | val log4j = "apache-log4j" % "log4j" % "1.2.15" 25 | val commonsLogging = "commons-logging" % "commons-logging" % "1.1" 26 | val commonsLang = "commons-lang" % "commons-lang" % "2.2" 27 | val oro = "oro" % "oro" % "2.0.7" 28 | val configgy = "net.lag" % "configgy" % "1.4.7" 29 | val mockito = "org.mockito" % "mockito-core" % "1.8.1" 30 | val xrayspecs = "com.twitter" % "xrayspecs" % "1.0.5" 31 | val hamcrest = "org.hamcrest" % "hamcrest-all" % "1.1" 32 | val asm = "asm" % "asm-all" % "2.2" 33 | val objenesis = "org.objenesis" % "objenesis" % "1.1" 34 | val javautils = "org.scala-tools" % "javautils" % "2.7.4-0.1" 35 | val ostrich = "com.twitter" % "ostrich" % "1.1.6" 36 | } 37 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/jackhammer/LoggingLoadTest.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.jackhammer 2 | 3 | import java.io.{BufferedReader, File, FileReader, FileWriter} 4 | import java.util.concurrent.{ConcurrentLinkedQueue, CountDownLatch} 5 | import scala.actors.Actor 6 | import scala.actors.Actor._ 7 | import scala.collection.{immutable, mutable} 8 | import com.twitter.ostrich.Stats 9 | import net.lag.configgy.{Config, Configgy} 10 | import net.lag.logging.Logger 11 | 12 | 13 | trait LoggingLoadTest { 14 | Configgy.configure("src/main/resources/config.conf") 15 | 16 | private val log = Logger.get 17 | private val config = Configgy.config 18 | private[jackhammer] val logOutput = new ConcurrentLinkedQueue[String]() 19 | 20 | def runWithTimingNTimes[T](runs: Int)(f: => T) { 21 | for (i <- 1 to runs) { 22 | runWithTiming(f) 23 | } 24 | } 25 | 26 | def runInActorNTimes[T](runs: Int)(f: => T) { 27 | val runner = actor { 28 | loop { 29 | react { 30 | case f: Function[_, _] => f 31 | } 32 | } 33 | } 34 | 35 | for (i <- 1 to runs) { 36 | runner ! runWithTiming(f) 37 | } 38 | } 39 | 40 | def runInNParallelThreadsMTimes[T](threads: Int, runs: Int)(f: => T) { 41 | val countDownLatch = new CountDownLatch(runs * threads) 42 | var threadList = new mutable.ListBuffer[Thread]() 43 | 44 | for (i <- 1 to threads) { 45 | threadList += new Thread { 46 | runWithTimingNTimes(runs) { 47 | f 48 | countDownLatch.countDown() 49 | } 50 | } 51 | } 52 | 53 | threadList.map { thread => thread.run } 54 | countDownLatch.await() 55 | } 56 | 57 | def runWithTiming[T](f: => T): T = { 58 | val time = System.currentTimeMillis 59 | val (result, duration) = Stats.duration[T] { f } 60 | 61 | // output columns: 62 | // 1. operation start time 63 | // 2. how long the operation took 64 | logOutput.offer("%s %d".format(time, duration)) 65 | 66 | result 67 | } 68 | 69 | def dumpLogOutput: File = { 70 | val tmpFile = File.createTempFile("loadtest", ".log") 71 | dumpLogOutput(tmpFile) 72 | tmpFile 73 | } 74 | 75 | def dumpLogOutput(outputFile: File) { 76 | val logOutputIter = logOutput.iterator 77 | val writer = new FileWriter(outputFile) 78 | 79 | while (logOutputIter.hasNext) { 80 | val logLine = logOutputIter.next 81 | writer.write(logLine + "\n") 82 | } 83 | 84 | writer.flush 85 | writer.close 86 | 87 | log.info("test run statistics dumped to %s", outputFile.getPath) 88 | } 89 | } 90 | --------------------------------------------------------------------------------