void runBenchmark(@Nullable GenericContainer<?> storage, GenericContainer<?> zipkin)
    throws Exception {
    GenericContainer<?> backend =
      new GenericContainer<>(parse("ghcr.io/openzipkin/brave-example:armeria"))
        .withNetwork(Network.SHARED)
        .withNetworkAliases("backend")
        .withCommand("backend")
        .withExposedPorts(9000)
        .waitingFor(Wait.forHealthcheck());

    GenericContainer<?> frontend =
      new GenericContainer<>(parse("ghcr.io/openzipkin/brave-example:armeria"))
        .withNetwork(Network.SHARED)
        .withNetworkAliases("frontend")
        .withCommand("frontend")
        .withExposedPorts(8081)
        .waitingFor(Wait.forHealthcheck());
    containers.add(frontend);

    // Use a quay.io mirror to prevent build outages due to Docker Hub pull quotas
    // Use same version as in docker/examples/docker-compose-prometheus.yml
    GenericContainer<?> prometheus =
      new GenericContainer<>(parse("quay.io/prometheus/prometheus:v2.55.1"))
        .withNetwork(Network.SHARED)
        .withNetworkAliases("prometheus")
        .withExposedPorts(9090)
        .withCopyFileToContainer(
          MountableFile.forClasspathResource("prometheus.yml"), "/etc/prometheus/prometheus.yml");
    containers.add(prometheus);

    // Use a quay.io mirror to prevent build outages due to Docker Hub pull quotas
    // Use same version as in docker/examples/docker-compose-prometheus.yml
    GenericContainer<?> grafana = new GenericContainer<>(parse("quay.io/giantswarm/grafana:7.5.12"))
      .withNetwork(Network.SHARED)
      .withNetworkAliases("grafana")
      .withExposedPorts(3000)
      .withEnv("GF_AUTH_ANONYMOUS_ENABLED", "true")
      .withEnv("GF_AUTH_ANONYMOUS_ORG_ROLE", "Admin");
    containers.add(grafana);

    // This is an arbitrary small image that has curl installed
    // Use a quay.io mirror to prevent build outages due to Docker Hub pull quotas
    // Use same version as in docker/examples/docker-compose-prometheus.yml
    GenericContainer<?> grafanaDashboards =
      new GenericContainer<>(parse("quay.io/cilium/alpine-curl:v1.10.0"))
        .withNetwork(Network.SHARED)
        .withWorkingDirectory("/tmp")
        .withLogConsumer(new Slf4jLogConsumer(LOG))
        .withCreateContainerCmdModifier(it -> it.withEntrypoint("/tmp/create.sh"))
        .withCopyFileToContainer(
          MountableFile.forClasspathResource("create-datasource-and-dashboard.sh", 555),
          "/tmp/create.sh");
    containers.add(grafanaDashboards);

    // Use a quay.io mirror to prevent build outages due to Docker Hub pull quotas
    GenericContainer<?> wrk = new GenericContainer<>(parse("quay.io/dim/wrk:stable"))
      .withNetwork(Network.SHARED)
      .withCreateContainerCmdModifier(it -> it.withEntrypoint("wrk"))
      .withCommand("-t4 -c128 -d100s http://frontend:8081 --latency");
    containers.add(wrk);

    grafanaDashboards.dependsOn(grafana);
    wrk.dependsOn(frontend, backend, prometheus, grafanaDashboards, zipkin);
    if (storage != null) wrk.dependsOn(storage);

    Startables.deepStart(Stream.of(wrk)).join();

    System.out.println("Benchmark started.");
    if (zipkin != null) printContainerMapping(zipkin);
    if (storage != null) printContainerMapping(storage);
    printContainerMapping(backend);
    printContainerMapping(frontend);
    printContainerMapping(prometheus);
    printContainerMapping(grafana);

    while (wrk.isRunning()) {
      Thread.sleep(1000);
    }

    // Wait for prometheus to do a final scrape.
    Thread.sleep(5000);

    System.out.println("Benchmark complete, wrk output:");
    System.out.println(wrk.getLogs().replace("\n\n", "\n"));

    WebClient prometheusClient = WebClient.of(
      "h1c://" + prometheus.getContainerIpAddress() + ":" + prometheus.getFirstMappedPort());

    System.out.printf("Messages received: %s%n", prometheusValue(
      prometheusClient, "sum(zipkin_collector_messages_total)"));
    System.out.printf("Spans received: %s%n", prometheusValue(
      prometheusClient, "sum(zipkin_collector_spans_total)"));
    System.out.printf("Spans dropped: %s%n", prometheusValue(
      prometheusClient, "sum(zipkin_collector_spans_dropped_total)"));

    System.out.println("Memory quantiles:");
    printQuartiles(prometheusClient, "jvm_memory_used_bytes{area=\"heap\"}");
    printQuartiles(prometheusClient, "jvm_memory_used_bytes{area=\"nonheap\"}");

    System.out.printf(
      "Total GC time (s): %s%n",
      prometheusValue(prometheusClient, "sum(jvm_gc_pause_seconds_sum)"));
    System.out.printf(
      "Number of GCs: %s%n", prometheusValue(prometheusClient, "sum(jvm_gc_pause_seconds_count)"));

    System.out.println("POST Spans latency (s)");
    printHistogram(prometheusClient, """
      http_server_requests_seconds_bucket{
      method="POST",status="202",uri="/api/v2/spans"}
      """);

    if (WAIT_AFTER_BENCHMARK) {
      System.out.println("""
        Keeping containers running until explicit termination. \
        Feel free to poke around in grafana.\
        """);
      Thread.sleep(Long.MAX_VALUE);
    }
  }
This benchmark runs zipkin-server, storage backends, an example application, prometheus, grafana,
and wrk using docker to generate a performance report.

<p>Currently there are two environment variable knobs

<ul>
  <li>
    RELEASE_VERSION - specify to a released zipkin server. If unspecified, will use the current code,
    i.e., the code currently displayed in your IDE.
  </li>
  <li>
    ZIPKIN_BENCHMARK_WAIT - set to true to have the benchmark wait until user manually terminates at the end.
    Useful to manually inspect prometheus / grafana.
  </li>
</ul>

<p>Note to Windows laptop users: you will probably need to restart the Docker daemon before a
session of benchmarks. Docker containers seem to have time get out of sync when a computer sleeps
until you restart the daemon - this causes Prometheus metrics to not scrape properly.
benchmarks/src/test/java/zipkin2/server/ServerIntegratedBenchmark.java