diff --git a/packages/graphql/package.json b/packages/graphql/package.json index fe1eb964a3..8c03a88fa8 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -42,7 +42,8 @@ "test:tck": "jest tests/tck -c jest.minimal.config.js", "test:unit": "jest src --coverage=true -c jest.minimal.config.js", "test": "jest", - "knip": "knip" + "knip": "knip", + "build-benchmark-workflow": "ts-node tests/performance/build-benchmark-workload.ts" }, "author": "Neo4j Inc.", "devDependencies": { diff --git a/packages/graphql/tests/performance/build-benchmark-workload.ts b/packages/graphql/tests/performance/build-benchmark-workload.ts new file mode 100644 index 0000000000..bcb1afe8d3 --- /dev/null +++ b/packages/graphql/tests/performance/build-benchmark-workload.ts @@ -0,0 +1,36 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import path from "path"; + +import { Neo4jGraphQL } from "../../src"; +import { typeDefs } from "./typedefs"; +import type * as Performance from "./types"; +import { WorkloadGenerator } from "./utils/WorkloadGenerator"; +import { collectTests } from "./utils/collect-test-files"; + +async function main() { + const neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + const gqlTests: Performance.TestInfo[] = await collectTests(path.join(__dirname, "graphql")); + await new WorkloadGenerator(neoSchema).generateWorkload(gqlTests); +} + +main().catch((err) => console.error(err)); diff --git a/packages/graphql/tests/performance/databaseQuery/TestRunner.ts b/packages/graphql/tests/performance/databaseQuery/TestRunner.ts index 3d3649b6a8..6eae14ae0e 100644 --- a/packages/graphql/tests/performance/databaseQuery/TestRunner.ts +++ b/packages/graphql/tests/performance/databaseQuery/TestRunner.ts @@ -85,7 +85,6 @@ export class TestRunner { const session = this.driver.session(); try { const profiledQuery = this.wrapQueryInProfile(cypher); - const t1 = new Date().getTime(); const result = await session.run(profiledQuery, params); const t2 = new Date().getTime(); diff --git a/packages/graphql/tests/performance/typedefs.ts b/packages/graphql/tests/performance/typedefs.ts index 9f0dba68ed..de6b849273 100644 --- a/packages/graphql/tests/performance/typedefs.ts +++ b/packages/graphql/tests/performance/typedefs.ts @@ -46,6 +46,9 @@ export const typeDefs = `#graphql title: String! tagline: String released: Int + floatScore: Float + intScore: Int + bigIntScore: BigInt actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN) directors: [Person!]! @relationship(type: "DIRECTED", direction: IN) reviewers: [Person!]! @relationship(type: "REVIEWED", direction: IN) diff --git a/packages/graphql/tests/performance/utils/WorkloadGenerator.ts b/packages/graphql/tests/performance/utils/WorkloadGenerator.ts new file mode 100644 index 0000000000..d865b40adb --- /dev/null +++ b/packages/graphql/tests/performance/utils/WorkloadGenerator.ts @@ -0,0 +1,271 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as fs from "fs/promises"; +import { Date, DateTime, Duration, Integer, LocalTime, Time } from "neo4j-driver"; +import * as path from "path"; +import type { Neo4jGraphQL } from "../../../src"; +import { translateQuery } from "../../tck/utils/tck-test-utils"; +import type * as Performance from "../types"; + +// Initial configuration, add available configuration options here +type QueryConfig = { + name: string; + description: string; + queryFile: string; + parameters?: { + file: string; + }; +}; + +type WorkloadConfig = { + name: string; + dataset: string; + queries: QueryConfig[]; +}; + +type DatasetConfig = { + name: string; + format: "aligned"; +}; + +type WorkloadFile = { + name: string; + path: string; + content: string; +}; + +export class WorkloadGenerator { + private schema: Neo4jGraphQL; + private name: string; + + constructor(schema: Neo4jGraphQL) { + this.schema = schema; + this.name = "graphql-workload"; + } + public async generateWorkload(tests: Array): Promise { + const directoryPath = `./${this.name}/`; + const queryConfigs: QueryConfig[] = []; + const queryFiles: WorkloadFile[] = []; + const paramFiles: WorkloadFile[] = []; + try { + for (const test of tests) { + const { cypher, params } = await this.getCypherAndParams(test); + + const queryFile = this.getQueryFile(test, cypher); + queryFiles.push(queryFile); + + const paramFile = this.getParamFile(test, params); + if (paramFile) { + paramFiles.push(paramFile); + } + + const queryConfig = this.getQueryConfig(test.name, queryFile, paramFile); + queryConfigs.push(queryConfig); + } + + await fs.mkdir(directoryPath, { recursive: true }); + await fs.mkdir(path.join(directoryPath, "queries"), { recursive: true }); + await fs.mkdir(path.join(directoryPath, "params"), { recursive: true }); + + const promises = [...queryFiles, ...paramFiles].map((file: WorkloadFile) => { + return fs.writeFile(path.join(directoryPath, file.path), file.content); + }); + await Promise.all(promises); + const workflowConfig = this.getWorkflowConfig(queryConfigs); + const datasetConfig = this.getDatasetConfig(); + await fs.writeFile(path.join(directoryPath, "config.json"), JSON.stringify(workflowConfig, null, 2)); + await fs.writeFile(path.join(directoryPath, "dataset.json"), JSON.stringify(datasetConfig, null, 2)); + await fs.writeFile(path.join(directoryPath, "schema.txt"), ""); + } catch (err) { + console.error("Error generating workflow"); + console.warn(err); + } + } + + private getQueryFile(test: Performance.TestInfo, cypher: string): WorkloadFile { + const name = test.name; + const fileName = `${name}.cypher`; + return { + name, + path: path.join("queries", fileName), + content: cypher, + }; + } + + private getParamFile(test: Performance.TestInfo, parameters: Record): WorkloadFile | undefined { + if (Object.keys(parameters).length === 0) { + return; + } + const name = test.name; + const fileName = `${name}.txt`; + return { + name, + path: path.join("params", fileName), + content: this.convertJSONtoCSV(parameters), + }; + } + + private getQueryConfig(queryName: string, queryFile: WorkloadFile, paramFile?: WorkloadFile): QueryConfig { + return { + name: queryName, + description: queryName, + queryFile: queryFile.path, + ...(paramFile + ? { + parameters: { + file: paramFile.path, + }, + } + : {}), + }; + } + + private getWorkflowConfig(queries: QueryConfig[]): WorkloadConfig { + return { + name: this.name, + dataset: "dataset.json", + queries, + }; + } + + private getDatasetConfig(): DatasetConfig { + return { + name: "graphql-benchmark-dataset", + format: "aligned", + }; + } + + private async getCypherAndParams( + test: Performance.TestInfo + ): Promise<{ cypher: string; params: Record }> { + const cypherQuery = await translateQuery(this.schema, test.query); + return { + cypher: cypherQuery.cypher, + params: cypherQuery.params, + }; + } + /** + * Convert our param format to the benchmarking tool's format + **/ + private convertJSONtoCSV(input: Record): string { + let header = ""; + let row = ""; + const separator = "|"; + + Object.entries(input).forEach(([key, value]) => { + if (header.length) { + header += separator; + } + if (row.length) { + row += separator; + } + header += headerColumnRewriter(key, value); + row += valueColumnRewriter(value); + }); + return `${header}\n${row}`; + } +} + +/** + * Add columnType to header colum, see https://github.com/neo-technology/neo4j/blob/dev/private/benchmarks/macro/macro-common/src/main/java/com/neo4j/bench/macro/workload/parameters/FileParametersReader.java + **/ +function headerColumnRewriter(key: string, value: any) { + return `${key}:${getColumnType(value)}`; +} + +function getColumnType(value: any) { + if (value instanceof Integer) { + return "Integer"; + } + + if (value instanceof Date) { + return "Date"; + } + + if (value instanceof DateTime) { + return "DateTime"; + } + + if (typeof value === "number") { + return "Float"; + } + if (typeof value === "string") { + return "String"; + } + + if (value instanceof LocalTime || value instanceof Time || value instanceof Duration) { + throw new Error("LocalTime, Time, Duration are not supported by the benchmarking tool"); + } + + if (typeof value === "boolean") { + throw new Error("Boolean is not supported by the benchmarking tool"); + } + + if (Array.isArray(value)) { + return `${getColumnType(value[0])}[]`; + } + + if (typeof value === "object" && value !== null) { + return "Map"; + } + + throw new Error(`Unknown type ${typeof value}`); +} + +function valueColumnRewriter(value: any) { + if (value instanceof Integer) { + return value.toNumber(); + } + + if (value instanceof Date || value instanceof DateTime) { + return value.toString(); + } + + if (typeof value === "number" || typeof value === "string") { + return value; + } + + if (value instanceof LocalTime || value instanceof Time || value instanceof Duration) { + throw new Error("LocalTime, Time, Duration are not supported by the benchmarking tool"); + } + + if (typeof value === "boolean") { + throw new Error("Boolean is not supported by the benchmarking tool"); + } + + if (Array.isArray(value)) { + if (value.length === 0) { + return ""; + } + return `[${value.map((v) => valueColumnRewriter(v)).join(", ")}]`; + } + + if (typeof value === "object" && value !== null) { + const mapEntries = Object.entries(value).map(([key, value]) => { + return `${key}:${valueColumnRewriter(value)}`; + }); + if (mapEntries.length === 0) { + return ""; + } + return `{${mapEntries.join(", ")}}`; + } + + throw new Error(`Unknown type ${typeof value}`); +}