LoadTest.java

/**
 * Copyright 2016-2018 Dell Inc. or its subsidiaries. All rights reserved.
 *
 * <p>Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file
 * except in compliance with the License. A copy of the License is located at
 *
 * <p>http://www.apache.org/licenses/LICENSE-2.0.txt
 *
 * <p>or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package com.emc.ecs.nfsclient.nfs.io;

import com.emc.ecs.nfsclient.nfs.nfs3.Nfs3;
import com.emc.ecs.nfsclient.rpc.CredentialUnix;
import com.emc.ecs.nfsclient.rpc.RpcRequest;
import java.io.IOException;
import java.util.Calendar;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
 * Command-line benchmark for concurrent reads from an NFS export.
 *
 * @author seibed
 */
public class LoadTest {

  /** Creates an instance of this command-line utility. */
  public LoadTest() {}

  /**
   * Runs the NFS benchmark with the supplied command-line arguments.
   *
   * @param args export path, file name, and benchmark options
   */
  public static void main(String[] args) {
    String export = args[0];
    String fileName = args[1];
    int fileSizeInKiloBytes = Integer.parseInt(args[2]);
    int maximumNumberOfThreads = Integer.parseInt(args[3]);
    int megaBytesToRead = Integer.parseInt(args[4]);
    try {
      testThreadedReadingPerformance(
          export, fileName, fileSizeInKiloBytes, maximumNumberOfThreads, megaBytesToRead);
    } catch (Exception e) {
      System.err.println("NFS load test failed: export=" + export + ", file=" + fileName);
      e.printStackTrace();
    }
  }

  /**
   * Creates a test file and measures reads with increasing thread counts.
   *
   * @param export NFS server and export path
   * @param fileName path of the temporary test file
   * @param fileSizeInKiloBytes requested test-file size in kilobytes
   * @param maximumNumberOfThreads maximum number of reader threads
   * @param megaBytesToRead amount each benchmark iteration reads
   * @throws IOException if a file operation fails
   * @throws InterruptedException if the benchmark is interrupted
   */
  public static void testThreadedReadingPerformance(
      String export,
      String fileName,
      int fileSizeInKiloBytes,
      int maximumNumberOfThreads,
      int megaBytesToRead)
      throws IOException, InterruptedException {
    Nfs3 nfs3 = new Nfs3(export, new CredentialUnix(0, 0, null), 3);
    int fileLength = writeFile(nfs3, fileName, fileSizeInKiloBytes);
    for (int numberOfThreads = 1;
        numberOfThreads <= maximumNumberOfThreads;
        numberOfThreads = 2 * numberOfThreads) {
      testThreadedReadingPerformance(nfs3, fileName, fileLength, numberOfThreads, megaBytesToRead);
    }
    new Nfs3File(nfs3, fileName).delete();
  }

  private static int writeFile(Nfs3 nfs3, String fileName, int fileSizeInKiloBytes)
      throws IOException {
    Nfs3File test = new Nfs3File(nfs3, fileName);

    byte[] dataChunk = "another chunk of data!".getBytes(RpcRequest.CHARSET);
    int longerSize = (1000 * fileSizeInKiloBytes) + dataChunk.length;
    byte[] expectedData = new byte[longerSize];
    int chunkStart = 0;
    while (chunkStart + dataChunk.length < longerSize) {
      System.arraycopy(dataChunk, 0, expectedData, chunkStart, dataChunk.length);
      chunkStart += dataChunk.length;
    }
    try (NfsFileOutputStream outputStream = new NfsFileOutputStream(test)) {
      outputStream.write(expectedData);
    }
    return longerSize;
  }

  private static void testThreadedReadingPerformance(
      final Nfs3 nfs3,
      final String fileName,
      final int fileLength,
      final int numberOfThreadsToUse,
      int megaBytesToRead)
      throws IOException, InterruptedException {
    final ThreadCounter threadCounter = new ThreadCounter();
    Future<?>[] futures = new Future<?>[numberOfThreadsToUse];
    ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreadsToUse);
    long timeInMillis = Calendar.getInstance().getTimeInMillis();
    while (((long) fileLength) * threadCounter.getNumberOfReadingThreadsCompleted()
        < (1000000L * megaBytesToRead)) {
      for (int i = 0; i < numberOfThreadsToUse; ++i) {
        if ((futures[i] == null) || futures[i].isDone()) {
          Thread thread =
              new Thread() {

                @Override
                public void run() {
                  NfsFileInputStream inputStream = null;
                  try {
                    inputStream = new NfsFileInputStream(new Nfs3File(nfs3, fileName));
                    byte[] buffer = new byte[fileLength + 5];
                    int bytesRead = inputStream.read(buffer);
                    if (NfsFileInputStream.EOF != inputStream.read()) {
                      throw new Exception(
                          "Reading error - should have been at the end of the file");
                    }
                    if (fileLength != bytesRead) {
                      throw new Exception(
                          "Reading error - read "
                              + bytesRead
                              + " bytes, should have been "
                              + fileLength);
                    }
                    threadCounter.incrementNumberOfReadingThreadsCompleted();
                  } catch (Exception e) {
                    System.err.println(
                        "NFS load-test read failed: server="
                            + nfs3.getServer()
                            + ", export="
                            + nfs3.getExportedPath()
                            + ", file="
                            + fileName
                            + ", thread="
                            + Thread.currentThread().getName());
                    e.printStackTrace(System.err);
                  } finally {
                    if (inputStream != null) {
                      try {
                        inputStream.close();
                      } catch (IOException e) {
                        System.err.println("NFS load-test stream close failed: file=" + fileName);
                        e.printStackTrace(System.err);
                      }
                    }
                  }
                }
              };
          thread.setDaemon(true);
          futures[i] = executorService.submit(thread);
        }
      }
      Thread.sleep(1);
    }

    timeInMillis = Calendar.getInstance().getTimeInMillis() - timeInMillis;
    System.out.println(
        "Milliseconds to read "
            + megaBytesToRead
            + " MB of "
            + (fileLength / 1000)
            + " kilobyte files with "
            + numberOfThreadsToUse
            + " threads: "
            + timeInMillis);

    // Wait until all threads have completed.
    for (int i = 0; i < numberOfThreadsToUse; ++i) {
      try {
        futures[i].get();
      } catch (Exception e) {
        System.err.println("NFS load-test worker failed: file=" + fileName + ", worker=" + i);
        e.printStackTrace(System.err);
      }
    }
  }

  /**
   * @author seibed
   */
  private static final class ThreadCounter {

    int numberOfReadingThreadsCompleted = 0;

    synchronized int getNumberOfReadingThreadsCompleted() {
      return numberOfReadingThreadsCompleted;
    }

    synchronized void incrementNumberOfReadingThreadsCompleted() {
      ++numberOfReadingThreadsCompleted;
    }
  }
}