Хабрахабр

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

Когда создавалась библиотека для валидации данных 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 Build661,639 ops/sec354,725 ops/sec628,443 ops/sec659,900 ops/sec557,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/sec3,893,780 ops/sec2,712,363 ops/sec5,926,415 ops/sec2,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/sec13,711,573 ops/sec13,123,812 ops/sec25,617,225 ops/sec17,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/sec78,334 ops/sec61,752 ops/sec77,395 ops/sec78,539 ops/sec51,922 ops/sec80,031 ops/sec77,687 ops/sec65,439 ops/sec79,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/sec301,134 ops/sec190,450 ops/sec195,595 ops/sec384,380 ops/sec193,358 ops/sec385,280 ops/sec239,009 ops/sec193,832 ops/sec392,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/sec14,401 ops/sec15,438 ops/sec26,852 ops/sec33,935 ops/sec16,010 ops/sec34,550 ops/sec33,148 ops/sec16,037 ops/sec36,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/sec435,844 ops/sec248,021 ops/sec238,931 ops/sec416,993 ops/sec281,904 ops/sec439,975 ops/sec242,074 ops/sec330,487 ops/sec421,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 на деле.

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

Показать больше

Похожие публикации

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»