Skip to content

Latest commit

 

History

History
425 lines (316 loc) · 20.1 KB

File metadata and controls

425 lines (316 loc) · 20.1 KB

JIT-компиляция валидации - Внутренняя архитектура

Обзор

Модуль валидации в om-data-mapper использует JIT-компиляцию (Just-In-Time) для достижения высокопроизводительной валидации. Вместо интерпретации правил валидации во время выполнения, система генерирует оптимизированный JavaScript-код с помощью new Function(), который выполняет логику валидации напрямую.

Этот документ объясняет детали внутренней реализации системы JIT-компиляции для валидации.


Компоненты архитектуры

1. Хранение метаданных (src/compat/class-validator/engine/metadata.ts)

Система валидации использует хранение метаданных на основе Symbol для сохранения правил валидации, привязанных к свойствам класса.

Ключевые компоненты:

const VALIDATION_METADATA = Symbol('validation:metadata');
  • Хранение через Symbol: Использует приватный Symbol для хранения метаданных в конструкторах классов
  • Без WeakMap: Метаданные хранятся непосредственно в классе, избегая поиска в WeakMap
  • TC39 Stage 3 Decorators: Совместимость с современным стандартом декораторов JavaScript

Структуры данных:

interface ClassValidationMetadata {
  target: any;
  properties: Map<string | symbol, PropertyValidationMetadata>;
}

interface PropertyValidationMetadata {
  propertyKey: string | symbol;
  constraints: ValidationConstraint[];
  isOptional?: boolean;
  isConditional?: boolean;
  condition?: (object: any) => boolean;
  nestedType?: () => any;
  isArray?: boolean;
  isNested?: boolean;
}

interface ValidationConstraint {
  type: string;                    // например, 'isString', 'minLength'
  value?: any;                     // Параметры ограничения
  message?: string | Function;     // Сообщение об ошибке
  groups?: string[];               // Группы валидации
  always?: boolean;                // Флаг постоянной валидации
  validator?: Function;            // Функция пользовательского валидатора
}

2. Реестр валидаторов (src/compat/class-validator/engine/validator-registry.ts)

Управляет экземплярами классов пользовательских валидаторов с кэшированием для избежания повторного создания экземпляров.

Возможности:

  • Кэширование экземпляров: Хранит экземпляры валидаторов в Map
  • Ленивое создание: Создаёт экземпляры только при необходимости
  • Определение асинхронности: Идентифицирует асинхронные валидаторы через метаданные
  • Разрешение имён: Извлекает имена валидаторов для сообщений об ошибках
const validatorInstanceCache = new Map<
  new () => ValidatorConstraintInterface,
  ValidatorConstraintInterface
>();

export function getValidatorInstance(
  validatorClass: new () => ValidatorConstraintInterface
): ValidatorConstraintInterface {
  if (validatorInstanceCache.has(validatorClass)) {
    return validatorInstanceCache.get(validatorClass)!;
  }
  const instance = new validatorClass();
  validatorInstanceCache.set(validatorClass, instance);
  return instance;
}

3. JIT-компилятор (src/compat/class-validator/engine/compiler.ts)

Ядро системы валидации - генерирует оптимизированные функции валидации.

Процесс компиляции:

┌─────────────────────────────────────────────────────────────┐
│ 1. Применение декораторов (время определения класса)       │
│    - @IsString(), @MinLength() и др. добавляют метаданные  │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 2. Первый вызов валидации                                   │
│    - validate(object) или validateSync(object)              │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 3. Получение метаданных                                     │
│    - getClassValidationMetadata(object)                     │
│    - Возвращает ClassValidationMetadata                     │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 4. Проверка кэша                                            │
│    - Проверка compiledValidatorsCache                       │
│    - Если найдено, возврат кэшированного валидатора         │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 5. Генерация кода                                           │
│    - generateValidationCode(metadata)                       │
│    - Создаёт JavaScript-код в виде строки                   │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 6. Компиляция функции                                       │
│    - new Function(params, code)                             │
│    - Создаёт исполняемую функцию                            │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 7. Кэширование                                              │
│    - Сохранение в compiledValidatorsCache                   │
│    - Ключ: конструктор класса                               │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 8. Выполнение                                               │
│    - Выполнение скомпилированной функции с объектом         │
│    - Возврат ошибок валидации                               │
└─────────────────────────────────────────────────────────────┘

