Quartet 9: Allegro | Производительность

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Когда создавалась библиотека для валидации данных quartet были поставленны следующие цели-ориентиры:


  • TypeScript
  • Краткость
  • Простота
  • Производительность

В этой статье я хотел бы рассмотреть производительность quartet и её причины.


Будем исследовать этот аспект в сравнении между quartet и другой намного более популярной ajv.


Hello world


Напишем простейшую проверку — является ли значение строкой "Hello World!".


Для того, чтобы сравнить библиотеки валидации необходимы данные, которые мы будем валидировать. Соответственно для этой задачи имеем такие наборы валидных и не валидных данных.


const positives = ["Hello World!"];
const negatives = [null, false, undefined, "", 1, Infinity, "Hello World"];

ajv


Как всегда всё начинается с импорта:


const Ajv = require("ajv");

Создадим экземпляр "компилятора":


const ajv = new Ajv();

Ajv на вход принимает описание валидируемого типа в виде JSON схемы.


Давайте создадим соответствующую схему для нашей задачи


const helloWorldSchema = {
  type: "string",
  enum: ["Hello World!"]
};

Далее необходимо "скомпилировать" функцию валидации, то есть из схемы — получить функцию, которая будет ожидать данные на вход, а на выходе возвращать true, если валидация прошла успешно, в ином случае будет возвращать false.


const ajvValidator = ajv.compile(helloWorldSchema);

Готово!


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


Проведя пять итераций замеров, на выходе имеем такие результаты:


Ajv Build
661,639 ops/sec
354,725 ops/sec
628,443 ops/sec
659,900 ops/sec
557,037 ops/sec

Среднее: 572,349 ops/sec

Теперь произведём замер производительности валидации:


for (let i = 0; i < positives.length; i++) {
  ajvValidator(positives[i]);
}
for (let i = 0; i < negatives.length; i++) {
  ajvValidator(negatives[i]);
}

Пять замеров и результат:


Ajv Validation

21,452,228 ops/sec
 3,066,770 ops/sec
 4,522,850 ops/sec
 2,522,777 ops/sec
 2,741,310 ops/sec

Среднее: 6,861,187 ops/sec

Первый замер вышел довольно странно производительным — но что есть, то есть.


quartet


Импортируем єкземпляр "комплиятора"


const { v } = require("quartet");

Скомпилируем функцию валидации:


const validator = v("Hello World!");

Пять замеров производительности компиляции:


Quartet 9: Allegro Build

6,019,078 ops/sec
3,893,780 ops/sec
2,712,363 ops/sec
5,926,415 ops/sec
2,729,369 ops/sec

Среднее: 4,256,201 ops/sec

Теперь произведём замер производительности валидации:


for (let i = 0; i < positives.length; i++) {
  validator(positives[i]);
}
for (let i = 0; i < negatives.length; i++) {
  validator(negatives[i]);
}

Пять замеров:


Quartet 9: Allegro Validation

15,073,432 ops/sec
13,711,573 ops/sec
13,123,812 ops/sec
25,617,225 ops/sec
17,588,846 ops/sec

Среднее: 17,022,977 ops/sec

Имеем такие результаты сравнения:


image


image


Причины


Причина такой большой разницы становится ясна из следующего кода:


console.log("Function");
console.log(validator.toString());

Результат:


function (value) { return value === c; }

Здесь c — это то значение, которое мы передали в параметр.


Итоги


На таком примере не нужно делать никаких выводов. Мы просто рассмотрели основной порядок действий, который нужно произвести, чтобы получить на выходе функцию валидации.


Мы все живые люди


Теперь приведём более реальный пример. Пусть мы получили данные про человека со стороннего API. Мы ожидаем, что эти данные будут следующего типа:


interface Person {
  id: number; // положительное целое число
  name: string; // непустая строка
  phone: string | null; // null или 12 цифр номера
  phoneBook: {
    [name: string]: string; // 12 цифр номер
  };
  gender: "male" | "female";
}

Будем производить замеры на таких наборах данных


Позитивные и негативные случаи для валидации
const positives = [
  {
    id: 1,
    name: "andrew",
    phone: "380975003434",
    phoneBook: {
      andrew: "380975003434",
      bohdan: "380975003434",
      vasilina: "380975003434"
    },
    gender: "male"
  },
  {
    id: 2,
    name: "bohdan",
    phone: null,
    phoneBook: {},
    gender: "male"
  },
  {
    id: 3,
    name: "Elena",
    phone: null,
    phoneBook: {
      siroja: "380975003434"
    },
    gender: "female"
  }
];

