├── README.txt ├── pom.xml └── src └── main ├── java └── com │ └── mycompany │ └── app │ ├── PerformanceTest.java │ ├── model │ └── TaxRate.java │ ├── servlets │ ├── DatabaseServlet.java │ └── PlainJavaServlet.java │ └── util │ ├── MyProperties.java │ └── UpdatePlot.java └── resources ├── application.properties ├── database.sql ├── plot_template.html └── simplelogger.properties /README.txt: -------------------------------------------------------------------------------- 1 | The servers/AMIs need: 2 | 3 | * Java installed (11+) 4 | * An SSH key on your machine, added to your ssh-agent, whose username matches with the SSH username in PerformanceTest#main() 5 | * The sar and mpstat utilities installed -> 6 | * Ubuntu: sudo apt install sysstat 7 | * For Flamegraphs to work -> 8 | * JDK Debug symbols: https://github.com/jvm-profiling-tools/async-profiler#installing-debug-symbols 9 | * Allow capturing Kernel call stack events: https://github.com/jvm-profiling-tools/async-profiler#basic-usage 10 | 11 | If you want to use a database for testing: 12 | 13 | * Create an RDS database and add its properties to application.properties 14 | * Execute database.sql against your new RDS database 15 | * Change MODE in PerformanceTest to "DEPLOY_DB" -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | com.mycompany.app 8 | performance-testing 9 | 1.0-SNAPSHOT 10 | 11 | performance-testing 12 | 13 | http://www.example.com 14 | 15 | 16 | UTF-8 17 | 17 18 | 17 19 | 20 | 21 | 22 | 23 | 24 | 25 | commons-codec 26 | commons-codec 27 | 1.15 28 | 29 | 30 | 31 | 32 | org.apache.commons 33 | commons-lang3 34 | 3.12.0 35 | 36 | 37 | 38 | org.mortbay.jetty.orchestrator 39 | jetty-cluster-orchestrator 40 | 1.0.3 41 | 42 | 43 | 44 | org.mortbay.jetty.loadgenerator 45 | jetty-load-generator-listeners 46 | 3.1.1 47 | 48 | 49 | org.mortbay.jetty.loadgenerator 50 | jetty-load-generator-client 51 | 3.1.1 52 | 53 | 54 | 55 | org.hdrhistogram 56 | HdrHistogram 57 | 2.1.12 58 | 59 | 60 | 61 | org.eclipse.jetty.toolchain 62 | jetty-perf-helper 63 | 1.0.7 64 | 65 | 66 | 67 | org.eclipse.jetty 68 | jetty-server 69 | 11.0.8 70 | 71 | 72 | org.eclipse.jetty 73 | jetty-servlet 74 | 11.0.8 75 | 76 | 77 | com.zaxxer 78 | HikariCP 79 | 5.0.1 80 | 81 | 82 | mysql 83 | mysql-connector-java 84 | 8.0.28 85 | 86 | 87 | 88 | org.slf4j 89 | slf4j-simple 90 | 2.0.0-alpha6 91 | 92 | 93 | org.mortbay.jetty.perf 94 | jetty-perf 95 | 1.0.0-SNAPSHOT 96 | 97 | 98 | ch.qos.logback 99 | logback-classic 100 | 101 | 102 | 103 | 104 | 105 | 106 | org.slf4j 107 | slf4j-simple 108 | 2.0.0-alpha6 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/app/PerformanceTest.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.app; 2 | 3 | import com.mycompany.app.servlets.DatabaseServlet; 4 | import com.mycompany.app.servlets.PlainJavaServlet; 5 | import org.HdrHistogram.AbstractHistogram; 6 | import org.HdrHistogram.Histogram; 7 | import org.HdrHistogram.HistogramLogReader; 8 | import org.apache.commons.io.IOUtils; 9 | import org.eclipse.jetty.perf.histogram.loader.ResponseStatusListener; 10 | import org.eclipse.jetty.perf.histogram.loader.ResponseTimeListener; 11 | import org.eclipse.jetty.perf.histogram.server.LatencyRecordingChannelListener; 12 | import org.eclipse.jetty.perf.monitoring.ConfigurableMonitor; 13 | import org.eclipse.jetty.perf.util.ReportUtil; 14 | import org.eclipse.jetty.server.Connector; 15 | import org.eclipse.jetty.server.Server; 16 | import org.eclipse.jetty.server.ServerConnector; 17 | import org.eclipse.jetty.server.handler.DefaultHandler; 18 | import org.eclipse.jetty.server.handler.HandlerList; 19 | import org.eclipse.jetty.servlet.ServletContextHandler; 20 | import org.eclipse.jetty.servlet.ServletHolder; 21 | import org.mortbay.jetty.load.generator.LoadGenerator; 22 | import org.mortbay.jetty.load.generator.Resource; 23 | import org.mortbay.jetty.orchestrator.Cluster; 24 | import org.mortbay.jetty.orchestrator.ClusterTools; 25 | import org.mortbay.jetty.orchestrator.NodeArray; 26 | import org.mortbay.jetty.orchestrator.NodeArrayFuture; 27 | import org.mortbay.jetty.orchestrator.configuration.*; 28 | import org.slf4j.Logger; 29 | import org.slf4j.LoggerFactory; 30 | 31 | import java.io.*; 32 | import java.net.URI; 33 | import java.net.http.HttpClient; 34 | import java.net.http.HttpRequest; 35 | import java.net.http.HttpResponse; 36 | import java.nio.file.Files; 37 | import java.nio.file.Path; 38 | import java.nio.file.Paths; 39 | import java.text.SimpleDateFormat; 40 | import java.time.Duration; 41 | import java.util.*; 42 | import java.util.concurrent.CompletableFuture; 43 | import java.util.concurrent.ConcurrentMap; 44 | import java.util.concurrent.TimeUnit; 45 | import java.util.stream.Collectors; 46 | 47 | /** 48 | * Hello world! 49 | */ 50 | public class PerformanceTest { 51 | 52 | 53 | /** 54 | * RPS 55 | */ 56 | 57 | /** e.g. play with 58 | * 250 (x 4 = 1000) 59 | * 1250 (x 4 = 5000) 60 | * 5000 (x 4 = 20000) 61 | * 7500 (x 4 = 30000) 62 | * 10000(x 4 = 40000) 63 | */ 64 | 65 | private static final int RPS_LOADER = 2500; 66 | public static final int RPS_PROBE = 2; 67 | 68 | 69 | /** 70 | * --------- IPS -------------- 71 | */ 72 | 73 | public static final String SERVER_IP = "3.68.71.212"; 74 | 75 | public static final String PROBE_IP = "3.122.97.183"; 76 | 77 | 78 | public static List LOADER_IPS = List.of("18.185.2.251", "3.125.157.86", "3.67.75.104", "3.71.75.125"); 79 | 80 | 81 | /** 82 | * --------- PARAMS -------------- 83 | */ 84 | private static Mode mode = Mode.MANUAL; 85 | 86 | 87 | 88 | private static final Duration WARMUP_DURATION = Duration.ofSeconds(30); 89 | private static final Duration RUN_DURATION = Duration.ofSeconds(60); 90 | 91 | public static final String BASE_FOLDER = ".\\target\\results"; 92 | 93 | public static final Resource REQUEST_URI = new Resource("/tax-html-freemarker"); 94 | public static final int PORT = 8080; 95 | 96 | 97 | public enum Mode { 98 | DEPLOY_JAVA, DEPLOY_DB, MANUAL 99 | } 100 | 101 | private static Logger LOGGER = LoggerFactory.getLogger(PerformanceTest.class); 102 | 103 | 104 | private static final EnumSet MONITORED_ITEMS = EnumSet.of( 105 | ConfigurableMonitor.Item.CMDLINE_CPU, 106 | ConfigurableMonitor.Item.CMDLINE_MEMORY, 107 | ConfigurableMonitor.Item.CMDLINE_NETWORK 108 | ); 109 | 110 | private static final EnumSet SERVER_ITEMS = copy(MONITORED_ITEMS, 111 | ConfigurableMonitor.Item.ASYNC_PROF_CPU, 112 | ConfigurableMonitor.Item.JHICCUP); 113 | 114 | 115 | public static void main(String[] args) throws IOException { 116 | // set SSH username here 117 | System.setProperty("user.name", "ubuntu"); 118 | 119 | Files.createDirectories(Paths.get(BASE_FOLDER)); 120 | 121 | SimpleNodeArrayConfiguration probeConfig = probeConfig(); 122 | SimpleNodeArrayConfiguration loadGeneratorConfig = loaderConfig(); 123 | SimpleNodeArrayConfiguration serverConfig = serverConfig(); 124 | 125 | SimpleClusterConfiguration cfg = new SimpleClusterConfiguration() 126 | .jvm(new Jvm((fs, h) -> "/usr/bin/java", "-Xmx3g")) 127 | .nodeArray(probeConfig) 128 | .nodeArray(serverConfig) 129 | .nodeArray(loadGeneratorConfig); 130 | 131 | 132 | 133 | int participantCount = cfg.nodeArrays().stream().mapToInt(na -> na.nodes().size()).sum() + 1; // + 1 b/c of the test its. 134 | 135 | try (Cluster cluster = new Cluster(cfg)) { 136 | NodeArray serverArray = cluster.nodeArray("server"); 137 | NodeArray loadersArray = cluster.nodeArray("load-generator"); 138 | NodeArray probeArray = cluster.nodeArray("probe"); 139 | 140 | serverArray.executeOnAll(tools -> startServer(tools.nodeEnvironment())).get(30, TimeUnit.SECONDS); 141 | loadersArray.executeOnAll(tools -> runLoadGenerator(tools.nodeEnvironment())).get(30, TimeUnit.SECONDS); 142 | probeArray.executeOnAll(tools -> runProbe(tools.nodeEnvironment())).get(30, TimeUnit.SECONDS); 143 | 144 | LOGGER.info("Warming up..."); 145 | Thread.sleep(WARMUP_DURATION.toMillis()); 146 | 147 | LOGGER.info("Running..."); 148 | //long before = System.nanoTime(); 149 | 150 | NodeArrayFuture serverFuture = serverArray.executeOnAll(tools -> recordServerMetrics(participantCount, tools)); 151 | NodeArrayFuture loadersFuture = loadersArray.executeOnAll(tools -> recordClientMetrics(participantCount, tools)); 152 | NodeArrayFuture probeFuture = probeArray.executeOnAll(tools -> recordClientMetrics(participantCount, tools)); 153 | 154 | 155 | try { 156 | // signal all participants to start 157 | cluster.tools().barrier("run-start-barrier", participantCount).await(30, TimeUnit.SECONDS); 158 | // wait for the run duration 159 | Thread.sleep(RUN_DURATION.toMillis()); 160 | // signal all participants to stop 161 | cluster.tools().barrier("run-end-barrier", participantCount).await(30, TimeUnit.SECONDS); 162 | } finally { 163 | // wait for all report files to be written; 164 | // do it in a finally so that if the above barrier awaits time out b/c a job threw an exception 165 | // the future.get() call will re-throw the exception and it'll be logged. 166 | try { 167 | serverFuture.get(30, TimeUnit.SECONDS); 168 | probeFuture.get(30, TimeUnit.SECONDS); 169 | loadersFuture.get(30, TimeUnit.SECONDS); 170 | } catch (Exception e){ 171 | e.printStackTrace(); 172 | } 173 | } 174 | 175 | // stop server 176 | serverArray.executeOnAll((tools) -> stopServer(tools.nodeEnvironment())).get(60, TimeUnit.SECONDS); 177 | 178 | downloadReports(cluster, probeConfig, loadGeneratorConfig); 179 | } catch (Exception e) { 180 | e.printStackTrace(); 181 | } 182 | } 183 | 184 | private static SimpleNodeArrayConfiguration loaderConfig() { 185 | SimpleNodeArrayConfiguration loadGeneratorConfig = new SimpleNodeArrayConfiguration("load-generator"); 186 | 187 | LOADER_IPS.forEach(ip -> { 188 | loadGeneratorConfig.node(new Node("loader-" + toDirectoryName(ip), ip)); 189 | }); 190 | return loadGeneratorConfig; 191 | } 192 | 193 | private static SimpleNodeArrayConfiguration probeConfig() { 194 | return new SimpleNodeArrayConfiguration("probe") 195 | .node(new Node("probe-" + toDirectoryName(PROBE_IP), PROBE_IP)); 196 | } 197 | 198 | private static String toDirectoryName(String ip) { 199 | return ip.replaceAll("\\.", "-"); 200 | } 201 | 202 | private static SimpleNodeArrayConfiguration serverConfig() { 203 | return new SimpleNodeArrayConfiguration("server") 204 | .node(new Node("server-" + toDirectoryName(SERVER_IP), SERVER_IP)); 205 | } 206 | 207 | private static void downloadReports(Cluster cluster, SimpleNodeArrayConfiguration probeConfig, SimpleNodeArrayConfiguration loadGeneratorConfig) throws IOException { 208 | String dateTimeString = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); 209 | Path targetPath = Paths.get(BASE_FOLDER).resolve(dateTimeString); 210 | 211 | List nodeArrays = List.of(cluster.nodeArray("server"), 212 | cluster.nodeArray("load-generator"), 213 | cluster.nodeArray("probe")); 214 | for (NodeArray nodeArray : nodeArrays) { 215 | 216 | ReportUtil.download(nodeArray, targetPath); 217 | ReportUtil.transformPerfHisto(nodeArray, targetPath); 218 | ReportUtil.transformJHiccupHisto(nodeArray, targetPath); 219 | transformAndAddToPlot(dateTimeString, nodeArray, targetPath); 220 | } 221 | 222 | long totalLoadersRequestCount = RPS_LOADER * LOADER_IPS.size() * RUN_DURATION.toSeconds(); 223 | assertThroughput(targetPath, loadGeneratorConfig, totalLoadersRequestCount, 1); 224 | 225 | long totalProbeRequestCount = RPS_PROBE * 1 * RUN_DURATION.toSeconds(); 226 | assertThroughput(targetPath, probeConfig, totalProbeRequestCount, 1); 227 | } 228 | 229 | private static void transformAndAddToPlot(String dateTimeString, NodeArray nodeArray, Path targetPath) { 230 | nodeArray.ids().stream().filter(id -> id.startsWith("probe")).forEach(id -> { 231 | Path plotFolder = Paths.get(BASE_FOLDER).resolve("plot"); 232 | if (!Files.exists(plotFolder)) { 233 | try { 234 | Files.createDirectories(plotFolder); 235 | } catch (IOException e) { 236 | e.printStackTrace(); 237 | } 238 | } 239 | try { 240 | Files.copy(targetPath.resolve(id).resolve("perf.hlog.hgrm"), plotFolder.resolve(dateTimeString + "_" + LOADER_IPS.size() + "x" + RPS_LOADER + ".hlog.hgrm")); 241 | updatePlot(plotFolder); 242 | } catch (IOException e) { 243 | System.err.println(e.toString()); 244 | e.printStackTrace(); 245 | } 246 | }); 247 | } 248 | 249 | private static void recordServerMetrics(int participantCount, ClusterTools tools) throws Exception { 250 | EnumSet items = isAutoDeploy() ? SERVER_ITEMS : MONITORED_ITEMS; 251 | 252 | HttpClient httpClient = HttpClient.newHttpClient(); 253 | 254 | 255 | try (ConfigurableMonitor ignore = new ConfigurableMonitor(items)) { 256 | LatencyRecordingChannelListener listener = null; 257 | if (isAutoDeploy()) { 258 | listener = (LatencyRecordingChannelListener) tools.nodeEnvironment().get(LatencyRecordingChannelListener.class.getName()); 259 | listener.startRecording(); 260 | } else { 261 | HttpRequest request = HttpRequest.newBuilder() 262 | .POST(HttpRequest.BodyPublishers.noBody()) 263 | .uri(URI.create("http://localhost:8080/performance-metrics/start?mode=cpu")) 264 | .setHeader("User-Agent", "Java 11 HttpClient Bot") // add request header 265 | .build(); 266 | 267 | HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); 268 | // print status code 269 | System.out.println("Starting metrics collection on server. Response Code = " + response.statusCode() + " | Response: " + response.body()); 270 | } 271 | tools.barrier("run-start-barrier", participantCount).await(); 272 | tools.barrier("run-end-barrier", participantCount).await(); 273 | if (isAutoDeploy()) { 274 | listener.stopRecording(); 275 | } else { 276 | HttpRequest request = HttpRequest.newBuilder() 277 | .POST(HttpRequest.BodyPublishers.noBody()) 278 | .uri(URI.create("http://localhost:8080/performance-metrics/stop")) 279 | .setHeader("User-Agent", "Java 11 HttpClient Bot") // add request header 280 | .build(); 281 | 282 | HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); 283 | // print status code 284 | System.out.println("Stopping metrics collection on server. Response Code = " + response.statusCode() + " | Response: " + response.body()); 285 | } 286 | } 287 | } 288 | 289 | private static void recordClientMetrics(int participantCount, ClusterTools tools) throws Exception { 290 | try (ConfigurableMonitor ignore = new ConfigurableMonitor(MONITORED_ITEMS)) { 291 | ResponseTimeListener responseTimeListener = (ResponseTimeListener) tools.nodeEnvironment().get(ResponseTimeListener.class.getName()); 292 | responseTimeListener.startRecording(); 293 | ResponseStatusListener responseStatusListener = (ResponseStatusListener) tools.nodeEnvironment().get(ResponseStatusListener.class.getName()); 294 | responseStatusListener.startRecording(); 295 | tools.barrier("run-start-barrier", participantCount).await(); 296 | tools.barrier("run-end-barrier", participantCount).await(); 297 | responseTimeListener.stopRecording(); 298 | responseStatusListener.stopRecording(); 299 | CompletableFuture cf = (CompletableFuture) tools.nodeEnvironment().get(CompletableFuture.class.getName()); 300 | cf.get(); 301 | } 302 | } 303 | 304 | private static boolean isAutoDeploy() { 305 | return mode == Mode.DEPLOY_DB | mode == Mode.DEPLOY_JAVA; 306 | } 307 | 308 | 309 | 310 | private static void startServer(ConcurrentMap env) throws Exception { 311 | if (mode.equals(Mode.MANUAL)) { 312 | startPreDeployedServer(env); 313 | } 314 | else { 315 | autoDeployServer(env); 316 | } 317 | } 318 | 319 | 320 | 321 | private static void startPreDeployedServer(ConcurrentMap env) { 322 | try { 323 | Process process = new ProcessBuilder("/usr/bin/java", "-jar", "/home/ubuntu/spring-performance-0.0.1-SNAPSHOT.jar", "&").start(); 324 | // Process process = new ProcessBuilder("/usr/bin/java", "-jar", "/home/ubuntu/spring-petclinic-3.0.0-SNAPSHOT.jar", "&").start(); 325 | env.put(Process.class.getName(), process); 326 | } catch (IOException e) { 327 | System.err.println("Could not start process" + e.getMessage()); 328 | } 329 | } 330 | 331 | private static void autoDeployServer(ConcurrentMap env) throws Exception { 332 | Server server1 = new Server(); 333 | 334 | ServerConnector connector = new ServerConnector(server1); 335 | connector.setPort(PORT); 336 | 337 | LatencyRecordingChannelListener listener = new LatencyRecordingChannelListener(); 338 | connector.addBean(listener); 339 | 340 | server1.setConnectors(new Connector[]{connector}); 341 | 342 | ServletContextHandler context = new ServletContextHandler(); 343 | context.setContextPath("/"); 344 | 345 | if (mode == Mode.DEPLOY_JAVA) { 346 | ServletHolder servletHolder = context.addServlet(PlainJavaServlet.class, "/"); 347 | servletHolder.setInitOrder(0); 348 | } else if (mode == Mode.DEPLOY_DB) { 349 | ServletHolder servletHolder = context.addServlet(DatabaseServlet.class, "/"); 350 | servletHolder.setInitOrder(0); 351 | } else { 352 | 353 | throw new IllegalStateException("not yet implemented"); 354 | } 355 | 356 | 357 | HandlerList mainHandlers = new HandlerList(); 358 | mainHandlers.addHandler(context); 359 | mainHandlers.addHandler(new DefaultHandler()); 360 | 361 | server1.setHandler(mainHandlers); 362 | 363 | server1.start(); 364 | env.put(Server.class.getName(), server1); 365 | env.put(LatencyRecordingChannelListener.class.getName(), listener); 366 | } 367 | 368 | private static void stopServer(Map env) throws Exception { 369 | if (mode.equals(Mode.MANUAL)) { 370 | stopPreDeployedServer(env); 371 | } 372 | else { 373 | stopAutoDeployedServer(env); 374 | } 375 | } 376 | 377 | private static void stopAutoDeployedServer(Map env) throws Exception { 378 | ((Server) env.get(Server.class.getName())).stop(); 379 | } 380 | 381 | private static void stopPreDeployedServer(Map env) { 382 | Process process = (Process) env.get(Process.class.getName()); 383 | process.destroy(); 384 | } 385 | 386 | public static void updatePlot(Path outputFolder) throws IOException { 387 | Path plot = outputFolder.resolve("plot.html"); 388 | if (Files.exists(plot)) { 389 | Files.delete(plot); 390 | } 391 | Path target = Files.createFile(plot); 392 | 393 | 394 | String extension = ".hlog.hgrm"; 395 | String histosPart = Files.list(outputFolder).filter(f -> f.getFileName().toString().endsWith(extension)) 396 | .map(p -> { 397 | try { 398 | return Files.readString(p); 399 | } catch (IOException e) { 400 | throw new UnsupportedOperationException(); 401 | } 402 | }) 403 | .collect(Collectors.joining("`,`")); 404 | 405 | 406 | String namesPart = Files.list(outputFolder).filter(f -> f.getFileName().toString().endsWith(extension)) 407 | .map(p -> { 408 | 409 | final String fileName = p.getFileName().toString(); 410 | final String machinesXRequests = fileName.substring(16, fileName.length() - extension.length()); 411 | final String[] xes = machinesXRequests.split("x"); 412 | 413 | String finalDescriptions = (Integer.parseInt(xes[0]) * Integer.parseInt(xes[1])) + " RPS"; 414 | return finalDescriptions; 415 | 416 | }) 417 | .collect(Collectors.joining("\",\"")); 418 | 419 | String js = "var histos = [`" + histosPart + "`];\n" + 420 | " var names = [\"" + namesPart + "\"];\n"; 421 | 422 | try (InputStream resourceAsStream = PerformanceTest.class.getResourceAsStream("/plot_template.html")) { 423 | String s = IOUtils.toString(resourceAsStream); 424 | s = s.replace(" // <<< TODO_CHART_DATA >>>", js); 425 | Files.writeString(target, s); 426 | 427 | } catch (IOException e) { 428 | e.printStackTrace(); 429 | } 430 | 431 | } 432 | 433 | 434 | private static void runProbe(Map env) throws IOException { 435 | ResponseTimeListener responseTimeListener = new ResponseTimeListener(); 436 | env.put(ResponseTimeListener.class.getName(), responseTimeListener); 437 | ResponseStatusListener responseStatusListener = new ResponseStatusListener(); 438 | env.put(ResponseStatusListener.class.getName(), responseStatusListener); 439 | 440 | LoadGenerator generator = LoadGenerator.builder() 441 | .scheme("http") 442 | .host(SERVER_IP) 443 | .port(PORT) 444 | .runFor(WARMUP_DURATION.plus(RUN_DURATION).toSeconds(), TimeUnit.SECONDS) 445 | .threads(1) 446 | .rateRampUpPeriod(WARMUP_DURATION.toSeconds() / 2) 447 | .resourceRate(RPS_PROBE) 448 | .resource(REQUEST_URI) 449 | .resourceListener(responseTimeListener) 450 | .listener(responseTimeListener) 451 | .resourceListener(responseStatusListener) 452 | .listener(responseStatusListener) 453 | .build(); 454 | 455 | CompletableFuture cf = generator.begin(); 456 | env.put(CompletableFuture.class.getName(), cf); 457 | 458 | cf = cf.whenComplete((x, f) -> { 459 | if (f == null) { 460 | LOGGER.info("probe generation complete"); 461 | } else { 462 | LOGGER.info("probe generation failure " + f); 463 | } 464 | }); 465 | 466 | env.put(CompletableFuture.class.getName(), cf); 467 | env.put(LoadGenerator.Config.class.getName(), generator.getConfig()); 468 | } 469 | 470 | /*// Start the load generation. 471 | }*/ 472 | 473 | private static void runLoadGenerator(Map env) throws IOException { 474 | ResponseTimeListener responseTimeListener = new ResponseTimeListener(); 475 | env.put(ResponseTimeListener.class.getName(), responseTimeListener); 476 | ResponseStatusListener responseStatusListener = new ResponseStatusListener(); 477 | env.put(ResponseStatusListener.class.getName(), responseStatusListener); 478 | 479 | LoadGenerator generator = LoadGenerator.builder() 480 | .scheme("http") 481 | .host(SERVER_IP) 482 | .port(PORT) 483 | .runFor(WARMUP_DURATION.plus(RUN_DURATION).toSeconds(), TimeUnit.SECONDS) 484 | .threads(2) 485 | .rateRampUpPeriod(WARMUP_DURATION.toSeconds() / 2) 486 | .resourceRate(RPS_LOADER) 487 | .resource(REQUEST_URI) 488 | .resourceListener(responseTimeListener) 489 | .listener(responseTimeListener) 490 | .resourceListener(responseStatusListener) 491 | .listener(responseStatusListener) 492 | .build(); 493 | 494 | 495 | CompletableFuture cf = generator.begin(); 496 | // complete.get(); //make sure the load generation is complete 497 | 498 | cf = cf.whenComplete((x, f) -> { 499 | if (f == null) { 500 | LOGGER.info("load generation complete"); 501 | } else { 502 | LOGGER.info("load generation failure " + f); 503 | } 504 | }); 505 | 506 | env.put(CompletableFuture.class.getName(), cf); 507 | env.put(LoadGenerator.Config.class.getName(), generator.getConfig()); 508 | } 509 | 510 | 511 | public static void createHgrmHistogram(File hlogFile, OutputStream out) throws FileNotFoundException { 512 | try (HistogramLogReader reader = new HistogramLogReader(hlogFile)) { 513 | Histogram total = new Histogram(3); 514 | while (reader.hasNext()) { 515 | Histogram histogram = (Histogram) reader.nextIntervalHistogram(); 516 | total.add(histogram); 517 | } 518 | PrintStream ps = new PrintStream(out); 519 | total.outputPercentileDistribution(ps, 1000000.00); // scale by 1000000.00 to report in milliseconds 520 | ps.flush(); 521 | } 522 | } 523 | 524 | 525 | public static boolean assertThroughput(Path reportRootPath, NodeArrayConfiguration nodeArray, long expectedValue, double errorMargin) throws FileNotFoundException 526 | { 527 | long totalCount = 0L; 528 | for (Node node : nodeArray.nodes()) 529 | { 530 | Path perfHlog = reportRootPath.resolve(node.getId()).resolve("perf.hlog"); 531 | try (HistogramLogReader histogramLogReader = new HistogramLogReader(perfHlog.toFile())) 532 | { 533 | while (true) 534 | { 535 | AbstractHistogram histogram = (AbstractHistogram)histogramLogReader.nextIntervalHistogram(); 536 | if (histogram == null) 537 | break; 538 | 539 | totalCount += histogram.getTotalCount(); 540 | } 541 | } 542 | } 543 | 544 | System.out.println(" " + nodeArray.id() + " throughput is " + totalCount + " vs expected " + expectedValue); 545 | double error = expectedValue * errorMargin / 100.0; 546 | double highBound = expectedValue + error; 547 | double lowBound = expectedValue - error; 548 | if (totalCount >= lowBound && totalCount <= highBound) 549 | { 550 | System.out.println(" OK; value within " + errorMargin + "% error margin"); 551 | return true; 552 | } 553 | else 554 | { 555 | System.out.println(" NOK; value out of " + errorMargin + "% error margin"); 556 | return false; 557 | } 558 | } 559 | 560 | 561 | 562 | private static EnumSet copy(EnumSet base, ConfigurableMonitor.Item... items) { 563 | EnumSet result = EnumSet.copyOf(base); 564 | result.addAll(Arrays.asList(items)); 565 | return result; 566 | } 567 | 568 | 569 | } 570 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/app/model/TaxRate.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.app.model; 2 | 3 | public class TaxRate { 4 | 5 | private final String name; 6 | private final Double rate; 7 | 8 | public TaxRate(String name, Double rate) { 9 | this.name = name; 10 | this.rate = rate; 11 | } 12 | 13 | public String getName() { 14 | return name; 15 | } 16 | 17 | public Double getRate() { 18 | return rate; 19 | } 20 | 21 | @Override 22 | public String toString() { 23 | return "{ \"name\":" + name + ", \"rate\" : " + rate + "}"; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/app/servlets/DatabaseServlet.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.app.servlets; 2 | 3 | import com.mycompany.app.util.MyProperties; 4 | import com.zaxxer.hikari.HikariConfig; 5 | import com.zaxxer.hikari.HikariDataSource; 6 | import jakarta.servlet.http.HttpServlet; 7 | import jakarta.servlet.http.HttpServletRequest; 8 | import jakarta.servlet.http.HttpServletResponse; 9 | 10 | import java.io.IOException; 11 | import java.sql.Connection; 12 | import java.sql.PreparedStatement; 13 | import java.sql.ResultSet; 14 | import java.sql.SQLException; 15 | 16 | public class DatabaseServlet extends HttpServlet { 17 | 18 | private HikariDataSource ds; 19 | 20 | static final String sql = "SELECT JSON_ARRAYAGG(JSON_OBJECT('name', name, 'id', id)) as result from tax_rates"; 21 | 22 | @Override 23 | public void init() { 24 | ds = createConnectionPool(); 25 | } 26 | 27 | @Override 28 | protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { 29 | resp.setContentType("application/json"); 30 | 31 | try (Connection connection = ds.getConnection(); 32 | PreparedStatement statement = connection.prepareStatement(sql)) { 33 | ResultSet resultSet = statement.executeQuery(); 34 | 35 | while (resultSet.next()) { 36 | resultSet.getBinaryStream("result") 37 | .transferTo(resp.getOutputStream()); 38 | } 39 | 40 | } catch (SQLException e) { 41 | e.printStackTrace(); 42 | } 43 | } 44 | 45 | 46 | private HikariDataSource createConnectionPool() { 47 | HikariConfig config = new HikariConfig(); 48 | final String hikariSize_ = MyProperties.INSTANCE.getProperty("hikari.size"); 49 | if (hikariSize_ != null) { 50 | final Integer hikariSize = Integer.valueOf(hikariSize_); 51 | 52 | System.out.println("Setting HikariCP to " + hikariSize + " connections"); 53 | config.setMinimumIdle(hikariSize); 54 | config.setMaximumPoolSize(hikariSize); 55 | } 56 | config.setJdbcUrl(MyProperties.INSTANCE.getProperty("jdbc.url")); 57 | config.setUsername(MyProperties.INSTANCE.getProperty("jdbc.user")); 58 | config.setPassword(MyProperties.INSTANCE.getProperty("jdbc.password")); 59 | config.addDataSourceProperty("cachePrepStmts", "true"); 60 | config.addDataSourceProperty("prepStmtCacheSize", "250"); 61 | config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); 62 | config.addDataSourceProperty("useServerPrepStmts","true"); 63 | config.addDataSourceProperty("useLocalSessionState","true"); 64 | config.addDataSourceProperty("rewriteBatchedStatements","true"); 65 | config.addDataSourceProperty("cacheResultSetMetadata","true"); 66 | config.addDataSourceProperty("cacheServerConfiguration","true"); 67 | config.addDataSourceProperty("elideSetAutoCommits","true"); 68 | config.addDataSourceProperty("maintainTimeStats", "false"); 69 | 70 | return new HikariDataSource(config); 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /src/main/java/com/mycompany/app/servlets/PlainJavaServlet.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.app.servlets; 2 | 3 | import com.mycompany.app.model.TaxRate; 4 | import jakarta.servlet.http.HttpServlet; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | 8 | import java.io.IOException; 9 | import java.security.SecureRandom; 10 | 11 | 12 | public class PlainJavaServlet extends HttpServlet { 13 | 14 | SecureRandom random = new SecureRandom(); 15 | 16 | @Override 17 | protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { 18 | final TaxRate rate = new TaxRate("German VAT", random.nextDouble()); 19 | 20 | resp.setContentType("application/json"); 21 | resp.getWriter().write(rate.toString()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/mycompany/app/util/MyProperties.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.app.util; 2 | 3 | import com.mycompany.app.PerformanceTest; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.nio.file.Files; 8 | import java.nio.file.Path; 9 | import java.nio.file.Paths; 10 | import java.util.Properties; 11 | 12 | public enum MyProperties { 13 | 14 | INSTANCE; 15 | 16 | private Properties properties = new Properties(); 17 | 18 | MyProperties() { 19 | final Path propsFile = Paths.get("./application.properties"); 20 | System.out.println("Curren Directory = " + Paths.get(".").normalize().toAbsolutePath().toString()); 21 | 22 | if (Files.exists(propsFile)) { 23 | try (InputStream is = Files.newInputStream(propsFile)) { 24 | properties.load(is); 25 | } catch (IOException e) { 26 | e.printStackTrace(); 27 | } 28 | } else { 29 | try (InputStream is = PerformanceTest.class.getResourceAsStream("/application.properties")) { 30 | properties.load(is); 31 | } catch (IOException e) { 32 | e.printStackTrace(); 33 | } 34 | } 35 | } 36 | 37 | public Properties getProperties() { 38 | return properties; 39 | } 40 | 41 | public String getProperty(String key) { 42 | return (String) properties.get(key); 43 | } 44 | } -------------------------------------------------------------------------------- /src/main/java/com/mycompany/app/util/UpdatePlot.java: -------------------------------------------------------------------------------- 1 | package com.mycompany.app.util; 2 | 3 | import com.mycompany.app.PerformanceTest; 4 | 5 | import java.io.IOException; 6 | import java.nio.file.Paths; 7 | 8 | public class UpdatePlot { 9 | public static void main(String[] args) throws IOException { 10 | PerformanceTest.updatePlot(Paths.get(".\\target\\results\\plot")); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | jdbc.url=jdbc:mysql://yourRdsURL/marco 2 | jdbc.user=chooseYourOwn 3 | jdbc.password=chooseYourOwn 4 | hikari.size = 50 -------------------------------------------------------------------------------- /src/main/resources/database.sql: -------------------------------------------------------------------------------- 1 | create table tax_rates (id int auto_increment primary key not null, name varchar(255)); 2 | insert into tax_rates (name) values ('DE VAT'); 3 | insert into tax_rates (name) values ('UK VAT'); 4 | insert into tax_rates (name) values ('FR VAT'); -------------------------------------------------------------------------------- /src/main/resources/plot_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 122 | 171 | 182 | 183 | 214 | 215 | 335 | 336 | 342 | 343 | 344 | 345 | 346 |

