From 206d88be63e07d9b15b47860a037918c95b7d16a Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 27 Aug 2024 11:15:52 -0400 Subject: [PATCH] Initial commit Signed-off-by: Juan Cruz Viotti --- .editorconfig | 19 +++++ .github/workflows/ci.yml | 15 ++++ .gitignore | 2 + Makefile | 42 +++++++++++ implementations/ajv/main.js | 46 ++++++++++++ implementations/jsontoolkit/CMakeLists.txt | 9 +++ implementations/jsontoolkit/main.cc | 36 +++++++++ package-lock.json | 67 +++++++++++++++++ package.json | 8 ++ report.sh | 20 +++++ schemas/example/instance.json | 46 ++++++++++++ schemas/example/schema.json | 87 ++++++++++++++++++++++ 12 files changed, 397 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 implementations/ajv/main.js create mode 100644 implementations/jsontoolkit/CMakeLists.txt create mode 100644 implementations/jsontoolkit/main.cc create mode 100644 package-lock.json create mode 100644 package.json create mode 100755 report.sh create mode 100644 schemas/example/instance.json create mode 100644 schemas/example/schema.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b18055c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +# editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[Makefile] +indent_style = tab + +[*.mk] +indent_style = tab + +[*.uk] +indent_style = tab diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0100476 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,15 @@ +name: Benchmark + +on: + push: + branches: + - main + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: make + - run: cat dist/report.csv diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c925c21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/dist +/node_modules diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3d81362 --- /dev/null +++ b/Makefile @@ -0,0 +1,42 @@ +.DEFAULT_GOAL := all +SCHEMAS = $(notdir $(wildcard schemas/*)) +IMPLEMENTATIONS = $(notdir $(wildcard implementations/*)) + +node_modules: package.json package-lock.json ; npm ci +.PHONY: clean +clean: ; rm -rf dist node_modules +dist: ; mkdir $@ +dist/results: | dist ; mkdir $@ +dist/temp: | dist ; mkdir $@ +define PREPARE_IMPLEMENTATION +dist/results/$1: | dist/results ; mkdir $$@ +dist/temp/$1: | dist/temp ; mkdir $$@ +ALL_TARGETS += $$(addprefix dist/results/$1/,$(SCHEMAS)) +endef +$(foreach implementation,$(IMPLEMENTATIONS),$(eval $(call PREPARE_IMPLEMENTATION,$(implementation)))) +dist/report.csv: report.sh $(ALL_TARGETS) | dist ; ./$< $(ALL_TARGETS) > $@ +.PHONY: all +all: dist/report.csv ; cat $< + +# JSON Toolkit + +dist/results/jsontoolkit/%: \ + implementations/jsontoolkit/CMakeLists.txt \ + implementations/jsontoolkit/main.cc \ + schemas/%/schema.json \ + schemas/%/instance.json \ + | dist/results/jsontoolkit dist/temp/jsontoolkit + [ -d $(word 2,$|)/repo ] && git -C $(word 2,$|)/repo pull || git clone https://github.com/sourcemeta/jsontoolkit $(word 2,$|)/repo + cmake -S $(dir $<) -B $(word 2,$|)/build -DCMAKE_BUILD_TYPE:STRING=Release -DBUILD_SHARED_LIBS:BOOL=OFF + cmake --build $(word 2,$|)/build --config Release --parallel 4 + $(word 2,$|)/build/jsontoolkit_benchmark $(dir $(word 3,$^)) > $@ + +# AJV + +dist/results/ajv/%: \ + implementations/ajv/main.js \ + schemas/%/schema.json \ + schemas/%/instance.json \ + node_modules \ + | dist/results/ajv + node $< $(word 2,$^) $(word 3,$^) > $@ diff --git a/implementations/ajv/main.js b/implementations/ajv/main.js new file mode 100644 index 0000000..aa29fad --- /dev/null +++ b/implementations/ajv/main.js @@ -0,0 +1,46 @@ +const Ajv = require('ajv'); +const draft4schema = require('ajv/lib/refs/json-schema-draft-04.json'); +const fs = require('fs'); +const { performance } = require('perf_hooks'); + +function readJSONFile(filePath) { + try { + const fileContent = fs.readFileSync(filePath, 'utf8'); + return JSON.parse(fileContent); + } catch (error) { + process.exit(1); + } +} + +function validateSchema(schemaPath, instancePath) { + const schema = readJSONFile(schemaPath); + const instance = readJSONFile(instancePath); + + const ajv = new Ajv({ + schemaId: 'id', + meta: false, + validateSchema: false + }); + + ajv.addMetaSchema(draft4schema); + const validate = ajv.compile(schema); + + const startTime = performance.now(); + if (!validate(instance)) { + process.exit(1); + } + + const endTime = performance.now(); + + const durationNs = (endTime - startTime) * 1e6; + console.log(durationNs.toFixed(0)); +} + +if (process.argv.length !== 4) { + process.exit(1); +} + +const schemaPath = process.argv[2]; +const instancePath = process.argv[3]; + +validateSchema(schemaPath, instancePath); diff --git a/implementations/jsontoolkit/CMakeLists.txt b/implementations/jsontoolkit/CMakeLists.txt new file mode 100644 index 0000000..6e4bca9 --- /dev/null +++ b/implementations/jsontoolkit/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.16) +project(jsontoolkit_benchmark) +include(../../dist/temp/jsontoolkit/repo/vendor/noa/cmake/noa.cmake) +set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3") +add_subdirectory(../../dist/temp/jsontoolkit/repo jsontoolkit) +add_executable(jsontoolkit_benchmark main.cc) +noa_add_default_options(PRIVATE jsontoolkit_benchmark) +target_link_libraries(jsontoolkit_benchmark PRIVATE sourcemeta::jsontoolkit::json) +target_link_libraries(jsontoolkit_benchmark PRIVATE sourcemeta::jsontoolkit::jsonschema) diff --git a/implementations/jsontoolkit/main.cc b/implementations/jsontoolkit/main.cc new file mode 100644 index 0000000..ce95c70 --- /dev/null +++ b/implementations/jsontoolkit/main.cc @@ -0,0 +1,36 @@ +#include +#include + +#include +#include +#include + +int main(int argc, char **argv) { + if (argc < 2) { + std::cerr << "Usage: " << argv[0] << " \n"; + return EXIT_FAILURE; + } + + const std::filesystem::path example{argv[1]}; + const auto schema{sourcemeta::jsontoolkit::from_file(example / "schema.json")}; + const auto instance{sourcemeta::jsontoolkit::from_file(example / "instance.json")}; + + const auto schema_template{sourcemeta::jsontoolkit::compile( + schema, sourcemeta::jsontoolkit::default_schema_walker, + sourcemeta::jsontoolkit::official_resolver, + sourcemeta::jsontoolkit::default_schema_compiler)}; + + const auto timestamp_start{std::chrono::high_resolution_clock::now()}; + const auto result{sourcemeta::jsontoolkit::evaluate( + schema_template, instance)}; + const auto timestamp_end{std::chrono::high_resolution_clock::now()}; + if (!result) { + return EXIT_FAILURE; + } + + const auto duration{ + std::chrono::duration_cast(timestamp_end - timestamp_start)}; + std::cout << duration.count() << "\n"; + + return EXIT_SUCCESS; +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c5a7ee1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,67 @@ +{ + "name": "jsonschema-benchmark", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "jsonschema-benchmark", + "version": "1.0.0", + "dependencies": { + "ajv": "^6.12.6" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2d5ec58 --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "name": "jsonschema-benchmark", + "version": "1.0.0", + "author": "Juan Cruz Viotti ", + "dependencies": { + "ajv": "^6.12.6" + } +} diff --git a/report.sh b/report.sh new file mode 100755 index 0000000..cce4696 --- /dev/null +++ b/report.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +if [ $# -lt 1 ] +then + echo "Usage: $0 " 1>&2 + exit 1 +fi + +echo "implementation,name,nanoseconds" + +for argument in "$@" +do + IMPLEMENTATION="$(basename "$(dirname "$argument")")" + EXAMPLE="$(basename "$argument")" + NANOSECONDS="$(tr -d '\n\r' < "$argument")" + echo "$IMPLEMENTATION,$EXAMPLE,$NANOSECONDS" +done diff --git a/schemas/example/instance.json b/schemas/example/instance.json new file mode 100644 index 0000000..6f452cb --- /dev/null +++ b/schemas/example/instance.json @@ -0,0 +1,46 @@ +{ + "storeName": "TechGadgets Online", + "lastUpdated": "2024-08-27T14:30:00Z", + "categories": ["Electronics", "Accessories", "Smart Home"], + "products": [ + { + "id": "PRD-EL-000001", + "name": "SuperSound Wireless Headphones", + "description": "High-quality wireless headphones with noise-cancelling technology.", + "price": 129.99, + "category": "Electronics", + "tags": ["audio", "wireless", "headphones"], + "inStock": true, + "rating": { + "average": 4.7, + "count": 253 + } + }, + { + "id": "PRD-AC-000015", + "name": "DuraPro Phone Case", + "description": "Rugged phone case for ultimate protection.", + "price": 24.95, + "category": "Accessories", + "tags": ["phone", "protection"], + "inStock": true, + "rating": { + "average": 4.2, + "count": 187 + } + }, + { + "id": "PRD-SH-000008", + "name": "SmartHome Hub", + "description": "Central control unit for your smart home devices.", + "price": 79.99, + "category": "Smart Home", + "tags": ["iot", "control", "automation"], + "inStock": false, + "rating": { + "average": 4.5, + "count": 42 + } + } + ] +} diff --git a/schemas/example/schema.json b/schemas/example/schema.json new file mode 100644 index 0000000..29c7457 --- /dev/null +++ b/schemas/example/schema.json @@ -0,0 +1,87 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "title": "Product Catalog", + "description": "A schema for a simplified e-commerce product catalog", + "properties": { + "storeName": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "lastUpdated": { + "type": "string", + "format": "date-time" + }, + "categories": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 50 + }, + "uniqueItems": true, + "minItems": 1 + }, + "products": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "name", "price", "category"], + "properties": { + "id": { + "type": "string", + "pattern": "^PRD-[A-Z]{2}-[0-9]{6}$" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "description": { + "type": "string", + "maxLength": 1000 + }, + "price": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + }, + "category": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 20 + }, + "uniqueItems": true, + "maxItems": 5 + }, + "inStock": { + "type": "boolean" + }, + "rating": { + "type": "object", + "properties": { + "average": { + "type": "number", + "minimum": 0, + "maximum": 5 + }, + "count": { + "type": "integer", + "minimum": 0 + } + }, + "required": ["average", "count"] + } + } + }, + "minItems": 1 + } + }, + "required": ["storeName", "lastUpdated", "categories", "products"] +}