Skip to content

Commit

Permalink
refactor: remove validate wrapper function (closure) to improve perfo…
Browse files Browse the repository at this point in the history
…rmance for cyclic schema refs
  • Loading branch information
epoberezkin committed Sep 12, 2020
1 parent 58bc64d commit 4bbb97f
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 57 deletions.
10 changes: 7 additions & 3 deletions lib/ajv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes"]
const META_SUPPORT_DATA = ["/properties"]
const EXT_SCOPE_NAMES = new Set([
"validate",
"wrapper",
"root",
"schema",
"keyword",
Expand Down Expand Up @@ -461,14 +462,17 @@ export default class Ajv {
}

private _compileSchemaEnv(sch: SchemaEnv): ValidateFunction {
return sch.meta ? this._compileMetaSchema(sch) : compileSchema.call(this, sch)
if (sch.meta) this._compileMetaSchema(sch)
else compileSchema.call(this, sch)
if (!sch.validate) throw new Error("ajv implementation error")
return sch.validate
}

private _compileMetaSchema(sch: SchemaEnv): ValidateFunction {
private _compileMetaSchema(sch: SchemaEnv): void {
const currentOpts = this._opts
this._opts = this._metaOpts
try {
return compileSchema.call(this, sch)
compileSchema.call(this, sch)
} finally {
this._opts = currentOpts
}
Expand Down
68 changes: 26 additions & 42 deletions lib/compile/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export class SchemaEnv implements SchemaEnvArgs {
meta?: boolean
cacheKey?: unknown
$async?: boolean
localRoot: {validate?: ValidateFunction} = {}
localRoot: {validate?: ValidateFunction}
refs: SchemaRefs = {}
validate?: ValidateFunction
validateName?: Name
Expand All @@ -49,53 +49,37 @@ export class SchemaEnv implements SchemaEnvArgs {
}
}

function validateWrapper(this: Ajv, sch: SchemaEnv): ValidateFunction {
if (!sch.validate) {
const wrapper: ValidateFunction = function (this: Ajv | unknown, ...args) {
if (wrapper.validate === undefined) throw new Error("ajv implementation error")
const v = wrapper.validate
const valid = v.apply(this, args)
wrapper.errors = v.errors
return valid
}
sch.validate = wrapper
sch.validateName = this._scope.value("validate", {ref: wrapper})
}
return sch.validate
}

function extendWrapper(wrapper: ValidateFunction, v: ValidateFunction): void {
wrapper.validate = v
Object.assign(wrapper, v)
}

// Compiles schema to validation function
export function compileSchema(this: Ajv, env: SchemaEnv): ValidateFunction {
// Compiles schema in SchemaEnv
export function compileSchema(this: Ajv, env: SchemaEnv): void {
const self = this
const opts = this._opts
return (env.localRoot.validate = localCompile(env))
localCompile(env)
if (env.validate) env.localRoot.validate = env.validate

function localCompile(sch: SchemaEnv): ValidateFunction {
function localCompile(sch: SchemaEnv): SchemaEnv {
// TODO refactor - remove compilations
const _sch = getCompilingSchema.call(self, sch)
if (_sch) return validateWrapper.call(self, _sch)
if (_sch) return _sch
const {schema, baseId} = sch
if (sch.root !== env.root) return compileSchema.call(self, sch)
if (sch.root !== env.root) {
compileSchema.call(self, sch)
return sch
}

const isRoot = sch.schema === sch.root.schema
const $async = typeof schema == "object" && schema.$async === true
const rootId = getFullPath(sch.root.baseId)

const gen = new CodeGen(self._scope, {...opts.codegen, forInOwn: opts.ownProperties})
let _ValidationError
if ($async) {
if (sch.$async) {
_ValidationError = gen.scopeValue("Error", {
ref: ValidationError,
code: _`require("ajv/dist/compile/error_classes").ValidationError`,
})
}

const validateName = gen.scopeName("validate")
sch.validateName = validateName

const schemaCxt: SchemaCxt = {
gen,
Expand All @@ -107,12 +91,12 @@ export function compileSchema(this: Ajv, env: SchemaEnv): ValidateFunction {
dataPathArr: [nil], // TODO can it's lenght be used as dataLevel if nil is removed?
dataLevel: 0,
topSchemaRef: gen.scopeValue("schema", {ref: schema}),
async: $async,
async: sch.$async,
validateName,
ValidationError: _ValidationError,
schema,
isRoot,
root: env.root,
root: sch.root,
rootId,
baseId: baseId || rootId,
schemaPath: nil,
Expand All @@ -138,18 +122,17 @@ export function compileSchema(this: Ajv, env: SchemaEnv): ValidateFunction {

validate.schema = schema
validate.errors = null
validate.root = env.root // TODO remove - only used by $comment keyword
if ($async) validate.$async = true
validate.root = sch.root // TODO remove - only used by $comment keyword
validate.env = sch
if (sch.$async) validate.$async = true
if (opts.sourceCode === true) {
validate.source = {
code: sourceCode,
scope: self._scope,
}
}
if (sch.validate) extendWrapper(sch.validate, validate)
sch.validate = validate
sch.validateName = validateName
return validate
return sch
} catch (e) {
delete sch.validate
delete sch.validateName
Expand All @@ -160,9 +143,11 @@ export function compileSchema(this: Ajv, env: SchemaEnv): ValidateFunction {
}
}

function resolveRef(baseId: string, ref: string): Schema | ValidateFunction | undefined {
function resolveRef(
baseId: string,
ref: string
): Schema | ValidateFunction | SchemaEnv | undefined {
ref = resolveUrl(baseId, ref)
// TODO root.refs check should be unnecessary, it is only needed because in some cases root is passed without refs (see type casts to SchemaEnv)
const schOrFunc = env.refs[ref] || env.root.refs[ref]
if (schOrFunc) return schOrFunc

Expand All @@ -176,10 +161,9 @@ export function compileSchema(this: Ajv, env: SchemaEnv): ValidateFunction {
return
}

function inlineOrCompile(sch: SchemaEnv): Schema | ValidateFunction {
return inlineRef(sch.schema, self._opts.inlineRefs)
? sch.schema
: sch.validate || localCompile(sch)
function inlineOrCompile(sch: SchemaEnv): Schema | SchemaEnv {
if (inlineRef(sch.schema, self._opts.inlineRefs)) return sch.schema
return sch.validate ? sch : localCompile(sch)
}
}

Expand Down
5 changes: 3 additions & 2 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export interface ValidateFunction {
): boolean | Promise<any>
schema?: Schema
errors?: null | ErrorObject[]
env?: SchemaEnv
root?: SchemaEnv
$async?: true
source?: SourceCode
Expand Down Expand Up @@ -156,7 +157,7 @@ export interface SchemaCxt {
dataPathArr: (Code | number)[]
dataLevel: number
topSchemaRef: Code
async: boolean
async?: boolean
validateName: Name
ValidationError?: Name
schema: Schema
Expand All @@ -171,7 +172,7 @@ export interface SchemaCxt {
compositeRule?: boolean
createErrors?: boolean
opts: InstanceOptions
resolveRef: (baseId: string, ref: string) => Schema | ValidateFunction | undefined
resolveRef: (baseId: string, ref: string) => Schema | SchemaEnv | undefined
self: Ajv
}

Expand Down
29 changes: 19 additions & 10 deletions lib/vocabularies/core/ref.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {CodeKeywordDefinition, Schema, ValidateFunction} from "../../types"
import {CodeKeywordDefinition, Schema} from "../../types"
import KeywordCxt from "../../compile/context"
import {MissingRefError} from "../../compile/error_classes"
import {applySubschema} from "../../compile/subschema"
import {callValidateCode} from "../util"
import {_, str, nil, Code, Name} from "../../compile/codegen"
import N from "../../compile/names"
import {SchemaEnv} from "../../compile"

const def: CodeKeywordDefinition = {
keyword: "$ref",
Expand All @@ -16,23 +17,31 @@ const def: CodeKeywordDefinition = {
if (schema === "#" || schema === "#/") return callRootRef()
const schOrFunc = resolveRef(baseId, schema)
if (schOrFunc === undefined) return missingRef()
if (typeof schOrFunc == "function") return callCompiledRef(schOrFunc)
if (schOrFunc instanceof SchemaEnv) return callValidate(schOrFunc)
return inlineRefSchema(schOrFunc)

function callRootRef(): void {
if (isRoot) return callRef(validateName, it.async)
// TODO use the same name as compiled function, so it can be dropped in shared scope
const rootName = gen.scopeValue("root", {ref: root.localRoot})
return callRef(_`${rootName}.validate`, root.$async)
return callRef(_`${rootName}.validate`, root.$async || it.async)
}

function callCompiledRef(func: ValidateFunction): void {
const v = gen.scopeValue("validate", {ref: func})
return callRef(v, func.$async)
function callValidate(sch: SchemaEnv): void {
let v: Code
if (sch.validate) {
v = gen.scopeValue("validate", {ref: sch.validate})
} else {
const code = _`{validate: ${sch.validateName}}`
const wrapper = gen.scopeValue("wrapper", {ref: sch, code})
v = _`${wrapper}.validate`
}
callRef(v, sch.$async)
}

function callRef(v: Code, $async?: boolean): void {
if ($async || it.async) validateAsyncRef(v)
else validateSyncRef(v)
if ($async) callAsyncRef(v)
else callSyncRef(v)
}

function inlineRefSchema(sch: Schema): void {
Expand Down Expand Up @@ -66,7 +75,7 @@ const def: CodeKeywordDefinition = {
}
}

function validateAsyncRef(v: Code): void {
function callAsyncRef(v: Code): void {
if (!it.async) throw new Error("async schema referenced by sync schema")
const valid = gen.let("valid")
gen.try(
Expand All @@ -83,7 +92,7 @@ const def: CodeKeywordDefinition = {
cxt.ok(valid)
}

function validateSyncRef(v: Code): void {
function callSyncRef(v: Code): void {
cxt.pass(callValidateCode(cxt, v, passCxt), () => addErrorsFrom(v))
}

Expand Down

0 comments on commit 4bbb97f

Please sign in to comment.