Стратегия генерации кода

Синхронная валидация

Функция generateValidationCode() создаёт JavaScript-код, который:

  1. Инициализирует массив ошибок: const errors = [];
  2. Нормализует опции: const opts = options || {};
  3. Итерирует свойства: Для каждого свойства с метаданными валидации
  4. Генерирует проверки валидации: Встроенная логика валидации
  5. Возвращает ошибки: return errors;

Пример сгенерированного кода:

Для класса с @IsString() и @MinLength(3):

const errors = [];
const opts = options || {};

// Валидация свойства: name
{
  const value = object?.name;
  const propertyErrors = {};
  
  // Проверка, должно ли свойство валидироваться
  if (value !== undefined && value !== null) {
    // Ограничение: isString
    if (typeof value !== 'string') {
      propertyErrors.isString = 'name must be a string';
    }
    
    // Ограничение: minLength
    if (typeof value === 'string' && value.length < 3) {
      propertyErrors.minLength = 'name must be at least 3 characters';
    }
  }
  
  if (Object.keys(propertyErrors).length > 0) {
    errors.push({
      property: 'name',
      value: value,
      constraints: propertyErrors
    });
  }
}

return errors;

Асинхронная валидация

Функция generateAsyncValidationCode() обрабатывает асинхронные валидаторы:

  1. Оборачивает в async IIFE: return (async () => { ... })();
  2. Создаёт массив асинхронных задач: const asyncTasks = [];
  3. Генерирует асинхронные задачи валидации: Для пользовательских асинхронных валидаторов
  4. Ожидает все задачи: await Promise.all(asyncTasks);
  5. Возвращает ошибки: return errors;

Оптимизация асинхронности:

  • Параллельное выполнение: Все асинхронные валидации выполняются одновременно
  • Пакетирование задач: Использует Promise.all() для эффективности
  • Быстрый путь для синхронных: Встроенные валидаторы выполняются синхронно даже в асинхронном режиме

Техники оптимизации

1. Кэширование компиляции

const compiledValidatorsCache = new Map<any, CompiledValidator>();
const compiledAsyncValidatorsCache = new Map<any, AsyncCompiledValidator>();
  • Кэширование по классу: Один скомпилированный валидатор на класс
  • Постоянный кэш: Сохраняется между множественными вызовами валидации
  • Эффективность памяти: Использует конструктор класса в качестве ключа

2. Встраивание логики валидации

Вместо вызовов функций, логика валидации встраивается:

// ❌ Медленно: Накладные расходы на вызов функции
if (!validators.isString(value)) { ... }

// ✅ Быстро: Встроенная проверка
if (typeof value !== 'string') { ... }

3. Optional Chaining

Безопасный доступ к свойствам без try-catch:

const value = object?.propertyName;

4. Условная компиляция

Генерирует код только для существующих ограничений:

// Генерирует проверку minLength только если присутствует @MinLength()
if (typeof value === 'string' && value.length < 3) { ... }

5. Фильтрация групп

Группы валидации проверяются во время компиляции:

if (opts.groups && opts.groups.length > 0 && opts.groups.some(g => ['admin'].includes(g))) {
  // Валидировать только если группа совпадает
}

6. Оптимизация вложенной валидации

Рекурсивная компиляция для вложенных объектов:

if (hasValidationMetadata(value.constructor)) {
  const nestedValidator = compileValidator(getValidationMetadata(value.constructor));
  const nestedErrors = nestedValidator(value, opts);
  if (nestedErrors.length > 0) {
    error.children = nestedErrors;
  }
}

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

