├── .gitignore ├── bin ├── benchmark-all ├── benchmark-apollo ├── benchmark-mosquitto ├── benchmark-report └── benchmark-setup ├── custom-scenario.md ├── license.txt ├── notice.md ├── pom.xml ├── project ├── build.properties └── build │ └── project.scala ├── readme.md ├── reports ├── index.html └── ubuntu-2600k │ ├── apollo-1.1-SNAPSHOT.json │ ├── apollo-1.1-SNAPSHOT.log │ ├── index.html │ ├── mosquitto-0.15.json │ ├── mosquitto-0.15.log │ ├── resources │ ├── ajax-loader.gif │ ├── blank.gif │ ├── jquery.flot.crosshair.js │ ├── jquery.flot.js │ └── jquery.js │ └── server-info.html └── src └── main ├── html ├── resources │ ├── ajax-loader.gif │ ├── blank.gif │ ├── jquery.flot.crosshair.js │ ├── jquery.flot.js │ └── jquery.js └── template.html └── scala └── com └── github └── mqtt └── benchmark ├── Benchmark.scala ├── BenchmarkResults.scala ├── FlexibleProperty.scala ├── NonBlockingScenario.scala └── Scenario.scala /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.swp 3 | .idea 4 | .idea/* 5 | *.iml 6 | *.ipr 7 | *.iws 8 | target 9 | workspace 10 | .DS_Store 11 | .scala_dependencies 12 | .project 13 | .classpath 14 | .settings 15 | eclipse-classes 16 | project/boot 17 | lib_managed 18 | -------------------------------------------------------------------------------- /bin/benchmark-all: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Runs all the benchmarks. 3 | # 4 | 5 | BASEDIR=`dirname "$0"`/.. ; cd "${BASEDIR}" ; BASEDIR=`pwd` ; cd - > /dev/null 6 | export SKIP_REPORT="true" 7 | "${BASEDIR}/bin/benchmark-apollo" $* 8 | "${BASEDIR}/bin/benchmark-mosquitto" $* 9 | export SKIP_REPORT= 10 | "${BASEDIR}/bin/benchmark-report" $* 11 | -------------------------------------------------------------------------------- /bin/benchmark-apollo: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This shell script automates running the mqtt-benchmark [1] against the 4 | # Apache Apollo project [2]. 5 | # 6 | # [1]: http://github.com/chirino/mqtt-benchmark 7 | # [2]: http://activemq.apache.org/apollo 8 | # 9 | 10 | true \ 11 | ${APOLLO_VERSION:=1.1} \ 12 | ${APOLLO_DOWNLOAD:="http://archive.apache.org/dist/activemq/activemq-apollo/${APOLLO_VERSION}/apache-apollo-${APOLLO_VERSION}-unix-distro.tar.gz"} \ 13 | ${MQTT_PLUIGN_VERSION:=1.0} \ 14 | ${MQTT_PLUIGN_DOWNLOAD:="http://repo.fusesource.com/nexus/content/repositories/public/org/fusesource/fuse-extra/fusemq-apollo-mqtt/${MQTT_PLUIGN_VERSION}/fusemq-apollo-mqtt-${MQTT_PLUIGN_VERSION}-uber.jar"} 15 | #${MQTT_PLUIGN_VERSION:=99-master-SNAPSHOT} \ 16 | #${MQTT_PLUIGN_DOWNLOAD:="http://repo.fusesource.com/nexus/service/local/artifact/maven/redirect?r=snapshots&g=org.fusesource.fabric.apollo&a=fabric-apollo-mqtt&v=${MQTT_PLUIGN_VERSION}"} 17 | # ${APOLLO_DOWNLOAD:="http://repository.apache.org/service/local/artifact/maven/redirect?r=snapshots&g=org.apache.activemq&a=apache-apollo&v=${APOLLO_VERSION}&e=tar.gz&c=unix-distro"} \ 18 | # ${APOLLO_DOWNLOAD:="https://repository.apache.org/content/repositories/orgapacheactivemq-162/org/apache/activemq/apache-apollo/${APOLLO_VERSION}/apache-apollo-${APOLLO_VERSION}-unix-distro.tar.gz"} 19 | 20 | 21 | BASEDIR=`dirname "$0"`/.. ; cd "${BASEDIR}" ; BASEDIR=`pwd` ; cd - > /dev/null 22 | . ${BASEDIR}/bin/benchmark-setup 23 | 24 | # 25 | # Install the apollo distro 26 | # 27 | APOLLO_HOME="${WORKSPACE}/apache-apollo-${APOLLO_VERSION}" 28 | if [ ! -d "${APOLLO_HOME}" ]; then 29 | cd ${WORKSPACE} 30 | wget "$APOLLO_DOWNLOAD" -O apache-apollo-${APOLLO_VERSION}.tar.gz 31 | tar -zxvf apache-apollo-${APOLLO_VERSION}.tar.gz 32 | rm -rf apache-apollo-${APOLLO_VERSION}.tar.gz 33 | fi 34 | 35 | if [ ! -f "${APOLLO_HOME}/lib/fabric-apollo-mqtt-${MQTT_PLUIGN_VERSION}.jar" ]; then 36 | wget "$MQTT_PLUIGN_DOWNLOAD" -O "${APOLLO_HOME}/lib/fabric-apollo-mqtt-${MQTT_PLUIGN_VERSION}.jar" 37 | fi 38 | 39 | APOLLO_BASE="${WORKSPACE}/apollo-${APOLLO_VERSION}" 40 | if [ ! -d "${APOLLO_BASE}" ]; then 41 | cd "${WORKSPACE}" 42 | "${APOLLO_HOME}/bin/apollo" create --with-ssl=false "apollo-${APOLLO_VERSION}" 43 | perl -pi -e 's|||' "${WORKSPACE}/apollo-${APOLLO_VERSION}/etc/apollo.xml" 44 | fi 45 | 46 | # 47 | # Cleanup preious executions. 48 | killall -9 java erl epmd apollo mosquitto > /dev/null 2>&1 49 | rm -rf ${APOLLO_BASE}/data/* ${APOLLO_BASE}/tmp/* ${APOLLO_BASE}/log/* 50 | 51 | # 52 | # Configuration 53 | export APOLLO_ASSERTIONS="false" 54 | export JVM_FLAGS="-server -Xmx4G -Xms1G" 55 | 56 | # 57 | # Start the server 58 | CONSOLE_LOG="${REPORTS_HOME}/apollo-${APOLLO_VERSION}.log" 59 | "${APOLLO_BASE}/bin/apollo-broker" run > "${CONSOLE_LOG}" 2>&1 & 60 | APOLLO_PID=$! 61 | echo "Started Apollo with PID: ${APOLLO_PID}" 62 | sleep 5 63 | cat "${CONSOLE_LOG}" 64 | 65 | # 66 | # Run the benchmark 67 | cd ${BASEDIR} 68 | "${WORKSPACE}/bin/sbt" run --port 61613 --user admin --password password "${REPORTS_HOME}/apollo-${APOLLO_VERSION}.json" 69 | 70 | # 71 | # Kill the server 72 | kill -9 ${APOLLO_PID} 73 | 74 | # Create a report. 75 | "${BASEDIR}/bin/benchmark-report" $* 76 | -------------------------------------------------------------------------------- /bin/benchmark-mosquitto: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This shell script automates running the mqtt-benchmark [1] against the 4 | # Mosquitto server [2]. 5 | # 6 | # [1]: http://github.com/chirino/mqtt-benchmark 7 | # [2]: http://mosquitto.org/ 8 | # 9 | true \ 10 | ${MOSQUITTO_VERSION:=0.15} \ 11 | ${MOSQUITTO_DOWNLOAD:="http://mosquitto.org/files/source/mosquitto-${MOSQUITTO_VERSION}.tar.gz"} 12 | 13 | BASEDIR=`dirname "$0"`/.. ; cd "${BASEDIR}" ; BASEDIR=`pwd` ; cd - > /dev/null 14 | . ${BASEDIR}/bin/benchmark-setup 15 | 16 | # 17 | # Install the distro 18 | # 19 | MOSQUITTO_HOME="${WORKSPACE}/mosquitto-${MOSQUITTO_VERSION}" 20 | if [ ! -d "${MOSQUITTO_HOME}" ]; then 21 | cd ${WORKSPACE} 22 | wget "$MOSQUITTO_DOWNLOAD" -O "mosquitto-${MOSQUITTO_VERSION}.tar.gz" 23 | tar -zxvf "mosquitto-${MOSQUITTO_VERSION}.tar.gz" 24 | rm -rf "mosquitto-${MOSQUITTO_VERSION}.tar.gz" 25 | fi 26 | if [ ! -f "${MOSQUITTO_HOME}/src/mosquitto" ]; then 27 | cd "${MOSQUITTO_HOME}" 28 | 29 | # Need the GCC toolchain to build 30 | which yum > /dev/null && sudo yum -y install make gcc gcc-c++ kernel-devel m4 ncurses-devel openssl-devel 31 | which apt-get > /dev/null && sudo apt-get install -y build-essential ncurses-dev libssl-dev 32 | 33 | make || exit 1 34 | fi 35 | if [ ! -f "${MOSQUITTO_HOME}/benchmark.conf" ]; then 36 | cat > "${MOSQUITTO_HOME}/benchmark.conf" < /dev/null 2>&1 47 | rm -rf "${MOSQUITTO_HOME}/mosquitto.db" 48 | 49 | 50 | # Start the server 51 | # 52 | cd "${MOSQUITTO_HOME}" 53 | CONSOLE_LOG="${REPORTS_HOME}/mosquitto-${MOSQUITTO_VERSION}.log" 54 | "${MOSQUITTO_HOME}/src/mosquitto" -c "${MOSQUITTO_HOME}/benchmark.conf" > "${CONSOLE_LOG}" 2>&1 & 55 | MOSQUITTO_PID=$! 56 | echo "Started Mosquitto with PID: ${MOSQUITTO_PID}" 57 | sleep 5 58 | cat "${CONSOLE_LOG}" 59 | 60 | # 61 | # Run the benchmark 62 | # 63 | cd ${BASEDIR} 64 | "${WORKSPACE}/bin/sbt" run "${REPORTS_HOME}/mosquitto-${MOSQUITTO_VERSION}.json" 65 | 66 | # Kill the server 67 | kill -9 ${MOSQUITTO_PID} 68 | 69 | # Create a report. 70 | "${BASEDIR}/bin/benchmark-report" $* 71 | -------------------------------------------------------------------------------- /bin/benchmark-report: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This shell script creates the html index file which displays 4 | # results of the benchmarks. 5 | # 6 | if [ -z "${SKIP_REPORT}" ] ; then 7 | BASEDIR=`dirname "$0"`/.. ; cd "${BASEDIR}" ; BASEDIR=`pwd` ; cd - > /dev/null 8 | . ${BASEDIR}/bin/benchmark-setup 9 | 10 | # Get a list of the log and json files. 11 | cd "${REPORTS_HOME}" 12 | LOG_FILES=`ls *.log 2> /dev/null` 13 | JSON_FILES=`ls *.json | sed 's|.json|",|' | sed 's|^|"|'` 14 | JSON_FILES=`echo $JSON_FILES | sed 's|,$||'` #trim the trailing comma 15 | cd - > /dev/null 16 | 17 | LOGS_HTML="" 18 | if [ ! -z "${LOG_FILES}" ] ; then 19 | LOGS_HTML=" 20 |

Console logs of the benchmarked servers

21 |
    " 22 | for f in ${LOG_FILES} ; do 23 | LOGS_HTML="${LOGS_HTML} 24 |
  • $f
  • " 25 | done 26 | LOGS_HTML="${LOGS_HTML} 27 |
" 28 | fi 29 | 30 | mkdir -p "${REPORTS_HOME}/resources" 31 | cat "${BASEDIR}/src/main/html/template.html" | sed "s|PRODUCT_LIST|$JSON_FILES|" | perl -pi -e "s|LOGS_HTML|$LOGS_HTML|" > "${REPORTS_HOME}/index.html" 32 | cp "${BASEDIR}/src/main/html/resources/"* "${REPORTS_HOME}/resources" 33 | 34 | # 35 | # Only create it if it does not exist as it may get 36 | # customized by hand later. 37 | if [ ! -f "${REPORTS_HOME}/server-info.html" ] ; then 38 | 39 | cat > "${REPORTS_HOME}/server-info.html" < 41 |

Machine Details

42 |
$(uname -a)
43 | EOF 44 | 45 | DATA=`lsb_release -sd 2>&1` 46 | if [ $? -eq 0 ] ; then 47 | echo "
${DATA}
" >> "${REPORTS_HOME}/server-info.html" 48 | fi 49 | 50 | DATA=`free -m 2>&1` 51 | if [ $? -eq 0 ] ; then 52 | echo "
${DATA}
" >> "${REPORTS_HOME}/server-info.html" 53 | fi 54 | 55 | DATA=`cat /proc/cpuinfo 2>&1` 56 | if [ $? -eq 0 ] ; then 57 | echo "
${DATA}
" >> "${REPORTS_HOME}/server-info.html" 58 | fi 59 | 60 | DATA=`java -version 2>&1` 61 | if [ $? -eq 0 ] ; then 62 | echo "
${DATA}
" >> "${REPORTS_HOME}/server-info.html" 63 | fi 64 | 65 | which erl > /dev/null 66 | if [ $? -eq 0 ] ; then 67 | DATA=`erl -version 2>&1` 68 | else 69 | DATA=`${WORKSPACE}/erlang/bin/erl -version 2>&1` 70 | fi 71 | if [ $? -eq 0 ] ; then 72 | echo "
" >> "${REPORTS_HOME}/server-info.html" 
73 |     echo "${DATA}" >> "${REPORTS_HOME}/server-info.html" 
74 |     echo "
" >> "${REPORTS_HOME}/server-info.html" 75 | fi 76 | 77 | fi 78 | 79 | echo "==========================================================================" 80 | echo "Results stored under: ${REPORTS_HOME}" 81 | echo "==========================================================================" 82 | 83 | fi -------------------------------------------------------------------------------- /bin/benchmark-setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | true \ 4 | ${REPORTS_HOME:=$1} \ 5 | ${REPORTS_HOME:=`pwd`/reports/`hostname`} 6 | WORKSPACE="${BASEDIR}/workspace" 7 | 8 | mkdir -p ${WORKSPACE} 9 | mkdir -p ${REPORTS_HOME} 10 | cd "${REPORTS_HOME}" ; REPORTS_HOME=`pwd` ; cd - > /dev/null 11 | 12 | # 13 | # Install SBT 14 | # 15 | if [ ! -f "${WORKSPACE}/bin/sbt" ] ; then 16 | mkdir ~/.ivy2 2> /dev/null 17 | mkdir "${WORKSPACE}/bin" 2> /dev/null 18 | cd "${WORKSPACE}/bin" 19 | wget http://simple-build-tool.googlecode.com/files/sbt-launch-0.7.4.jar 20 | cat > ${WORKSPACE}/bin/sbt < val scenario = new com.github.mqtt.benchmark.NonBlockingScenario 22 | scenario: com.github.mqtt.benchmark.Scenario = 23 | -------------------------------------- 24 | Scenario Settings 25 | -------------------------------------- 26 | host = 127.0.0.1 27 | port = 61613 28 | destination_type = queue 29 | destination_count = 1 30 | destination_name = load 31 | sample_interval (ms) = 1000 32 | 33 | --- Producer Properties --- 34 | producers = 1 35 | message_size = 1024 36 | persistent = false 37 | sync_send = false 38 | content_length = true 39 | producer_sleep (ms) = 0 40 | headers = List() 41 | 42 | --- Consumer Properties --- 43 | consumers = 1 44 | consumer_sleep (ms) = 0 45 | ack = auto 46 | selector = null 47 | durable = false 48 | 49 | This creates a new NonBlockingScenario object which you can adjust it's properties and 50 | then run by executing `scenario.run`. For example, to run 10 producers and no 51 | consumer on a topic, you would update the scenario object properties as follows: 52 | 53 | scala> scenario.producers = 10 54 | 55 | scala> scenario.consumers = 0 56 | 57 | scala> scenario.destination_type = "topic" 58 | 59 | When you actually run the scenario, you it will report back the throughput metrics. 60 | Press enter to stop the run. 61 | 62 | scala> scenario.run 63 | -------------------------------------- 64 | Scenario Settings 65 | -------------------------------------- 66 | host = 127.0.0.1 67 | port = 61613 68 | destination_type = topic 69 | destination_count = 1 70 | destination_name = load 71 | sample_interval (ms) = 1000 72 | 73 | --- Producer Properties --- 74 | producers = 10 75 | message_size = 1024 76 | persistent = false 77 | sync_send = false 78 | content_length = true 79 | producer_sleep (ms) = 0 80 | headers = List() 81 | 82 | --- Consumer Properties --- 83 | consumers = 0 84 | consumer_sleep (ms) = 0 85 | ack = auto 86 | selector = null 87 | durable = false 88 | -------------------------------------- 89 | Running: Press ENTER to stop 90 | -------------------------------------- 91 | 92 | Producer total: 345,362, rate: 345,333.688 per second 93 | Producer total: 725,058, rate: 377,908.125 per second 94 | Producer total: 1,104,673, rate: 379,252.813 per second 95 | Producer total: 1,479,280, rate: 373,913.750 per second 96 | ... ... 97 | 98 | scala> 99 | 100 | ## Running a custom scenario from XML file 101 | 102 | Is it possible to run mqtt-benchmark using scenarios defined in XML files providing the option `--scenario-file` 103 | 104 | Also, to be able to display this scenario results using generic_report.html, you need to provide the option `--new-json` 105 | 106 | The scenario file has a "scenarios" root element. Inside, first you define some information that will be displayed on the report. 107 | Afterwards, you can place a common section, defining values for some properties to be used in all the scenarios (exceptions can be made, redefining the value in a lower level). 108 | 109 | After the common section, you can define one or more groups, and give them a name. Also, it can have a description, and a common section as before. 110 | 111 | For simple groups, you can just start defining scenarios. 112 | 113 | Then you can define one or more scenarios, and give them a name (internal name) and a label (it will be displayed in the report). You can also define a common section here. 114 | 115 | Then, you just have to create one or more clients sections, and define the properties for this clients. All clients in one scenario will run in parallel, but scenarios will run in sequence. 116 | 117 | For more complex groups, you can define variables inside a loop section, and give different values to each variable. All the possible combinations of the values for each variable will be generated, 118 | and a scenario for each combination will be generated using a scenario template. A scenario template is defined as a normal scenario, but you can use placeholders like ${variable_name} that will be substituted with the real value. 119 | 120 | The use of multiple scenario templates in one group with loop variables is supported in mqtt-benchmark, but only the first one will be displayed using the generic_report.html. Also note, that if more than 1 variable is defined, a table will be used to display the results. 121 | The odd variables (in definition order) will be horizontal headers, the even ones, vertical headers. 122 | 123 | The last thing to note, is that for the properties producer_sleep and consumer_sleep, message_size and messages_per_connection, instead of providing a single value, different values or functions for different time ranges can be provided. 124 | 125 | In a range, you can specify the value to be used up to the millisecond specified in the `end` attribute. The `end` attribute can take positive values, negative values counting from the end, or the word "end". This way, it's possible to write scenarios that are independent from the scenario duration. 126 | 127 | For the values, it's possible to provide three different functions: burst (with fast value, slow value, duration of the fast period, and period of bursts), random (with min and max values) and normal(with mean and variance values). 128 | 129 | For example you could define: 130 | 131 | 132 | sleep 133 | 0 134 | 135 | sleep 136 | 137 | 138 | That means, form 0ms until 10000ms, don't send any message. From 10000ms to 15000ms, send as fast as possible. From 15000ms to 70000ms, send in bursts, sometimes fast, sometimes slow. 139 | The fast value is the sleep time when it's in a burst, the slow value is the sleep time when it isn't in a burst, the duration of the burst, and period is the period of time when, in average, a burst should occur. 140 | So, in this case, in average, every 10 seconds we will have a burst of 0.5 seconds, sending as fast as possible. The rest of the time, is sending slow. 141 | 142 | There are some properties that can only be defined in the common sections: sample_interval, sample_count, drain, blocking_io and warm_up_count. 143 | 144 | These properties can be defined anywhere: login, passcode, host, port, producers, consumers, 145 | destination_type, destination_name, consumer_prefix, queue_prefix, topic_prefix, message_size, content_length, 146 | drain_timeout, persistent, durable, sync_send, ack, messages_per_connection, producers_per_sample, consumers_per_sample, 147 | selector, producer_sleep, consumer_sleep 148 | 149 | An example from default_scenarios.xml: 150 | 151 | 152 | Test Broker 153 | This is the general description for the scenarios in this file. 154 | Test Platform 155 | Platform description 156 | 157 | 1000 158 | false 159 | 3 160 | true 161 | 162 | 163 | 164 | Persistent Queue Load/Unload - Non Persistent Queue Load 165 | 166 | 167 | 30 168 | queue 169 | 1 170 | load_me_up 171 | 172 | 173 | 174 | 1 175 | 0 176 | 20 177 | false 178 | false 179 | 180 | 181 | 182 | 183 | false 184 | 185 | 186 | 1 187 | 0 188 | 20 189 | true 190 | true 191 | 192 | 193 | 194 | 195 | 0 196 | 1 197 | 198 | 199 | 200 | 201 | 202 | 203 | queue 204 | topic 205 | 206 | 207 | 208 | Scenario with fast and slow consumers 209 | 210 | 211 | 212 | 15 213 | ${destination_type} 214 | 1 215 | 216 | 217 | 1 218 | 1 219 | 20 220 | false 221 | false 222 | 223 | 224 | 0 225 | 1 226 | 100 227 | 228 | 229 | 230 | 231 | 232 | ## Display the results of a custom XML scenario using the generic_report.html 233 | 234 | To display the results on generic_report.html, first you need to serve all the files from the same domain. In google chrome, if you use file:///, the same origin policy wont allow to load the results. 235 | You can relax that restriction in Chrome by starting it with the `--allow-file-access-from-files` argument. On OS X you can do that with the following command: `open -a 'Google Chrome' --args --allow-file-access-from-files` 236 | 237 | Please note that, to be able to display the scenario results using generic_report.html, you need to provide the option `--new-json` when you run the benchmark. 238 | 239 | Then, you can create different directories for the different platforms you have and copy the json files to each directory. 240 | 241 | Finally, you need to modify the generic_report.html to include the names of your json files (without extension) in the products array, and the names of the platform directories in the platforms array. 242 | Platform is an array of arrays, each element is an array where the first element it's the name of the directory, and the second, the name we want to display in the report. 243 | 244 | Now, you can just open generic_report.html to see the results. 245 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | -------------------------------------------------------------------------------- /notice.md: -------------------------------------------------------------------------------- 1 | MQTT Benchmark Copyright Notices 2 | ================================= 3 | 4 | Copyright 2010 FuseSource Corp 5 | 6 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 22 | 4.0.0 23 | 24 | com.github.mqtt 25 | mqtt-benchmark 26 | jar 27 | 1.0-SNAPSHOT 28 | 29 | ${project.artifactId} 30 | 31 | 32 | 33 | 34 | 2.9.1 35 | 2.15.0 36 | 2.2.1 37 | 1.6.0 38 | 2.2.1 39 | 40 | 1.0 41 | 1.9 42 | 43 | 44 | 45 | 46 | 47 | 48 | org.scala-lang 49 | scala-library 50 | compile 51 | ${scala-version} 52 | 53 | 54 | 55 | org.fusesource.hawtdispatch 56 | hawtdispatch-scala 57 | ${hawtdispatch-version} 58 | 59 | 60 | org.fusesource.mqtt-client 61 | mqtt-client 62 | ${mqtt-client-version} 63 | 64 | 65 | org.apache.karaf.shell 66 | org.apache.karaf.shell.console 67 | ${karaf-version} 68 | 69 | 70 | org.slf4j 71 | slf4j-nop 72 | ${slf4j-version} 73 | 74 | 75 | 76 | 77 | 78 | 79 | install 80 | src/main/scala 81 | src/test/scala 82 | 83 | 84 | 85 | target/schema 86 | 87 | **/* 88 | 89 | 90 | 91 | src/main/resources 92 | 93 | **/* 94 | 95 | true 96 | 97 | 98 | 99 | 100 | 101 | 102 | org.scala-tools 103 | maven-scala-plugin 104 | ${maven-scala-plugin-version} 105 | 106 | 107 | 108 | compile 109 | testCompile 110 | 111 | 112 | 113 | 114 | 115 | -Xmx1024m 116 | -Xss8m 117 | 118 | 119 | -deprecation 120 | 121 | ${scala-version} 122 | 123 | 124 | 125 | org.apache.maven.plugins 126 | maven-assembly-plugin 127 | ${maven-assembly-plugin-version} 128 | 129 | ${project.build.finalName}-standalone 130 | false 131 | 132 | jar-with-dependencies 133 | 134 | 135 | 136 | com.github.mqtt.benchmark.Benchmark 137 | 138 | 139 | 140 | 141 | 142 | package 143 | 144 | single 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | fusesource-public 155 | http://repo.fusesource.com/nexus/content/groups/public 156 | true 157 | fals 158 | 159 | 160 | fusesource-snapshots 161 | http://repo.fusesource.com/nexus/content/groups/public-snapshots 162 | false 163 | true 164 | 165 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | #Project properties 2 | #Sat Sep 04 21:06:49 JST 2010 3 | project.organization=com.github.mqtt 4 | project.name=mqtt-benchmark 5 | sbt.version=0.7.4 6 | project.version=1.0-SNAPSHOT 7 | build.scala.versions=2.8.1 8 | project.initialize=false 9 | -------------------------------------------------------------------------------- /project/build/project.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | class BenchmarkProject(info: ProjectInfo) extends DefaultProject(info) { 4 | lazy val mavenLocal = "Local Maven Repository" at "file://" + Path.userHome + "/.m2/repository" 5 | lazy val jansi_repo = MavenRepository("jansi","http://jansi.fusesource.org/repo/release") 6 | lazy val smx_repo = MavenRepository("smx","http://svn.apache.org/repos/asf/servicemix/m2-repo") 7 | lazy val fusesource_snapshot_repo = MavenRepository("fusesource-snapshot","http://repo.fusesource.com/nexus/content/repositories/snapshots/") 8 | lazy val fusesource_public_repo = MavenRepository("fusesource-public","http://repo.fusesource.com/nexus/content/repositories/public/") 9 | 10 | lazy val karaf_console = "org.apache.karaf.shell" % "org.apache.karaf.shell.console" % "2.2.1" 11 | lazy val slf4j_nop = "org.slf4j" % "slf4j-nop" % "1.6.0" 12 | lazy val hawtdispatch = "org.fusesource.hawtdispatch" % "hawtdispatch-scala" % "1.9" 13 | lazy val mqtt_client = "org.fusesource.mqtt-client" % "mqtt-client" % "1.0" 14 | 15 | } 16 | 17 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # MQTT Benchmark 2 | 3 | A benchmarking tool for [MQTT v3.1](http://mqtt.org/) servers. 4 | The benchmark covers a wide variety of common usage scenarios. 5 | 6 | 22 | 23 | ## Running the Benchmark 24 | 25 | Just run: 26 | 27 | ./bin/benchmark-all 28 | 29 | or one of the server specific benchmark scripts like: 30 | 31 | ./bin/benchmark-apollo 32 | 33 | 40 | 41 | The benchmark report will be stored in the `reports/$(hostname)` directory. 42 | 43 | ## Running the Benchmark on an EC2 Amazon Linux 64 bit AMI 44 | 45 | If you want to run the benchmark on EC2, we recommend using at least the 46 | c1.xlarge instance type. Once you have the instance started just execute 47 | the following commands on the instance: 48 | 49 | sudo yum install -y screen 50 | curl https://nodeload.github.com/chirino/mqtt-benchmark/tarball/master | tar -zxv 51 | mv chirino-mqtt-benchmark-* mqtt-benchmark 52 | screen ./mqtt-benchmark/bin/benchmark-all 53 | 54 | The results will be stored in the ~/reports directory. 55 | 56 | -------------------------------------------------------------------------------- /reports/index.html: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 22 | 38 | 39 | 40 | 41 |
42 | 45 | 53 |
54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /reports/ubuntu-2600k/apollo-1.1-SNAPSHOT.log: -------------------------------------------------------------------------------- 1 | 2 | _____ .__ .__ 3 | / _ \ ______ ____ | | | | ____ 4 | / /_\ \\____ \ / _ \| | | | / _ \ 5 | / | \ |_> > <_> ) |_| |_( <_> ) 6 | \____|__ / __/ \____/|____/____/\____/ 7 | \/|__| Apache Apollo (1.1-SNAPSHOT) 8 | 9 | 10 | Loading configuration file '/home/chirino/sandbox/mqtt-benchmark/workspace/apollo-1.1-SNAPSHOT/etc/apollo.xml'. 11 | INFO | OS : Linux 3.0.0-15-generic (Ubuntu 11.10) 12 | INFO | JVM : Java HotSpot(TM) 64-Bit Server VM 1.6.0_30 (Sun Microsystems Inc.) 13 | INFO | Apollo : 1.1-SNAPSHOT (at: /mnt/md0/mqtt-benchmark/workspace/apache-apollo-1.1-SNAPSHOT) 14 | INFO | OS is restricting the open file limit to: 4096 15 | WARN | Please increase the process file limit using 'ulimit -n 4500' or configure lower connection limits on the broker connectors. 16 | INFO | Starting store: leveldb store at /home/chirino/sandbox/mqtt-benchmark/workspace/apollo-1.1-SNAPSHOT/data 17 | INFO | Accepting connections at: tcp://0.0.0.0:61613 18 | INFO | Accepting connections at: ws://0.0.0.0:61623/ 19 | INFO | Administration interface available at: http://127.0.0.1:61680/ 20 | -------------------------------------------------------------------------------- /reports/ubuntu-2600k/resources/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chirino/mqtt-benchmark/da9664486b5c9648542f0a9b7ce37ec6abbb3d63/reports/ubuntu-2600k/resources/ajax-loader.gif -------------------------------------------------------------------------------- /reports/ubuntu-2600k/resources/blank.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chirino/mqtt-benchmark/da9664486b5c9648542f0a9b7ce37ec6abbb3d63/reports/ubuntu-2600k/resources/blank.gif -------------------------------------------------------------------------------- /reports/ubuntu-2600k/resources/jquery.flot.crosshair.js: -------------------------------------------------------------------------------- 1 | /* 2 | Flot plugin for showing crosshairs, thin lines, when the mouse hovers 3 | over the plot. 4 | 5 | crosshair: { 6 | mode: null or "x" or "y" or "xy" 7 | color: color 8 | lineWidth: number 9 | } 10 | 11 | Set the mode to one of "x", "y" or "xy". The "x" mode enables a 12 | vertical crosshair that lets you trace the values on the x axis, "y" 13 | enables a horizontal crosshair and "xy" enables them both. "color" is 14 | the color of the crosshair (default is "rgba(170, 0, 0, 0.80)"), 15 | "lineWidth" is the width of the drawn lines (default is 1). 16 | 17 | The plugin also adds four public methods: 18 | 19 | - setCrosshair(pos) 20 | 21 | Set the position of the crosshair. Note that this is cleared if 22 | the user moves the mouse. "pos" is in coordinates of the plot and 23 | should be on the form { x: xpos, y: ypos } (you can use x2/x3/... 24 | if you're using multiple axes), which is coincidentally the same 25 | format as what you get from a "plothover" event. If "pos" is null, 26 | the crosshair is cleared. 27 | 28 | - clearCrosshair() 29 | 30 | Clear the crosshair. 31 | 32 | - lockCrosshair(pos) 33 | 34 | Cause the crosshair to lock to the current location, no longer 35 | updating if the user moves the mouse. Optionally supply a position 36 | (passed on to setCrosshair()) to move it to. 37 | 38 | Example usage: 39 | var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } }; 40 | $("#graph").bind("plothover", function (evt, position, item) { 41 | if (item) { 42 | // Lock the crosshair to the data point being hovered 43 | myFlot.lockCrosshair({ x: item.datapoint[0], y: item.datapoint[1] }); 44 | } 45 | else { 46 | // Return normal crosshair operation 47 | myFlot.unlockCrosshair(); 48 | } 49 | }); 50 | 51 | - unlockCrosshair() 52 | 53 | Free the crosshair to move again after locking it. 54 | */ 55 | 56 | (function ($) { 57 | var options = { 58 | crosshair: { 59 | mode: null, // one of null, "x", "y" or "xy", 60 | color: "rgba(170, 0, 0, 0.80)", 61 | lineWidth: 1 62 | } 63 | }; 64 | 65 | function init(plot) { 66 | // position of crosshair in pixels 67 | var crosshair = { x: -1, y: -1, locked: false }; 68 | 69 | plot.setCrosshair = function setCrosshair(pos) { 70 | if (!pos) 71 | crosshair.x = -1; 72 | else { 73 | var o = plot.p2c(pos); 74 | crosshair.x = Math.max(0, Math.min(o.left, plot.width())); 75 | crosshair.y = Math.max(0, Math.min(o.top, plot.height())); 76 | } 77 | 78 | plot.triggerRedrawOverlay(); 79 | }; 80 | 81 | plot.clearCrosshair = plot.setCrosshair; // passes null for pos 82 | 83 | plot.lockCrosshair = function lockCrosshair(pos) { 84 | if (pos) 85 | plot.setCrosshair(pos); 86 | crosshair.locked = true; 87 | } 88 | 89 | plot.unlockCrosshair = function unlockCrosshair() { 90 | crosshair.locked = false; 91 | } 92 | 93 | function onMouseOut(e) { 94 | if (crosshair.locked) 95 | return; 96 | 97 | if (crosshair.x != -1) { 98 | crosshair.x = -1; 99 | plot.triggerRedrawOverlay(); 100 | } 101 | } 102 | 103 | function onMouseMove(e) { 104 | if (crosshair.locked) 105 | return; 106 | 107 | if (plot.getSelection && plot.getSelection()) { 108 | crosshair.x = -1; // hide the crosshair while selecting 109 | return; 110 | } 111 | 112 | var offset = plot.offset(); 113 | crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width())); 114 | crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height())); 115 | plot.triggerRedrawOverlay(); 116 | } 117 | 118 | plot.hooks.bindEvents.push(function (plot, eventHolder) { 119 | if (!plot.getOptions().crosshair.mode) 120 | return; 121 | 122 | eventHolder.mouseout(onMouseOut); 123 | eventHolder.mousemove(onMouseMove); 124 | }); 125 | 126 | plot.hooks.drawOverlay.push(function (plot, ctx) { 127 | var c = plot.getOptions().crosshair; 128 | if (!c.mode) 129 | return; 130 | 131 | var plotOffset = plot.getPlotOffset(); 132 | 133 | ctx.save(); 134 | ctx.translate(plotOffset.left, plotOffset.top); 135 | 136 | if (crosshair.x != -1) { 137 | ctx.strokeStyle = c.color; 138 | ctx.lineWidth = c.lineWidth; 139 | ctx.lineJoin = "round"; 140 | 141 | ctx.beginPath(); 142 | if (c.mode.indexOf("x") != -1) { 143 | ctx.moveTo(crosshair.x, 0); 144 | ctx.lineTo(crosshair.x, plot.height()); 145 | } 146 | if (c.mode.indexOf("y") != -1) { 147 | ctx.moveTo(0, crosshair.y); 148 | ctx.lineTo(plot.width(), crosshair.y); 149 | } 150 | ctx.stroke(); 151 | } 152 | ctx.restore(); 153 | }); 154 | 155 | plot.hooks.shutdown.push(function (plot, eventHolder) { 156 | eventHolder.unbind("mouseout", onMouseOut); 157 | eventHolder.unbind("mousemove", onMouseMove); 158 | }); 159 | } 160 | 161 | $.plot.plugins.push({ 162 | init: init, 163 | options: options, 164 | name: 'crosshair', 165 | version: '1.0' 166 | }); 167 | })(jQuery); 168 | -------------------------------------------------------------------------------- /reports/ubuntu-2600k/server-info.html: -------------------------------------------------------------------------------- 1 |

Machine Details

2 |
    3 |
  • CPU: 3.4 GHz Intel 2600K 4 |
    model name	: Intel(R) Core(TM) i7-2600K CPU @ 3.40GHz
    5 |
  • 6 |
  • Memory: 8 GB 2000 MHz DDR3
  • 7 |
  • Disk: 3 x 10k RPM Raptor drives in software Raid 0 (Striping)
  • 8 |
  • OS: Ubuntu 11.10 9 |
    Linux ubuntu-2600k 3.0.0-15-generic #26-Ubuntu SMP Fri Jan 20 17:23:00 UTC 2012 x86_64 x86_64 x86_64 GNU/Linux
    10 |
  • 11 |
  • Java: OpenJDK 64-Bit 1.6.0_23 12 |
    java version "1.6.0_23"
    13 | OpenJDK Runtime Environment (IcedTea6 1.11pre) (6b23~pre11-0ubuntu1.11.10)
    14 | OpenJDK 64-Bit Server VM (build 20.0-b11, mixed mode)
    15 |
  • 16 |
17 | -------------------------------------------------------------------------------- /src/main/html/resources/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chirino/mqtt-benchmark/da9664486b5c9648542f0a9b7ce37ec6abbb3d63/src/main/html/resources/ajax-loader.gif -------------------------------------------------------------------------------- /src/main/html/resources/blank.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chirino/mqtt-benchmark/da9664486b5c9648542f0a9b7ce37ec6abbb3d63/src/main/html/resources/blank.gif -------------------------------------------------------------------------------- /src/main/html/resources/jquery.flot.crosshair.js: -------------------------------------------------------------------------------- 1 | /* 2 | Flot plugin for showing crosshairs, thin lines, when the mouse hovers 3 | over the plot. 4 | 5 | crosshair: { 6 | mode: null or "x" or "y" or "xy" 7 | color: color 8 | lineWidth: number 9 | } 10 | 11 | Set the mode to one of "x", "y" or "xy". The "x" mode enables a 12 | vertical crosshair that lets you trace the values on the x axis, "y" 13 | enables a horizontal crosshair and "xy" enables them both. "color" is 14 | the color of the crosshair (default is "rgba(170, 0, 0, 0.80)"), 15 | "lineWidth" is the width of the drawn lines (default is 1). 16 | 17 | The plugin also adds four public methods: 18 | 19 | - setCrosshair(pos) 20 | 21 | Set the position of the crosshair. Note that this is cleared if 22 | the user moves the mouse. "pos" is in coordinates of the plot and 23 | should be on the form { x: xpos, y: ypos } (you can use x2/x3/... 24 | if you're using multiple axes), which is coincidentally the same 25 | format as what you get from a "plothover" event. If "pos" is null, 26 | the crosshair is cleared. 27 | 28 | - clearCrosshair() 29 | 30 | Clear the crosshair. 31 | 32 | - lockCrosshair(pos) 33 | 34 | Cause the crosshair to lock to the current location, no longer 35 | updating if the user moves the mouse. Optionally supply a position 36 | (passed on to setCrosshair()) to move it to. 37 | 38 | Example usage: 39 | var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } }; 40 | $("#graph").bind("plothover", function (evt, position, item) { 41 | if (item) { 42 | // Lock the crosshair to the data point being hovered 43 | myFlot.lockCrosshair({ x: item.datapoint[0], y: item.datapoint[1] }); 44 | } 45 | else { 46 | // Return normal crosshair operation 47 | myFlot.unlockCrosshair(); 48 | } 49 | }); 50 | 51 | - unlockCrosshair() 52 | 53 | Free the crosshair to move again after locking it. 54 | */ 55 | 56 | (function ($) { 57 | var options = { 58 | crosshair: { 59 | mode: null, // one of null, "x", "y" or "xy", 60 | color: "rgba(170, 0, 0, 0.80)", 61 | lineWidth: 1 62 | } 63 | }; 64 | 65 | function init(plot) { 66 | // position of crosshair in pixels 67 | var crosshair = { x: -1, y: -1, locked: false }; 68 | 69 | plot.setCrosshair = function setCrosshair(pos) { 70 | if (!pos) 71 | crosshair.x = -1; 72 | else { 73 | var o = plot.p2c(pos); 74 | crosshair.x = Math.max(0, Math.min(o.left, plot.width())); 75 | crosshair.y = Math.max(0, Math.min(o.top, plot.height())); 76 | } 77 | 78 | plot.triggerRedrawOverlay(); 79 | }; 80 | 81 | plot.clearCrosshair = plot.setCrosshair; // passes null for pos 82 | 83 | plot.lockCrosshair = function lockCrosshair(pos) { 84 | if (pos) 85 | plot.setCrosshair(pos); 86 | crosshair.locked = true; 87 | } 88 | 89 | plot.unlockCrosshair = function unlockCrosshair() { 90 | crosshair.locked = false; 91 | } 92 | 93 | function onMouseOut(e) { 94 | if (crosshair.locked) 95 | return; 96 | 97 | if (crosshair.x != -1) { 98 | crosshair.x = -1; 99 | plot.triggerRedrawOverlay(); 100 | } 101 | } 102 | 103 | function onMouseMove(e) { 104 | if (crosshair.locked) 105 | return; 106 | 107 | if (plot.getSelection && plot.getSelection()) { 108 | crosshair.x = -1; // hide the crosshair while selecting 109 | return; 110 | } 111 | 112 | var offset = plot.offset(); 113 | crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width())); 114 | crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height())); 115 | plot.triggerRedrawOverlay(); 116 | } 117 | 118 | plot.hooks.bindEvents.push(function (plot, eventHolder) { 119 | if (!plot.getOptions().crosshair.mode) 120 | return; 121 | 122 | eventHolder.mouseout(onMouseOut); 123 | eventHolder.mousemove(onMouseMove); 124 | }); 125 | 126 | plot.hooks.drawOverlay.push(function (plot, ctx) { 127 | var c = plot.getOptions().crosshair; 128 | if (!c.mode) 129 | return; 130 | 131 | var plotOffset = plot.getPlotOffset(); 132 | 133 | ctx.save(); 134 | ctx.translate(plotOffset.left, plotOffset.top); 135 | 136 | if (crosshair.x != -1) { 137 | ctx.strokeStyle = c.color; 138 | ctx.lineWidth = c.lineWidth; 139 | ctx.lineJoin = "round"; 140 | 141 | ctx.beginPath(); 142 | if (c.mode.indexOf("x") != -1) { 143 | ctx.moveTo(crosshair.x, 0); 144 | ctx.lineTo(crosshair.x, plot.height()); 145 | } 146 | if (c.mode.indexOf("y") != -1) { 147 | ctx.moveTo(0, crosshair.y); 148 | ctx.lineTo(plot.width(), crosshair.y); 149 | } 150 | ctx.stroke(); 151 | } 152 | ctx.restore(); 153 | }); 154 | 155 | plot.hooks.shutdown.push(function (plot, eventHolder) { 156 | eventHolder.unbind("mouseout", onMouseOut); 157 | eventHolder.unbind("mousemove", onMouseMove); 158 | }); 159 | } 160 | 161 | $.plot.plugins.push({ 162 | init: init, 163 | options: options, 164 | name: 'crosshair', 165 | version: '1.0' 166 | }); 167 | })(jQuery); 168 | -------------------------------------------------------------------------------- /src/main/html/template.html: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 313 | 314 | 357 | 358 | 359 | 360 |
361 | 364 |
365 |

Table of Contents

366 |
    367 |
  1. Throughput to an Unsubscribed Topic
  2. 368 |
  3. Queue Load/Unload
  4. 369 |
  5. Partitioned Load Scenarios
  6. 370 |
  7. Fan In/Out Load Scenarios
  8. 371 |
  9. Request/Reply Scenarios
  10. 372 |
373 | 374 |

Overview

375 |

376 | This report was generated using the mqtt-benchmark tool. It 377 | provides a comparative benchmark of messaging servers that implement the MQTT 1.0 378 | specification. It covers a wide variety of common usage scenarios. Each scenario is warmed up for 3 seconds before the performance samples are taken. 379 |

380 | 381 |
382 |
383 |
384 | 385 |

Console logs of the benchmarked servers

386 | 390 |
391 | 398 | 399 |

Throughput to an Unsubscribed Topic

400 |

401 | A single publisher sending non-persistent messages to a topic that does not have an subscribers attached. 402 |

403 |
404 | NOTE: hover over the graphs to see the legends. 405 |
406 | 407 | 408 | 412 | < 419 | 420 |
409 |

With a 20 b Playload

410 |
411 |
413 |

With a 1 k Playload

414 |
415 |
416 |

With a 256 k Playload

417 |
418 |
421 | 422 |

Client Session/Subscription Load/Unload

423 | 424 | 425 | 450 | 451 |
426 |

Loading

427 |

428 | A publisher on a clean session is sending messages with 20 byte payloads to 429 | with 1 subscribed non-clean session which is not currently connected. 430 |

431 |

QoS 0 Publish

432 |
433 |

QoS 1 Publish

434 |
435 |

QoS 2 Publish

436 |
437 |
438 |

Unloading

439 |

440 | The non-clean session client then reconnects and starts receiving the messages 441 | previously sent to the subscription but now the publisher has stopped. 442 |

443 |

QoS 0 Subscribe

444 |
445 |

QoS 1 Subscribe

446 |
447 |

QoS 2 Subscribe

448 |
449 |
452 | 453 |

Fan In/Out Load Scenarios

454 |

455 | These scenarios multiple subscribers and publishers communicate via one shared topic. 456 | All messages sent have a 20 byte payload. 457 |

458 | 459 | 460 | 541 | 622 | 703 |
 CleanNon-Clean
QoS 0
461 | 462 | 463 | 475 | 487 | 499 |
 1 Producer5 Producers10 Producers
1 Consumer
464 |

Producer Rates (msg/s):

465 |
466 |

Consumer Rates (msg/s):

467 |
468 |
469 |
470 |
471 |
472 |
473 |
474 |
5 Consumers
476 |

Producer Rates (msg/s):

477 |
478 |

Consumer Rates (msg/s):

479 |
480 |
481 |
482 |
483 |
484 |
485 |
486 |
10 Consumers
488 |

Producer Rates (msg/s):

489 |
490 |

Consumer Rates (msg/s):

491 |
492 |
493 |
494 |
495 |
496 |
497 |
498 |
500 |
501 | 502 | 503 | 515 | 527 | 539 |
 1 Producer5 Producers10 Producers
1 Consumer
504 |

Producer Rates (msg/s):

505 |
506 |

Consumer Rates (msg/s):

507 |
508 |
509 |
510 |
511 |
512 |
513 |
514 |
5 Consumers
516 |

Producer Rates (msg/s):

517 |
518 |

Consumer Rates (msg/s):

519 |
520 |
521 |
522 |
523 |
524 |
525 |
526 |
10 Consumers
528 |

Producer Rates (msg/s):

529 |
530 |

Consumer Rates (msg/s):

531 |
532 |
533 |
534 |
535 |
536 |
537 |
538 |
540 |
QoS 1
542 | 543 | 544 | 556 | 568 | 580 |
 1 Producer5 Producers10 Producers
1 Consumer
545 |

Producer Rates (msg/s):

546 |
547 |

Consumer Rates (msg/s):

548 |
549 |
550 |
551 |
552 |
553 |
554 |
555 |
5 Consumers
557 |

Producer Rates (msg/s):

558 |
559 |

Consumer Rates (msg/s):

560 |
561 |
562 |
563 |
564 |
565 |
566 |
567 |
10 Consumers
569 |

Producer Rates (msg/s):

570 |
571 |

Consumer Rates (msg/s):

572 |
573 |
574 |
575 |
576 |
577 |
578 |
579 |
581 |
582 | 583 | 584 | 596 | 608 | 620 |
 1 Producer5 Producers10 Producers
1 Consumer
585 |

Producer Rates (msg/s):

586 |
587 |

Consumer Rates (msg/s):

588 |
589 |
590 |
591 |
592 |
593 |
594 |
595 |
5 Consumers
597 |

Producer Rates (msg/s):

598 |
599 |

Consumer Rates (msg/s):

600 |
601 |
602 |
603 |
604 |
605 |
606 |
607 |
10 Consumers
609 |

Producer Rates (msg/s):

610 |
611 |

Consumer Rates (msg/s):

612 |
613 |
614 |
615 |
616 |
617 |
618 |
619 |
621 |
QoS 2
623 | 624 | 625 | 637 | 649 | 661 |
 1 Producer5 Producers10 Producers
1 Consumer
626 |

Producer Rates (msg/s):

627 |
628 |

Consumer Rates (msg/s):

629 |
630 |
631 |
632 |
633 |
634 |
635 |
636 |
5 Consumers
638 |

Producer Rates (msg/s):

639 |
640 |

Consumer Rates (msg/s):

641 |
642 |
643 |
644 |
645 |
646 |
647 |
648 |
10 Consumers
650 |

Producer Rates (msg/s):

651 |
652 |

Consumer Rates (msg/s):

653 |
654 |
655 |
656 |
657 |
658 |
659 |
660 |
662 |
663 | 664 | 665 | 677 | 689 | 701 |
 1 Producer5 Producers10 Producers
1 Consumer
666 |

Producer Rates (msg/s):

667 |
668 |

Consumer Rates (msg/s):

669 |
670 |
671 |
672 |
673 |
674 |
675 |
676 |
5 Consumers
678 |

Producer Rates (msg/s):

679 |
680 |

Consumer Rates (msg/s):

681 |
682 |
683 |
684 |
685 |
686 |
687 |
688 |
10 Consumers
690 |

Producer Rates (msg/s):

691 |
692 |

Consumer Rates (msg/s):

693 |
694 |
695 |
696 |
697 |
698 |
699 |
700 |
702 |
704 | 705 |

Partitioned Load Scenarios

706 |

707 | These scenarios sample performance as partitioned load is applied. 708 | Each topic only has 1 and only 1 publisher and subscriber attached. 709 |

710 | 711 | 712 | 794 | 876 | 958 |
 CleanNon Clean
QoS 0
713 | 714 | 715 | 727 | 739 | 751 | 752 |
 20 b Message1 k Message256 k Message
1x1x1
716 |

Producer Rates (msg/s):

717 |
718 |

Consumer Rates (msg/s):

719 |
720 |
721 |
722 |
723 |
724 |
725 |
726 |
5x5x5
728 |

Producer Rates (msg/s):

729 |
730 |

Consumer Rates (msg/s):

731 |
732 |
733 |
734 |
735 |
736 |
737 |
738 |
10x10x10
740 |

Producer Rates (msg/s):

741 |
742 |

Consumer Rates (msg/s):

743 |
744 |
745 |
746 |
747 |
748 |
749 |
750 |
753 |
754 | 755 | 756 | 768 | 780 | 792 |
 20 b Message1 k Message256 k Message
1x1x1
757 |

Producer Rates (msg/s):

758 |
759 |

Consumer Rates (msg/s):

760 |
761 |
762 |
763 |
764 |
765 |
766 |
767 |
5x5x5
769 |

Producer Rates (msg/s):

770 |
771 |

Consumer Rates (msg/s):

772 |
773 |
774 |
775 |
776 |
777 |
778 |
779 |
10x10x10
781 |

Producer Rates (msg/s):

782 |
783 |

Consumer Rates (msg/s):

784 |
785 |
786 |
787 |
788 |
789 |
790 |
791 |
793 |
QoS 1
795 | 796 | 797 | 809 | 821 | 833 | 834 |
 20 b Message1 k Message256 k Message
1x1x1
798 |

Producer Rates (msg/s):

799 |
800 |

Consumer Rates (msg/s):

801 |
802 |
803 |
804 |
805 |
806 |
807 |
808 |
5x5x5
810 |

Producer Rates (msg/s):

811 |
812 |

Consumer Rates (msg/s):

813 |
814 |
815 |
816 |
817 |
818 |
819 |
820 |
10x10x10
822 |

Producer Rates (msg/s):

823 |
824 |

Consumer Rates (msg/s):

825 |
826 |
827 |
828 |
829 |
830 |
831 |
832 |
835 |
836 | 837 | 838 | 850 | 862 | 874 |
 20 b Message1 k Message256 k Message
1x1x1
839 |

Producer Rates (msg/s):

840 |
841 |

Consumer Rates (msg/s):

842 |
843 |
844 |
845 |
846 |
847 |
848 |
849 |
5x5x5
851 |

Producer Rates (msg/s):

852 |
853 |

Consumer Rates (msg/s):

854 |
855 |
856 |
857 |
858 |
859 |
860 |
861 |
10x10x10
863 |

Producer Rates (msg/s):

864 |
865 |

Consumer Rates (msg/s):

866 |
867 |
868 |
869 |
870 |
871 |
872 |
873 |
875 |
QoS 2
877 | 878 | 879 | 891 | 903 | 915 | 916 |
 20 b Message1 k Message256 k Message
1x1x1
880 |

Producer Rates (msg/s):

881 |
882 |

Consumer Rates (msg/s):

883 |
884 |
885 |
886 |
887 |
888 |
889 |
890 |
5x5x5
892 |

Producer Rates (msg/s):

893 |
894 |

Consumer Rates (msg/s):

895 |
896 |
897 |
898 |
899 |
900 |
901 |
902 |
10x10x10
904 |

Producer Rates (msg/s):

905 |
906 |

Consumer Rates (msg/s):

907 |
908 |
909 |
910 |
911 |
912 |
913 |
914 |
917 |
918 | 919 | 920 | 932 | 944 | 956 |
 20 b Message1 k Message256 k Message
1x1x1
921 |

Producer Rates (msg/s):

922 |
923 |

Consumer Rates (msg/s):

924 |
925 |
926 |
927 |
928 |
929 |
930 |
931 |
5x5x5
933 |

Producer Rates (msg/s):

934 |
935 |

Consumer Rates (msg/s):

936 |
937 |
938 |
939 |
940 |
941 |
942 |
943 |
10x10x10
945 |

Producer Rates (msg/s):

946 |
947 |

Consumer Rates (msg/s):

948 |
949 |
950 |
951 |
952 |
953 |
954 |
955 |
957 |
959 | 1054 |
1055 |
1056 | 1057 | 1058 | -------------------------------------------------------------------------------- /src/main/scala/com/github/mqtt/benchmark/Benchmark.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2009-2011 the original author or authors. 3 | * See the notice.md file distributed with this work for additional 4 | * information regarding copyright ownership. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package com.github.mqtt.benchmark 19 | 20 | import scala.collection.mutable.HashMap 21 | import scala.xml.{XML, NodeSeq} 22 | import scala.util.control.Exception.catching 23 | import scala.util.Random 24 | 25 | import java.io.{PrintStream, FileOutputStream, File} 26 | import collection.JavaConversions 27 | import java.lang.{String, Class} 28 | 29 | import org.apache.felix.gogo.commands.basic.DefaultActionPreparator 30 | import org.apache.felix.service.command.CommandSession 31 | import org.apache.felix.gogo.commands.{CommandException, Action, Option => option, Argument => argument, Command => command} 32 | 33 | import sun.misc.Signal; 34 | import sun.misc.SignalHandler; 35 | 36 | object Benchmark { 37 | def main(args: Array[String]):Unit = { 38 | val session = new CommandSession { 39 | def getKeyboard = System.in 40 | def getConsole = System.out 41 | def put(p1: String, p2: AnyRef) = {} 42 | def get(p1: String) = null 43 | def format(p1: AnyRef, p2: Int) = throw new UnsupportedOperationException 44 | def execute(p1: CharSequence) = throw new UnsupportedOperationException 45 | def convert(p1: Class[_], p2: AnyRef) = throw new UnsupportedOperationException 46 | def close = {} 47 | } 48 | 49 | val action = new Benchmark() 50 | val p = new DefaultActionPreparator 51 | try { 52 | if( p.prepare(action, session, JavaConversions.asJavaList(args.toList)) ) { 53 | action.execute(session) 54 | } 55 | } catch { 56 | case x:CommandException=> 57 | println(x.getMessage) 58 | System.exit(-1); 59 | } 60 | } 61 | } 62 | 63 | @command(scope="mqtt", name = "benchmark", description = "The MQTT benchmarking tool") 64 | class Benchmark extends Action { 65 | 66 | // Helpers needed to diferenciate between default value and not set on the CLI value for primitive values 67 | def toIntOption(x: java.lang.Integer): Option[Int] = if(x!=null) Some(x.intValue) else None 68 | def toLongOption(x: java.lang.Long): Option[Long] = if(x!=null) Some(x.longValue) else None 69 | def toBooleanOption(x: java.lang.Boolean): Option[Boolean] = if(x!=null) Some(x.booleanValue) else None 70 | 71 | @option(name = "--broker_name", description = "The name of the broker being benchmarked.") 72 | var cl_broker_name:String = _ 73 | var broker_name = FlexibleProperty(default = None, high_priority = () => Option(cl_broker_name)) 74 | 75 | @option(name = "--protocol", description = "protocol to use (tcp, ssl, tls, tlsv2, etc.)") 76 | var cl_protocol: String = _ 77 | var protocol = FlexibleProperty(default = Some("tcp"), high_priority = () => Option(cl_protocol)) 78 | 79 | @option(name = "--key-store-file", description = "The JKS keystore file to use for keys and certs when using ssl connections") 80 | var cl_key_store_file: String = _ 81 | var key_store_file = FlexibleProperty(default = None, high_priority = () => Option(cl_key_store_file)) 82 | 83 | @option(name = "--key-store-password", description = "The JKS keystore password") 84 | var cl_key_store_password: String = _ 85 | var key_store_password = FlexibleProperty(default = None, high_priority = () => Option(cl_key_store_password)) 86 | 87 | @option(name = "--key-password", description = "The password the key in the JKS keystore") 88 | var cl_key_password: String = _ 89 | var key_password = FlexibleProperty(default = None, high_priority = () => Option(cl_key_password)) 90 | 91 | @option(name = "--host", description = "server host name") 92 | var cl_host: String = _ 93 | var host = FlexibleProperty(default = Some("127.0.0.1"), high_priority = () => Option(cl_host)) 94 | @option(name = "--port", description = "server port") 95 | var cl_port: java.lang.Integer = _ 96 | var port = FlexibleProperty(default = Some(1883), high_priority = () => toIntOption(cl_port)) 97 | 98 | @option(name = "--user", description = "user name to connect with") 99 | var cl_user:String = _ 100 | var user = FlexibleProperty(default = None, high_priority = () => Option(cl_user)) 101 | @option(name = "--password", description = "password to connect with") 102 | var cl_password:String = _ 103 | var password = FlexibleProperty(default = None, high_priority = () => Option(cl_password)) 104 | 105 | @option(name = "--sample-count", description = "number of samples to take") 106 | var cl_sample_count: java.lang.Integer = _ 107 | var sample_count = FlexibleProperty(default = Some(15), high_priority = () => toIntOption(cl_sample_count)) 108 | @option(name = "--sample-interval", description = "number of milli seconds that data is collected.") 109 | var cl_sample_interval: java.lang.Integer = _ 110 | var sample_interval = FlexibleProperty(default = Some(1000), high_priority = () => toIntOption(cl_sample_interval)) 111 | @option(name = "--warm-up-count", description = "number of warm up samples to ignore") 112 | var cl_warm_up_count: java.lang.Integer = _ 113 | var warm_up_count = FlexibleProperty(default = Some(3), high_priority = () => toIntOption(cl_warm_up_count)) 114 | 115 | @argument(index=0, name = "out", description = "The file to store benchmark metrics in", required=true) 116 | var cl_out: File = _ 117 | var out = FlexibleProperty(default = None, high_priority = () => Option(cl_out)) 118 | 119 | @option(name = "--scenario-connection-scale", description = "enable the connection scale scenarios") 120 | var cl_scenario_connection_scale: java.lang.Boolean = _ 121 | var scenario_connection_scale = FlexibleProperty(default = Some(false), high_priority = () => toBooleanOption(cl_scenario_connection_scale)) 122 | 123 | @option(name = "--scenario-request-response", description = "enable the request response scenarios") 124 | var cl_scenario_request_response: java.lang.Boolean = _ 125 | var scenario_request_response = FlexibleProperty(default = Some(false), high_priority = () => toBooleanOption(cl_scenario_request_response)) 126 | 127 | @option(name = "--scenario-connection-scale-rate", description = "How many connection to add after each sample") 128 | var cl_scenario_connection_scale_rate: java.lang.Integer = _ 129 | var scenario_connection_scale_rate = FlexibleProperty(default = Some(50), high_priority = () => toIntOption(cl_scenario_connection_scale_rate)) 130 | @option(name = "--scenario-connection-max-samples", description = "The maximum number of sample to take in the connection scale scenario") 131 | var cl_scenario_connection_scale_max_samples: java.lang.Integer = _ 132 | var scenario_connection_scale_max_samples = FlexibleProperty(default = Some(100), high_priority = () => toIntOption(cl_scenario_connection_scale_max_samples)) 133 | 134 | @option(name = "--scenario-producer-throughput", description = "enable the producer throughput scenarios") 135 | var cl_scenario_producer_throughput: java.lang.Boolean = _ 136 | var scenario_producer_throughput = FlexibleProperty(default = Some(true), high_priority = () => toBooleanOption(cl_scenario_producer_throughput)) 137 | 138 | @option(name = "--scenario-subscription-loading", description = "enable the subscription load/unload scenarios") 139 | var cl_scenario_subscription_loading: java.lang.Boolean = _ 140 | var scenario_subscription_loading = FlexibleProperty(default = Some(true), high_priority = () => toBooleanOption(cl_scenario_subscription_loading)) 141 | 142 | @option(name = "--scenario-partitioned", description = "enable the partitioned load scenarios") 143 | var cl_scenario_partitioned: java.lang.Boolean = _ 144 | var scenario_partitioned = FlexibleProperty(default = Some(true), high_priority = () => toBooleanOption(cl_scenario_partitioned)) 145 | @option(name = "--scenario-fan-in-out", description = "enable the fan in/fan out scenarios") 146 | var cl_scenario_fan_in_out: java.lang.Boolean = _ 147 | var scenario_fan_in_out = FlexibleProperty(default = Some(true), high_priority = () => toBooleanOption(cl_scenario_fan_in_out)) 148 | 149 | @option(name = "--scenario-file", description = "uses a scenario defined in an XML file instead of the default ones") 150 | var cl_scenario_file: File = _ 151 | var scenario_file = FlexibleProperty(default = None, high_priority = () => Option(cl_scenario_file)) 152 | 153 | @option(name = "--destination-prefix", description = "prefix used for the destination names.") 154 | var cl_destination_prefix: String = _ 155 | var destination_prefix = FlexibleProperty(default = Some(""), high_priority = () => Option(cl_destination_prefix)) 156 | 157 | @option(name = "--drain-timeout", description = "How long to wait for a drain to timeout in ms.") 158 | var cl_drain_timeout: java.lang.Long = _ 159 | var drain_timeout = FlexibleProperty(default = Some(3000L), high_priority = () => toLongOption(cl_drain_timeout)) 160 | 161 | @option(name = "--messages-per-connection", description = "The number of messages that are sent before the client reconnect.") 162 | var cl_messages_per_connection: java.lang.Integer = _ 163 | var messages_per_connection = FlexibleProperty( 164 | default = Some(new propertyFunction { override def apply() = -1 }), 165 | high_priority = () => { 166 | if (cl_messages_per_connection != null) 167 | Some(new propertyFunction { override def apply() = cl_messages_per_connection.intValue }) 168 | else 169 | None 170 | } 171 | ) 172 | 173 | @option(name = "--message-retain", description = "Should the producer set the retain flag?") 174 | var cl_message_retain: java.lang.Boolean = _ 175 | var message_retain = FlexibleProperty(default = Some(false), high_priority = () => toBooleanOption(cl_message_retain)) 176 | 177 | @option(name = "--display-errors", description = "Should errors get dumped to the screen when they occur?") 178 | var cl_display_errors: java.lang.Boolean = _ 179 | var display_errors = FlexibleProperty(default = Some(true), high_priority = () => toBooleanOption(cl_display_errors)) 180 | 181 | var samples = HashMap[String, List[(Long,Long)]]() 182 | var benchmark_results = new BenchmarkResults() 183 | 184 | def json_format(value:Option[List[String]]):String = { 185 | value.map { json_format _ }.getOrElse("null") 186 | } 187 | 188 | def json_format(value:List[String]):String = { 189 | "[ "+value.mkString(",")+" ]" 190 | } 191 | 192 | def write_results() { 193 | val parent_dir = out.get.getParentFile 194 | if (parent_dir != null) { 195 | parent_dir.mkdirs 196 | } 197 | val os = new PrintStream(new FileOutputStream(out.get)) 198 | 199 | if( scenario_file.getOption.isEmpty ) { 200 | os.println("{") 201 | os.println(""" "benchmark_settings": {""") 202 | os.println(""" "broker_name": "%s",""".format(broker_name.get)) 203 | os.println(""" "host": "%s",""".format(host.get)) 204 | os.println(""" "port": %d,""".format(port.get)) 205 | os.println(""" "sample_count": %d,""".format(sample_count.get)) 206 | os.println(""" "sample_interval": %d,""".format(sample_interval.get)) 207 | os.println(""" "warm_up_count": %d,""".format(warm_up_count.get)) 208 | os.println(""" "scenario_connection_scale_rate": %d""".format(scenario_connection_scale_rate.get)) 209 | os.println(""" },""") 210 | os.println(samples.map { case (name, sample)=> 211 | """ "%s": %s""".format(name, json_format(sample.map(x=> "[%d,%d]".format(x._1,x._2)))) 212 | }.mkString(",\n")) 213 | os.println("}") 214 | } else { 215 | os.print(benchmark_results.to_json()) 216 | } 217 | 218 | os.close 219 | 220 | println("===================================================================") 221 | println("Stored: "+out.get) 222 | println("===================================================================") 223 | } 224 | 225 | def execute(session: CommandSession): AnyRef = { 226 | 227 | FlexibleProperty.init_all() 228 | 229 | broker_name.set_default(out.get.getName.stripSuffix(".json")) 230 | 231 | // Protect against ctrl-c, write the results we have in any case 232 | Signal.handle(new Signal("INT"), new SignalHandler () { 233 | def handle(sig: Signal) { 234 | println("\n\n**** Program interruption requested by the user, writing the results ****\n") 235 | write_results() 236 | System.exit(0) 237 | } 238 | }); 239 | 240 | println("===================================================================") 241 | println("Benchmarking %s at: %s:%d".format(broker_name.get, host.get, port.get)) 242 | println("===================================================================") 243 | 244 | try { 245 | if( scenario_file.getOption.isEmpty ) { 246 | run_benchmarks 247 | } else { 248 | load_and_run_benchmarks 249 | } 250 | } catch { 251 | case e : Exception => { 252 | println("There was an error, we proceed to write the results we got:") 253 | println(e) 254 | println(e.getStackTraceString) 255 | } 256 | } 257 | 258 | write_results() 259 | 260 | null 261 | } 262 | 263 | private def benchmark(name:String, drain:Boolean=true, sc:Int=sample_count.get, is_done: (List[Scenario])=>Boolean = null)(init_func: (Scenario)=>Unit ):Unit = { 264 | multi_benchmark(List(name), drain, sc, is_done) { scenarios => 265 | init_func(scenarios.head) 266 | } 267 | } 268 | 269 | private def multi_benchmark(names:List[String], drain:Boolean=true, sc:Int=sample_count.get, is_done: (List[Scenario])=>Boolean = null, results: HashMap[String, ClientResults] = HashMap.empty)(init_func: (List[Scenario])=>Unit ):Unit = { 270 | val scenarios:List[Scenario] = names.map { name=> 271 | val scenario = new NonBlockingScenario 272 | scenario.name = name 273 | scenario.sample_interval = sample_interval.get 274 | scenario.protocol = protocol.get 275 | scenario.host = host.get 276 | scenario.port = port.get 277 | scenario.key_store_file = key_store_file.getOption() 278 | scenario.key_store_password = key_store_password.getOption() 279 | scenario.key_password = key_password.getOption() 280 | scenario.user = user.getOption 281 | scenario.password = password.getOption 282 | scenario.destination_prefix = destination_prefix.get 283 | scenario.drain_timeout = drain_timeout.get 284 | scenario.display_errors = display_errors.get 285 | scenario.message_retain = message_retain.get 286 | scenario 287 | } 288 | 289 | init_func(scenarios) 290 | 291 | scenarios.foreach{ scenario=> 292 | if (scenario.destination_name.isEmpty) { 293 | if( scenario.destination_prefix == "queue" ) { 294 | scenario.destination_name = "loadq" 295 | } else if( scenario.destination_prefix == "topic" ) { 296 | scenario.destination_name = "loadt" 297 | } 298 | } 299 | } 300 | 301 | print("scenario : %s ".format(names.mkString(" and "))) 302 | 303 | def with_load[T](s:List[Scenario])(proc: => T):T = { 304 | s.headOption match { 305 | case Some(senario) => 306 | senario.with_load { 307 | with_load(s.drop(1)) { 308 | proc 309 | } 310 | } 311 | case None => 312 | proc 313 | } 314 | } 315 | 316 | Thread.currentThread.setPriority(Thread.MAX_PRIORITY) 317 | val sample_set = with_load(scenarios) { 318 | for( i <- 0 until warm_up_count.get ) { 319 | Thread.sleep(sample_interval.get) 320 | print(".") 321 | } 322 | scenarios.foreach(_.collection_start) 323 | 324 | if( is_done!=null ) { 325 | while( !is_done(scenarios) ) { 326 | print(".") 327 | Thread.sleep(sample_interval.get) 328 | scenarios.foreach(_.collection_sample) 329 | } 330 | 331 | } else { 332 | var remaining = sc 333 | while( remaining > 0 ) { 334 | print(".") 335 | Thread.sleep(sample_interval.get) 336 | scenarios.foreach(_.collection_sample) 337 | remaining-=1 338 | } 339 | } 340 | 341 | 342 | println(".") 343 | scenarios.foreach{ scenario=> 344 | val collected = scenario.collection_end 345 | collected.foreach{ x=> 346 | if( !x._1.startsWith("e_") || x._2.find( _._2 != 0 ).isDefined ) { 347 | println("%s samples: %s".format(x._1, json_format(x._2.map(_._2.toString))) ) 348 | 349 | if (results.contains(scenario.name)) { 350 | // Copy the scenario results to the results structure 351 | val client_results = results(scenario.name) 352 | client_results.producers_data = collected.getOrElse("p_"+scenario.name, Nil) 353 | client_results.consumers_data = collected.getOrElse("c_"+scenario.name, Nil) 354 | client_results.error_data = collected.getOrElse("e_"+scenario.name, Nil) 355 | client_results.request_p90 = collected.getOrElse("p90_"+scenario.name, Nil) 356 | client_results.request_p99 = collected.getOrElse("p99_"+scenario.name, Nil) 357 | client_results.request_p999 = collected.getOrElse("p999_"+scenario.name, Nil) 358 | 359 | if ( client_results.error_data.foldLeft(0L)((a,x) => a + x._2) == 0 ) { 360 | // If there are no errors, we keep an empty list 361 | client_results.error_data = Nil 362 | } 363 | } 364 | } 365 | } 366 | samples ++= collected 367 | } 368 | } 369 | Thread.currentThread.setPriority(Thread.NORM_PRIORITY) 370 | 371 | if( drain) { 372 | scenarios.headOption.foreach( _.drain ) 373 | } 374 | } 375 | 376 | trait propertyFunction { 377 | 378 | protected val SLEEP = -500 379 | 380 | protected var init_time: Long = 0 381 | 382 | def init(time: Long) { init_time = time } 383 | 384 | def now() = { System.currentTimeMillis() - init_time } 385 | 386 | def apply() = 0 387 | 388 | /* Alternates two values for short periods of time (fast) or long ones (slow) in bursts */ 389 | def burst(slow: Int, fast: Int, duration: Int, period: Int) = { 390 | new Function1[Long, Int] { 391 | var burstLeft: Long = 0 392 | var previousTime: Long = 0 393 | def apply(time: Long) = { 394 | if (time != previousTime) { 395 | if (burstLeft > 0) { 396 | burstLeft -= time-previousTime 397 | if(burstLeft < 0){ 398 | burstLeft = 0 399 | } 400 | } else { 401 | if (util.Random.nextInt(period) == 0) { 402 | burstLeft = duration 403 | } 404 | } 405 | previousTime = time 406 | } 407 | if (burstLeft > 0) fast else slow 408 | } 409 | } 410 | } 411 | 412 | /* Returns random numbers uniformly distributed between min (included) and max (not included) */ 413 | def random (min: Int, max: Int) = { 414 | if (min == max) { 415 | new Function1[Long, Int] { 416 | def apply(time: Long): Int = { 417 | return min 418 | } 419 | } 420 | } else if (max > min) { 421 | new Function1[Long, Int] { 422 | def apply(time: Long): Int = { 423 | return Random.nextInt(max-min) + min 424 | } 425 | } 426 | } else { 427 | throw new Exception("Error in random function, min bigger than max.") 428 | } 429 | } 430 | 431 | /* Returns random numbers normally (gaussian) distributed with a mean and a variance */ 432 | def normal (mean: Int, variance: Int) = { 433 | new Function1[Long, Int] { 434 | def apply(time: Long): Int = { 435 | return (Random.nextGaussian*variance + mean).toInt 436 | } 437 | } 438 | } 439 | } 440 | 441 | private def mlabel(size:Int) = if((size%1024)==0) (size/1024)+"k" else size+"b" 442 | private def plabel(persistent:Boolean) = if(persistent) "p" else "" 443 | private def slabel(sync_send:Boolean) = if(sync_send) "" else "a" 444 | 445 | def run_benchmarks = { 446 | 447 | val clean_values = List(true, false) 448 | def clean_text(v:Boolean) = if(v) "clean" else "dirty" 449 | def client_text(count:Int, clean:Boolean, qos:Int) = "(%d * %s qos%d)".format(count, clean_text(clean), qos) 450 | 451 | if(scenario_request_response.get) { 452 | for( producers <- List(1, 10, 100); clean <- List(false, true); consumers <- List(1, 5, 10); qos <- List(0,1,2) ) { 453 | val name = "rr:20b:%s_qos%d_%d>1<%s_qos%d_%d".format(clean_text(clean), qos, producers, clean_text(clean), qos, consumers) 454 | benchmark(name) { g=> 455 | g.message_size = 20 456 | g.producer_clean = clean 457 | g.producer_qos = qos 458 | g.producers = producers 459 | g.consumer_qos = qos 460 | g.consumers = consumers 461 | g.request_response = true 462 | g.producer_clean = clean 463 | } 464 | } 465 | } 466 | 467 | // if(scenario_connection_scale.get ) { 468 | // for( messages_per_connection <- List(-1)) { 469 | // 470 | // /** this test keeps going until we start getting a large number of errors */ 471 | // var remaining = scenario_connection_scale_max_samples.get 472 | // def is_done(scenarios:List[Scenario]):Boolean = { 473 | // remaining -= 1; 474 | // var errors = 0L 475 | // scenarios.foreach( _.error_samples.lastOption.foreach( errors+= _._2 ) ) 476 | // return errors >= scenario_connection_scale_rate.get || remaining <= 0 477 | // } 478 | // 479 | // benchmark("20b_Xa%s_1queue_1".format(messages_per_connection)+"m", true, 0, is_done) { scenario=> 480 | // scenario.message_size = 20 481 | // scenario.producers = 0 482 | // scenario.messages_per_connection = messages_per_connection 483 | // scenario.producers_per_sample = scenario_connection_scale_rate.get 484 | // scenario.producer_sleep = 1000 485 | // scenario.producer_clean = true 486 | // scenario.destination_count = 1 487 | // scenario.destination_prefix = "queue" 488 | // scenario.consumers = 1 489 | // } 490 | // } 491 | // } 492 | 493 | if( scenario_subscription_loading.get ) { 494 | 495 | // Setup the sub 496 | benchmark("load_setup", false, 1) { g=> 497 | g.producers = 0 498 | g.destination_count = 1 499 | g.destination_name = "load_me_up" 500 | g.consumer_clean = false 501 | g.consumer_qos = 1 502 | g.consumers = 1 503 | g.clear_subscriptions_when_finished = false 504 | } 505 | 506 | // Load with the 3 QoSes 507 | for ( qos <- List(0,1,2)) { 508 | val name = "load_20b:%s->[1]".format(client_text(1, true, qos)) 509 | benchmark(name, false, 30) { g=> 510 | g.message_size = 20 511 | g.producer_clean = true 512 | g.producer_qos = qos 513 | g.producers = 1 514 | g.destination_count = 1 515 | g.destination_name = "load_me_up" 516 | g.consumers = 0 517 | g.clear_subscriptions_when_finished = false 518 | } 519 | } 520 | 521 | // Unload with the 3 QoSes 522 | for ( qos <- List(2,1,0)) { 523 | val name = "load_20b:[1]->%s".format(client_text(1, false, qos)) 524 | benchmark(name, false, 15) { g=> 525 | g.producers = 0 526 | g.destination_count = 1 527 | g.destination_name = "load_me_up" 528 | g.consumer_clean = false 529 | g.consumer_qos = qos 530 | g.consumers = 1 531 | g.clear_subscriptions_when_finished = qos==2 // Clear them after the last one. 532 | } 533 | } 534 | 535 | } 536 | 537 | // // Setup a scenario /w fast and slow consumers 538 | // if(scenario_slow_consumer.get) { 539 | // for( dt <- destination_types) { 540 | // multi_benchmark(List("20b_1a_1%s_1fast".format(dt), "20b_0_1%s_1slow".format(dt))) { 541 | // case List(fast:Scenario, slow:Scenario) => 542 | // fast.message_size = 20 543 | // fast.producers = 1 544 | // fast.producer_clean = true 545 | // fast.destination_count = 1 546 | // fast.destination_prefix = dt 547 | // fast.consumers = 1 548 | // 549 | // slow.producers = 0 550 | // slow.destination_count = 1 551 | // slow.destination_prefix = dt 552 | // slow.consumer_sleep = 100 // He can only process 10 /sec 553 | // slow.consumers = 1 554 | // case _ => 555 | // } 556 | // } 557 | // } 558 | 559 | if( scenario_producer_throughput.get ) { 560 | // Benchmark for figuring out the max producer throughput 561 | for( size <- List(20, 1024, 1024 * 256) ) { 562 | val name = "pt_%s:%s->[1]".format(mlabel(size), client_text(1, true, 0)) 563 | benchmark(name) { g=> 564 | g.message_size = size 565 | g.producer_clean = true 566 | g.producer_qos = 0 567 | g.producers = 1 568 | g.destination_count = 1 569 | g.consumers = 0 570 | } 571 | } 572 | } 573 | 574 | // Benchmark for the parallel scenarios 575 | if( scenario_partitioned.get ) { 576 | 577 | var sizes = List(20, 1024, 1024 * 256) 578 | val destinations = List(1, 5, 10) 579 | for( clean <- clean_values ; size <- sizes; load <- destinations ; qos <- List(0,1,2)) { 580 | val name = "par_%s:%s->[%d]->%s".format(mlabel(size), client_text(load, clean, qos), load, client_text(load, clean, qos)) 581 | benchmark(name) { g=> 582 | g.message_size = size 583 | g.producer_clean = clean 584 | g.producer_qos = qos 585 | g.producers = load 586 | g.destination_count = load 587 | g.consumer_clean = clean 588 | g.consumer_qos = qos 589 | g.consumers = load 590 | } 591 | } 592 | } 593 | 594 | if( scenario_fan_in_out.get ) { 595 | val client_count = List(1, 5, 10) 596 | var sizes = List(20) 597 | for( clean <- clean_values; size <- sizes; consumers <- client_count; producers <- client_count; qos <- List(0,1,2) ) { 598 | val name = "fan_%s:%s->[1]->%s".format(mlabel(size), client_text(producers, clean, qos), client_text(consumers, clean, qos)) 599 | benchmark(name) { g=> 600 | g.message_size = size 601 | g.producer_clean = clean 602 | g.producer_qos = qos 603 | g.producers = producers 604 | g.destination_count = 1 605 | g.consumer_clean = clean 606 | g.consumer_qos = qos 607 | g.consumers = consumers 608 | } 609 | } 610 | } 611 | 612 | } 613 | 614 | def load_and_run_benchmarks = { 615 | 616 | var producers = FlexibleProperty[Int]() 617 | var consumers = FlexibleProperty[Int]() 618 | var destination_type = FlexibleProperty[String]() 619 | var destination_name = FlexibleProperty[String]() 620 | var destination_count = FlexibleProperty[Int]() 621 | var consumer_prefix = FlexibleProperty[String]() 622 | 623 | var content_length = FlexibleProperty[Boolean]() 624 | 625 | var drain = FlexibleProperty[Boolean](default = Some(false)) 626 | var persistent = FlexibleProperty[Boolean]() 627 | var durable = FlexibleProperty[Boolean]() 628 | var sync_send = FlexibleProperty[Boolean]() 629 | 630 | var ack = FlexibleProperty[String]() 631 | 632 | var producers_per_sample = FlexibleProperty[Int]() 633 | var consumers_per_sample = FlexibleProperty[Int]() 634 | 635 | var headers = FlexibleProperty[Array[Array[String]]](default = Some(Array[Array[String]]())) 636 | var selector = FlexibleProperty[String]() 637 | 638 | var producer_sleep = FlexibleProperty[propertyFunction](default = Some(new propertyFunction { override def apply() = 0 })) 639 | var consumer_sleep = FlexibleProperty[propertyFunction](default = Some(new propertyFunction { override def apply() = 0 })) 640 | var message_size = FlexibleProperty[propertyFunction](default = Some(new propertyFunction { override def apply() = 1024 })) 641 | 642 | def getStringValue(property_name: String, ns_xml: NodeSeq, vars: Map[String, String] = Map.empty[String, String]): Option[String] = { 643 | val value = ns_xml \ property_name 644 | if (value.length == 1) Some(substituteVariables(value.text.trim, vars)) else None 645 | } 646 | 647 | def getIntValue(property_name: String, ns_xml: NodeSeq, vars: Map[String, String] = Map.empty[String, String]): Option[Int] = { 648 | val value = getStringValue(property_name, ns_xml, vars) 649 | try { 650 | value.map((x:String) => x.toInt) 651 | } catch { 652 | case e: NumberFormatException => throw new Exception("Error in XML scenario, not integer provided: " + value.getOrElse("\"\"")) 653 | } 654 | } 655 | 656 | def getBooleanValue(property_name: String, ns_xml: NodeSeq, vars: Map[String, String] = Map.empty[String, String]): Option[Boolean] = { 657 | val value = getStringValue(property_name, ns_xml, vars) 658 | try { 659 | value.map((x:String) => x.toBoolean) 660 | } catch { 661 | case e: NumberFormatException => throw new Exception("Error in XML scenario, not boolean provided: " + value.getOrElse("\"\"")) 662 | } 663 | } 664 | 665 | def getPropertyFunction(property_name: String, clients_xml: NodeSeq, vars: Map[String, String] = Map.empty[String, String]): Option[propertyFunction] = { 666 | val format_catcher = catching(classOf[NumberFormatException]) 667 | val property_function_nodeset = clients_xml \ property_name 668 | val property_function_value: Option[Int] = format_catcher.opt(substituteVariables(property_function_nodeset.text.trim, vars).toInt) 669 | if (property_function_nodeset.length == 1 && property_function_value.isDefined) { 670 | Some(new propertyFunction { override def apply() = property_function_value.get }) 671 | } else if ((property_function_nodeset \ "range").length > 0) { 672 | Some(new propertyFunction { 673 | var ranges: List[Tuple2[Int, (Long) => Int]] = Nil 674 | for (range_node <- property_function_nodeset \ "range") { 675 | val range_value = format_catcher.opt(substituteVariables(range_node.text.trim, vars).toInt) 676 | val range_end = getStringValue("@end", range_node, vars). 677 | map((x:String) => x.toLowerCase().replace("end", Int.MaxValue.toString).toInt). 678 | map((x: Int) => if (x >= 0) x else sample_count.get*sample_interval.get + x) 679 | val range_burst = range_node \ "burst" 680 | val range_random = range_node \ "random" 681 | val range_normal = range_node \ "normal" 682 | if (range_node.text == "sleep") { 683 | ranges :+= Tuple2(range_end.get, (time: Long) => SLEEP) 684 | } else if (range_value.isDefined) { 685 | ranges :+= Tuple2(range_end.get, (time: Long) => range_value.get) 686 | } else if (range_burst.length == 1) { 687 | var (slow, fast, duration, period) = (100, 0 , 1, 10) 688 | slow = getIntValue("@slow", range_burst, vars).getOrElse(slow) 689 | fast = getIntValue("@fast", range_burst, vars).getOrElse(fast) 690 | duration = getIntValue("@duration", range_burst, vars).getOrElse(duration) 691 | period = getIntValue("@period", range_burst, vars).getOrElse(period) 692 | ranges :+= Tuple2(range_end.get, burst(slow, fast, duration, period)) 693 | } else if (range_random.length == 1) { 694 | var (min, max) = (0 , 1024) 695 | min = getIntValue("@min", range_random, vars).getOrElse(min) 696 | max = getIntValue("@max", range_random, vars).getOrElse(max) 697 | ranges :+= Tuple2(range_end.get, random(min, max)) 698 | } else if (range_normal.length == 1) { 699 | var (mean, variance) = (0 , 1) 700 | mean = getIntValue("@mean", range_normal, vars).getOrElse(mean) 701 | variance = getIntValue("@variance", range_normal, vars).getOrElse(variance) 702 | ranges :+= Tuple2(range_end.get, normal(mean, variance)) 703 | } else { 704 | throw new Exception("Error in XML scenario, unsuported property function: "+range_node.text) 705 | } 706 | } 707 | ranges = ranges.sortBy(_._1) 708 | 709 | override def apply() = { 710 | val n = now 711 | val r = ranges.find( r => n < r._1 ) 712 | if (r.isDefined) { 713 | r.get._2(n) 714 | } else { 715 | // Default values for diferent property names 716 | property_name match { 717 | case "producer_sleep" => SLEEP 718 | case "consumer_sleep" => SLEEP 719 | case "message_size" => 1024 720 | case "messages_per_connection" => -1 721 | } 722 | } 723 | } 724 | }) 725 | } else { 726 | None 727 | } 728 | } 729 | 730 | def getPropertyHeaders(property_name: String, ns_xml: NodeSeq, vars: Map[String, String] = Map.empty[String, String]): Option[Array[Array[String]]] = { 731 | val headers = ns_xml \ property_name 732 | if (headers.length == 1) { 733 | Some((headers(0) \ "client_type") map { client_type => 734 | (client_type \ "header") map { header => 735 | substituteVariables(header.text.trim, vars) 736 | } toArray 737 | } toArray) 738 | } else { 739 | None 740 | } 741 | 742 | //Some() else None 743 | } 744 | 745 | def push_properties(node: NodeSeq, vars: Map[String, String] = Map.empty[String, String]) { 746 | sample_count.push(getIntValue("sample_count", node, vars)) 747 | drain.push(getBooleanValue("drain", node, vars)) 748 | warm_up_count.push(getIntValue("warm_up_count", node, vars)) 749 | sample_interval.push(getIntValue("sample_interval", node, vars)) 750 | 751 | user.push(getStringValue("login", node, vars)) 752 | password.push(getStringValue("passcode", node, vars)) 753 | host.push(getStringValue("host", node, vars)) 754 | port.push(getIntValue("port", node, vars)) 755 | producers.push(getIntValue("producers", node, vars)) 756 | consumers.push(getIntValue("consumers", node, vars)) 757 | destination_type.push(getStringValue("destination_type", node, vars)) 758 | destination_name.push(getStringValue("destination_name", node, vars)) 759 | destination_count.push(getIntValue("destination_count", node, vars)) 760 | 761 | consumer_prefix.push(getStringValue("consumer_prefix", node, vars)) 762 | destination_prefix.push(getStringValue("queue_prefix", node, vars)) 763 | content_length.push(getBooleanValue("content_length", node, vars)) 764 | drain_timeout.push(getIntValue("drain_timeout", node, vars).map(_.toLong)) 765 | persistent.push(getBooleanValue("persistent", node, vars)) 766 | durable.push(getBooleanValue("durable", node, vars)) 767 | sync_send.push(getBooleanValue("sync_send", node, vars)) 768 | ack.push(getStringValue("ack", node, vars)) 769 | producers_per_sample.push(getIntValue("producers_per_sample", node, vars)) 770 | consumers_per_sample.push(getIntValue("consumers_per_sample", node, vars)) 771 | 772 | headers.push(getPropertyHeaders("headers", node, vars)) 773 | selector.push(getStringValue("selector", node, vars)) 774 | 775 | producer_sleep.push(getPropertyFunction("producer_sleep", node, vars)) 776 | consumer_sleep.push(getPropertyFunction("consumer_sleep", node, vars)) 777 | message_size.push(getPropertyFunction("message_size", node, vars)) 778 | messages_per_connection.push(getPropertyFunction("messages_per_connection", node, vars)) 779 | } 780 | 781 | def pop_properties() { 782 | sample_count.pop() 783 | drain.pop() 784 | warm_up_count.pop() 785 | sample_interval.pop() 786 | 787 | user.pop() 788 | password.pop() 789 | host.pop() 790 | port.pop() 791 | producers.pop() 792 | consumers.pop() 793 | destination_type.pop() 794 | destination_name.pop() 795 | destination_count.pop() 796 | 797 | consumer_prefix.pop() 798 | destination_prefix.pop() 799 | message_size.pop() 800 | content_length.pop() 801 | drain_timeout.pop() 802 | persistent.pop() 803 | durable.pop() 804 | sync_send.pop() 805 | ack.pop() 806 | messages_per_connection.pop() 807 | producers_per_sample.pop() 808 | consumers_per_sample.pop() 809 | 810 | headers.pop() 811 | selector.pop() 812 | 813 | producer_sleep.pop() 814 | consumer_sleep.pop() 815 | } 816 | 817 | /** This fucntion generates a list of tuples, each of them containing the 818 | * variables to be replaced in the scenario template and the SingleScenarioResults 819 | * object that will keep the results for this scenario. 820 | * 821 | * The list is generated from a list of variables and posible values, and 822 | * the parent of the ScenarioResults tree structure. The ScenarioResults 823 | * objects are linked properly. */ 824 | def combineLoopVariables(loop_vars: List[LoopVariable], parent: LoopScenarioResults): List[(Map[String, String], SingleScenarioResults)] = loop_vars match { 825 | case LoopVariable(name, _, values) :: Nil => values map { v => 826 | var scenario_results = new SingleScenarioResults() 827 | parent.scenarios :+= (v.label, scenario_results) 828 | (Map(name -> v.value), scenario_results) 829 | } 830 | case LoopVariable(name, _, values) :: tail => { 831 | values flatMap { lv => 832 | var scenario_results = new LoopScenarioResults() 833 | parent.scenarios :+= (lv.label, scenario_results) 834 | val combined_tail = combineLoopVariables(tail, scenario_results) 835 | combined_tail map { vv => (vv._1 + (name -> lv.value), vv._2) } 836 | } 837 | } 838 | case _ => Nil 839 | } 840 | 841 | def substituteVariables(orig: String, vars: Map[String, String]): String = { 842 | val format_catcher = catching(classOf[NumberFormatException]) 843 | var modified = orig 844 | for ((key, value) <- vars) { 845 | modified = modified.replaceAll("\\$\\{"+key+"\\}", value) 846 | 847 | // Functions applied to the variable 848 | val int_value: Option[Int] = format_catcher.opt( value.toInt ) 849 | val boolean_value: Option[Boolean] = format_catcher.opt( value.toBoolean ) 850 | 851 | if (int_value.isDefined) { 852 | modified = modified.replaceAll("\\$\\{mlabel\\("+key+"\\)\\}", mlabel(int_value.get).toString) 853 | } 854 | if (boolean_value.isDefined) { 855 | modified = modified.replaceAll("\\$\\{slabel\\("+key+"\\)\\}", slabel(boolean_value.get).toString) 856 | modified = modified.replaceAll("\\$\\{plabel\\("+key+"\\)\\}", plabel(boolean_value.get).toString) 857 | } 858 | } 859 | modified 860 | } 861 | 862 | val scenarios_xml = XML.loadFile(scenario_file.get) 863 | 864 | val global_common_xml = scenarios_xml \ "common" 865 | push_properties(global_common_xml) 866 | 867 | broker_name.push(getStringValue("broker_name", scenarios_xml)) 868 | 869 | benchmark_results.broker_name = broker_name.get 870 | benchmark_results.description = getStringValue("description", scenarios_xml).getOrElse("").replaceAll("\n", "\\\\n") 871 | benchmark_results.platform_name = getStringValue("platform_name", scenarios_xml).getOrElse("").replaceAll("\n", "\\\\n") 872 | benchmark_results.platform_desc = getStringValue("platform_desc", scenarios_xml).getOrElse("").replaceAll("\n", "\\\\n") 873 | 874 | for (group_xml <- scenarios_xml \ "group") { 875 | 876 | val group_common_xml = group_xml \ "common" 877 | push_properties(group_common_xml) 878 | 879 | var group_results = new GroupResults() 880 | benchmark_results.groups :+= group_results 881 | group_results.name = getStringValue("@name", group_xml).get 882 | group_results.description = getStringValue("description", group_xml).getOrElse("").replaceAll("\n", "\\\\n") 883 | 884 | // Parse the loop variables 885 | var loop_vars = (group_xml \ "loop" \ "var") map { var_xml => 886 | val values = (var_xml \ "value") map { value_xml => 887 | val value = value_xml.text 888 | var label = (value_xml \ "@label").text 889 | label = if (label == "") value else label // If there is no label, we use the value 890 | val description = (value_xml \ "@description").text 891 | LoopValue(value, label, description) 892 | } toList 893 | val name = (var_xml \ "@name").text 894 | var label = (var_xml \ "@label").text 895 | label = if (label == "") name else label // If there is no label, we use the name 896 | LoopVariable(name, label, values) 897 | } toList 898 | 899 | group_results.loop = loop_vars 900 | 901 | for (scenario_xml <- group_xml \ "scenario") { 902 | 903 | // If there are no loop variables, we just have one empty map and a SingleScenarioResults 904 | // Otherwise, we combine the diferent values of the loop variables and generate a ScenarioResults tree 905 | val variables_and_result_list = if (loop_vars.isEmpty) { 906 | val scenario_results = new SingleScenarioResults() 907 | group_results.scenarios :+= scenario_results 908 | List((Map.empty[String, String], scenario_results)) 909 | } else { 910 | val scenario_results = new LoopScenarioResults() 911 | group_results.scenarios :+= scenario_results 912 | combineLoopVariables(loop_vars, scenario_results) 913 | } 914 | 915 | for (variables_and_result <- variables_and_result_list) { 916 | 917 | val vars = variables_and_result._1 918 | val scenario_results = variables_and_result._2 919 | 920 | val scenario_common_xml = scenario_xml \ "common" 921 | push_properties(scenario_common_xml, vars) 922 | 923 | scenario_results.name = substituteVariables(getStringValue("@name", scenario_xml, vars).get, vars) 924 | scenario_results.label = substituteVariables(getStringValue("@label", scenario_xml, vars).getOrElse(scenario_results.name), vars) 925 | 926 | val names = (scenario_xml \ "clients").map( client => substituteVariables((client \ "@name").text, vars) ).toList 927 | 928 | var scenario_client_results = new HashMap[String, ClientResults]() 929 | 930 | multi_benchmark(names = names, drain = drain.get, results = scenario_client_results) { scenarios => 931 | for (scenario <- scenarios) { 932 | val clients_xml = (scenario_xml \ "clients").filter( clients => substituteVariables((clients \ "@name").text, vars) == scenario.name ) 933 | push_properties(clients_xml, vars) 934 | 935 | var client_results = new ClientResults() 936 | scenario_results.clients :+= client_results 937 | client_results.name = getStringValue("@name", clients_xml, vars).get 938 | 939 | scenario_client_results += (scenario.name -> client_results) // To be able to fill the results from multi_benchmark 940 | 941 | // Load all the properties in the scenario 942 | scenario.user = user.getOption() 943 | scenario.password = password.getOption() 944 | scenario.host = host.getOrElse(scenario.host) 945 | scenario.port = port.getOrElse(scenario.port) 946 | scenario.producers = producers.getOrElse(0) 947 | scenario.consumers = consumers.getOrElse(0) 948 | scenario.destination_prefix = destination_type.getOrElse(scenario.destination_prefix) 949 | scenario.destination_name = destination_name.getOrElse(scenario.destination_name) 950 | scenario.destination_count = destination_count.getOrElse(scenario.destination_count) 951 | 952 | scenario.drain_timeout = drain_timeout.getOrElse(scenario.drain_timeout) 953 | scenario.producers_per_sample = producers_per_sample.getOrElse(scenario.producers_per_sample) 954 | scenario.consumers_per_sample = consumers_per_sample.getOrElse(scenario.consumers_per_sample) 955 | 956 | scenario.producer_sleep = producer_sleep.get 957 | scenario.consumer_sleep = consumer_sleep.get 958 | scenario.message_size = message_size.get 959 | scenario.messages_per_connection = messages_per_connection.get 960 | 961 | // Copy the scenario settings to the results 962 | client_results.settings = scenario.settings 963 | 964 | pop_properties() 965 | } 966 | } 967 | pop_properties() 968 | } 969 | } 970 | pop_properties() 971 | } 972 | pop_properties() 973 | } 974 | } 975 | -------------------------------------------------------------------------------- /src/main/scala/com/github/mqtt/benchmark/BenchmarkResults.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2009-2011 the original author or authors. 3 | * See the notice.md file distributed with this work for additional 4 | * information regarding copyright ownership. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package com.github.mqtt.benchmark 19 | 20 | import scala.collection.mutable.LinkedHashMap 21 | import scala.collection.mutable.StringBuilder 22 | 23 | class BenchmarkResults { 24 | var broker_name: String = "" 25 | var description: String = "" 26 | var platform_name: String = "" 27 | var platform_desc: String = "" 28 | var groups: List[GroupResults] = Nil 29 | 30 | def to_json(level: Int = 0): String = { 31 | var sb = new StringBuilder() 32 | 33 | val indent = " " * level 34 | 35 | sb ++= indent + "{\n" 36 | sb ++= indent + " \"broker_name\": \"" + broker_name + "\",\n" 37 | sb ++= indent + " \"description\": \"" + description + "\",\n" 38 | sb ++= indent + " \"platform_name\": \"" + platform_name + "\",\n" 39 | sb ++= indent + " \"platform_desc\": \"" + platform_desc + "\",\n" 40 | sb ++= indent + " \"groups\": [\n" 41 | sb ++= groups map { group => 42 | group.to_json(level + 2) 43 | } mkString(",\n") 44 | sb ++= "\n" 45 | sb ++= indent + " ]\n" 46 | sb ++= indent + "}\n" 47 | 48 | sb.toString 49 | } 50 | } 51 | 52 | case class LoopValue(val value: String, val label: String, val description: String) 53 | case class LoopVariable(val name: String, val label: String, val values: List[LoopValue]) 54 | 55 | class GroupResults { 56 | var name: String = "" 57 | var description: String = "" 58 | var loop: List[LoopVariable] = Nil 59 | var scenarios: List[ScenarioResults] = Nil 60 | 61 | def to_json(level: Int = 0): String = { 62 | var sb = new StringBuilder() 63 | 64 | val indent = " " * level 65 | 66 | sb ++= indent + "{\n" 67 | sb ++= indent + " \"name\": \"" + name + "\",\n" 68 | sb ++= indent + " \"description\": \"" + description + "\",\n" 69 | 70 | sb ++= indent + " \"loop\": [\n" 71 | sb ++= loop map { loop_var => 72 | val values = loop_var.values map { value => 73 | indent + " { \"label\": \"" + value.label + "\", \"description\": \"" + value.description + "\"}" 74 | } mkString(",\n") 75 | 76 | indent + " [\"" + loop_var.label + "\", [\n" + 77 | values + "\n" + 78 | indent + " ]]" 79 | } mkString(",\n") 80 | sb ++= "\n" 81 | sb ++= indent + " ],\n" 82 | 83 | sb ++= indent + " \"scenarios\": [\n" 84 | sb ++= scenarios map { scenario => 85 | scenario.to_json(level + 2) 86 | } mkString(",\n") 87 | sb ++= "\n" 88 | 89 | sb ++= indent + " ]\n" 90 | sb ++= indent + "}" 91 | 92 | sb.toString 93 | } 94 | } 95 | 96 | abstract class ScenarioResults { 97 | def to_json(level: Int): String; 98 | } 99 | 100 | class LoopScenarioResults extends ScenarioResults { 101 | var scenarios: List[(String, ScenarioResults)] = Nil 102 | 103 | def to_json(level: Int = 0): String = { 104 | var sb = new StringBuilder() 105 | 106 | val indent = " " * level 107 | 108 | sb ++= indent + "{\n" 109 | 110 | sb ++= scenarios map { scenario => 111 | indent + " \"" + scenario._1 + "\": \n" + scenario._2.to_json(level + 2) 112 | } mkString(",\n") 113 | sb ++= "\n" 114 | sb ++= indent + "}" 115 | 116 | sb.toString 117 | } 118 | } 119 | 120 | class SingleScenarioResults extends ScenarioResults { 121 | var name: String = "" 122 | var label: String = "" 123 | var clients: List[ClientResults] = Nil 124 | 125 | def to_json(level: Int = 0): String = { 126 | var sb = new StringBuilder() 127 | 128 | val indent = " " * level 129 | 130 | sb ++= indent + "{\n" 131 | sb ++= indent + " \"name\": \"" + name + "\",\n" 132 | sb ++= indent + " \"label\": \"" + label + "\",\n" 133 | sb ++= indent + " \"clients\": [\n" 134 | sb ++= clients map { client => 135 | client.to_json(level + 2) 136 | } mkString(",\n") 137 | sb ++= "\n" 138 | sb ++= indent + " ]\n" 139 | sb ++= indent + "}" 140 | 141 | sb.toString 142 | } 143 | } 144 | 145 | class ClientResults { 146 | var name: String = "" 147 | var settings: List[(String, String)] = Nil 148 | var producers_data: List[(Long,Long)] = Nil 149 | var consumers_data: List[(Long,Long)] = Nil 150 | var request_p90: List[(Long,Long)] = Nil 151 | var request_p99: List[(Long,Long)] = Nil 152 | var request_p999: List[(Long,Long)] = Nil 153 | var error_data: List[(Long,Long)] = Nil 154 | 155 | def to_json(level: Int = 0): String = { 156 | var sb = new StringBuilder() 157 | 158 | val indent = " " * level 159 | 160 | sb ++= indent + "{\n" 161 | sb ++= indent + " \"name\": \"" + name + "\",\n" 162 | sb ++= indent + " \"settings\": {\n" 163 | sb ++= settings map { setting => 164 | indent + " \"" + setting._1 + "\": \"" + setting._2 + "\"" 165 | } mkString(",\n") 166 | sb ++= "\n" 167 | sb ++= indent + " },\n" 168 | sb ++= indent + " \"data\": {\n" 169 | sb ++= indent + " \"producers\": [ " + producers_data.map(x=> "[%d,%d]".format(x._1,x._2)).mkString(",") + " ],\n" 170 | sb ++= indent + " \"consumers\": [ " + consumers_data.map(x=> "[%d,%d]".format(x._1,x._2)).mkString(",") + " ],\n" 171 | 172 | if( request_p90 != Nil ) { 173 | sb ++= indent + " \"request_p90\": [ " + request_p90.map(x=> "[%d,%d]".format(x._1,x._2)).mkString(",") + " ],\n" 174 | } 175 | if( request_p99 != Nil ) { 176 | sb ++= indent + " \"request_p99\": [ " + request_p99.map(x=> "[%d,%d]".format(x._1,x._2)).mkString(",") + " ],\n" 177 | } 178 | if( request_p999 != Nil ) { 179 | sb ++= indent + " \"request_p999\": [ " + request_p999.map(x=> "[%d,%d]".format(x._1,x._2)).mkString(",") + " ],\n" 180 | } 181 | 182 | sb ++= indent + " \"error\": [ " + error_data.map(x=> "[%d,%d]".format(x._1,x._2)).mkString(",") + " ]\n" 183 | sb ++= indent + " }\n" 184 | sb ++= indent + "}" 185 | 186 | sb.toString 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/main/scala/com/github/mqtt/benchmark/FlexibleProperty.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2009-2011 the original author or authors. 3 | * See the notice.md file distributed with this work for additional 4 | * information regarding copyright ownership. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package com.github.mqtt.benchmark 19 | 20 | object FlexibleProperty { 21 | 22 | private var properties: List[FlexibleProperty[_]] = Nil 23 | 24 | def apply[T](default: Option[T] = None): FlexibleProperty[T] = { 25 | val p = new FlexibleProperty[T]() 26 | p.default_value = default 27 | properties = p :: properties 28 | return p 29 | } 30 | 31 | def apply[T](default: Option[T], high_priority: () => Option[T]): FlexibleProperty[T] = { 32 | val p = new FlexibleProperty[T]() 33 | p.default_value = default 34 | p.high_priority_function = high_priority 35 | properties = p :: properties 36 | return p 37 | } 38 | 39 | def init_all() { 40 | properties.foreach( _.init() ) 41 | } 42 | 43 | } 44 | 45 | class FlexibleProperty[T]() { 46 | 47 | private var default_value: Option[T] = None 48 | private var high_priority_value: Option[T] = None 49 | private var values: List[Option[T]] = Nil 50 | private var high_priority_function: () => Option[T] = () => None 51 | 52 | def init() { 53 | high_priority_value = high_priority_function() 54 | } 55 | 56 | def set_high_priority(high_priority: T) { 57 | high_priority_value = Some(high_priority) 58 | } 59 | 60 | def clear_high_priority() { 61 | high_priority_value = None 62 | } 63 | 64 | def set_default(default: T) { 65 | default_value = Some(default) 66 | } 67 | 68 | def clear_default() { 69 | default_value = None 70 | } 71 | 72 | def push(value: Option[T]) { 73 | values = value :: values 74 | } 75 | 76 | def pop(): Option[T] = { 77 | val h = values.head 78 | values = values.tail 79 | h 80 | } 81 | 82 | def getOption(): Option[T] = { 83 | if (high_priority_value.isDefined) { 84 | high_priority_value 85 | } else { 86 | val filtered_values = values.filter(_.isDefined) 87 | if (! filtered_values.isEmpty){ 88 | filtered_values.head 89 | } else { 90 | default_value 91 | } 92 | } 93 | } 94 | 95 | def get(): T = { 96 | getOption.get 97 | } 98 | 99 | def getOrElse(default: T): T = { 100 | getOption.getOrElse(default) 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/main/scala/com/github/mqtt/benchmark/NonBlockingScenario.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2009-2011 the original author or authors. 3 | * See the notice.md file distributed with this work for additional 4 | * information regarding copyright ownership. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package com.github.mqtt.benchmark 19 | 20 | import org.fusesource.hawtdispatch._ 21 | import java.util.concurrent.{CountDownLatch, TimeUnit} 22 | import java.lang.Throwable 23 | import org.fusesource.hawtbuf.Buffer._ 24 | import org.fusesource.mqtt.client._ 25 | import scala.collection.mutable.HashMap 26 | import java.net.URI 27 | import org.fusesource.hawtbuf.{Buffer, UTF8Buffer, AsciiBuffer} 28 | 29 | //object NonBlockingScenario { 30 | // def main(args:Array[String]):Unit = { 31 | // val scenario = new com.github.mqtt.benchmark.NonBlockingScenario 32 | // scenario.display_errors = true 33 | // 34 | //// scenario.protocol = "tls" 35 | //// scenario.key_store_file = Some("/Users/chirino/sandbox/mqtt-benchmark/keystore") 36 | //// scenario.key_store_password = Some("password") 37 | //// scenario.key_password = Some("password") 38 | // 39 | // scenario.user = Some("admin") 40 | // scenario.password = Some("password") 41 | // scenario.port = 61613 42 | // scenario.message_size = 20 43 | // 44 | // scenario.consumer_clean = false 45 | // scenario.consumer_qos = 1 46 | // scenario.clear_subscriptions_when_finished = false 47 | // scenario.producer_clean = false 48 | // scenario.producer_qos = 0 49 | // 50 | //// scenario.consumers = 0; scenario.producers = 1 51 | // scenario.consumers = 1; scenario.producers = 0 52 | // 53 | // scenario.run 54 | // } 55 | //} 56 | 57 | /** 58 | *

59 | * Simulates load on the a mqtt broker using non blocking io. 60 | *

61 | * 62 | * @author Hiram Chirino 63 | */ 64 | class NonBlockingScenario extends Scenario { 65 | 66 | def createProducer(i:Int) = { 67 | if(this.request_response) { 68 | new RequestingClient((i)) 69 | } else { 70 | new ProducerClient(i) 71 | } 72 | } 73 | def createConsumer(i:Int) = { 74 | if(this.request_response) { 75 | new RespondingClient(i) 76 | } else { 77 | new ConsumerClient(i) 78 | } 79 | } 80 | 81 | trait NonBlockingClient extends Client { 82 | 83 | protected var queue = createQueue(client_id) 84 | 85 | var message_counter=0L 86 | var reconnect_delay = 0L 87 | 88 | def client_id:String = null 89 | def clean = true 90 | 91 | sealed trait State 92 | 93 | case class INIT() extends State 94 | 95 | case class CONNECTING(host: String, port: Int, on_complete: ()=>Unit) extends State { 96 | 97 | def connect() = { 98 | val mqtt = new MQTT() 99 | mqtt.setDispatchQueue(queue) 100 | mqtt.setSslContext(ssl_context) 101 | mqtt.setHost(new URI(protocol+"://" + host + ":" + port)) 102 | mqtt.setClientId(client_id) 103 | mqtt.setCleanSession(clean) 104 | mqtt.setReconnectAttemptsMax(0) 105 | mqtt.setConnectAttemptsMax(0) 106 | 107 | user.foreach(mqtt.setUserName(_)) 108 | password.foreach(mqtt.setPassword(_)) 109 | val connection = mqtt.callbackConnection(); 110 | connection.connect(new Callback[Void](){ 111 | def onSuccess(na: Void) { 112 | state match { 113 | case x:CONNECTING => 114 | state = CONNECTED(connection) 115 | on_complete() 116 | case _ => 117 | connection.disconnect(null) 118 | } 119 | } 120 | def onFailure(value: Throwable) { 121 | on_failure(value) 122 | } 123 | }) 124 | } 125 | 126 | // We may need to delay the connection attempt. 127 | if( reconnect_delay==0 ) { 128 | connect 129 | } else { 130 | queue.after(5, TimeUnit.SECONDS) { 131 | if ( this == state ) { 132 | reconnect_delay=0 133 | connect 134 | } 135 | } 136 | } 137 | 138 | def close() = { 139 | state = DISCONNECTED() 140 | } 141 | 142 | def on_failure(e:Throwable) = { 143 | if( display_errors ) { 144 | e.printStackTrace 145 | } 146 | error_counter.incrementAndGet 147 | reconnect_delay = 1000 148 | close 149 | } 150 | 151 | } 152 | 153 | case class CONNECTED(val connection:CallbackConnection) extends State { 154 | 155 | connection.listener(new Listener { 156 | def onConnected() {} 157 | def onDisconnected() {} 158 | def onPublish(topic: UTF8Buffer, body: Buffer, ack: Runnable) { 159 | on_receive(topic, body, ack) 160 | } 161 | 162 | def onFailure(value: Throwable) { 163 | on_failure(value) 164 | } 165 | }) 166 | 167 | def close() = { 168 | state = CLOSING() 169 | connection.disconnect(new Callback[Void] { 170 | def onSuccess(value: Void) { 171 | state = DISCONNECTED() 172 | } 173 | def onFailure(value: Throwable) = onSuccess(null) 174 | }) 175 | } 176 | 177 | def on_failure(e:Throwable) = { 178 | if( display_errors ) { 179 | e.printStackTrace 180 | } 181 | error_counter.incrementAndGet 182 | reconnect_delay = 1000 183 | close 184 | } 185 | 186 | } 187 | case class CLOSING() extends State 188 | 189 | case class DISCONNECTED() extends State { 190 | queue { 191 | if( state==this ){ 192 | if( done.get ) { 193 | has_shutdown.countDown 194 | } else { 195 | reconnect_action 196 | } 197 | } 198 | } 199 | } 200 | 201 | var state:State = INIT() 202 | 203 | val has_shutdown = new CountDownLatch(1) 204 | def reconnect_action:Unit 205 | 206 | def on_failure(e:Throwable) = state match { 207 | case x:CONNECTING => x.on_failure(e) 208 | case x:CONNECTED => x.on_failure(e) 209 | case _ => 210 | } 211 | 212 | def start = queue { 213 | state = DISCONNECTED() 214 | } 215 | 216 | def queue_check = queue.assertExecuting() 217 | 218 | def open(host: String, port: Int)(on_complete: =>Unit) = { 219 | assert ( state.isInstanceOf[DISCONNECTED] ) 220 | queue_check 221 | state = CONNECTING(host, port, ()=>on_complete) 222 | } 223 | 224 | def close() = { 225 | queue_check 226 | state match { 227 | case x:CONNECTING => x.close 228 | case x:CONNECTED => x.close 229 | case _ => 230 | } 231 | } 232 | 233 | def shutdown = { 234 | assert(done.get) 235 | queue { 236 | close 237 | } 238 | has_shutdown.await() 239 | } 240 | 241 | def receive_suspend = { 242 | queue_check 243 | state match { 244 | case state:CONNECTED => state.connection.suspend() 245 | case _ => 246 | } 247 | } 248 | 249 | def receive_resume = { 250 | queue_check 251 | state match { 252 | case state:CONNECTED => state.connection.resume() 253 | case _ => 254 | } 255 | } 256 | 257 | def connection = { 258 | queue_check 259 | state match { 260 | case state:CONNECTED => Some(state.connection) 261 | case _ => None 262 | } 263 | } 264 | 265 | def on_receive(topic: UTF8Buffer, body: Buffer, ack: Runnable) = { 266 | ack.run() 267 | } 268 | 269 | def connect(proc: =>Unit) = { 270 | queue_check 271 | if( !done.get ) { 272 | open(host, port) { 273 | proc 274 | } 275 | } 276 | } 277 | 278 | } 279 | 280 | class ProducerClient(val id: Int) extends NonBlockingClient { 281 | 282 | override def client_id = "producer-"+id 283 | override def clean = producer_clean 284 | 285 | val message_cache = HashMap.empty[Int, AsciiBuffer] 286 | 287 | override def reconnect_action = { 288 | connect { 289 | write_action 290 | } 291 | } 292 | 293 | def write_action:Unit = { 294 | def retry:Unit = { 295 | if(done.get) { 296 | close 297 | } else { 298 | if(producer_sleep >= 0) { 299 | connection.foreach{ connection=> 300 | connection.publish(utf8(destination(id)), get_message(), QoS.values()(producer_qos), message_retain, new Callback[Void](){ 301 | def onSuccess(value: Void) = { 302 | producer_counter.incrementAndGet() 303 | message_counter += 1 304 | write_completed_action 305 | } 306 | def onFailure(value: Throwable) = { 307 | on_failure(value) 308 | } 309 | }) 310 | } 311 | } else { 312 | write_completed_action 313 | } 314 | } 315 | } 316 | retry 317 | } 318 | 319 | def write_completed_action:Unit = { 320 | def doit = { 321 | val m_p_connection = messages_per_connection.toLong 322 | if(m_p_connection > 0 && message_counter >= m_p_connection) { 323 | message_counter = 0 324 | close 325 | } else { 326 | write_action 327 | } 328 | } 329 | 330 | if(done.get) { 331 | close 332 | } else { 333 | if(producer_sleep != 0) { 334 | queue.after(math.abs(producer_sleep), TimeUnit.MILLISECONDS) { 335 | doit 336 | } 337 | } else { 338 | queue { doit } 339 | } 340 | } 341 | } 342 | 343 | def get_message() = { 344 | val m_s = message_size 345 | 346 | if(! message_cache.contains(m_s)) { 347 | message_cache(m_s) = message(client_id, m_s) 348 | } 349 | 350 | message_cache(m_s) 351 | } 352 | 353 | def message(name:String, size:Int) = { 354 | val buffer = new StringBuffer(size) 355 | buffer.append("Message from " + name + "\n") 356 | for( i <- buffer.length to size ) { 357 | buffer.append(('a'+(i%26)).toChar) 358 | } 359 | var rc = buffer.toString 360 | if( rc.length > size ) { 361 | rc.substring(0, size) 362 | } else { 363 | rc 364 | } 365 | ascii(rc) 366 | } 367 | 368 | } 369 | 370 | class ConsumerClient(val id: Int) extends NonBlockingClient { 371 | 372 | override def client_id = "consumer-"+id 373 | 374 | override def clean = consumer_clean 375 | 376 | override def reconnect_action = { 377 | connect { 378 | connection.foreach { connection => 379 | connection.subscribe(Array(new Topic(destination(id), QoS.values()(consumer_qos))), null) 380 | } 381 | } 382 | } 383 | 384 | def index_of(haystack:Array[Byte], needle:Array[Byte]):Int = { 385 | var i = 0 386 | while( haystack.length >= i+needle.length ) { 387 | if( haystack.startsWith(needle, i) ) { 388 | return i 389 | } 390 | i += 1 391 | } 392 | return -1 393 | } 394 | 395 | 396 | override def on_receive(topic: UTF8Buffer, body: Buffer, ack: Runnable) = { 397 | if( consumer_sleep != 0 && ((consumer_counter.get()%consumer_sleep_modulo) == 0)) { 398 | if( consumer_qos==0 ) { 399 | receive_suspend 400 | } 401 | queue.after(math.abs(consumer_sleep), TimeUnit.MILLISECONDS) { 402 | if( consumer_qos==0 ) { 403 | receive_resume 404 | } 405 | process_message(body, ack) 406 | } 407 | } else { 408 | process_message(body, ack) 409 | } 410 | } 411 | 412 | def process_message(body: Buffer, ack: Runnable):Unit = { 413 | ack.run() 414 | consumer_counter.incrementAndGet() 415 | } 416 | 417 | } 418 | 419 | class RequestingClient(id: Int) extends ProducerClient(id) { 420 | override def client_id = "requestor-"+id 421 | 422 | override def reconnect_action = { 423 | connect { 424 | connection.foreach { connection => 425 | connection.subscribe(Array(new Topic(response_destination(id), QoS.values()(consumer_qos))), null) 426 | } 427 | // give the response queue a chance to drain before doing new requests. 428 | queue.after(1000, TimeUnit.MILLISECONDS) { 429 | write_action 430 | } 431 | } 432 | } 433 | 434 | var request_start = 0L 435 | 436 | override def write_action:Unit = { 437 | def retry:Unit = { 438 | if(done.get) { 439 | close 440 | } else { 441 | if(producer_sleep >= 0) { 442 | connection.foreach{ connection=> 443 | var msg = get_message().deepCopy() // we have to copy since we are modifyiing.. 444 | msg.buffer.moveHead(msg.length-4).bigEndianEditor().writeInt(id) // write the last 4 bytes.. 445 | 446 | request_start = System.nanoTime() 447 | connection.publish(utf8(destination(id)), msg, QoS.values()(producer_qos), message_retain, new Callback[Void](){ 448 | def onSuccess(value: Void) = { 449 | // don't do anything.. we complete when 450 | // on_receive gets called. 451 | } 452 | def onFailure(value: Throwable) = { 453 | on_failure(value) 454 | } 455 | }) 456 | } 457 | } else { 458 | write_completed_action 459 | } 460 | } 461 | } 462 | retry 463 | } 464 | 465 | override def on_receive(topic: UTF8Buffer, body: Buffer, ack: Runnable) = { 466 | if(request_start != 0L) { 467 | request_times.add(System.nanoTime() - request_start) 468 | request_start = 0 469 | producer_counter.incrementAndGet() 470 | message_counter += 1 471 | write_completed_action 472 | } 473 | ack.run(); 474 | } 475 | 476 | } 477 | 478 | class RespondingClient(id: Int) extends ConsumerClient(id) { 479 | 480 | override def client_id = "responder-"+id 481 | 482 | val EMPTY = new Buffer(0); 483 | 484 | override def process_message(body: Buffer, ack: Runnable) = { 485 | connection.foreach{ connection=> 486 | body.moveHead(body.length-4) //lets read the last 4 bytes 487 | val rid = body.bigEndianEditor().readInt() 488 | connection.publish(utf8(response_destination(rid)), EMPTY, QoS.values()(producer_qos), message_retain, new Callback[Void](){ 489 | def onSuccess(value: Void) = { 490 | ack.run() 491 | consumer_counter.incrementAndGet() 492 | } 493 | def onFailure(value: Throwable) = { 494 | on_failure(value) 495 | } 496 | }) 497 | } 498 | } 499 | } 500 | } 501 | -------------------------------------------------------------------------------- /src/main/scala/com/github/mqtt/benchmark/Scenario.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2009-2011 the original author or authors. 3 | * See the notice.md file distributed with this work for additional 4 | * information regarding copyright ownership. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package com.github.mqtt.benchmark 19 | 20 | import java.util.concurrent.atomic._ 21 | import java.util.concurrent.TimeUnit._ 22 | import scala.collection.mutable.ListBuffer 23 | import java.util.concurrent.ConcurrentLinkedQueue 24 | import java.security.KeyStore 25 | import java.io.FileInputStream 26 | import javax.net.ssl._ 27 | import org.fusesource.hawtdispatch.transport.{SslTransport, TcpTransport} 28 | 29 | object Scenario { 30 | val MESSAGE_ID:Array[Byte] = "message-id" 31 | val NEWLINE = '\n'.toByte 32 | val NANOS_PER_SECOND = NANOSECONDS.convert(1, SECONDS) 33 | val NANOS_PER_MS = NANOSECONDS.convert(1, MILLISECONDS) 34 | 35 | implicit def toBytes(value: String):Array[Byte] = value.getBytes("UTF-8") 36 | 37 | def o[T](value:T):Option[T] = value match { 38 | case null => None 39 | case x => Some(x) 40 | } 41 | 42 | def percentiles(percentiles:Array[Double], values:Array[Long]) = { 43 | if (values.length > 0) { 44 | java.util.Arrays.sort(values) 45 | percentiles.map { p => 46 | val pos = p * (values.length + 1); 47 | if (pos < 1) { 48 | values(0) 49 | } else if (pos >= values.length) { 50 | values(values.length - 1); 51 | } else { 52 | val lower = values((pos - 1).toInt); 53 | val upper = values(pos.toInt); 54 | lower + ((pos - pos.floor) * (upper - lower)).toLong; 55 | } 56 | } 57 | } else { 58 | percentiles.map(p => -1L) 59 | } 60 | } 61 | } 62 | 63 | trait Scenario { 64 | import Scenario._ 65 | 66 | var user: Option[String] = None 67 | var password: Option[String] = None 68 | var request_response = false 69 | 70 | private var _producer_sleep: { def apply(): Int; def init(time: Long) } = new { def apply() = 0; def init(time: Long) {} } 71 | def producer_sleep = _producer_sleep() 72 | def producer_sleep_= (new_value: Int) = _producer_sleep = new { def apply() = new_value; def init(time: Long) {} } 73 | def producer_sleep_= (new_func: { def apply(): Int; def init(time: Long) }) = _producer_sleep = new_func 74 | 75 | private var _consumer_sleep: { def apply(): Int; def init(time: Long) } = new { def apply() = 0; def init(time: Long) {} } 76 | def consumer_sleep = _consumer_sleep() 77 | def consumer_sleep_= (new_value: Int) = _consumer_sleep = new { def apply() = new_value; def init(time: Long) {} } 78 | def consumer_sleep_= (new_func: { def apply(): Int; def init(time: Long) }) = _consumer_sleep = new_func 79 | 80 | var producers = 1 81 | var producers_per_sample = 0 82 | 83 | var consumers = 1 84 | var consumers_per_sample = 0 85 | var sample_interval = 1000 86 | var protocol = "tcp" 87 | var host = "127.0.0.1" 88 | var port = 61613 89 | var buffer_size = 32*1024 90 | 91 | var consumer_clean = true 92 | var consumer_qos = 0 93 | var clear_subscriptions_when_finished = true 94 | 95 | var producer_clean = true 96 | var producer_qos = 0 97 | 98 | var message_retain = false 99 | private var _message_size: { def apply(): Int; def init(time: Long) } = new { def apply() = 1024; def init(time: Long) {} } 100 | def message_size = _message_size() 101 | def message_size_= (new_value: Int) = _message_size = new { def apply() = new_value; def init(time: Long) {} } 102 | def message_size_= (new_func: { def apply(): Int; def init(time: Long) }) = _message_size = new_func 103 | 104 | var consumer_sleep_modulo = 1 105 | var producer_sleep_modulo = 1 106 | private var _messages_per_connection: { def apply(): Int; def init(time: Long) } = new { def apply() = -1; def init(time: Long) {} } 107 | def messages_per_connection = _messages_per_connection() 108 | def messages_per_connection_= (new_value: Int) = _messages_per_connection = new { def apply() = new_value; def init(time: Long) {} } 109 | def messages_per_connection_= (new_func: { def apply(): Int; def init(time: Long) }) = _messages_per_connection = new_func 110 | 111 | var display_errors = false 112 | 113 | var destination_prefix = "" 114 | 115 | private var _destination_name: () => String = () => "" 116 | def destination_name = _destination_name() 117 | def destination_name_=(new_name: String) = _destination_name = () => new_name 118 | def destination_name_=(new_func: () => String) = _destination_name = new_func 119 | 120 | private var _response_destination_name: () => String = () => "response" 121 | def response_destination_name = _response_destination_name() 122 | def response_destination_name_=(new_name: String) = _response_destination_name = () => new_name 123 | def response_destination_name_=(new_func: () => String) = _response_destination_name = new_func 124 | 125 | var destination_count = 1 126 | 127 | val producer_counter = new AtomicLong() 128 | val consumer_counter = new AtomicLong() 129 | val request_times = new ConcurrentLinkedQueue[Long]() 130 | 131 | val error_counter = new AtomicLong() 132 | val done = new AtomicBoolean() 133 | 134 | var name = "custom" 135 | 136 | var drain_timeout = 2000L 137 | 138 | var key_store_file:Option[String] = None 139 | var key_store_password:Option[String] = None 140 | var key_password:Option[String] = None 141 | 142 | var key_store:KeyStore = _ 143 | var trust_managers:Array[TrustManager] = _ 144 | var key_managers:Array[KeyManager] = _ 145 | 146 | def ssl_context:SSLContext = { 147 | Option(SslTransport.protocol(protocol)).map { protocol => 148 | val rc = SSLContext.getInstance(protocol) 149 | rc.init(get_key_managers, get_trust_managers, null) 150 | rc 151 | }.getOrElse(null) 152 | } 153 | 154 | def get_key_store = { 155 | if( key_store==null && key_store_file.isDefined ) { 156 | key_store = { 157 | val store = KeyStore.getInstance("JKS") 158 | store.load(new FileInputStream(key_store_file.get), key_store_password.getOrElse("").toCharArray()) 159 | store 160 | } 161 | } 162 | key_store 163 | } 164 | 165 | def get_trust_managers = { 166 | val store = get_key_store 167 | if( trust_managers==null && store!=null ) { 168 | val factory = TrustManagerFactory.getInstance("SunX509") 169 | factory.init(store) 170 | trust_managers = factory.getTrustManagers 171 | } 172 | trust_managers 173 | } 174 | 175 | def get_key_managers = { 176 | val store = get_key_store 177 | if( key_managers==null && store!=null) { 178 | val factory = KeyManagerFactory.getInstance("SunX509") 179 | factory.init(store, key_password.getOrElse("").toCharArray()) 180 | key_managers = factory.getKeyManagers 181 | } 182 | key_managers 183 | } 184 | 185 | 186 | def run() = { 187 | print(toString) 188 | println("--------------------------------------") 189 | println(" Running: Press ENTER to stop") 190 | println("--------------------------------------") 191 | println("") 192 | 193 | with_load { 194 | 195 | // start a sampling client... 196 | val sample_thread = new Thread() { 197 | override def run() = { 198 | 199 | def print_rate(name: String, periodCount:Long, totalCount:Long, nanos: Long) = { 200 | val rate_per_second: java.lang.Float = ((1.0f * periodCount / nanos) * NANOS_PER_SECOND) 201 | println("%s total: %,d, rate: %,.3f per second".format(name, totalCount, rate_per_second)) 202 | } 203 | def print_percentil(name: String, value:Long, max:Long) = { 204 | println("%sth percentile response time: %,.3f ms, max: %,.3f ms".format(name, (1.0f * value / NANOS_PER_MS), (1.0f * max / NANOS_PER_MS))) 205 | } 206 | 207 | try { 208 | var start = System.nanoTime 209 | var total_producer_count = 0L 210 | var total_consumer_count = 0L 211 | var total_error_count = 0L 212 | var max_p90 = 0L 213 | var max_p99 = 0L 214 | var max_p999 = 0L 215 | 216 | collection_start 217 | while( !done.get ) { 218 | Thread.sleep(sample_interval) 219 | val end = System.nanoTime 220 | collection_sample 221 | val samples = collection_end 222 | samples.get("p_custom").foreach { case (_, count)::Nil => 223 | total_producer_count += count 224 | print_rate("Producer", count, total_producer_count, end - start) 225 | case _ => 226 | } 227 | samples.get("c_custom").foreach { case (_, count)::Nil => 228 | total_consumer_count += count 229 | print_rate("Consumer", count, total_consumer_count, end - start) 230 | case _ => 231 | } 232 | samples.get("p90_custom").foreach { case (_, value)::Nil => 233 | max_p90 = max_p90.max(value) 234 | print_percentil("90", value, max_p90) 235 | case _ => 236 | } 237 | samples.get("p99_custom").foreach { case (_, value)::Nil => 238 | max_p99 = max_p99.max(value) 239 | print_percentil("99", value, max_p99) 240 | case _ => 241 | } 242 | samples.get("p999_custom").foreach { case (_, value)::Nil => 243 | max_p999 = max_p999.max(value) 244 | print_percentil("99.9", value, max_p999) 245 | case _ => 246 | } 247 | samples.get("e_custom").foreach { case (_, count)::Nil => 248 | if( count!= 0 ) { 249 | total_error_count += count 250 | print_rate("Error", count, total_error_count, end - start) 251 | } 252 | case _ => 253 | } 254 | start = end 255 | } 256 | } catch { 257 | case e:InterruptedException => 258 | } 259 | } 260 | } 261 | sample_thread.start() 262 | 263 | System.in.read() 264 | done.set(true) 265 | 266 | sample_thread.interrupt 267 | sample_thread.join 268 | } 269 | 270 | } 271 | 272 | override def toString() = { 273 | "--------------------------------------\n"+ 274 | "Scenario Settings\n"+ 275 | "--------------------------------------\n"+ 276 | " host = "+host+"\n"+ 277 | " port = "+port+"\n"+ 278 | " destination_count = "+destination_count+"\n" + 279 | " destination_prefix = "+destination_prefix+"\n"+ 280 | " destination_name = "+destination_name+"\n" + 281 | " sample_interval (ms) = "+sample_interval+"\n" + 282 | " \n"+ 283 | " --- Producer Properties ---\n"+ 284 | " producers = "+producers+"\n"+ 285 | " message_size = "+message_size+"\n"+ 286 | " producer_sleep (ms) = "+producer_sleep+"\n"+ 287 | " producer_qos = "+producer_qos+"\n"+ 288 | " producer_retain = "+message_retain+"\n"+ 289 | " \n"+ 290 | " --- Consumer Properties ---\n"+ 291 | " consumers = "+consumers+"\n"+ 292 | " consumer_sleep (ms) = "+consumer_sleep+"\n"+ 293 | " consumer_qos = "+consumer_qos+"\n"+ 294 | " clean_session = "+producer_clean+"\n"+ 295 | "" 296 | 297 | } 298 | 299 | def settings(): List[(String, String)] = { 300 | var s: List[(String, String)] = Nil 301 | 302 | s :+= ("host", host) 303 | s :+= ("port", port.toString) 304 | s :+= ("destination_prefix", destination_prefix) 305 | s :+= ("destination_count", destination_count.toString) 306 | s :+= ("destination_name", destination_name) 307 | s :+= ("sample_interval", sample_interval.toString) 308 | s :+= ("producers", producers.toString) 309 | s :+= ("message_size", message_size.toString) 310 | s :+= ("producer_sleep", producer_sleep.toString) 311 | s :+= ("consumers", consumers.toString) 312 | s :+= ("consumer_sleep", consumer_sleep.toString) 313 | s :+= ("consumer_qos", consumer_qos.toString) 314 | s :+= ("producer_qos", producer_qos.toString) 315 | s :+= ("producer_retain", message_retain.toString) 316 | s :+= ("durable", producer_clean.toString) 317 | 318 | s 319 | } 320 | 321 | protected def destination(i:Int) = destination_prefix+destination_name+"-"+(i%destination_count) 322 | 323 | protected def response_destination(i:Int) = destination_prefix+response_destination_name+"-"+i 324 | 325 | var producer_samples:Option[ListBuffer[(Long,Long)]] = None 326 | var consumer_samples:Option[ListBuffer[(Long,Long)]] = None 327 | var error_samples = ListBuffer[(Long,Long)]() 328 | 329 | var request_time_p90_samples = ListBuffer[(Long,Long)]() 330 | var request_time_p99_samples = ListBuffer[(Long,Long)]() 331 | var request_time_p999_samples = ListBuffer[(Long,Long)]() 332 | 333 | def collection_start: Unit = { 334 | producer_counter.set(0) 335 | consumer_counter.set(0) 336 | error_counter.set(0) 337 | request_times.clear() 338 | 339 | producer_samples = if (producers > 0 || producers_per_sample>0 ) { 340 | Some(ListBuffer[(Long,Long)]()) 341 | } else { 342 | None 343 | } 344 | consumer_samples = if (consumers > 0 || consumers_per_sample>0 ) { 345 | Some(ListBuffer[(Long,Long)]()) 346 | } else { 347 | None 348 | } 349 | request_time_p90_samples = ListBuffer[(Long,Long)]() 350 | request_time_p99_samples = ListBuffer[(Long,Long)]() 351 | request_time_p999_samples = ListBuffer[(Long,Long)]() 352 | } 353 | 354 | def collection_end: Map[String, scala.List[(Long,Long)]] = { 355 | var rc = Map[String, List[(Long,Long)]]() 356 | producer_samples.foreach{ samples => 357 | rc += "p_"+name -> samples.toList 358 | samples.clear 359 | } 360 | consumer_samples.foreach{ samples => 361 | rc += "c_"+name -> samples.toList 362 | samples.clear 363 | } 364 | rc += "e_"+name -> error_samples.toList 365 | error_samples.clear 366 | if(request_response) { 367 | rc += "p90_"+name -> request_time_p90_samples.toList 368 | request_time_p90_samples.clear 369 | rc += "p99_"+name -> request_time_p99_samples.toList 370 | request_time_p99_samples.clear 371 | rc += "p999_"+name -> request_time_p999_samples.toList 372 | request_time_p999_samples.clear 373 | } 374 | 375 | rc 376 | } 377 | 378 | trait Client { 379 | def start():Unit 380 | def shutdown():Unit 381 | } 382 | 383 | var producer_clients = List[Client]() 384 | var consumer_clients = List[Client]() 385 | 386 | def with_load[T](func: =>T ):T = { 387 | done.set(false) 388 | 389 | val now = System.currentTimeMillis() 390 | _producer_sleep.init(now) 391 | _consumer_sleep.init(now) 392 | _message_size.init(now) 393 | _messages_per_connection.init(now) 394 | 395 | for (i <- 0 until producers) { 396 | val client = createProducer(i) 397 | producer_clients ::= client 398 | client.start() 399 | } 400 | 401 | for (i <- 0 until consumers) { 402 | val client = createConsumer(i) 403 | consumer_clients ::= client 404 | client.start() 405 | } 406 | 407 | try { 408 | func 409 | } finally { 410 | done.set(true) 411 | // wait for the threads to finish.. 412 | for( client <- consumer_clients ) { 413 | client.shutdown 414 | } 415 | consumer_clients = List() 416 | for( client <- producer_clients ) { 417 | client.shutdown 418 | } 419 | producer_clients = List() 420 | } 421 | } 422 | 423 | def drain = { 424 | done.set(false) 425 | if( clear_subscriptions_when_finished && !consumer_clean ) { 426 | consumer_clean=true 427 | print("clearing subscriptions") 428 | consumer_counter.set(0) 429 | var consumer_clients = List[Client]() 430 | for (i <- 0 until destination_count.max(consumers)) { 431 | val client = createConsumer(i) 432 | consumer_clients ::= client 433 | client.start() 434 | } 435 | 436 | // Keep sleeping until we stop draining messages. 437 | var drained = 0L 438 | try { 439 | Thread.sleep(drain_timeout); 440 | def done() = { 441 | val c = consumer_counter.getAndSet(0) 442 | drained += c 443 | c == 0 444 | } 445 | while( !done ) { 446 | print(".") 447 | Thread.sleep(drain_timeout); 448 | } 449 | } finally { 450 | done.set(true) 451 | for( client <- consumer_clients ) { 452 | client.shutdown 453 | } 454 | println(". (drained %d)".format(drained)) 455 | } 456 | consumer_clean=false 457 | } 458 | } 459 | 460 | 461 | def collection_sample: Unit = { 462 | 463 | val now = System.currentTimeMillis() 464 | producer_samples.foreach(_.append((now, producer_counter.getAndSet(0)))) 465 | consumer_samples.foreach(_.append((now, consumer_counter.getAndSet(0)))) 466 | 467 | if( request_response ) { 468 | 469 | var count = producer_samples.get.last._2.toInt 470 | val times = new Array[Long](count) 471 | while(count > 0 ) { 472 | count -= 1 473 | times(count) = request_times.poll() 474 | } 475 | val p = percentiles(Array(0.9, 0.99, 0.999), times) 476 | request_time_p90_samples.append((now, p(0))) 477 | request_time_p99_samples.append((now, p(1))) 478 | request_time_p999_samples.append((now, p(2))) 479 | } 480 | 481 | error_samples.append((now, error_counter.getAndSet(0))) 482 | 483 | // we might need to increment number the producers.. 484 | for (i <- 0 until producers_per_sample) { 485 | val client = createProducer(producer_clients.length) 486 | producer_clients ::= client 487 | client.start() 488 | } 489 | 490 | // we might need to increment number the consumers.. 491 | for (i <- 0 until consumers_per_sample) { 492 | val client = createConsumer(consumer_clients.length) 493 | consumer_clients ::= client 494 | client.start() 495 | } 496 | 497 | } 498 | 499 | def createProducer(i:Int):Client 500 | def createConsumer(i:Int):Client 501 | 502 | protected def ignore_failure(func: =>Unit):Unit = try { 503 | func 504 | } catch { case _ => 505 | } 506 | 507 | } 508 | 509 | 510 | --------------------------------------------------------------------------------