HdrHistogram Plotter

347 | 348 | 349 | 350 |
Please select file(s) above.
351 | 352 | 353 |
None Loaded
354 | 355 | Latency time units: 356 | 362 | 363 | 364 |         365 |

366 | Percentile range: 367 | 368 | 372 | 99.99999% 373 | 383 |

384 |

385 |
386 | *** Note: Input files are expected to be in the .hgrm format produced by 387 | HistogramLogProcessor, or the percentile output format for HdrHistogram. 388 | See example file format 389 | here 390 |

391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 |
414 | Value Percentile TotalCount 1/(1-Percentile) 415 | 416 | 2.759 0.000000000000 1 1.00 417 | 2.990 0.100000000000 10 1.11 418 | 3.056 0.200000000000 19 1.25 419 | 3.115 0.300000000000 31 1.43 420 | 3.164 0.400000000000 38 1.67 421 | 3.326 0.500000000000 47 2.00 422 | 3.977 0.550000000000 52 2.22 423 | 6.193 0.600000000000 57 2.50 424 | 21.889 0.650000000000 62 2.86 425 | 36.438 0.700000000000 66 3.33 426 | 68.551 0.750000000000 71 4.00 427 | 84.935 0.775000000000 73 4.44 428 | 102.760 0.800000000000 76 5.00 429 | 111.477 0.825000000000 78 5.71 430 | 122.618 0.850000000000 80 6.67 431 | 199.623 0.875000000000 83 8.00 432 | 200.802 0.887500000000 84 8.89 433 | 222.953 0.900000000000 85 10.00 434 | 258.474 0.912500000000 86 11.43 435 | 340.787 0.925000000000 87 13.33 436 | 410.518 0.937500000000 89 16.00 437 | 410.518 0.943750000000 89 17.78 438 | 433.324 0.950000000000 90 20.00 439 | 433.324 0.956250000000 90 22.86 440 | 448.528 0.962500000000 91 26.67 441 | 483.131 0.968750000000 92 32.00 442 | 483.131 0.971875000000 92 35.56 443 | 483.131 0.975000000000 92 40.00 444 | 483.131 0.978125000000 92 45.71 445 | 620.757 0.981250000000 93 53.33 446 | 620.757 0.984375000000 93 64.00 447 | 620.757 0.985937500000 93 71.11 448 | 620.757 0.987500000000 93 80.00 449 | 620.757 0.989062500000 93 91.43 450 | 904.397 0.990625000000 94 106.67 451 | 904.397 1.000000000000 94 452 | #[Mean = 72.069, StdDeviation = 150.578] 453 | #[Max = 904.397, Total count = 94] 454 | #[Buckets = 17, SubBuckets = 2048] 455 |
456 |
457 | Value Percentile TotalCount 1/(1-Percentile) 458 | 459 | 460 | 2.626 0.000000000000 1 1.00 461 | 2.689 0.100000000000 9 1.11 462 | 2.730 0.200000000000 18 1.25 463 | 2.753 0.300000000000 27 1.43 464 | 2.777 0.400000000000 36 1.67 465 | 2.804 0.500000000000 45 2.00 466 | 2.828 0.550000000000 49 2.22 467 | 2.845 0.600000000000 56 2.50 468 | 2.851 0.650000000000 60 2.86 469 | 2.879 0.700000000000 63 3.33 470 | 2.888 0.750000000000 67 4.00 471 | 2.894 0.775000000000 69 4.44 472 | 2.916 0.800000000000 72 5.00 473 | 2.970 0.825000000000 74 5.71 474 | 2.974 0.850000000000 77 6.67 475 | 2.984 0.875000000000 78 8.00 476 | 2.986 0.887500000000 79 8.89 477 | 3.006 0.900000000000 81 10.00 478 | 3.015 0.912500000000 82 11.43 479 | 3.017 0.925000000000 83 13.33 480 | 3.025 0.937500000000 84 16.00 481 | 3.025 0.943750000000 84 17.78 482 | 3.129 0.950000000000 85 20.00 483 | 3.164 0.956250000000 86 22.86 484 | 3.164 0.962500000000 86 26.67 485 | 3.250 0.968750000000 87 32.00 486 | 3.250 0.971875000000 87 35.56 487 | 3.250 0.975000000000 87 40.00 488 | 6.316 0.978125000000 88 45.71 489 | 6.316 0.981250000000 88 53.33 490 | 6.316 0.984375000000 88 64.00 491 | 6.316 0.985937500000 88 71.11 492 | 6.316 0.987500000000 88 80.00 493 | 98.304 0.989062500000 89 91.43 494 | 98.304 1.000000000000 89 495 | #[Mean = 3.936, StdDeviation = 10.064] 496 | #[Max = 98.304, Total count = 89] 497 | #[Buckets = 17, SubBuckets = 2048] 498 |
499 |
500 | Value Percentile TotalCount 1/(1-Percentile) 501 | 2.791 0.000000000000 1 1.00 502 | 2.810 0.100000000000 3 1.11 503 | 2.845 0.200000000000 6 1.25 504 | 2.867 0.300000000000 9 1.43 505 | 2.929 0.400000000000 12 1.67 506 | 2.951 0.500000000000 15 2.00 507 | 2.953 0.550000000000 16 2.22 508 | 2.968 0.600000000000 18 2.50 509 | 2.970 0.650000000000 19 2.86 510 | 3.013 0.700000000000 22 3.33 511 | 3.013 0.750000000000 22 4.00 512 | 3.029 0.775000000000 23 4.44 513 | 3.033 0.800000000000 24 5.00 514 | 3.033 0.825000000000 24 5.71 515 | 3.135 0.850000000000 25 6.67 516 | 3.176 0.875000000000 26 8.00 517 | 3.176 0.887500000000 26 8.89 518 | 3.242 0.900000000000 27 10.00 519 | 3.242 0.912500000000 27 11.43 520 | 3.242 0.925000000000 27 13.33 521 | 5.317 0.937500000000 28 16.00 522 | 5.317 0.943750000000 28 17.78 523 | 5.317 0.950000000000 28 20.00 524 | 5.317 0.956250000000 28 22.86 525 | 5.317 0.962500000000 28 26.67 526 | 90.309 0.968750000000 29 32.00 527 | 90.309 1.000000000000 29 528 | #[Mean = 6.037, StdDeviation = 15.926] 529 | #[Max = 90.309, Total count = 29] 530 | #[Buckets = 17, SubBuckets = 2048] 531 |
532 | 533 | -------------------------------------------------------------------------------- /src/main/resources/simplelogger.properties: -------------------------------------------------------------------------------- 1 | org.slf4j.simpleLogger.logFile=System.out 2 | org.slf4j.simpleLogger.log=error 3 | org.slf4j.simpleLogger.log.org.apache=error 4 | org.slf4j.simpleLogger.log.com.mycompany=info --------------------------------------------------------------------------------