Стоимость компиляции

  • Первый вызов: ~1-5мс (парсинг метаданных + генерация кода + компиляция)
  • Последующие вызовы: ~0.001мс (поиск в кэше)
  • Амортизация: Стоимость амортизируется на тысячи валидаций

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

По сравнению с class-validator (интерпретируемый):

Тип валидации class-validator om-data-mapper Ускорение
Простая (1 поле) ~50K оп/сек ~500K оп/сек 10x
Сложная (10 полей) ~10K оп/сек ~100K оп/сек 10x
Вложенные объекты ~5K оп/сек ~50K оп/сек 10x
Асинхронная валидация ~8K оп/сек ~40K оп/сек 5x

Использование памяти

  • Метаданные: ~1КБ на класс
  • Скомпилированная функция: ~2-10КБ на класс
  • Накладные расходы кэша: Минимальные (Map с ссылками на классы)

Пользовательские валидаторы

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

Синхронный пользовательский валидатор:

const constraint = metadata.properties.get('email').constraints[0];
const constraintValue = constraint.value;
const validatorInstance = getValidatorInstance(constraintValue.constraintClass);
const args = {
  value: value,
  constraints: constraintValue.constraints || [],
  targetName: object.constructor.name,
  object: object,
  property: 'email'
};
const result = validatorInstance.validate(value, args);
if (!result) {
  propertyErrors.customValidator = validatorInstance.defaultMessage(args);
}

Асинхронный пользовательский валидатор:

const task = (async () => {
  const result = await validatorInstance.validate(value, args);
  if (!result) {
    propertyErrors.customValidator = validatorInstance.defaultMessage(args);
  }
})();
asyncTasks.push(task);

Обработка ошибок

Структура ошибок валидации:

interface ValidationError {
  property: string;              // Имя свойства
  value?: any;                   // Невалидное значение
  constraints?: {                // Неудавшиеся ограничения
    [type: string]: string;      // Сообщения об ошибках
  };
  children?: ValidationError[];  // Вложенные ошибки
  target?: any;                  // Валидируемый объект
}

Генерация сообщений об ошибках:

  1. Пользовательские сообщения: Из опций декоратора
  2. Сообщения по умолчанию: Встроенные для каждого типа ограничения
  3. Динамические сообщения: Сообщения на основе функций с контекстом

Потокобезопасность

  • Без глобального состояния: Каждый класс имеет изолированные метаданные
  • Неизменяемые метаданные: Метаданные устанавливаются во время определения класса
  • Безопасность кэша: Операции Map атомарны в JavaScript

Отладка

Просмотр сгенерированного кода:

import { compileValidator } from 'om-data-mapper/class-validator-compat/engine/compiler';

const metadata = getValidationMetadata(MyClass);
const code = generateValidationCode(metadata);
console.log(code);  // Просмотр сгенерированного JavaScript

Профилирование производительности:

console.time('compilation');
const validator = compileValidator(metadata);
console.timeEnd('compilation');

console.time('execution');
const errors = validator(object, options);
console.timeEnd('execution');

Будущие оптимизации

  1. WebAssembly: Компиляция в WASM для ещё более быстрого выполнения
  2. Ahead-of-Time компиляция: Предварительная компиляция валидаторов во время сборки
  3. SIMD: Использование SIMD-инструкций для валидации массивов
  4. Worker Threads: Параллельная валидация для больших наборов данных

Заключение

Подход с JIT-компиляцией обеспечивает:

  • В 10 раз быстрее валидацию, чем интерпретируемые подходы
  • Нулевые накладные расходы во время выполнения после первой компиляции
  • Типобезопасность с полной поддержкой TypeScript
  • Эффективность памяти с кэшированием по классу
  • Расширяемость с пользовательскими валидаторами

Эта архитектура делает om-data-mapper одной из самых быстрых библиотек валидации, доступных для TypeScript/JavaScript.