Перевод «JUnitBenchmarks: Tutorial».

timer

Оригинал: тут.

JUnitBenchmarks: Учебник.

Быстрый старт.
В овладении JUnitBenchmarks есть два ключевых элемента: аннотации и системные свойства, к счастью, большинство из них являются необязательными и требуются только для достижения специфических целей.

Превратите ваши JUnit4 тесты в тесты производительности.
Предполагается, что ваш класс уже является тестом Junit4. Например, предположим, что ваш тестовый класс выглядит следующим образом:

public class MyTest {
  @Test
  public void twentyMillis() throws Exception {
    Thread.sleep(20);
  }
}

Чтобы превратить этот тест в тест производительности, вам необходимо добавить правило для JUnit4 (rule), которое сообщит исполнителям тестов JUnit4 о необходимости прикрепить измерительный код к вашему тесту. Добавить такое правило легко, просто добавьте соответствующее поле, как в нижеследующем примере (строки с импортом опущены):

public class MyTest {
  @Rule
  public TestRule benchmarkRun = new BenchmarkRule();

  @Test
  public void twentyMillis() throws Exception {
    Thread.sleep(20);
  }
}

Как альтернатива — ваш тестовый класс должен быть объявлен наследником класса AbstractBenchmark, в котором объявлено необходимое поле:

public class MyTest extends AbstractBenchmark {
  @Test
  public void twentyMillis() throws Exception {
    Thread.sleep(20);
  }
}

Когда вы запустите тест при помощи JUnit4, результаты измерений будут выведены на консоль. Отметки о времени выполнения теста в Eclipse, например:
eclipse-junit-plugin
Это намного бОльший интервал сна, чем мы установили методом sleep.Так получается потому что замеры повторяются несколько раз чтобы получить более точную оценку среднего времени выполнения тестового метода. Сообщение, отображаемое в консоли содержит детальное описание этого конкретного примера:

MyTest.twentyMillis: [measured 10 out of 15 rounds]
 round: 0.02 [+- 0.00], round.gc: 0.00 [+- 0.00], GC.calls: 0, GC.time: 0.00, time.total: 0.32, time.warmup: 0.12, time.bench: 0.20

Таким образом тест был повторен 15 раз: 5 первых прогревочных повторов были не учтены (чтобы дать возможность JVM оптимизировать код), последующие 10 повторов время выполнения было подсчитано и вычислено среднее время прохождения теста, которое было ровно 0,02 секунды или 20 миллисекунд. Дополнительная информация содержит количество запусков сборщика мусора и время, затраченное на сборку мусора.

Формирование тестов производительности при помощи аннотаций.
Вы можете настроить отображение основных замеров через добавление аннотаций в тестовом методе. Например вы можете настроить количество прогревочных и измерительных проходов в аннотации BenchmarkOptions, как приведено ниже:

public class MyTest extends AbstractBenchmark {
  @BenchmarkOptions(benchmarkRounds = 20, warmupRounds = 0)
  @Test
  public void twentyMillis() throws Exception {
    Thread.sleep(20);
  }
}

Мы установили количество измерительных проходов в 20 и прогревочных в 0, эффективно замеряя время выполнение всех тестовых методов. Следующий раздел постепенно представляет больше этих настроечных аннотаций, демонстрируя их использование в контексте.

Полный тест производительности.

Проектирование вашего теста, запуск грубого теста производительности.

Предположим, что наша задача сравнить производительности трех стандартных реализаций списков в Java : ArrayList, Vector и LinkedList.

Прежде всего, нам необходимо разработать тест, который имитирует код нашего возможного приложения.  Давайте предположим, что приложение добавляет некоторые элементы в список, а затем удаляет элементы из него в случайном порядке. Следующий тест делает именно это:

public class Lists1
{
    private static Object singleton = new Object();
    private static int COUNT = 50000;
    private static int [] rnd;

    /** Prepare random numbers for tests. */
    @BeforeClass
    public static void prepare()
    {
        rnd = new int [COUNT];

        final Random random = new Random();
        for (int i = 0; i < COUNT; i++)
        {
            rnd[i] = Math.abs(random.nextInt());
        }
    }

    @Test
    public void arrayList() throws Exception
    {
        runTest(new ArrayList<Object>());
    }

    @Test
    public void linkedList() throws Exception
    {
        runTest(new LinkedList<Object>());
    }

    @Test
    public void vector() throws Exception
    {
        runTest(new Vector<Object>());
    }

