Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/graphql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
36 changes: 36 additions & 0 deletions packages/graphql/tests/performance/build-benchmark-workload.ts
Original file line number Diff line number Diff line change
@@ -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));
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
3 changes: 3 additions & 0 deletions packages/graphql/tests/performance/typedefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
271 changes: 271 additions & 0 deletions packages/graphql/tests/performance/utils/WorkloadGenerator.ts
Original file line number Diff line number Diff line change
@@ -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<Performance.TestInfo>): Promise<void> {
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<string, any>): 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<string, any> }> {
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, any>): 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}`);
}
Loading