fastify-ata 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +23 -0
- package/.github/workflows/release.yml +45 -0
- package/compat/COMPATIBILITY.md +1 -1
- package/package.json +2 -2
- package/bench-realtime.js +0 -131
- package/bench-turbo.js +0 -118
- package/profile-fastify.mjs +0 -87
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, master]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
node: [20, 22, 24]
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: actions/setup-node@v4
|
|
20
|
+
with:
|
|
21
|
+
node-version: ${{ matrix.node }}
|
|
22
|
+
- run: npm install
|
|
23
|
+
- run: npm test
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishing a GitHub release publishes the same version to npm via trusted
|
|
4
|
+
# publishing (OIDC). No tokens: the trusted publisher for this repo and
|
|
5
|
+
# workflow must be configured on npmjs.com for the package.
|
|
6
|
+
on:
|
|
7
|
+
release:
|
|
8
|
+
types: [published]
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: 22
|
|
21
|
+
- run: npm install
|
|
22
|
+
- run: npm test
|
|
23
|
+
|
|
24
|
+
publish:
|
|
25
|
+
needs: test
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
permissions:
|
|
28
|
+
contents: read
|
|
29
|
+
id-token: write
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v4
|
|
32
|
+
- uses: actions/setup-node@v4
|
|
33
|
+
with:
|
|
34
|
+
node-version: 22
|
|
35
|
+
- run: npm install -g npm@latest
|
|
36
|
+
- name: Refuse a version that does not match the tag
|
|
37
|
+
run: |
|
|
38
|
+
TAG="${GITHUB_REF_NAME#v}"
|
|
39
|
+
PKG="$(node -p "require('./package.json').version")"
|
|
40
|
+
if [ "$TAG" != "$PKG" ]; then
|
|
41
|
+
echo "tag $TAG does not match package.json $PKG" >&2
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
- run: npm install
|
|
45
|
+
- run: npm publish
|
package/compat/COMPATIBILITY.md
CHANGED
|
@@ -12,7 +12,7 @@ The preload swaps the single `require('@fastify/ajv-compiler')` inside Fastify's
|
|
|
12
12
|
|
|
13
13
|
## Current score
|
|
14
14
|
|
|
15
|
-
**
|
|
15
|
+
**182 of 188 tests pass** (Fastify v5.8.5 checkout, ata-validator 1.3.0, fastify-ata main).
|
|
16
16
|
|
|
17
17
|
Everything Fastify's suite asserts about validation behavior passes: type checks and coercion (including the `array` coercion mode), defaults, `removeAdditional`, required and enum handling, cross-schema `$ref` through `addSchema`, draft-07 `$id` anchors, `nullable`, `oneOf`/`anyOf` branching, custom error messages via `errorMessage`, `$merge`/`$patch` keywords, fail-fast startup errors for unresolvable references, encapsulation scoping, error shape (exact default error object layout, mutable for plugins like ajv-i18n), the error paths in `validation-error-handling`, and shared-schema `$ref` into `/definitions` across the validator AND the serializer (ata never mutates caller-provided schema objects, so fast-json-stringify sees them untouched).
|
|
18
18
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fastify-ata",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Fastify plugin for ata-validator. Runtime-competitive with the default, 24x faster serverless cold start, Standard Schema V1.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"homepage": "https://github.com/ata-core/fastify-ata#readme",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"ata-validator": "^1.
|
|
29
|
+
"ata-validator": "^1.7.1",
|
|
30
30
|
"fastify-plugin": "^5.1.0",
|
|
31
31
|
"sanitize-filename": "^1.6.4"
|
|
32
32
|
},
|
package/bench-realtime.js
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
// Real-time Fastify simulation: actual HTTP requests with autocannon
|
|
4
|
-
// Shows: startup time + request throughput + latency
|
|
5
|
-
|
|
6
|
-
const { writeFileSync, unlinkSync } = require('fs')
|
|
7
|
-
const { fork, execSync } = require('child_process')
|
|
8
|
-
const path = require('path')
|
|
9
|
-
|
|
10
|
-
const ROUTES = 50
|
|
11
|
-
const DURATION = 5 // seconds
|
|
12
|
-
const CONNECTIONS = 50
|
|
13
|
-
|
|
14
|
-
function makeRoutes(count) {
|
|
15
|
-
return Array.from({ length: count }, (_, i) => ({
|
|
16
|
-
path: `/api/v1/resource${i}`,
|
|
17
|
-
schema: {
|
|
18
|
-
body: {
|
|
19
|
-
type: 'object',
|
|
20
|
-
properties: {
|
|
21
|
-
id: { type: 'integer', minimum: 1 },
|
|
22
|
-
name: { type: 'string', minLength: 1, maxLength: 100 },
|
|
23
|
-
email: { type: 'string', format: 'email' },
|
|
24
|
-
age: { type: 'integer', minimum: 0, maximum: 150 },
|
|
25
|
-
active: { type: 'boolean' },
|
|
26
|
-
role: { enum: ['admin', 'user', 'moderator'] },
|
|
27
|
-
},
|
|
28
|
-
required: ['id', 'name', 'email', 'active', 'role'],
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
}))
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const routes = makeRoutes(ROUTES)
|
|
35
|
-
const payload = JSON.stringify({
|
|
36
|
-
id: 42, name: 'Mert', email: 'mert@example.com',
|
|
37
|
-
age: 26, active: true, role: 'admin'
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
function serverScript(useAta) {
|
|
41
|
-
return `
|
|
42
|
-
'use strict'
|
|
43
|
-
const t0 = process.hrtime.bigint()
|
|
44
|
-
const fastify = require('fastify')()
|
|
45
|
-
${useAta ? "fastify.register(require('./index'))" : ''}
|
|
46
|
-
const routes = ${JSON.stringify(routes)}
|
|
47
|
-
for (const r of routes) {
|
|
48
|
-
fastify.post(r.path, { schema: r.schema }, (req, reply) => {
|
|
49
|
-
reply.send({ ok: true, id: req.body.id })
|
|
50
|
-
})
|
|
51
|
-
}
|
|
52
|
-
fastify.listen({ port: 0 }).then(() => {
|
|
53
|
-
const startupMs = Number(process.hrtime.bigint() - t0) / 1e6
|
|
54
|
-
const port = fastify.server.address().port
|
|
55
|
-
process.send({ port, startupMs })
|
|
56
|
-
})
|
|
57
|
-
`
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function runTest(label, useAta) {
|
|
61
|
-
const scriptPath = path.join(__dirname, '_realtime_server.js')
|
|
62
|
-
writeFileSync(scriptPath, serverScript(useAta))
|
|
63
|
-
|
|
64
|
-
const child = fork(scriptPath, { stdio: ['pipe', 'pipe', 'pipe', 'ipc'] })
|
|
65
|
-
const { port, startupMs } = await new Promise(resolve => {
|
|
66
|
-
child.on('message', msg => resolve(msg))
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
// Pick a random route for each request
|
|
70
|
-
const targetRoute = routes[Math.floor(Math.random() * routes.length)].path
|
|
71
|
-
const escaped = payload.replace(/'/g, "'\\''")
|
|
72
|
-
|
|
73
|
-
let result
|
|
74
|
-
try {
|
|
75
|
-
const raw = execSync(
|
|
76
|
-
`npx autocannon -c ${CONNECTIONS} -d ${DURATION} -j http://localhost:${port}${targetRoute} -m POST -H "content-type: application/json" -b '${escaped}'`,
|
|
77
|
-
{ cwd: __dirname, timeout: 30000 }
|
|
78
|
-
).toString()
|
|
79
|
-
result = JSON.parse(raw)
|
|
80
|
-
} catch (e) {
|
|
81
|
-
child.kill()
|
|
82
|
-
try { unlinkSync(scriptPath) } catch {}
|
|
83
|
-
return null
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
child.kill()
|
|
87
|
-
try { unlinkSync(scriptPath) } catch {}
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
label,
|
|
91
|
-
startupMs,
|
|
92
|
-
reqPerSec: result.requests.average,
|
|
93
|
-
latencyAvg: result.latency.average,
|
|
94
|
-
latencyP99: result.latency.p99,
|
|
95
|
-
throughputMB: (result.throughput.average / 1024 / 1024).toFixed(1),
|
|
96
|
-
errors: result.errors,
|
|
97
|
-
timeouts: result.timeouts,
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function main() {
|
|
102
|
-
console.log('\n' + '='.repeat(60))
|
|
103
|
-
console.log(' Real-Time Fastify Simulation')
|
|
104
|
-
console.log(` ${ROUTES} routes, ${CONNECTIONS} connections, ${DURATION}s duration`)
|
|
105
|
-
console.log(' POST with 6-field validated body')
|
|
106
|
-
console.log('='.repeat(60) + '\n')
|
|
107
|
-
|
|
108
|
-
const ata = await runTest('fastify-ata', true)
|
|
109
|
-
const ajv = await runTest('fastify (ajv)', false)
|
|
110
|
-
|
|
111
|
-
if (!ata || !ajv) { console.log('Benchmark failed'); return }
|
|
112
|
-
|
|
113
|
-
console.log(' fastify-ata fastify (ajv)')
|
|
114
|
-
console.log(' ─────────────────────────────────────────────────')
|
|
115
|
-
console.log(` Startup ${ata.startupMs.toFixed(1).padStart(8)} ms ${ajv.startupMs.toFixed(1).padStart(8)} ms`)
|
|
116
|
-
console.log(` Requests/sec ${ata.reqPerSec.toLocaleString().padStart(8)} ${ajv.reqPerSec.toLocaleString().padStart(8)}`)
|
|
117
|
-
console.log(` Latency (avg) ${ata.latencyAvg.toFixed(2).padStart(8)} ms ${ajv.latencyAvg.toFixed(2).padStart(8)} ms`)
|
|
118
|
-
console.log(` Latency (p99) ${ata.latencyP99.toFixed(2).padStart(8)} ms ${ajv.latencyP99.toFixed(2).padStart(8)} ms`)
|
|
119
|
-
console.log(` Throughput ${ata.throughputMB.padStart(8)} MB/s ${ajv.throughputMB.padStart(8)} MB/s`)
|
|
120
|
-
console.log(` Errors ${String(ata.errors).padStart(8)} ${String(ajv.errors).padStart(8)}`)
|
|
121
|
-
console.log(` Timeouts ${String(ata.timeouts).padStart(8)} ${String(ajv.timeouts).padStart(8)}`)
|
|
122
|
-
console.log()
|
|
123
|
-
|
|
124
|
-
const startupRatio = ajv.startupMs / ata.startupMs
|
|
125
|
-
const rpsRatio = ata.reqPerSec / ajv.reqPerSec
|
|
126
|
-
console.log(` Startup: ata ${startupRatio.toFixed(1)}x faster`)
|
|
127
|
-
console.log(` Throughput: ata ${rpsRatio.toFixed(2)}x ${rpsRatio >= 1 ? 'faster' : 'slower'}`)
|
|
128
|
-
console.log()
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
main().catch(err => { console.error(err); process.exit(1) })
|
package/bench-turbo.js
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { execSync } = require('child_process')
|
|
4
|
-
const { writeFileSync, unlinkSync } = require('fs')
|
|
5
|
-
const path = require('path')
|
|
6
|
-
|
|
7
|
-
const schema = {
|
|
8
|
-
body: {
|
|
9
|
-
type: 'object',
|
|
10
|
-
properties: {
|
|
11
|
-
users: {
|
|
12
|
-
type: 'array',
|
|
13
|
-
items: {
|
|
14
|
-
type: 'object',
|
|
15
|
-
properties: {
|
|
16
|
-
id: { type: 'integer', minimum: 1 },
|
|
17
|
-
name: { type: 'string', minLength: 1 },
|
|
18
|
-
email: { type: 'string', format: 'email' },
|
|
19
|
-
age: { type: 'integer', minimum: 0, maximum: 150 },
|
|
20
|
-
active: { type: 'boolean' },
|
|
21
|
-
role: { enum: ['admin', 'user', 'moderator'] },
|
|
22
|
-
},
|
|
23
|
-
required: ['id', 'name', 'email', 'active', 'role'],
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
metadata: {
|
|
27
|
-
type: 'object',
|
|
28
|
-
properties: {
|
|
29
|
-
total: { type: 'integer' },
|
|
30
|
-
page: { type: 'integer', minimum: 1 },
|
|
31
|
-
},
|
|
32
|
-
required: ['total', 'page'],
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
required: ['users', 'metadata'],
|
|
36
|
-
},
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function makePayload(n) {
|
|
40
|
-
const users = []
|
|
41
|
-
for (let i = 0; i < n; i++) {
|
|
42
|
-
users.push({
|
|
43
|
-
id: i + 1,
|
|
44
|
-
name: `User ${i}`,
|
|
45
|
-
email: `user${i}@example.com`,
|
|
46
|
-
age: 25,
|
|
47
|
-
active: true,
|
|
48
|
-
role: 'user',
|
|
49
|
-
})
|
|
50
|
-
}
|
|
51
|
-
return JSON.stringify({ users, metadata: { total: n, page: 1 } })
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function serverScript(turbo) {
|
|
55
|
-
return `
|
|
56
|
-
'use strict'
|
|
57
|
-
const fastify = require('fastify')()
|
|
58
|
-
fastify.register(require('./index'), { turbo: ${turbo} })
|
|
59
|
-
const schema = ${JSON.stringify(schema)}
|
|
60
|
-
fastify.post('/users', { schema }, (req, reply) => {
|
|
61
|
-
reply.send({ ok: true, count: req.body.users.length })
|
|
62
|
-
})
|
|
63
|
-
fastify.listen({ port: 0 }).then(() => {
|
|
64
|
-
process.send({ port: fastify.server.address().port })
|
|
65
|
-
})
|
|
66
|
-
`
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function bench(label, turbo, payload) {
|
|
70
|
-
const scriptPath = path.join(__dirname, '_bench_turbo_server.js')
|
|
71
|
-
writeFileSync(scriptPath, serverScript(turbo))
|
|
72
|
-
|
|
73
|
-
const { fork } = require('child_process')
|
|
74
|
-
const child = fork(scriptPath, { stdio: ['pipe', 'pipe', 'pipe', 'ipc'] })
|
|
75
|
-
const port = await new Promise((resolve) => {
|
|
76
|
-
child.on('message', (msg) => resolve(msg.port))
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
const escaped = payload.replace(/'/g, "'\\''")
|
|
80
|
-
const result = execSync(
|
|
81
|
-
`npx autocannon -c 10 -d 5 -j http://localhost:${port}/users -m POST -H "content-type: application/json" -b '${escaped}'`,
|
|
82
|
-
{ cwd: __dirname, timeout: 30000 },
|
|
83
|
-
).toString()
|
|
84
|
-
|
|
85
|
-
child.kill()
|
|
86
|
-
try { unlinkSync(scriptPath) } catch {}
|
|
87
|
-
|
|
88
|
-
const parsed = JSON.parse(result)
|
|
89
|
-
return parsed.requests.average
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function main() {
|
|
93
|
-
console.log('\n==============================================')
|
|
94
|
-
console.log(' Turbo Mode Benchmark: normal vs turbo')
|
|
95
|
-
console.log(' 10 connections, 5 seconds, real HTTP')
|
|
96
|
-
console.log('==============================================\n')
|
|
97
|
-
|
|
98
|
-
for (const count of [1, 10, 50, 100]) {
|
|
99
|
-
const payload = makePayload(count)
|
|
100
|
-
console.log(`--- ${count} users (${(payload.length / 1024).toFixed(1)} KB) ---`)
|
|
101
|
-
|
|
102
|
-
const normal = await bench('normal', false, payload)
|
|
103
|
-
const turbo = await bench('turbo', true, payload)
|
|
104
|
-
|
|
105
|
-
console.log(` normal: ${normal.toLocaleString().padStart(10)} req/sec`)
|
|
106
|
-
console.log(` turbo: ${turbo.toLocaleString().padStart(10)} req/sec`)
|
|
107
|
-
|
|
108
|
-
const ratio = turbo / normal
|
|
109
|
-
if (ratio >= 1) console.log(` >>> turbo ${ratio.toFixed(2)}x faster`)
|
|
110
|
-
else console.log(` >>> normal ${(1 / ratio).toFixed(2)}x faster`)
|
|
111
|
-
console.log()
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
main().catch((err) => {
|
|
116
|
-
console.error(err)
|
|
117
|
-
process.exit(1)
|
|
118
|
-
})
|
package/profile-fastify.mjs
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
// Profile validation overhead in Fastify context
|
|
4
|
-
import { createRequire } from 'module'
|
|
5
|
-
const require = createRequire(import.meta.url)
|
|
6
|
-
|
|
7
|
-
const { Validator } = require('ata-validator')
|
|
8
|
-
|
|
9
|
-
// Simulate what Fastify does: JSON.parse + validate
|
|
10
|
-
const schema = {
|
|
11
|
-
type: 'object',
|
|
12
|
-
properties: {
|
|
13
|
-
users: {
|
|
14
|
-
type: 'array',
|
|
15
|
-
items: {
|
|
16
|
-
type: 'object',
|
|
17
|
-
properties: {
|
|
18
|
-
id: { type: 'integer', minimum: 1 },
|
|
19
|
-
name: { type: 'string', minLength: 1 },
|
|
20
|
-
email: { type: 'string', format: 'email' },
|
|
21
|
-
age: { type: 'integer', minimum: 0, maximum: 150 },
|
|
22
|
-
active: { type: 'boolean' },
|
|
23
|
-
role: { enum: ['admin', 'user', 'moderator'] },
|
|
24
|
-
},
|
|
25
|
-
required: ['id', 'name', 'email', 'active', 'role'],
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
metadata: {
|
|
29
|
-
type: 'object',
|
|
30
|
-
properties: {
|
|
31
|
-
total: { type: 'integer' },
|
|
32
|
-
page: { type: 'integer', minimum: 1 },
|
|
33
|
-
},
|
|
34
|
-
required: ['total', 'page'],
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
required: ['users', 'metadata'],
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function makePayload(n) {
|
|
41
|
-
const users = []
|
|
42
|
-
for (let i = 0; i < n; i++) {
|
|
43
|
-
users.push({ id: i+1, name: `User ${i}`, email: `u${i}@x.com`, age: 25, active: true, role: 'user' })
|
|
44
|
-
}
|
|
45
|
-
return JSON.stringify({ users, metadata: { total: n, page: 1 } })
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const v = new Validator(schema)
|
|
49
|
-
const payloads = [1, 10, 50, 100].map(n => ({ n, json: makePayload(n) }))
|
|
50
|
-
|
|
51
|
-
// Warmup
|
|
52
|
-
for (const p of payloads) {
|
|
53
|
-
for (let i = 0; i < 1000; i++) {
|
|
54
|
-
const obj = JSON.parse(p.json)
|
|
55
|
-
v.validate(obj)
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
console.log('Breakdown: JSON.parse vs validate vs total\n')
|
|
60
|
-
|
|
61
|
-
const N = 100000
|
|
62
|
-
for (const p of payloads) {
|
|
63
|
-
// JSON.parse only
|
|
64
|
-
let t = process.hrtime.bigint()
|
|
65
|
-
for (let i = 0; i < N; i++) JSON.parse(p.json)
|
|
66
|
-
const dtParse = Number(process.hrtime.bigint() - t) / 1e6
|
|
67
|
-
|
|
68
|
-
// validate only (pre-parsed)
|
|
69
|
-
const obj = JSON.parse(p.json)
|
|
70
|
-
t = process.hrtime.bigint()
|
|
71
|
-
for (let i = 0; i < N; i++) v.validate(obj)
|
|
72
|
-
const dtValidate = Number(process.hrtime.bigint() - t) / 1e6
|
|
73
|
-
|
|
74
|
-
// parse + validate
|
|
75
|
-
t = process.hrtime.bigint()
|
|
76
|
-
for (let i = 0; i < N; i++) { const o = JSON.parse(p.json); v.validate(o) }
|
|
77
|
-
const dtTotal = Number(process.hrtime.bigint() - t) / 1e6
|
|
78
|
-
|
|
79
|
-
const parsePct = (dtParse / dtTotal * 100).toFixed(0)
|
|
80
|
-
const valPct = (dtValidate / dtTotal * 100).toFixed(0)
|
|
81
|
-
|
|
82
|
-
console.log(`${p.n} users (${(p.json.length/1024).toFixed(1)}KB):`)
|
|
83
|
-
console.log(` JSON.parse: ${(dtParse/N*1000).toFixed(1)} us (${parsePct}%)`)
|
|
84
|
-
console.log(` validate: ${(dtValidate/N*1000).toFixed(1)} us (${valPct}%)`)
|
|
85
|
-
console.log(` total: ${(dtTotal/N*1000).toFixed(1)} us`)
|
|
86
|
-
console.log()
|
|
87
|
-
}
|