docs: public void setup(ExecutorType clientExecutor,
                      ExecutorType serverExecutor,
                      MessageSize requestSize,
                      MessageSize responseSize,
                      FlowWindowSize windowSize,
                      ChannelType channelType,
                      int maxConcurrentStreams,
                      int channelCount) throws Exception {
      NettyServerBuilder serverBuilder;
      NettyChannelBuilder channelBuilder;
      if (channelType == ChannelType.LOCAL) {
        LocalAddress address = new LocalAddress("netty-e2e-benchmark");
        serverBuilder = NettyServerBuilder.forAddress(address);
        serverBuilder.channelType(LocalServerChannel.class);
        channelBuilder = NettyChannelBuilder.forAddress(address);
        channelBuilder.channelType(LocalChannel.class);
      } else {
        ServerSocket sock = new ServerSocket();
        // Pick a port using an ephemeral socket.
        sock.bind(new InetSocketAddress(BENCHMARK_ADDR, 0));
        SocketAddress address = sock.getLocalSocketAddress();
        sock.close();
        serverBuilder = NettyServerBuilder.forAddress(address);
        channelBuilder = NettyChannelBuilder.forAddress(address);
      }
  
      if (serverExecutor == ExecutorType.DIRECT) {
        serverBuilder.directExecutor();
      }
      if (clientExecutor == ExecutorType.DIRECT) {
        channelBuilder.directExecutor();
      }
  
      // Always use a different worker group from the client.
      ThreadFactory serverThreadFactory = new DefaultThreadFactory("STF pool", true /* daemon */);
      serverBuilder.workerEventLoopGroup(new NioEventLoopGroup(0, serverThreadFactory));
  
      // Always set connection and stream window size to same value
      serverBuilder.flowControlWindow(windowSize.bytes());
      channelBuilder.flowControlWindow(windowSize.bytes());
  
      channelBuilder.negotiationType(NegotiationType.PLAINTEXT);
      serverBuilder.maxConcurrentCallsPerConnection(maxConcurrentStreams);
  
      // Create buffers of the desired size for requests and responses.
      PooledByteBufAllocator alloc = PooledByteBufAllocator.DEFAULT;
      // Use a heap buffer for now, since MessageFramer doesn't know how to directly convert this
      // into a WritableBuffer
      // TODO(carl-mastrangelo): convert this into a regular buffer() call.  See
      // https://github.com/grpc/grpc-java/issues/2062#issuecomment-234646216
      request = alloc.heapBuffer(requestSize.bytes());
      request.writerIndex(request.capacity() - 1);
      response = alloc.heapBuffer(responseSize.bytes());
      response.writerIndex(response.capacity() - 1);
  
      // Simple method that sends and receives NettyByteBuf
      unaryMethod = MethodDescriptor.<ByteBuf, ByteBuf>newBuilder()
          .setType(MethodType.UNARY)
          .setFullMethodName("benchmark/unary")
          .setRequestMarshaller(new ByteBufOutputMarshaller())
          .setResponseMarshaller(new ByteBufOutputMarshaller())
          .build();
  
      pingPongMethod = unaryMethod.toBuilder()
          .setType(MethodType.BIDI_STREAMING)
          .setFullMethodName("benchmark/pingPong")
          .build();
      flowControlledStreaming = pingPongMethod.toBuilder()
          .setFullMethodName("benchmark/flowControlledStreaming")
          .build();
  
      // Server implementation of unary & streaming methods
      serverBuilder.addService(
          ServerServiceDefinition.builder(
              new ServiceDescriptor("benchmark",
                  unaryMethod,
                  pingPongMethod,
                  flowControlledStreaming))
              .addMethod(unaryMethod, new ServerCallHandler<ByteBuf, ByteBuf>() {
                    @Override
                    public ServerCall.Listener<ByteBuf> startCall(
                        final ServerCall<ByteBuf, ByteBuf> call,
                        Metadata headers) {
                      call.sendHeaders(new Metadata());
                      call.request(1);
                      return new ServerCall.Listener<ByteBuf>() {
                        @Override
                        public void onMessage(ByteBuf message) {
                          // no-op
                          message.release();
                          call.sendMessage(response.slice());
                        }
  
                        @Override
                        public void onHalfClose() {
                          call.close(Status.OK, new Metadata());
                        }
  
                        @Override
                        public void onCancel() {
  
                        }
  
                        @Override
                        public void onComplete() {
                        }
                      };
                    }
                  })
              .addMethod(pingPongMethod, new ServerCallHandler<ByteBuf, ByteBuf>() {
                    @Override
                    public ServerCall.Listener<ByteBuf> startCall(
                        final ServerCall<ByteBuf, ByteBuf> call,
                        Metadata headers) {
                      call.sendHeaders(new Metadata());
                      call.request(1);
                      return new ServerCall.Listener<ByteBuf>() {
                        @Override
                        public void onMessage(ByteBuf message) {
                          message.release();
                          call.sendMessage(response.slice());
                          // Request next message
                          call.request(1);
                        }
  
                        @Override
                        public void onHalfClose() {
                          call.close(Status.OK, new Metadata());
                        }
  
                        @Override
                        public void onCancel() {
  
                        }
  
                        @Override
                        public void onComplete() {
  
                        }
                      };
                    }
                  })
              .addMethod(flowControlledStreaming, new ServerCallHandler<ByteBuf, ByteBuf>() {
                    @Override
                    public ServerCall.Listener<ByteBuf> startCall(
                        final ServerCall<ByteBuf, ByteBuf> call,
                        Metadata headers) {
                      call.sendHeaders(new Metadata());
                      call.request(1);
                      return new ServerCall.Listener<ByteBuf>() {
                        @Override
                        public void onMessage(ByteBuf message) {
                          message.release();
                          while (call.isReady()) {
                            call.sendMessage(response.slice());
                          }
                          // Request next message
                          call.request(1);
                        }
  
                        @Override
                        public void onHalfClose() {
                          call.close(Status.OK, new Metadata());
                        }
  
                        @Override
                        public void onCancel() {
  
                        }
  
                        @Override
                        public void onComplete() {
  
                        }
  
                        @Override
                        public void onReady() {
                          while (call.isReady()) {
                            call.sendMessage(response.slice());
                          }
                        }
                      };
                    }
                  })
              .build());
  
      // Build and start the clients and servers
      server = serverBuilder.build();
      server.start();
      channels = new ManagedChannel[channelCount];
      ThreadFactory clientThreadFactory = new DefaultThreadFactory("CTF pool", true /* daemon */);
      for (int i = 0; i < channelCount; i++) {
        // Use a dedicated event-loop for each channel
        channels[i] = channelBuilder
            .eventLoopGroup(new NioEventLoopGroup(1, clientThreadFactory))
            .build();
      }
    }