    private void runTest(List<Object> list)
    {
        assert list.isEmpty();

        // First, add a number of objects to the list.
        for (int i = 0; i < COUNT; i++)
            list.add(singleton);

        // Randomly delete objects from the list.
        for (int i = 0; i < rnd.length; i++)
            list.remove(rnd[i] % list.size());
    }
}

Обратите внимание на следующие ключевые аспекты приведенного выше кода:

  • псевдослучайная последовательность удаления создается один раз и остается неизменной для всех тестов,
  • код теста использует синглтон чтобы избежать выделения памяти больше, чем требуется для списков,
  • тест очень прост, может быть даже слишком прост; в реальной жизни, вы предпочтете что-то, что напоминает сценарий вашего приложения.

Чтобы включить этот тест в микро-замер производительности, мы будем добавлять уже знакомое правило BenchmarkRule.

@Rule
public TestRule benchmarkRun = new BenchmarkRule();

Когда тест запустится в Eclipse мы получим следующие результаты в консоли:

Lists1.arrayList: [measured 10 out of 15 rounds]
 round: 0.60 [+- 0.00], round.gc: 0.00 [+- 0.00], GC.calls: 15, GC.time: 0.02, time.total: 9.02, time.warmup: 3.01, time.bench: 6.01
Lists1.linkedList: [measured 10 out of 15 rounds]
 round: 1.14 [+- 0.07], round.gc: 0.00 [+- 0.00], GC.calls: 23, GC.time: 0.07, time.total: 17.09, time.warmup: 5.67, time.bench: 11.43
Lists1.vector: [measured 10 out of 15 rounds]
 round: 0.60 [+- 0.01], round.gc: 0.00 [+- 0.00], GC.calls: 15, GC.time: 0.00, time.total: 9.04, time.warmup: 3.02, time.bench: 6.02

Разница между списками с произвольным доступом (Vector, ArrayList) и LinkedList по времени исполнения теста ясно видна. Другое отличие в существенно бОльшем времени сборки мусора (и количества вызовов сборщика мусора) из-за дополнительно работы сборщика мусора, необходимой для внутренней структуры узлов LinkedList (хотя это только предположение наугад). Не наблюдается разницы между потоко-безопасным Vector и не синхронизированным ArrayList. На самом деле lock-и в Vector-е не утверждены и JVM скорее всего удаляет их полностью. Теперь поменяем JVM на более новую (с 1.5.0_18 на 1.6.0_18), результаты сильно меняются, сравните:

Lists2.arrayList: [measured 10 out of 15 rounds]
 round: 0.25 [+- 0.01], round.gc: 0.00 [+- 0.00], GC.calls: 1, GC.time: 0.00, time.total: 3.77, time.warmup: 1.29, time.bench: 2.48
Lists2.linkedList: [measured 10 out of 15 rounds]
 round: 1.27 [+- 0.02], round.gc: 0.00 [+- 0.00], GC.calls: 1, GC.time: 0.00, time.total: 19.16, time.warmup: 6.42, time.bench: 12.74
Lists2.vector: [measured 10 out of 15 rounds]
 round: 0.24 [+- 0.00], round.gc: 0.00 [+- 0.00], GC.calls: 0, GC.time: 0.00, time.total: 3.73, time.warmup: 1.28, time.bench: 2.45

Списки с произвольным доступом почти в три раза быстрее, чем раньше, в то время как связный список даже медленнее, чем это было, но без (глобальной) активности сборщика мусора. Подчеркнем еще раз: это тот же код, и та же машина, только другая JVM в действии. Почему это так, мы оставим в качестве упражнения для читателя …

Изменение настроек теста.

Если Вы хотите изменить количество раундов по умолчанию или другие аспекты тестового окружения, используйте аннотацию BenchmarkOptions. Её можно применить к методам или классам. Тестовые методы наследуют параметры в следующем порядке:

  • глобальные параметры, передаваемые с помощью системных свойств (если jub.ignore.annotations установлен в true),
  • BenchmarkOptions аннотации уровня метода,
  • BenchmarkOptions аннотации уровня класса,
  • текущие настройки фрэймворка.

JUnit-benchmark вызывает полную сборку мусора перед каждым запуском метода, чтобы обеспечить одинаковые условия для каждого вызова. На практике методы не исполняются с предварительно очищенной памятью, поэтому может быть разумно отключить очистку мусора и просто брать среднее время выполнения нескольких тестов. Таким образом мы добавим следующую декларацию к классу (все методы ее унаследуют):

@BenchmarkOptions(callgc = false, benchmarkRounds = 20, warmupRounds = 3)

