Skip to content
This repository has been archived by the owner on Jan 6, 2025. It is now read-only.

Commit

Permalink
move missing keys checks to improve perfs (#224)
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti authored Apr 6, 2023
1 parent 162c6cd commit 6bf7243
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 38 deletions.
5 changes: 5 additions & 0 deletions .changeset/fluffy-sloths-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/schema": patch
---

move missing keys checks to improve perfs
12 changes: 6 additions & 6 deletions benchmark/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import { z } from "zod"
D.runtimeDebug.tracingEnabled = true

/*
parseEither (good) x 258,771 ops/sec ±0.47% (86 runs sampled)
zod (good) x 171,941 ops/sec ±8.44% (77 runs sampled)
parseEither (bad) x 218,765 ops/sec ±2.90% (86 runs sampled)
zod (bad) x 54,979 ops/sec ±4.90% (83 runs sampled)
parseEither (bad2) x 198,661 ops/sec ±9.61% (78 runs sampled)
zod (bad2) x 182,703 ops/sec ±0.94% (86 runs sampled)
parseEither (good) x 283,841 ops/sec ±0.55% (86 runs sampled)
zod (good) x 176,785 ops/sec ±6.98% (81 runs sampled)
parseEither (bad) x 231,839 ops/sec ±3.18% (83 runs sampled)
zod (bad) x 55,584 ops/sec ±4.29% (83 runs sampled)
parseEither (bad2) x 220,214 ops/sec ±9.78% (78 runs sampled)
zod (bad2) x 185,401 ops/sec ±0.85% (85 runs sampled)
*/

const suite = new Benchmark.Suite()
Expand Down
156 changes: 156 additions & 0 deletions benchmark/unfair.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import * as D from "@effect/data/Debug"
import type { ParseOptions } from "@effect/schema/AST"
import * as P from "@effect/schema/Parser"
import * as t from "@effect/schema/Schema"
import * as Benchmark from "benchmark"
import { z } from "zod"

D.runtimeDebug.tracingEnabled = true

/*
parseEither (good) x 276,758 ops/sec ±0.56% (89 runs sampled)
zod (good) x 32,104 ops/sec ±6.04% (81 runs sampled)
parseEither (bad) x 805,793 ops/sec ±6.90% (81 runs sampled)
zod (bad) x 9,543 ops/sec ±4.28% (81 runs sampled)
*/

const suite = new Benchmark.Suite()

const Vector = t.tuple(t.number, t.number, t.number)
const VectorZod = z.tuple([z.number(), z.number(), z.number()])

const Asteroid = t.struct({
type: t.literal("asteroid"),
location: Vector,
mass: t.number
})
const AsteroidZod = z.object({
type: z.literal("asteroid"),
location: VectorZod,
mass: z.number()
})

const Planet = t.struct({
type: t.literal("planet"),
location: Vector,
mass: t.number,
population: t.number,
habitable: t.boolean
})
const PlanetZod = z.object({
type: z.literal("planet"),
location: VectorZod,
mass: z.number(),
population: z.number(),
habitable: z.boolean()
})

const Rank = t.union(
t.literal("captain"),
t.literal("first mate"),
t.literal("officer"),
t.literal("ensign")
)
const RankZod = z.union([
z.literal("captain"),
z.literal("first mate"),
z.literal("officer"),
z.literal("ensign")
])

const CrewMember = t.struct({
name: t.string,
age: t.number,
rank: Rank,
home: Planet
})
const CrewMemberZod = z.object({
name: z.string(),
age: z.number(),
rank: RankZod,
home: PlanetZod
})

const Ship = t.struct({
type: t.literal("ship"),
location: Vector,
mass: t.number,
name: t.string,
crew: t.array(CrewMember)
})
const ShipZod = z.object({
type: z.literal("ship"),
location: VectorZod,
mass: z.number(),
name: z.string(),
crew: z.array(CrewMemberZod)
})

export const schema = t.union(Asteroid, Planet, Ship)
export const schemaZod = z.union([AsteroidZod, PlanetZod, ShipZod]) // unfair: no discriminated union

export const parseEither = P.parseEither(schema)
const options: ParseOptions = { errors: "first" } // unfair: "first" instead of "all"

const good = {
type: "ship",
location: [1, 2, 3],
mass: 4,
name: "foo",
crew: [
{
name: "bar",
age: 44,
rank: "captain",
home: {
type: "planet",
location: [5, 6, 7],
mass: 8,
population: 1000,
habitable: true
}
}
]
}

const bad = {
type: "ship",
location: [1, 2, "a"],
mass: 4,
name: "foo",
crew: [
{
name: "bar",
age: 44,
rank: "captain",
home: {
type: "planet",
location: [5, 6, 7],
mass: 8,
population: 1000,
habitable: "true" // unfair: take advantage of /schema's "sort fields by weight" internal feature
}
}
]
}

suite
.add("parseEither (good)", function() {
parseEither(good, options)
})
.add("zod (good)", function() {
schemaZod.safeParse(good)
})
.add("parseEither (bad)", function() {
parseEither(bad, options)
})
.add("zod (bad)", function() {
schemaZod.safeParse(bad)
})
.on("cycle", function(event: any) {
console.log(String(event.target))
})
.on("complete", function(this: any) {
console.log("Fastest is " + this.filter("fastest").map("name"))
})
.run({ async: true })
8 changes: 4 additions & 4 deletions benchmark/union.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ D.runtimeDebug.tracingEnabled = true

/*
n = 100
parseEither (good) x 1,588,627 ops/sec ±0.48% (89 runs sampled)
zod (good) x 795,941 ops/sec ±10.56% (73 runs sampled)
parseEither (bad) x 1,072,065 ops/sec ±3.08% (82 runs sampled)
zod (bad) x 971,883 ops/sec ±0.55% (90 runs sampled)
parseEither (good) x 1,777,205 ops/sec ±0.49% (89 runs sampled)
zod (good) x 797,123 ops/sec ±7.51% (80 runs sampled)
parseEither (bad) x 1,103,955 ops/sec ±5.06% (80 runs sampled)
zod (bad) x 929,104 ops/sec ±2.03% (88 runs sampled)
*/

const suite = new Benchmark.Suite()
Expand Down
8 changes: 4 additions & 4 deletions benchmark/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import * as Benchmark from "benchmark"
import { z } from "zod"

/*
schema (good) x 538,914 ops/sec ±1.46% (88 runs sampled)
zod (good) x 426,637 ops/sec ±6.52% (79 runs sampled)
schema (bad) x 458,860 ops/sec ±2.63% (84 runs sampled)
zod (bad) x 113,038 ops/sec ±4.92% (86 runs sampled)
schema (good) x 580,995 ops/sec ±1.10% (89 runs sampled)
zod (good) x 423,724 ops/sec ±6.53% (78 runs sampled)
schema (bad) x 493,920 ops/sec ±2.51% (81 runs sampled)
zod (bad) x 98,215 ops/sec ±4.96% (82 runs sampled)
*/

const suite = new Benchmark.Suite()
Expand Down
8 changes: 4 additions & 4 deletions benchmark/zod2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import * as Benchmark from "benchmark"
import { z } from "zod"

/*
schema (good) x 2,013,098 ops/sec ±0.52% (88 runs sampled)
zod (good) x 1,338,523 ops/sec ±7.56% (79 runs sampled)
schema (bad) x 1,554,092 ops/sec ±3.37% (80 runs sampled)
zod (bad) x 138,241 ops/sec ±4.44% (85 runs sampled)
schema (good) x 2,055,273 ops/sec ±4.37% (84 runs sampled)
zod (good) x 1,339,354 ops/sec ±6.95% (77 runs sampled)
schema (bad) x 1,593,588 ops/sec ±3.22% (82 runs sampled)
zod (bad) x 132,012 ops/sec ±4.79% (83 runs sampled)
*/

const suite = new Benchmark.Suite()
Expand Down
31 changes: 13 additions & 18 deletions src/Parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -600,24 +600,6 @@ const go = untracedMethod(() =>
const allErrors = options?.errors === "all"
const es: Array<[number, PR.ParseErrors]> = []
let stepKey = 0
// ---------------------------------------------
// handle missing keys
// ---------------------------------------------
for (let i = 0; i < propertySignaturesTypes.length; i++) {
const ps = ast.propertySignatures[i]
const name = ps.name
if (!Object.prototype.hasOwnProperty.call(input, name)) {
if (!ps.isOptional) {
const e = PR.key(name, [PR.missing])
if (allErrors) {
es.push([stepKey++, e])
continue
} else {
return PR.failure(e)
}
}
}
}

// ---------------------------------------------
// handle excess properties
Expand Down Expand Up @@ -694,6 +676,19 @@ const go = untracedMethod(() =>
)
)
}
} else {
// ---------------------------------------------
// handle missing keys
// ---------------------------------------------
if (!ps.isOptional) {
const e = PR.key(name, [PR.missing])
if (allErrors) {
es.push([stepKey++, e])
continue
} else {
return PR.failure(e)
}
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions test/Decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -720,7 +720,7 @@ describe.concurrent("Decoder", () => {
await Util.expectParseFailure(schema, {}, `/a is missing`)
await Util.expectParseFailure(schema, { a: 1 }, `/b is missing`)
await Util.expectParseFailure(schema, { b: 2 }, `/a is missing`)
await Util.expectParseFailure(schema, { a: "a" }, `/b is missing`)
await Util.expectParseFailure(schema, { a: "a" }, `/a Expected number, actual "a"`)
})

it("struct/ record(keyof struct({ a, b } & Record<string, string>), number)", async () => {
Expand Down Expand Up @@ -753,7 +753,7 @@ describe.concurrent("Decoder", () => {
{ a: 1, b: 2, [c]: "c" },
`/Symbol(@effect/schema/test/c) Expected number, actual "c"`
)
await Util.expectParseFailure(schema, { a: "a" }, `/b is missing`)
await Util.expectParseFailure(schema, { a: "a" }, `/a Expected number, actual "a"`)
})

it("struct/ record(Symbol('a') | Symbol('b'), number)", async () => {
Expand Down

0 comments on commit 6bf7243

Please sign in to comment.