const negatives = [
  null, // не объект
  false, // не объект
  undefined, // не объект
  "", // не объект
  1, // не объект
  Infinity, // не объект
  "Hello World", // не объект
  {
    id: 0, // не положительное число
    name: "andrew",
    phone: "380975003434",
    phoneBook: {
      andrew: "380975003434",
      bohdan: "380975003434",
      vasilina: "380975003434"
    },
    gender: "male"
  },
  {
    // отсутствует id
    name: "andrew",
    phone: "380975003434",
    phoneBook: {
      andrew: "380975003434",
      bohdan: "380975003434",
      vasilina: "380975003434"
    },
    gender: "male"
  },
  {
    id: 1.5, // Не целое
    name: "andrew",
    phone: "380975003434",
    phoneBook: {
      andrew: "380975003434",
      bohdan: "380975003434",
      vasilina: "380975003434"
    },
    gender: "male"
  },
  {
    id: 1,
    name: "", // пустая строка
    phone: "380975003434",
    phoneBook: {
      andrew: "380975003434",
      bohdan: "380975003434",
      vasilina: "380975003434"
    },
    gender: "male"
  },
  {
    id: 1,
    // отсутствует name
    phone: "380975003434",
    phoneBook: {
      andrew: "380975003434",
      bohdan: "380975003434",
      vasilina: "380975003434"
    },
    gender: "male"
  },
  {
    id: 1,
    name: "andrew",
    phone: "38097500434", // 11 цифр
    phoneBook: {
      andrew: "380975003434",
      bohdan: "380975003434",
      vasilina: "380975003434"
    },
    gender: "male"
  },
  {
    id: 1,
    name: "andrew",
    // отсутствует phone
    phoneBook: {
      andrew: "380975003434",
      bohdan: "380975003434",
      vasilina: "380975003434"
    },
    gender: "male"
  },
  {
    id: 1,
    name: "andrew",
    phone: "380975003434",
    phoneBook: {
      andrew: "380975003434",
      bohdan: "380975003434",
      vasilina: "38097503434" // 11 цифр
    },
    gender: "male"
  },
  {
    id: 1,
    name: "andrew",
    phone: "380975003434",
    // phoneBook отсутствует
    gender: "male"
  },
  {
    id: 1,
    name: "andrew",
    phone: "380975003434",
    phoneBook: {
      andrew: "380975003434",
      bohdan: "380975003434",
      vasilina: "380975003434"
    },
    gender: "Male" // 'male'
  },
  {
    id: 1,
    name: "andrew",
    phone: "380975003434",
    phoneBook: {
      andrew: "380975003434",
      bohdan: "380975003434",
      vasilina: "380975003434"
    }
  }
];

ajv


Создадим схему:


const personSchema = {
  type: "object",
  required: ["id", "name", "phone", "phoneBook", "gender"],
  properties: {
    id: {
      type: "integer",
      exclusiveMinimum: 0
    },
    name: {
      type: "string",
      minLength: 1
    },
    phone: {
      anyOf: [
        { type: "null" },
        {
          type: "string",
          pattern: "^\\d{12}$"
        }
      ]
    },
    phoneBook: {
      type: "object",
      additionalProperties: {
        type: "string",
        pattern: "^\\d{12}$"
      }
    },
    gender: {
      type: "string",
      enum: ["male", "female"]
    }
  }
};

Скомпилируем:


const ajvCheckPerson = ajv.compile(personSchema);

Проведя десять замеров имеем такую производительность:


Ajv Build

79,476 ops/sec
78,334 ops/sec
61,752 ops/sec
77,395 ops/sec
78,539 ops/sec
51,922 ops/sec
80,031 ops/sec
77,687 ops/sec
65,439 ops/sec
79,805 ops/sec

Среднее: 73,038 ops/sec

Проведём замеры валидаций:


for (let i = 0; i < positives.length; i++) {
  ajvCheckPerson(positives[i]);
}
for (let i = 0; i < negatives.length; i++) {
  ajvCheckPerson(negatives[i]);
}

Десять итераций замеров:


Ajv Validation

227,640 ops/sec
301,134 ops/sec
190,450 ops/sec
195,595 ops/sec
384,380 ops/sec
193,358 ops/sec
385,280 ops/sec
239,009 ops/sec
193,832 ops/sec
392,808 ops/sec