Теперь мы будем запускать тесты в еще более новой JVM (1.7.0, ea-b83). Мы получаем:

Lists2.arrayList: [measured 20 out of 23 rounds]
 round: 0.12 [+- 0.00], round.gc: 0.00 [+- 0.00], GC.calls: 0, GC.time: 0.00, time.total: 2.76, time.warmup: 0.39, time.bench: 2.36
Lists2.linkedList: [measured 20 out of 23 rounds]
 round: 0.82 [+- 0.01], round.gc: 0.00 [+- 0.00], GC.calls: 1, GC.time: 0.00, time.total: 18.78, time.warmup: 2.48, time.bench: 16.30
Lists2.vector: [measured 20 out of 23 rounds]
 round: 0.12 [+- 0.00], round.gc: 0.00 [+- 0.00], GC.calls: 1, GC.time: 0.00, time.total: 2.80, time.warmup: 0.40, time.bench: 2.40

Впечатляюще, не так ли ?

Хранение истории тестов производительности.

Когда вы работаете над своим кодом и ставите эксперименты, полезно сохранять результаты каждого измерения, так вы сможете сравнивать их в последствии. Еще это имеет значение когда вы хотите настроить автоматическую сборку, исполняющую измерения на других виртуальных машинах, сохранять результаты в какой-то базе данных и рисовать графики сравнения результатов. Для этого, JUnitBenchmark обеспечивает возможность сохранить результаты тестов в, базирующейся на файлах, реляционной базе данных H2.
Чтобы включить запись результатов, определите следующие свойства системы:

  • jub.consumers — определяет перечень потребителей результатов работы тестов. По умолчанию, это параметр указывает на CONSOLE, но и любая из следующих констант (или некоторых из них, через запятую) будет работать: H2, XML, CONSOLE.
  • jub.db.file — определяет путь к файлу базы данных H2 в локальной файловой системе (H2 consumer должен быть добавлен).

Например, для вывода результатов на консоль и в базу данных H2, используют следующие свойства:

-Djub.consumers=CONSOLE,H2 -Djub.db.file=.benchmarks

В Eclipse, вы можете ввести эти свойства в строку аргументов VM, как на рисунке ниже.

eclipse-launcher

Вы также должны загрузить H2 JAR отдельно и добавить его в свой classpath.

Рисование диаграмм для сравнения.

Цифры расскажут вам много, но картинка стоит тысячи цифр … или что-то в этом роде. Тем не менее, давайте добавим следующую аннотацию к тестовому классу:

@AxisRange(min = 0, max = 1)
@BenchmarkMethodChart(filePrefix = "benchmark-lists")

Эта аннотация заставляет consumer-ов JUnitBenchmark-а на основе H2 построить график, сравнивающий результаты всех методов внутри тестового класса. После того, как JUnit отработает, в папке проекта по умолчанию будут созданы новые файлы: benchmark-lists.html и benchmark-lists.json. HTML файл записывается с помощью Google Charts, требуется подключение к интернету. После открытия, график выглядит следующим образом:

chart-methods

Другой тип графической визуализации отображает историю запуска измерений данного класса (и всех его тестовых методов). Сравним различия в графиках для разных используемых JVM. Мы будем запускать тот же JUnit тест из Eclipse с использованием разных JVM, каждый раз добавляя ключ запуска, так мы узнаем какой запуск какой JVM соответствует. Ключ — это системное свойство, оно хранится в базе данных H2 вместе с данными выполнения. Мы будем записывать в него имя каждой JVM. Мы выполняем JUnit тест из Eclipse четыре раза, каждый раз меняя используемую для теста JRE и модифицируя свойства jub.customkey.

Затем, мы добавляем следующую аннотацию к тестовому классу:

@BenchmarkHistoryChart(labelWith = LabelType.CUSTOM_KEY, maxRuns = 20)

Мы снова запускаем тест и открываем результирующий файл.

chart-history

Интересно, что виртуальная машина IBM имеет заметную разницу между Vector и ArrayList.

Целевой каталог для генерации диаграммы может быть изменен через системные свойства (см. JavaDoc). Используете и экспериментируйте с базой данных Н2 напрямую для получения более продвинутых графиков или аналитики; Н2 — обычная SQL базы данных и обеспечивает удобный доступ через браузерный GUI. Вы можете запустить его из командной строки с помощью: java -jar lib/h2*.

Перевод сделан в рамках изучения английского языка.
Потенциальная польза для интересующихся тематикой текста — побочный эффект.

Добавить комментарий