Skip to content

Latest commit

 

History

History
593 lines (451 loc) · 18 KB

File metadata and controls

593 lines (451 loc) · 18 KB

Transformer JIT Compilation - Internal Architecture

Overview

The transformer module in om-data-mapper uses Just-In-Time (JIT) compilation to achieve high-performance object transformations. The system generates optimized JavaScript code using new Function() that executes transformation logic directly, eliminating the overhead of runtime interpretation.

This document explains the internal implementation details of the JIT compilation system for the Transformer module.


Architecture Components

1. Metadata Storage

The transformer uses two different metadata storage systems depending on the API:

Decorator API (src/decorators/metadata.ts)

Uses Symbol-based metadata storage for the native decorator API:

const MAPPER_METADATA = Symbol('om-data-mapper:metadata');

interface MapperMetadata {
  properties: Map<string | symbol, PropertyMapping>;
  options?: MapperOptions;
}

interface PropertyMapping {
  type: 'path' | 'transform' | 'nested' | 'ignore';
  sourcePath?: string;
  transformer?: Function;
  transformValue?: Function;
  nestedMapper?: any;
  defaultValue?: any;
  condition?: Function;
}

class-transformer Compatibility API (src/compat/class-transformer/metadata.ts)

Uses WeakMap-based metadata storage for class-transformer compatibility:

const metadataStorage = new WeakMap<Function, ClassMetadata>();

interface ClassMetadata {
  properties: Map<string | symbol, PropertyMetadata>;
  classOptions?: {
    expose?: boolean;
    exclude?: boolean;
  };
}

interface PropertyMetadata {
  expose?: boolean;
  exclude?: boolean;
  exposeOptions?: ExposeOptions;
  excludeOptions?: ExcludeOptions;
  typeFunction?: TypeHelpFunction;
  transformFn?: TransformFn;
  name?: string;  // Property name mapping
}

Key Differences:

  • Decorator API: Symbol-based, attached to class constructor
  • Compatibility API: WeakMap-based, prevents memory leaks
  • Both: Use TC39 Stage 3 decorators

2. JIT Compilation Process

┌─────────────────────────────────────────────────────────────┐
│ 1. Decorator Application (Class Definition Time)           │
│    - @Map(), @MapFrom(), @Transform(), etc.                │
│    - Metadata stored on class constructor                  │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 2. Class Instantiation                                      │
│    - new UserMapper() or plainToInstance(UserMapper, data) │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 3. Compilation Trigger (context.addInitializer)            │
│    - Runs once during first instantiation                  │
│    - Calls _compileMapper()                                │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 4. Metadata Retrieval                                       │
│    - getMapperMetadata(this.constructor)                    │
│    - Returns MapperMetadata with all property mappings     │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 5. Code Generation                                          │
│    - For each property: _generatePropertyCode()             │
│    - Produces JavaScript code strings                       │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 6. Function Compilation                                     │
│    - new Function('source', 'target', '__errors', code)     │
│    - Creates executable transformation function             │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 7. Mapper Creation                                          │
│    - Wraps compiled function in BaseMapper-compatible API   │
│    - Stores in compiledMapper variable                      │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 8. Execution                                                │
│    - mapper.transform(source) calls compiled function       │
│    - Returns transformed object                             │
└─────────────────────────────────────────────────────────────┘

Code Generation Strategy

1. Path Mapping (@Map('sourcePath'))

Generates safe property access with optional chaining:

// Decorator: @Map('user.profile.email')
// Generated code:
target.email = source?.user?.profile?.email;

// With default value: @Map('score') @Default(0)
target.score = source?.score ?? cache['__defValues']['score'];

// With value transform: @Map('name') @Transform(v => v.toUpperCase())
target.name = cache['name__valueTransform'](source?.name);

Optimization: Uses optional chaining (?.) instead of try-catch for null safety.

2. Transform Function (@MapFrom(fn))

Stores transformer in cache and calls it:

// Decorator: @MapFrom((src) => src.firstName + ' ' + src.lastName)
// Generated code:
target.fullName = cache['fullName__transformer'](source);

// With default: @MapFrom(fn) @Default('Unknown')
target.fullName = cache['fullName__transformer'](source) ?? cache['__defValues']['fullName'];

// With condition: @When(condition) @MapFrom(fn)
if (cache['fullName__condition'](source)) {
  target.fullName = cache['fullName__transformer'](source);
}

Optimization: Functions stored in cache to avoid closure overhead.

3. Nested Mapper (@MapNested(NestedMapper))

Recursively calls nested mapper:

// Decorator: @MapNested(AddressMapper)
// Generated code:
if (source?.address) {
  const nestedMapper = cache['address__nestedMapper'];
  const nestedResult = nestedMapper.execute(source.address);
  target.address = nestedResult.result;
  if (nestedResult.errors.length > 0) {
    __errors.push(...nestedResult.errors.map(e => 'address.' + e));
  }
}

Optimization: Nested mappers are pre-compiled and cached.

4. Array Mapping

Handles array transformations:

// Decorator: @MapFrom((src) => src.items.map(i => i.name))
// Generated code:
target.itemNames = cache['itemNames__transformer'](source);

// For nested arrays with mapper:
if (Array.isArray(source?.items)) {
  target.items = source.items.map(item => {
    const nestedMapper = cache['items__nestedMapper'];
    return nestedMapper.execute(item).result;
  });
}

Safe Property Access Generation

The generateSafePropertyAccess() function converts dot-notation paths to optional chaining:

function generateSafePropertyAccess(sourcePath: string): string {
  const parts = sourcePath.split('.');
  if (parts.length === 1) {
    return sourcePath;
  }
  return parts.join('?.');
}

// Examples:
// 'name' → 'name'
// 'user.name' → 'user?.name'
// 'user.profile.email' → 'user?.profile?.email'

Why Optional Chaining?

  • ✅ Faster than try-catch
  • ✅ More readable generated code
  • ✅ Native JavaScript feature (ES2020+)
  • ✅ No runtime overhead

Error Handling Strategies

Unsafe Mode (Default)

No error handling - maximum performance:

// Generated code (unsafe mode):
target.name = source?.firstName;
target.email = source?.user?.email;

Use when: Data is trusted and performance is critical.

Safe Mode

Wraps each property in try-catch:

// Generated code (safe mode):
try {
  target.name = source?.firstName;
} catch (error) {
  __errors.push("Mapping error at field 'name': " + error.message);
}

try {
  target.email = source?.user?.email;
} catch (error) {
  __errors.push("Mapping error at field 'email': " + error.message);
}

Use when: Data is untrusted or debugging is needed.


Optimization Techniques

1. Pre-Compilation

Mappers are compiled once at class instantiation:

// Compilation happens here (once)
const mapper = new UserMapper();

// Execution is fast (uses pre-compiled function)
const result1 = mapper.transform(source1);
const result2 = mapper.transform(source2);
const result3 = mapper.transform(source3);

2. Function Caching

Transformer functions are stored in a cache object:

const cache = {
  'fullName__transformer': (src) => src.firstName + ' ' + src.lastName,
  'age__condition': (src) => src.age !== undefined,
  '__defValues': { score: 0, status: 'active' }
};

Benefits:

  • Avoids closure overhead
  • Enables function reuse
  • Simplifies generated code

3. Inline Code Generation

Simple operations are inlined instead of function calls:

// ❌ Slow: Function call
target.name = transformName(source.firstName);

// ✅ Fast: Inlined
target.name = source?.firstName;

4. Conditional Compilation

Only generates code for properties that exist:

// If no @Ignore() decorator, generates code
// If @Ignore() decorator, skips code generation
if (mapping.type === 'ignore') {
  continue;  // Skip this property
}

5. Optional Chaining

Uses native optional chaining instead of manual null checks:

// ❌ Slow: Manual checks
if (source && source.user && source.user.profile) {
  target.email = source.user.profile.email;
}

// ✅ Fast: Optional chaining
target.email = source?.user?.profile?.email;

class-transformer Compatibility Layer

The compatibility layer provides a different transformation approach:

Transformation Flow:

plainToClass(UserDto, plainObject)
         ↓
transformPlainToClass(UserDto, plainObject, 'plainToClass', options)
         ↓
1. Create instance: new UserDto()
2. Get metadata: getCompatMetadata(UserDto)
3. For each property:
   - Check if should expose: shouldExposeProperty()
   - Get source property name: getSourcePropertyName()
   - Transform value: transformValue()
   - Apply @Transform function if exists
   - Apply @Type transformation if exists
   - Set on instance
4. Return instance

Key Differences from Decorator API:

Feature Decorator API class-transformer Compat
Compilation JIT at instantiation Interpreted at runtime
Performance 10x faster Compatible with class-transformer
Metadata Symbol-based WeakMap-based
API @Map(), @MapFrom() @Expose(), @Type()
Use Case New projects Migration from class-transformer

Performance Characteristics

Compilation Cost

  • First Instantiation: ~1-3ms (metadata parsing + code generation + compilation)
  • Subsequent Instantiations: ~0.001ms (uses same compiled function)
  • Amortization: Cost is amortized over thousands of transformations

Execution Performance

Compared to class-transformer:

Transformation Type class-transformer om-data-mapper Speedup
Simple mapping 326K ops/sec 3.2M ops/sec 10x
Complex transformations 150K ops/sec 1.5M ops/sec 10x
Nested objects 80K ops/sec 800K ops/sec 10x
Array transformations 50K ops/sec 500K ops/sec 10x

Memory Usage

  • Metadata: ~500 bytes per property
  • Compiled Function: ~1-5KB per mapper class
  • Cache Object: ~100 bytes per transformer function
  • Total: ~5-10KB per mapper class

Generated Code Examples

Example 1: Simple Mapping

@Mapper<Source, Target>()
class UserMapper {
  @Map('firstName')
  name!: string;

  @Map('email')
  email!: string;
}

// Generated code:
function transform(source, target, __errors, cache) {
  target.name = source?.firstName;
  target.email = source?.email;
}

Example 2: Complex Transformations

@Mapper<Source, Target>()
class UserMapper {
  @MapFrom((src) => src.firstName + ' ' + src.lastName)
  fullName!: string;

  @MapFrom((src) => src.age >= 18)
  isAdult!: boolean;

  @Default(0)
  @Map('score')
  score!: number;
}

// Generated code:
function transform(source, target, __errors, cache) {
  target.fullName = cache['fullName__transformer'](source);
  target.isAdult = cache['isAdult__transformer'](source);
  target.score = source?.score ?? cache['__defValues']['score'];
}

Example 3: Conditional Mapping

@Mapper<Source, Target>()
class UserMapper {
  @When((src) => src.isPremium)
  @Map('premiumFeatures')
  features?: string[];
}

// Generated code:
function transform(source, target, __errors, cache) {
  if (cache['features__condition'](source)) {
    target.features = source?.premiumFeatures;
  }
}

Example 4: Nested Mapping

@Mapper<Source, Target>()
class UserMapper {
  @MapNested(AddressMapper)
  address!: Address;
}

// Generated code:
function transform(source, target, __errors, cache) {
  if (source?.address) {
    const nestedMapper = cache['address__nestedMapper'];
    const nestedResult = nestedMapper.execute(source.address);
    target.address = nestedResult.result;
    if (nestedResult.errors.length > 0) {
      __errors.push(...nestedResult.errors.map(e => 'address.' + e));
    }
  }
}

Debugging Generated Code

View Generated Code:

@Mapper<Source, Target>()
class UserMapper {
  @Map('name')
  name!: string;

  // Add this method to view generated code
  _debugGeneratedCode() {
    const metadata = getMapperMetadata(this.constructor);
    const cache = {};
    const defaultValues = {};
    const codeLines = [];

    for (const [propertyKey, mapping] of metadata.properties) {
      const code = this._generatePropertyCode(
        String(propertyKey),
        mapping,
        cache,
        defaultValues,
        false
      );
      if (code) codeLines.push(code);
    }

    console.log(codeLines.join('\n'));
  }
}

const mapper = new UserMapper();
mapper._debugGeneratedCode();

Performance Profiling:

console.time('compilation');
const mapper = new UserMapper();
console.timeEnd('compilation');

console.time('execution');
const result = mapper.transform(source);
console.timeEnd('execution');

console.time('1000 executions');
for (let i = 0; i < 1000; i++) {
  mapper.transform(source);
}
console.timeEnd('1000 executions');

Thread Safety

  • No Global State: Each mapper instance has isolated compiled function
  • Immutable Metadata: Metadata is set at class definition time
  • Cache Isolation: Each mapper has its own cache object
  • Concurrent Safe: Multiple mappers can run concurrently

Comparison with BaseMapper

The Decorator API uses the same JIT compilation approach as BaseMapper but with better ergonomics:

Feature BaseMapper Decorator API
API Style Imperative Declarative
Type Safety Manual Automatic
Code Generation ✅ Yes ✅ Yes
Performance Fast Faster (less overhead)
Maintainability Medium High
Recommended ❌ Legacy ✅ Modern

Future Optimizations

  1. Ahead-of-Time Compilation: Pre-compile mappers at build time
  2. WebAssembly: Compile to WASM for even faster execution
  3. SIMD: Use SIMD instructions for array transformations
  4. Parallel Execution: Transform arrays in parallel using Worker Threads
  5. Memoization: Cache transformation results for identical inputs

Conclusion

The JIT compilation approach provides:

  • 10x faster than class-transformer
  • Zero runtime overhead after compilation
  • Type-safe with full TypeScript support
  • Memory efficient with function caching
  • Extensible with custom transformers

This architecture makes om-data-mapper one of the fastest object transformation libraries available for TypeScript/JavaScript.