Среднее: 270,349 ops/sec

quartet


Скомпилируем функцию валидации:


const checkPerson = v({
  id: v.and(v.safeInteger, v.positive),
  name: v.and(v.string, v.minLength(1)),
  phone: [null, v.test(/^\d{12}$/)],
  phoneBook: {
    [v.rest]: v.test(/^\d{12}$/)
  },
  gender: ["male", "female"]
});

Десять замеров производительности:


Quartet 9: Allegro Build

35,564 ops/sec
14,401 ops/sec
15,438 ops/sec
26,852 ops/sec
33,935 ops/sec
16,010 ops/sec
34,550 ops/sec
33,148 ops/sec
16,037 ops/sec
36,828 ops/sec

Среднее: 26,276 ops/sec

Проведём замеры производительности валидаций:


for (let i = 0; i < positives.length; i++) {
  checkPerson(positives[i]);
}
for (let i = 0; i < negatives.length; i++) {
  checkPerson(negatives[i]);
}

Десять итераций, результат:


Quartet 9: Allegro Validation

237,059 ops/sec
435,844 ops/sec
248,021 ops/sec
238,931 ops/sec
416,993 ops/sec
281,904 ops/sec
439,975 ops/sec
242,074 ops/sec
330,487 ops/sec
421,704 ops/sec

Среднее: 329,299 ops/sec

Сравним теперь результаты обоих библиотек:


image
image


Причины


Причины такой производительности валидации, и такого отставания во времени компиляции станут ясными, когда мы посмотрим на код функции checkPerson и её поля и методы.


console.log(checkPerson.toString());
console.log({ ...checkPerson });

Результат


function validator(value) {
  if (value == null) return false
  if (!validator.and(value.id)) return false
  if (!validator["and-1"](value.name)) return false
  if (!validator["value.phone"](value.phone)) return false
  if (value.phoneBook == null) return false
  validator.keys = Object.keys(value.phoneBook)
  for (let i = 0; i < validator.keys.length; i++) {
    validator.elem = value.phoneBook[validator.keys[i]]
    if (!validator["tester-1"].test(validator.elem)) return false
  }

  if (!validator["value.gender"](value.gender)) return false
  return true
};

// Check person properties
{
  and: function validator(value) {
    if (!Number.isSafeInteger(value)) return false
    if (value <= 0) return false
    return true
  },
  'and-1': validator(value) {
    if (typeof value !== 'string') return false
    if (value == null || value.length < 1) return false
    return true
  },
  'value.phone': function validator(value) {
    if (value === null) return true;
    if (validator.tester.test(value)) return true;
    return false
  }
  ['value.phone']['tester']: /^\d{12}$/,
  'tester-1': /^\d{12}$/,
  'value.gender': function validator(value) {
    if (validator.__validValuesDict[value] === true) return true
    return false
  },
  ['value.gender']['__validValuesDict']: {
    male: true,
    female: true
  }
}

Код сгенерированный алгоритмом — не самый легкий для чтения, но при медленном рассмотрении станет ясно — что он действительно проводит проверку типа и довольно еффективно.


Итоги


Я воодушевлён таким результатом. Надеюсь читателю захочется опробовать quartet@9 на деле.


Спасибо за внимание, интересно почитать комментарии.

Источник: https://habr.com/ru/post/494632/


Интересные статьи

Интересные статьи

Прим. перев.: эта статья — итоги мини-исследования, проведенного инженерами IBM Cloud в поисках решения реальной проблемы, связанной с эксплуатацией базы данных etcd. Для нас была акт...
Привет, Хабр! Представляю Вашему вниманию перевод статьи «Improve SPA performance by splitting your Angular libraries in multiple chunks» автора Kevin Kreuzer. Angular — отличный фреймворк. Мы в...
Но если для интернет-магазина, разработанного 3–4 года назад «современные» ошибки вполне простительны потому что перед разработчиками «в те далекие времена» не стояло таких задач, то в магазинах, сдел...
Однажды, в понедельник, мне пришла в голову мысль — "а покопаюсь ка я в новом ядре" (новым относительно, но об этом позже). Мысль не появилась на ровном месте, а предпосылками для нее стали: ...
Бизнес-смыслы появились в Битриксе в начале 2016 года, но мало кто понимает, как их правильно использовать для удобной настройки интернет-магазинов.