Skip to content

Commit 2f23002

Browse files
committed
perf: series of validation execution optimisations
1 parent ae56f48 commit 2f23002

13 files changed

Lines changed: 423 additions & 136 deletions

File tree

benchmark.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import 'reflect-metadata';
2-
import { validate, IsString, IsInt, IsBoolean, IsEmail, IsOptional, MinLength, MaxLength, Min, Max, IsNotEmpty, ValidateNested } from './src';
2+
import { validate, validateSync, IsString, IsInt, IsBoolean, IsEmail, IsOptional, MinLength, MaxLength, Min, Max, IsNotEmpty, ValidateNested } from './src';
33

44
// --- Classes with inheritance and nesting ---
55
class BaseEntity {
@@ -109,6 +109,18 @@ async function bench(label: string, iterations: number, fn: () => Promise<void>)
109109
console.log(`${label}: ${iterations} iterations in ${elapsed.toFixed(1)}ms (${opsPerSec.toLocaleString()} ops/sec)`);
110110
}
111111

112+
function benchSync(label: string, iterations: number, fn: () => void): void {
113+
// Warmup
114+
for (let i = 0; i < 100; i++) fn();
115+
116+
const start = performance.now();
117+
for (let i = 0; i < iterations; i++) fn();
118+
const elapsed = performance.now() - start;
119+
120+
const opsPerSec = Math.round((iterations / elapsed) * 1000);
121+
console.log(`${label}: ${iterations} iterations in ${elapsed.toFixed(1)}ms (${opsPerSec.toLocaleString()} ops/sec)`);
122+
}
123+
112124
async function main(): Promise<void> {
113125
const iterations = 10_000;
114126
const user = createValidUser();
@@ -136,6 +148,20 @@ async function main(): Promise<void> {
136148
await bench('Valid object with strictGroups', iterations, async () => {
137149
await validate(user, { strictGroups: true });
138150
});
151+
152+
console.log('\n--- validateSync (no Promise overhead) ---\n');
153+
154+
benchSync('Valid object (sync)', iterations, () => {
155+
validateSync(user);
156+
});
157+
158+
benchSync('Invalid object (sync)', iterations, () => {
159+
validateSync(invalidUser);
160+
});
161+
162+
benchSync('Valid object with strictGroups (sync)', iterations, () => {
163+
validateSync(user, { strictGroups: true });
164+
});
139165
}
140166

141167
main().catch(console.error);

src/decorator/array/ArrayContains.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ export const ARRAY_CONTAINS = 'arrayContains';
1010
export function arrayContains(array: unknown, values: any[]): boolean {
1111
if (!Array.isArray(array)) return false;
1212

13-
return values.every(value => array.indexOf(value) !== -1);
13+
const arraySet = new Set(array);
14+
return values.every(value => arraySet.has(value));
1415
}
1516

1617
/**

src/decorator/array/ArrayNotContains.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ export const ARRAY_NOT_CONTAINS = 'arrayNotContains';
1010
export function arrayNotContains(array: unknown, values: any[]): boolean {
1111
if (!Array.isArray(array)) return false;
1212

13-
return values.every(value => array.indexOf(value) === -1);
13+
const arraySet = new Set(array);
14+
return values.every(value => !arraySet.has(value));
1415
}
1516

1617
/**

src/decorator/array/ArrayUnique.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ export function arrayUnique(array: unknown[], identifier?: ArrayUniqueIdentifier
1515
array = array.map(o => (o != null ? identifier(o) : o));
1616
}
1717

18-
const uniqueItems = array.filter((a, b, c) => c.indexOf(a) === b);
19-
return array.length === uniqueItems.length;
18+
return new Set(array).size === array.length;
2019
}
2120

2221
/**

src/decorator/common/IsIn.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const IS_IN = 'isIn';
77
* Checks if given value is in a array of allowed values.
88
*/
99
export function isIn(value: unknown, possibleValues: readonly unknown[]): boolean {
10-
return Array.isArray(possibleValues) && possibleValues.some(possibleValue => possibleValue === value);
10+
return Array.isArray(possibleValues) && possibleValues.includes(value);
1111
}
1212

1313
/**

src/decorator/common/IsNotIn.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const IS_NOT_IN = 'isNotIn';
77
* Checks if given value not in a array of allowed values.
88
*/
99
export function isNotIn(value: unknown, possibleValues: readonly unknown[]): boolean {
10-
return !Array.isArray(possibleValues) || !possibleValues.some(possibleValue => possibleValue === value);
10+
return !Array.isArray(possibleValues) || !possibleValues.includes(value);
1111
}
1212

1313
/**

src/decorator/object/IsNotEmptyObject.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,15 @@ export function isNotEmptyObject(value: unknown, options?: { nullable?: boolean
1414
}
1515

1616
if (options?.nullable === false) {
17-
return !Object.values(value).every(propertyValue => propertyValue === null || propertyValue === undefined);
17+
for (const key in value as object) {
18+
if ((value as object).hasOwnProperty(key)) {
19+
const propertyValue = (value as any)[key];
20+
if (propertyValue !== null && propertyValue !== undefined) {
21+
return true;
22+
}
23+
}
24+
}
25+
return false;
1826
}
1927

2028
for (const key in value) {

src/decorator/typechecker/IsEnum.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const IS_ENUM = 'isEnum';
77
* Checks if a given value is the member of the provided enum.
88
*/
99
export function isEnum(value: unknown, entity: any): boolean {
10-
const enumValues = Object.keys(entity).map(k => entity[k]);
10+
const enumValues = Object.values(entity);
1111
return enumValues.includes(value);
1212
}
1313

@@ -24,12 +24,13 @@ function validEnumValues(entity: any): string[] {
2424
* Checks if a given value is the member of the provided enum.
2525
*/
2626
export function IsEnum(entity: object, validationOptions?: ValidationOptions): PropertyDecorator {
27+
const enumValuesSet = new Set(Object.values(entity));
2728
return ValidateBy(
2829
{
2930
name: IS_ENUM,
3031
constraints: [entity, validEnumValues(entity)],
3132
validator: {
32-
validate: (value, args): boolean => isEnum(value, args?.constraints[0]),
33+
validate: (value): boolean => enumValuesSet.has(value),
3334
defaultMessage: buildMessage(
3435
eachPrefix => eachPrefix + '$property must be one of the following values: $constraint2',
3536
validationOptions

src/metadata/MetadataStorage.ts

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,20 @@ import { ConstraintMetadata } from './ConstraintMetadata';
33
import { ValidationSchema } from '../validation-schema/ValidationSchema';
44
import { ValidationSchemaToMetadataTransformer } from '../validation-schema/ValidationSchemaToMetadataTransformer';
55
import { getGlobal } from '../utils';
6+
import { ValidationTypes } from '../validation/ValidationTypes';
7+
8+
export interface PartitionedPropertyMetadata {
9+
defined: ValidationMetadata[];
10+
custom: ValidationMetadata[];
11+
nested: ValidationMetadata[];
12+
conditional: ValidationMetadata[];
13+
all: ValidationMetadata[];
14+
hasPromiseValidation: boolean;
15+
/** True when only custom validators exist (no defined/nested/conditional) — enables fast path */
16+
customOnly: boolean;
17+
}
18+
19+
export type PartitionedMetadata = Record<string, PartitionedPropertyMetadata>;
620

721
/**
822
* Storage all metadatas.
@@ -18,6 +32,7 @@ export class MetadataStorage {
1832
private constraintMetadatas: Map<any, ConstraintMetadata[]> = new Map();
1933
private targetMetadataCache: Map<string, ValidationMetadata[]> = new Map();
2034
private groupedMetadataCache: Map<string, Record<string, ValidationMetadata[]>> = new Map();
35+
private partitionedMetadataCache: Map<string, PartitionedMetadata> = new Map();
2136

2237
get hasValidationMetaData(): boolean {
2338
return !!this.validationMetadatas.size;
@@ -41,6 +56,7 @@ export class MetadataStorage {
4156
addValidationMetadata(metadata: ValidationMetadata): void {
4257
this.targetMetadataCache.clear();
4358
this.groupedMetadataCache.clear();
59+
this.partitionedMetadataCache.clear();
4460

4561
const existingMetadata = this.validationMetadatas.get(metadata.target);
4662

@@ -89,6 +105,57 @@ export class MetadataStorage {
89105
return grouped;
90106
}
91107

108+
/**
109+
* Returns pre-partitioned metadata grouped by property name, with each property's
110+
* metadata split by type. Cached for repeated validations of the same class.
111+
*/
112+
getPartitionedMetadata(
113+
groupedMetadatas: Record<string, ValidationMetadata[]>,
114+
cacheKey: string
115+
): PartitionedMetadata {
116+
const cached = this.partitionedMetadataCache.get(cacheKey);
117+
if (cached) return cached;
118+
119+
const result: PartitionedMetadata = {};
120+
for (const propertyName in groupedMetadatas) {
121+
const allMetadatas = groupedMetadatas[propertyName];
122+
const defined: ValidationMetadata[] = [];
123+
const custom: ValidationMetadata[] = [];
124+
const nested: ValidationMetadata[] = [];
125+
const conditional: ValidationMetadata[] = [];
126+
const all: ValidationMetadata[] = [];
127+
let hasPromiseValidation = false;
128+
129+
for (const metadata of allMetadatas) {
130+
if (metadata.type === ValidationTypes.IS_DEFINED) {
131+
defined.push(metadata);
132+
} else if (metadata.type !== ValidationTypes.WHITELIST) {
133+
all.push(metadata);
134+
switch (metadata.type) {
135+
case ValidationTypes.CUSTOM_VALIDATION:
136+
custom.push(metadata);
137+
break;
138+
case ValidationTypes.NESTED_VALIDATION:
139+
nested.push(metadata);
140+
break;
141+
case ValidationTypes.CONDITIONAL_VALIDATION:
142+
conditional.push(metadata);
143+
break;
144+
case ValidationTypes.PROMISE_VALIDATION:
145+
hasPromiseValidation = true;
146+
break;
147+
}
148+
}
149+
}
150+
151+
const customOnly = defined.length === 0 && nested.length === 0 && conditional.length === 0 && !hasPromiseValidation;
152+
result[propertyName] = { defined, custom, nested, conditional, all, hasPromiseValidation, customOnly };
153+
}
154+
155+
this.partitionedMetadataCache.set(cacheKey, result);
156+
return result;
157+
}
158+
92159
/**
93160
* Gets all validation metadatas for the given object with the given groups.
94161
*/
@@ -109,10 +176,11 @@ export class MetadataStorage {
109176
targetSchema: string,
110177
always: boolean,
111178
strictGroups: boolean,
112-
groups?: string[]
179+
groups?: string[],
180+
cacheKey?: string
113181
): ValidationMetadata[] {
114-
const cacheKey = this.buildCacheKey(targetConstructor, targetSchema, always, strictGroups, groups);
115-
const cached = this.targetMetadataCache.get(cacheKey);
182+
const key = cacheKey ?? this.buildCacheKey(targetConstructor, targetSchema, always, strictGroups, groups);
183+
const cached = this.targetMetadataCache.get(key);
116184
if (cached) return cached;
117185

118186
const includeMetadataBecauseOfAlwaysOption = (metadata: ValidationMetadata): boolean => {
@@ -182,7 +250,7 @@ export class MetadataStorage {
182250
});
183251

184252
const result = originalMetadatas.concat(uniqueInheritedMetadatas);
185-
this.targetMetadataCache.set(cacheKey, result);
253+
this.targetMetadataCache.set(key, result);
186254
return result;
187255
}
188256

src/metadata/ValidationMetadata.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,23 @@ export class ValidationMetadata {
6969
*/
7070
validationTypeOptions: any;
7171

72+
/**
73+
* Cached resolved constraint metadatas for this validation.
74+
* Populated on first access to avoid repeated Map lookups.
75+
*/
76+
resolvedConstraints: any[] | undefined = undefined;
77+
78+
/**
79+
* Inline validate function for built-in validators, bypassing constraint metadata dispatch.
80+
*/
81+
inlineValidate?: (value: any, args?: any) => Promise<boolean> | boolean;
82+
83+
/**
84+
* Inline defaultMessage function for built-in validators.
85+
*/
86+
inlineDefaultMessage?: (args?: any) => string;
87+
88+
7289
// -------------------------------------------------------------------------
7390
// Constructor
7491
// -------------------------------------------------------------------------

0 commit comments

Comments
 (0)