diff --git a/.github/workflows/diff.yaml b/.github/workflows/diff.yaml index 95e1248ee..8b47b3bb3 100644 --- a/.github/workflows/diff.yaml +++ b/.github/workflows/diff.yaml @@ -198,6 +198,15 @@ jobs: cd build ctest -R memgraph__simulation --output-on-failure -j$THREADS + - name: Run single benchmark test + run: | + # Activate toolchain. + source /opt/toolchain-v4/activate + + # Run simulation tests. + cd tests/mgbench + ./benchmark.py accesscontrol/small --num-workers-for-import 1 --test-system-arg "split-file splitfiles/accesscontrol_small.shard_configuration bolt-num-workers 1" + release_build: name: "Release build" runs-on: [self-hosted, Linux, X64, Diff] @@ -245,6 +254,15 @@ jobs: cd build ctest -R memgraph__simulation --output-on-failure -j$THREADS + - name: Run single benchmark test + run: | + # Activate toolchain. + source /opt/toolchain-v4/activate + + # Run simulation tests. + cd tests/mgbench + ./benchmark.py accesscontrol/small --num-workers-for-import 1 --test-system-arg "split-file splitfiles/accesscontrol_small.shard_configuration bolt-num-workers 1" + - name: Run e2e tests run: | # TODO(gitbuda): Setup mgclient and pymgclient properly. diff --git a/tests/mgbench/benchmark.py b/tests/mgbench/benchmark.py index 5ce715571..9d8423e89 100755 --- a/tests/mgbench/benchmark.py +++ b/tests/mgbench/benchmark.py @@ -15,15 +15,18 @@ import argparse import collections import copy import fnmatch +import importlib import inspect import json import multiprocessing +import os import random import sys +import time import datasets -import log import helpers +import log import runners @@ -37,8 +40,7 @@ def get_queries(gen, count): return ret -def match_patterns(dataset, variant, group, test, is_default_variant, - patterns): +def match_patterns(dataset, variant, group, test, is_default_variant, patterns): for pattern in patterns: verdict = [fnmatch.fnmatchcase(dataset, pattern[0])] if pattern[1] != "": @@ -58,7 +60,7 @@ def filter_benchmarks(generators, patterns): pattern = patterns[i].split("/") if len(pattern) > 4 or len(pattern) == 0: raise Exception("Invalid benchmark description '" + pattern + "'!") - pattern.extend(["", "*", "*"][len(pattern) - 1:]) + pattern.extend(["", "*", "*"][len(pattern) - 1 :]) patterns[i] = pattern filtered = [] for dataset in sorted(generators.keys()): @@ -68,8 +70,7 @@ def filter_benchmarks(generators, patterns): current = collections.defaultdict(list) for group in tests: for test_name, test_func in tests[group]: - if match_patterns(dataset, variant, group, test_name, - is_default_variant, patterns): + if match_patterns(dataset, variant, group, test_name, is_default_variant, patterns): current[group].append((test_name, test_func)) if len(current) > 0: filtered.append((generator(variant), dict(current))) @@ -78,54 +79,64 @@ def filter_benchmarks(generators, patterns): # Parse options. parser = argparse.ArgumentParser( - description="Memgraph benchmark executor.", - formatter_class=argparse.ArgumentDefaultsHelpFormatter) -parser.add_argument("benchmarks", nargs="*", default="", - help="descriptions of benchmarks that should be run; " - "multiple descriptions can be specified to run multiple " - "benchmarks; the description is specified as " - "dataset/variant/group/test; Unix shell-style wildcards " - "can be used in the descriptions; variant, group and test " - "are optional and they can be left out; the default " - "variant is '' which selects the default dataset variant; " - "the default group is '*' which selects all groups; the " - "default test is '*' which selects all tests") -parser.add_argument("--memgraph-binary", - default=helpers.get_binary_path("memgraph"), - help="Memgraph binary used for benchmarking") -parser.add_argument("--client-binary", - default=helpers.get_binary_path("tests/mgbench/client"), - help="client binary used for benchmarking") -parser.add_argument("--num-workers-for-import", type=int, - default=multiprocessing.cpu_count() // 2, - help="number of workers used to import the dataset") -parser.add_argument("--num-workers-for-benchmark", type=int, - default=1, - help="number of workers used to execute the benchmark") -parser.add_argument("--single-threaded-runtime-sec", type=int, - default=10, - help="single threaded duration of each test") -parser.add_argument("--no-load-query-counts", action="store_true", - help="disable loading of cached query counts") -parser.add_argument("--no-save-query-counts", action="store_true", - help="disable storing of cached query counts") -parser.add_argument("--export-results", default="", - help="file path into which results should be exported") -parser.add_argument("--temporary-directory", default="/tmp", - help="directory path where temporary data should " - "be stored") -parser.add_argument("--no-properties-on-edges", action="store_true", - help="disable properties on edges") + description="Memgraph benchmark executor.", formatter_class=argparse.ArgumentDefaultsHelpFormatter +) +parser.add_argument( + "benchmarks", + nargs="*", + default="", + help="descriptions of benchmarks that should be run; " + "multiple descriptions can be specified to run multiple " + "benchmarks; the description is specified as " + "dataset/variant/group/test; Unix shell-style wildcards " + "can be used in the descriptions; variant, group and test " + "are optional and they can be left out; the default " + "variant is '' which selects the default dataset variant; " + "the default group is '*' which selects all groups; the " + "default test is '*' which selects all tests", +) +parser.add_argument( + "--memgraph-binary", default=helpers.get_binary_path("memgraph"), help="Memgraph binary used for benchmarking" +) +parser.add_argument( + "--client-binary", + default=helpers.get_binary_path("tests/mgbench/client"), + help="client binary used for benchmarking", +) +parser.add_argument( + "--num-workers-for-import", + type=int, + default=multiprocessing.cpu_count() // 2, + help="number of workers used to import the dataset", +) +parser.add_argument( + "--num-workers-for-benchmark", type=int, default=1, help="number of workers used to execute the benchmark" +) +parser.add_argument("--single-threaded-runtime-sec", type=int, default=10, help="single threaded duration of each test") +parser.add_argument("--no-load-query-counts", action="store_true", help="disable loading of cached query counts") +parser.add_argument("--no-save-query-counts", action="store_true", help="disable storing of cached query counts") +parser.add_argument("--export-results", default="", help="file path into which results should be exported") +parser.add_argument( + "--temporary-directory", default="/tmp", help="directory path where temporary data should " "be stored" +) +parser.add_argument("--no-properties-on-edges", action="store_true", help="disable properties on edges") +parser.add_argument("--datasets-path", default="datasets", help="path to datasets to scan") +parser.add_argument("--test-system-args", default="") args = parser.parse_args() +head_tail = os.path.split(args.datasets_path) +path_without_dataset_name = head_tail[0] +dataset_name = head_tail[1].split(".")[0] +sys.path.append(path_without_dataset_name) +dataset_to_use = importlib.import_module(dataset_name) + # Detect available datasets. generators = {} -for key in dir(datasets): +for key in dir(dataset_to_use): if key.startswith("_"): continue - dataset = getattr(datasets, key) - if not inspect.isclass(dataset) or dataset == datasets.Dataset or \ - not issubclass(dataset, datasets.Dataset): + dataset = getattr(dataset_to_use, key) + if not inspect.isclass(dataset) or dataset == datasets.Dataset or not issubclass(dataset, datasets.Dataset): continue tests = collections.defaultdict(list) for funcname in dir(dataset): @@ -135,8 +146,9 @@ for key in dir(datasets): tests[group].append((test, funcname)) generators[dataset.NAME] = (dataset, dict(tests)) if dataset.PROPERTIES_ON_EDGES and args.no_properties_on_edges: - raise Exception("The \"{}\" dataset requires properties on edges, " - "but you have disabled them!".format(dataset.NAME)) + raise Exception( + 'The "{}" dataset requires properties on edges, ' "but you have disabled them!".format(dataset.NAME) + ) # List datasets if there is no specified dataset. if len(args.benchmarks) == 0: @@ -144,8 +156,7 @@ if len(args.benchmarks) == 0: for name in sorted(generators.keys()): print("Dataset:", name) dataset, tests = generators[name] - print(" Variants:", ", ".join(dataset.VARIANTS), - "(default: " + dataset.DEFAULT_VARIANT + ")") + print(" Variants:", ", ".join(dataset.VARIANTS), "(default: " + dataset.DEFAULT_VARIANT + ")") for group in sorted(tests.keys()): print(" Group:", group) for test_name, test_func in tests[group]: @@ -165,31 +176,44 @@ benchmarks = filter_benchmarks(generators, args.benchmarks) # Run all specified benchmarks. for dataset, tests in benchmarks: - log.init("Preparing", dataset.NAME + "/" + dataset.get_variant(), - "dataset") - dataset.prepare(cache.cache_directory("datasets", dataset.NAME, - dataset.get_variant())) + log.init("Preparing", dataset.NAME + "/" + dataset.get_variant(), "dataset") + dataset.prepare(cache.cache_directory("datasets", dataset.NAME, dataset.get_variant())) # Prepare runners and import the dataset. - memgraph = runners.Memgraph(args.memgraph_binary, args.temporary_directory, - not args.no_properties_on_edges) + memgraph = runners.Memgraph( + args.memgraph_binary, + args.temporary_directory, + not args.no_properties_on_edges, + args.test_system_args, + ) client = runners.Client(args.client_binary, args.temporary_directory) memgraph.start_preparation() - ret = client.execute(file_path=dataset.get_file(), - num_workers=args.num_workers_for_import) + time.sleep(5.0) # giving enough time to machine manager and all to start up + ret = client.execute(file_path=dataset.get_file(), num_workers=args.num_workers_for_import) usage = memgraph.stop() # Display import statistics. print() for row in ret: - print("Executed", row["count"], "queries in", row["duration"], - "seconds using", row["num_workers"], - "workers with a total throughput of", row["throughput"], - "queries/second.") + print( + "Executed", + row["count"], + "queries in", + row["duration"], + "seconds using", + row["num_workers"], + "workers with a total throughput of", + row["throughput"], + "queries/second.", + ) print() - print("The database used", usage["cpu"], - "seconds of CPU time and peaked at", - usage["memory"] / 1024 / 1024, "MiB of RAM.") + print( + "The database used", + usage["cpu"], + "seconds of CPU time and peaked at", + usage["memory"] / 1024 / 1024, + "MiB of RAM.", + ) # Save import results. import_key = [dataset.NAME, dataset.get_variant(), "__import__"] @@ -208,24 +232,26 @@ for dataset, tests in benchmarks: config_key = [dataset.NAME, dataset.get_variant(), group, test] cached_count = config.get_value(*config_key) if cached_count is None: - print("Determining the number of queries necessary for", - args.single_threaded_runtime_sec, - "seconds of single-threaded runtime...") + print( + "Determining the number of queries necessary for", + args.single_threaded_runtime_sec, + "seconds of single-threaded runtime...", + ) # First run to prime the query caches. memgraph.start_benchmark() client.execute(queries=get_queries(func, 1), num_workers=1) # Get a sense of the runtime. count = 1 while True: - ret = client.execute(queries=get_queries(func, count), - num_workers=1) + ret = client.execute(queries=get_queries(func, count), num_workers=1) duration = ret[0]["duration"] - should_execute = int(args.single_threaded_runtime_sec / - (duration / count)) - print("executed_queries={}, total_duration={}, " - "query_duration={}, estimated_count={}".format( - count, duration, duration / count, - should_execute)) + should_execute = int(args.single_threaded_runtime_sec / (duration / count)) + print( + "executed_queries={}, total_duration={}, " + "query_duration={}, estimated_count={}".format( + count, duration, duration / count, should_execute + ) + ) # We don't have to execute the next iteration when # `should_execute` becomes the same order of magnitude as # `count * 10`. @@ -235,45 +261,45 @@ for dataset, tests in benchmarks: else: count = count * 10 memgraph.stop() - config.set_value(*config_key, value={ - "count": count, - "duration": args.single_threaded_runtime_sec}) + config.set_value(*config_key, value={"count": count, "duration": args.single_threaded_runtime_sec}) else: - print("Using cached query count of", cached_count["count"], - "queries for", cached_count["duration"], - "seconds of single-threaded runtime.") - count = int(cached_count["count"] * - args.single_threaded_runtime_sec / - cached_count["duration"]) + print( + "Using cached query count of", + cached_count["count"], + "queries for", + cached_count["duration"], + "seconds of single-threaded runtime.", + ) + count = int(cached_count["count"] * args.single_threaded_runtime_sec / cached_count["duration"]) # Benchmark run. print("Sample query:", get_queries(func, 1)[0][0]) - print("Executing benchmark with", count, "queries that should " - "yield a single-threaded runtime of", - args.single_threaded_runtime_sec, "seconds.") - print("Queries are executed using", args.num_workers_for_benchmark, - "concurrent clients.") + print( + "Executing benchmark with", + count, + "queries that should yield a single-threaded runtime of", + args.single_threaded_runtime_sec, + "seconds.", + ) + print("Queries are executed using", args.num_workers_for_benchmark, "concurrent clients.") memgraph.start_benchmark() - ret = client.execute(queries=get_queries(func, count), - num_workers=args.num_workers_for_benchmark)[0] + ret = client.execute(queries=get_queries(func, count), num_workers=args.num_workers_for_benchmark)[0] usage = memgraph.stop() ret["database"] = usage # Output summary. print() - print("Executed", ret["count"], "queries in", - ret["duration"], "seconds.") + print("Executed", ret["count"], "queries in", ret["duration"], "seconds.") print("Queries have been retried", ret["retries"], "times.") - print("Database used {:.3f} seconds of CPU time.".format( - usage["cpu"])) - print("Database peaked at {:.3f} MiB of memory.".format( - usage["memory"] / 1024.0 / 1024.0)) - print("{:<31} {:>20} {:>20} {:>20}".format("Metadata:", "min", - "avg", "max")) + print("Database used {:.3f} seconds of CPU time.".format(usage["cpu"])) + print("Database peaked at {:.3f} MiB of memory.".format(usage["memory"] / 1024.0 / 1024.0)) + print("{:<31} {:>20} {:>20} {:>20}".format("Metadata:", "min", "avg", "max")) metadata = ret["metadata"] for key in sorted(metadata.keys()): - print("{name:>30}: {minimum:>20.06f} {average:>20.06f} " - "{maximum:>20.06f}".format(name=key, **metadata[key])) + print( + "{name:>30}: {minimum:>20.06f} {average:>20.06f} " + "{maximum:>20.06f}".format(name=key, **metadata[key]) + ) log.success("Throughput: {:02f} QPS".format(ret["throughput"])) # Save results. diff --git a/tests/mgbench/dataset_creator.py b/tests/mgbench/dataset_creator.py new file mode 100644 index 000000000..9ebeb8cd1 --- /dev/null +++ b/tests/mgbench/dataset_creator.py @@ -0,0 +1,118 @@ +# Copyright 2022 Memgraph Ltd. +# +# Use of this software is governed by the Business Source License +# included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +# License, and you may not use this file except in compliance with the Business Source License. +# +# As of the Change Date specified in that file, in accordance with +# the Business Source License, use of this software will be governed +# by the Apache License, Version 2.0, included in the file +# licenses/APL.txt. + +import argparse +import random + +import helpers + +# Explaination of datasets: +# - empty_only_index: contains index; contains no data +# - small: contains index; contains data (small dataset) +# +# Datamodel is as follow: +# +# ┌──────────────┐ +# │ Permission │ +# ┌────────────────┐ │ Schema:uuid │ ┌────────────┐ +# │:IS_FOR_IDENTITY├────┤ Index:name ├───┤:IS_FOR_FILE│ +# └┬───────────────┘ └──────────────┘ └────────────┤ +# │ │ +# ┌──────▼──────────────┐ ┌──▼────────────────┐ +# │ Identity │ │ File │ +# │ Schema:uuid │ │ Schema:uuid │ +# │ Index:email │ │ Index:name │ +# └─────────────────────┘ │ Index:platformId │ +# └───────────────────┘ +# +# - File: attributes: ["uuid", "name", "platformId"] +# - Permission: attributes: ["uuid", "name"] +# - Identity: attributes: ["uuid", "email"] +# +# Indexes: +# - File: [File(uuid), File(platformId), File(name)] +# - Permission: [Permission(uuid), Permission(name)] +# - Identity: [Identity(uuid), Identity(email)] +# +# Edges: +# - (:Permission)-[:IS_FOR_FILE]->(:File) +# - (:Permission)-[:IS_FOR_IDENTITYR]->(:Identity) +# +# AccessControl specific: uuid is the schema + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--number_of_identities", type=int, default=10) + parser.add_argument("--number_of_files", type=int, default=10) + parser.add_argument("--percentage_of_permissions", type=float, default=1.0) + parser.add_argument("--filename", default="dataset.cypher") + + args = parser.parse_args() + + number_of_identities = args.number_of_identities + number_of_files = args.number_of_files + percentage_of_permissions = args.percentage_of_permissions + filename = args.filename + + assert number_of_identities >= 0 + assert number_of_files >= 0 + assert percentage_of_permissions > 0.0 and percentage_of_permissions <= 1.0 + assert filename != "" + + with open(filename, "w") as f: + f.write("MATCH (n) DETACH DELETE n;\n") + + # Create the indexes + f.write("CREATE INDEX ON :File;\n") + f.write("CREATE INDEX ON :Permission;\n") + f.write("CREATE INDEX ON :Identity;\n") + f.write("CREATE INDEX ON :File(platformId);\n") + f.write("CREATE INDEX ON :File(name);\n") + f.write("CREATE INDEX ON :Permission(name);\n") + f.write("CREATE INDEX ON :Identity(email);\n") + + # Create extra index: in distributed, this will be the schema + f.write("CREATE INDEX ON :File(uuid);\n") + f.write("CREATE INDEX ON :Permission(uuid);\n") + f.write("CREATE INDEX ON :Identity(uuid);\n") + + uuid = 1 + + # Create the nodes File + for index in range(0, number_of_files): + f.write(f'CREATE (:File {{uuid: {uuid}, platformId: "platform_id", name: "name_file_{uuid}"}});\n') + uuid += 1 + + identities = [] + # Create the nodes Identity + for index in range(0, number_of_identities): + f.write(f'CREATE (:Identity {{uuid: {uuid}, name: "mail_{uuid}@something.com"}});\n') + uuid += 1 + + for outer_index in range(0, number_of_files): + for inner_index in range(0, number_of_identities): + file_uuid = outer_index + 1 + identity_uuid = number_of_files + inner_index + 1 + + if random.random() <= percentage_of_permissions: + f.write(f'CREATE (:Permission {{uuid: {uuid}, name: "name_permission_{uuid}"}});\n') + f.write( + f"MATCH (permission:Permission {{uuid: {uuid}}}), (file:File {{uuid: {file_uuid}}}) CREATE (permission)-[e: IS_FOR_FILE]->(file);\n" + ) + f.write( + f"MATCH (permission:Permission {{uuid: {uuid}}}), (identity:Identity {{uuid: {identity_uuid}}}) CREATE (permission)-[e: IS_FOR_IDENTITY]->(identity);\n" + ) + uuid += 1 + + +if __name__ == "__main__": + main() diff --git a/tests/mgbench/datasets.py b/tests/mgbench/datasets.py index dbaaa2de9..3a5806629 100644 --- a/tests/mgbench/datasets.py +++ b/tests/mgbench/datasets.py @@ -45,13 +45,10 @@ class Dataset: variant = self.DEFAULT_VARIANT if variant not in self.VARIANTS: raise ValueError("Invalid test variant!") - if (self.FILES and variant not in self.FILES) and \ - (self.URLS and variant not in self.URLS): - raise ValueError("The variant doesn't have a defined URL or " - "file path!") + if (self.FILES and variant not in self.FILES) and (self.URLS and variant not in self.URLS): + raise ValueError("The variant doesn't have a defined URL or " "file path!") if variant not in self.SIZES: - raise ValueError("The variant doesn't have a defined dataset " - "size!") + raise ValueError("The variant doesn't have a defined dataset " "size!") self._variant = variant if self.FILES is not None: self._file = self.FILES.get(variant, None) @@ -63,8 +60,7 @@ class Dataset: self._url = None self._size = self.SIZES[variant] if "vertices" not in self._size or "edges" not in self._size: - raise ValueError("The size defined for this variant doesn't " - "have the number of vertices and/or edges!") + raise ValueError("The size defined for this variant doesn't " "have the number of vertices and/or edges!") self._num_vertices = self._size["vertices"] self._num_edges = self._size["edges"] @@ -76,8 +72,7 @@ class Dataset: cached_input, exists = directory.get_file("dataset.cypher") if not exists: print("Downloading dataset file:", self._url) - downloaded_file = helpers.download_file( - self._url, directory.get_path()) + downloaded_file = helpers.download_file(self._url, directory.get_path()) print("Unpacking and caching file:", downloaded_file) helpers.unpack_and_move_file(downloaded_file, cached_input) print("Using cached dataset file:", cached_input) @@ -109,9 +104,9 @@ class Pokec(Dataset): DEFAULT_VARIANT = "small" FILES = None URLS = { - "small": "https://s3-eu-west-1.amazonaws.com/deps.memgraph.io/pokec_small.setup.cypher", - "medium": "https://s3-eu-west-1.amazonaws.com/deps.memgraph.io/pokec_medium.setup.cypher", - "large": "https://s3-eu-west-1.amazonaws.com/deps.memgraph.io/pokec_large.setup.cypher.gz", + "small": "https://s3.eu-west-1.amazonaws.com/deps.memgraph.io/dataset/pokec/pokec_small.setup.cypher", + "medium": "https://s3.eu-west-1.amazonaws.com/deps.memgraph.io/dataset/pokec/pokec_medium.setup.cypher", + "large": "https://s3.eu-west-1.amazonaws.com/deps.memgraph.io/dataset/pokec/pokec_large.setup.cypher.gz", } SIZES = { "small": {"vertices": 10000, "edges": 121716}, @@ -137,18 +132,17 @@ class Pokec(Dataset): # Arango benchmarks def benchmark__arango__single_vertex_read(self): - return ("MATCH (n:User {id : $id}) RETURN n", - {"id": self._get_random_vertex()}) + return ("MATCH (n:User {id : $id}) RETURN n", {"id": self._get_random_vertex()}) def benchmark__arango__single_vertex_write(self): - return ("CREATE (n:UserTemp {id : $id}) RETURN n", - {"id": random.randint(1, self._num_vertices * 10)}) + return ("CREATE (n:UserTemp {id : $id}) RETURN n", {"id": random.randint(1, self._num_vertices * 10)}) def benchmark__arango__single_edge_write(self): vertex_from, vertex_to = self._get_random_from_to() - return ("MATCH (n:User {id: $from}), (m:User {id: $to}) WITH n, m " - "CREATE (n)-[e:Temp]->(m) RETURN e", - {"from": vertex_from, "to": vertex_to}) + return ( + "MATCH (n:User {id: $from}), (m:User {id: $to}) WITH n, m " "CREATE (n)-[e:Temp]->(m) RETURN e", + {"from": vertex_from, "to": vertex_to}, + ) def benchmark__arango__aggregate(self): return ("MATCH (n:User) RETURN n.age, COUNT(*)", {}) @@ -157,92 +151,94 @@ class Pokec(Dataset): return ("MATCH (n:User) WHERE n.age >= 18 RETURN n.age, COUNT(*)", {}) def benchmark__arango__expansion_1(self): - return ("MATCH (s:User {id: $id})-->(n:User) " - "RETURN n.id", - {"id": self._get_random_vertex()}) + return ("MATCH (s:User {id: $id})-->(n:User) " "RETURN n.id", {"id": self._get_random_vertex()}) def benchmark__arango__expansion_1_with_filter(self): - return ("MATCH (s:User {id: $id})-->(n:User) " - "WHERE n.age >= 18 " - "RETURN n.id", - {"id": self._get_random_vertex()}) + return ( + "MATCH (s:User {id: $id})-->(n:User) " "WHERE n.age >= 18 " "RETURN n.id", + {"id": self._get_random_vertex()}, + ) def benchmark__arango__expansion_2(self): - return ("MATCH (s:User {id: $id})-->()-->(n:User) " - "RETURN DISTINCT n.id", - {"id": self._get_random_vertex()}) + return ("MATCH (s:User {id: $id})-->()-->(n:User) " "RETURN DISTINCT n.id", {"id": self._get_random_vertex()}) def benchmark__arango__expansion_2_with_filter(self): - return ("MATCH (s:User {id: $id})-->()-->(n:User) " - "WHERE n.age >= 18 " - "RETURN DISTINCT n.id", - {"id": self._get_random_vertex()}) + return ( + "MATCH (s:User {id: $id})-->()-->(n:User) " "WHERE n.age >= 18 " "RETURN DISTINCT n.id", + {"id": self._get_random_vertex()}, + ) def benchmark__arango__expansion_3(self): - return ("MATCH (s:User {id: $id})-->()-->()-->(n:User) " - "RETURN DISTINCT n.id", - {"id": self._get_random_vertex()}) + return ( + "MATCH (s:User {id: $id})-->()-->()-->(n:User) " "RETURN DISTINCT n.id", + {"id": self._get_random_vertex()}, + ) def benchmark__arango__expansion_3_with_filter(self): - return ("MATCH (s:User {id: $id})-->()-->()-->(n:User) " - "WHERE n.age >= 18 " - "RETURN DISTINCT n.id", - {"id": self._get_random_vertex()}) + return ( + "MATCH (s:User {id: $id})-->()-->()-->(n:User) " "WHERE n.age >= 18 " "RETURN DISTINCT n.id", + {"id": self._get_random_vertex()}, + ) def benchmark__arango__expansion_4(self): - return ("MATCH (s:User {id: $id})-->()-->()-->()-->(n:User) " - "RETURN DISTINCT n.id", - {"id": self._get_random_vertex()}) + return ( + "MATCH (s:User {id: $id})-->()-->()-->()-->(n:User) " "RETURN DISTINCT n.id", + {"id": self._get_random_vertex()}, + ) def benchmark__arango__expansion_4_with_filter(self): - return ("MATCH (s:User {id: $id})-->()-->()-->()-->(n:User) " - "WHERE n.age >= 18 " - "RETURN DISTINCT n.id", - {"id": self._get_random_vertex()}) + return ( + "MATCH (s:User {id: $id})-->()-->()-->()-->(n:User) " "WHERE n.age >= 18 " "RETURN DISTINCT n.id", + {"id": self._get_random_vertex()}, + ) def benchmark__arango__neighbours_2(self): - return ("MATCH (s:User {id: $id})-[*1..2]->(n:User) " - "RETURN DISTINCT n.id", - {"id": self._get_random_vertex()}) + return ("MATCH (s:User {id: $id})-[*1..2]->(n:User) " "RETURN DISTINCT n.id", {"id": self._get_random_vertex()}) def benchmark__arango__neighbours_2_with_filter(self): - return ("MATCH (s:User {id: $id})-[*1..2]->(n:User) " - "WHERE n.age >= 18 " - "RETURN DISTINCT n.id", - {"id": self._get_random_vertex()}) + return ( + "MATCH (s:User {id: $id})-[*1..2]->(n:User) " "WHERE n.age >= 18 " "RETURN DISTINCT n.id", + {"id": self._get_random_vertex()}, + ) def benchmark__arango__neighbours_2_with_data(self): - return ("MATCH (s:User {id: $id})-[*1..2]->(n:User) " - "RETURN DISTINCT n.id, n", - {"id": self._get_random_vertex()}) + return ( + "MATCH (s:User {id: $id})-[*1..2]->(n:User) " "RETURN DISTINCT n.id, n", + {"id": self._get_random_vertex()}, + ) def benchmark__arango__neighbours_2_with_data_and_filter(self): - return ("MATCH (s:User {id: $id})-[*1..2]->(n:User) " - "WHERE n.age >= 18 " - "RETURN DISTINCT n.id, n", - {"id": self._get_random_vertex()}) + return ( + "MATCH (s:User {id: $id})-[*1..2]->(n:User) " "WHERE n.age >= 18 " "RETURN DISTINCT n.id, n", + {"id": self._get_random_vertex()}, + ) def benchmark__arango__shortest_path(self): vertex_from, vertex_to = self._get_random_from_to() - return ("MATCH (n:User {id: $from}), (m:User {id: $to}) WITH n, m " - "MATCH p=(n)-[*bfs..15]->(m) " - "RETURN extract(n in nodes(p) | n.id) AS path", - {"from": vertex_from, "to": vertex_to}) + return ( + "MATCH (n:User {id: $from}), (m:User {id: $to}) WITH n, m " + "MATCH p=(n)-[*bfs..15]->(m) " + "RETURN extract(n in nodes(p) | n.id) AS path", + {"from": vertex_from, "to": vertex_to}, + ) def benchmark__arango__shortest_path_with_filter(self): vertex_from, vertex_to = self._get_random_from_to() - return ("MATCH (n:User {id: $from}), (m:User {id: $to}) WITH n, m " - "MATCH p=(n)-[*bfs..15 (e, n | n.age >= 18)]->(m) " - "RETURN extract(n in nodes(p) | n.id) AS path", - {"from": vertex_from, "to": vertex_to}) + return ( + "MATCH (n:User {id: $from}), (m:User {id: $to}) WITH n, m " + "MATCH p=(n)-[*bfs..15 (e, n | n.age >= 18)]->(m) " + "RETURN extract(n in nodes(p) | n.id) AS path", + {"from": vertex_from, "to": vertex_to}, + ) # Our benchmark queries def benchmark__create__edge(self): vertex_from, vertex_to = self._get_random_from_to() - return ("MATCH (a:User {id: $from}), (b:User {id: $to}) " - "CREATE (a)-[:TempEdge]->(b)", - {"from": vertex_from, "to": vertex_to}) + return ( + "MATCH (a:User {id: $from}), (b:User {id: $to}) " "CREATE (a)-[:TempEdge]->(b)", + {"from": vertex_from, "to": vertex_to}, + ) def benchmark__create__pattern(self): return ("CREATE ()-[:TempEdge]->()", {}) @@ -251,9 +247,12 @@ class Pokec(Dataset): return ("CREATE ()", {}) def benchmark__create__vertex_big(self): - return ("CREATE (:L1:L2:L3:L4:L5:L6:L7 {p1: true, p2: 42, " - "p3: \"Here is some text that is not extremely short\", " - "p4:\"Short text\", p5: 234.434, p6: 11.11, p7: false})", {}) + return ( + "CREATE (:L1:L2:L3:L4:L5:L6:L7 {p1: true, p2: 42, " + 'p3: "Here is some text that is not extremely short", ' + 'p4:"Short text", p5: 234.434, p6: 11.11, p7: false})', + {}, + ) def benchmark__aggregation__count(self): return ("MATCH (n) RETURN count(n), count(n.age)", {}) @@ -262,29 +261,124 @@ class Pokec(Dataset): return ("MATCH (n) RETURN min(n.age), max(n.age), avg(n.age)", {}) def benchmark__match__pattern_cycle(self): - return ("MATCH (n:User {id: $id})-[e1]->(m)-[e2]->(n) " - "RETURN e1, m, e2", - {"id": self._get_random_vertex()}) + return ("MATCH (n:User {id: $id})-[e1]->(m)-[e2]->(n) " "RETURN e1, m, e2", {"id": self._get_random_vertex()}) def benchmark__match__pattern_long(self): - return ("MATCH (n1:User {id: $id})-[e1]->(n2)-[e2]->" - "(n3)-[e3]->(n4)<-[e4]-(n5) " - "RETURN n5 LIMIT 1", - {"id": self._get_random_vertex()}) + return ( + "MATCH (n1:User {id: $id})-[e1]->(n2)-[e2]->" "(n3)-[e3]->(n4)<-[e4]-(n5) " "RETURN n5 LIMIT 1", + {"id": self._get_random_vertex()}, + ) def benchmark__match__pattern_short(self): - return ("MATCH (n:User {id: $id})-[e]->(m) " - "RETURN m LIMIT 1", - {"id": self._get_random_vertex()}) + return ("MATCH (n:User {id: $id})-[e]->(m) " "RETURN m LIMIT 1", {"id": self._get_random_vertex()}) def benchmark__match__vertex_on_label_property(self): - return ("MATCH (n:User) WITH n WHERE n.id = $id RETURN n", - {"id": self._get_random_vertex()}) + return ("MATCH (n:User) WITH n WHERE n.id = $id RETURN n", {"id": self._get_random_vertex()}) def benchmark__match__vertex_on_label_property_index(self): - return ("MATCH (n:User {id: $id}) RETURN n", - {"id": self._get_random_vertex()}) + return ("MATCH (n:User {id: $id}) RETURN n", {"id": self._get_random_vertex()}) def benchmark__match__vertex_on_property(self): - return ("MATCH (n {id: $id}) RETURN n", - {"id": self._get_random_vertex()}) + return ("MATCH (n {id: $id}) RETURN n", {"id": self._get_random_vertex()}) + + +class AccessControl(Dataset): + + # Explaination of datasets: + # - empty_only_index: contains index; contains no data + # - small/medium/large: contains index; contains data (respectively small/medium/large dataset) + # + # See dataset_creator.py to understand the datamodel and generate a dataset + + NAME = "accesscontrol" + VARIANTS = ["empty_only_index", "small", "medium", "large"] + DEFAULT_VARIANT = "empty_only_index" + URLS = { + "empty_only_index": "https://s3.eu-west-1.amazonaws.com/deps.memgraph.io/dataset/accesscontrol/accesscontrol_empty_only_index.setup.cypher.gz", + "small": "https://s3.eu-west-1.amazonaws.com/deps.memgraph.io/dataset/accesscontrol/accesscontrol_small.setup.cypher.gz", + "medium": "https://s3.eu-west-1.amazonaws.com/deps.memgraph.io/dataset/accesscontrol/accesscontrol_medium.setup.cypher.gz", + "large": "https://s3.eu-west-1.amazonaws.com/deps.memgraph.io/dataset/accesscontrol/accesscontrol_large.setup.cypher.gz", + } + SIZES = { + "empty_only_index": { + "vertices": 0, + "edges": -1, # not used + "uuid_ranges": { + "File": {"first_uuid": 0, "last_uuid": 0}, + "Permission": {"first_uuid": 0, "last_uuid": 0}, + "Identity": {"first_uuid": 0, "last_uuid": 0}, + }, + }, + "small": { + "vertices": 30, + "edges": -1, # not used + "uuid_ranges": { + "File": {"first_uuid": 1, "last_uuid": 10}, + "Identity": {"first_uuid": 11, "last_uuid": 20}, + "Permission": {"first_uuid": 21, "last_uuid": 120}, # 120=10*10+20 + }, + }, + "medium": { + "vertices": 3000, + "edges": -1, # not used + "uuid_ranges": { + "File": {"first_uuid": 1, "last_uuid": 1000}, + "Identity": {"first_uuid": 1001, "last_uuid": 2000}, + "Permission": {"first_uuid": 2001, "last_uuid": 1002000}, # 1002000=1000*1000+2000 + }, + }, + "large": { + "vertices": 30000, + "edges": -1, # not used + "uuid_ranges": { + "File": {"first_uuid": 1, "last_uuid": 10000}, + "Identity": {"first_uuid": 10001, "last_uuid": 20000}, + "Permission": {"first_uuid": 20001, "last_uuid": 100020000}, # 100020000=10000*10000+20000 + }, + }, + } + + def _get_random_uuid(self, type): + assert type in ["File", "Permission", "Identity"] + + first_uuid = self.get_size()["uuid_ranges"][type]["first_uuid"] + last_uuid = self.get_size()["uuid_ranges"][type]["last_uuid"] + + random_value = random.randint(first_uuid, last_uuid) + return random_value + + def __init__(self, variant=None): + super().__init__(variant) + self.next_value_idx = self.get_size()["vertices"] + 1 + + def benchmark__create__vertex(self): + self.next_value_idx += 1 + query = (f"CREATE (:File {{uuid: {self.next_value_idx}}});", {}) + return query + + def benchmark__create__edges(self): + permission_uuid = self._get_random_uuid("Permission") + file_uuid = self._get_random_uuid("File") + + query = ( + "MATCH (permission:Permission {uuid: $permission_uuid}), (file:File {uuid: $file_uuid}) " + "CREATE (permission)-[:IS_FOR_FILE]->(file)", + {"permission_uuid": permission_uuid, "file_uuid": file_uuid}, + ) + + return query + + def benchmark__match__match_all_vertices(self): + self.next_value_idx += 1 + query = ("MATCH (n) RETURN *", {}) + return query + + def benchmark__match__match_on_labelled_vertices(self): + self.next_value_idx += 1 + query = ("MATCH (n:File) RETURN *", {}) + return query + + def benchmark__match__match_all_vertices_with_edges(self): + self.next_value_idx += 1 + query = ("MATCH (permission:Permission)-[e:IS_FOR_FILE]->(file:File) RETURN *", {}) + return query diff --git a/tests/mgbench/helpers.py b/tests/mgbench/helpers.py index 7488b1443..b46e51db4 100644 --- a/tests/mgbench/helpers.py +++ b/tests/mgbench/helpers.py @@ -14,7 +14,6 @@ import json import os import subprocess - SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) @@ -28,18 +27,25 @@ def get_binary_path(path, base=""): def download_file(url, path): - ret = subprocess.run(["wget", "-nv", "--content-disposition", url], - stderr=subprocess.PIPE, cwd=path, check=True) - data = ret.stderr.decode("utf-8") - tmp = data.split("->")[1] - name = tmp[tmp.index('"') + 1:tmp.rindex('"')] - return os.path.join(path, name) + if "https://" in url: + ret = subprocess.run( + ["wget", "-nv", "--content-disposition", url], stderr=subprocess.PIPE, cwd=path, check=True + ) + data = ret.stderr.decode("utf-8") + tmp = data.split("->")[1] + name = tmp[tmp.index('"') + 1 : tmp.rindex('"')] + return os.path.join(path, name) + else: + assert os.path.exists(url) + subprocess.run(["cp", url, path], stderr=subprocess.PIPE, cwd=path, check=True) + tmp = url.split("/") + name = tmp[len(tmp) - 1] + return os.path.join(path, name) def unpack_and_move_file(input_path, output_path): if input_path.endswith(".gz"): - subprocess.run(["gunzip", input_path], - stdout=subprocess.DEVNULL, check=True) + subprocess.run(["gunzip", input_path], stdout=subprocess.DEVNULL, check=True) input_path = input_path[:-3] os.rename(input_path, output_path) diff --git a/tests/mgbench/runners.py b/tests/mgbench/runners.py index 891a7cddd..2b69a811f 100644 --- a/tests/mgbench/runners.py +++ b/tests/mgbench/runners.py @@ -40,8 +40,7 @@ def _convert_args_to_flags(*args, **kwargs): def _get_usage(pid): total_cpu = 0 with open("/proc/{}/stat".format(pid)) as f: - total_cpu = (sum(map(int, f.read().split(")")[1].split()[11:15])) / - os.sysconf(os.sysconf_names["SC_CLK_TCK"])) + total_cpu = sum(map(int, f.read().split(")")[1].split()[11:15])) / os.sysconf(os.sysconf_names["SC_CLK_TCK"]) peak_rss = 0 with open("/proc/{}/status".format(pid)) as f: for row in f: @@ -52,18 +51,17 @@ def _get_usage(pid): class Memgraph: - def __init__(self, memgraph_binary, temporary_dir, properties_on_edges): + def __init__(self, memgraph_binary, temporary_dir, properties_on_edges, extra_args): self._memgraph_binary = memgraph_binary self._directory = tempfile.TemporaryDirectory(dir=temporary_dir) self._properties_on_edges = properties_on_edges self._proc_mg = None + self._extra_args = extra_args atexit.register(self._cleanup) # Determine Memgraph version - ret = subprocess.run([memgraph_binary, "--version"], - stdout=subprocess.PIPE, check=True) - version = re.search(r"[0-9]+\.[0-9]+\.[0-9]+", - ret.stdout.decode("utf-8")).group(0) + ret = subprocess.run([memgraph_binary, "--version"], stdout=subprocess.PIPE, check=True) + version = re.search(r"[0-9]+\.[0-9]+\.[0-9]+", ret.stdout.decode("utf-8")).group(0) self._memgraph_version = tuple(map(int, version.split("."))) def __del__(self): @@ -79,8 +77,14 @@ class Memgraph: if self._memgraph_version >= (0, 50, 0): kwargs["storage_properties_on_edges"] = self._properties_on_edges else: - assert self._properties_on_edges, \ - "Older versions of Memgraph can't disable properties on edges!" + assert self._properties_on_edges, "Older versions of Memgraph can't disable properties on edges!" + + if self._extra_args != "": + args_list = self._extra_args.split(" ") + assert len(args_list) % 2 == 0 + for i in range(0, len(args_list), 2): + kwargs[args_list[i]] = args_list[i + 1] + return _convert_args_to_flags(self._memgraph_binary, **kwargs) def _start(self, **kwargs): @@ -94,8 +98,7 @@ class Memgraph: raise Exception("The database process died prematurely!") wait_for_server(7687) ret = self._proc_mg.poll() - assert ret is None, "The database process died prematurely " \ - "({})!".format(ret) + assert ret is None, "The database process died prematurely " "({})!".format(ret) def _cleanup(self): if self._proc_mg is None: @@ -121,8 +124,7 @@ class Memgraph: def stop(self): ret, usage = self._cleanup() - assert ret == 0, "The database process exited with a non-zero " \ - "status ({})!".format(ret) + assert ret == 0, "The database process exited with a non-zero " "status ({})!".format(ret) return usage @@ -135,8 +137,7 @@ class Client: return _convert_args_to_flags(self._client_binary, **kwargs) def execute(self, queries=None, file_path=None, num_workers=1): - if (queries is None and file_path is None) or \ - (queries is not None and file_path is not None): + if (queries is None and file_path is None) or (queries is not None and file_path is not None): raise ValueError("Either queries or input_path must be specified!") # TODO: check `file_path.endswith(".json")` to support advanced @@ -151,8 +152,8 @@ class Client: json.dump(query, f) f.write("\n") - args = self._get_args(input=file_path, num_workers=num_workers, - queries_json=queries_json) + args = self._get_args(input=file_path, num_workers=num_workers, queries_json=queries_json) ret = subprocess.run(args, stdout=subprocess.PIPE, check=True) data = ret.stdout.decode("utf-8").strip().split("\n") + data = [x for x in data if not x.startswith("[")] return list(map(json.loads, data)) diff --git a/tests/mgbench/splitfiles/accesscontrol_large.shard_configuration b/tests/mgbench/splitfiles/accesscontrol_large.shard_configuration new file mode 100644 index 000000000..34dca66be --- /dev/null +++ b/tests/mgbench/splitfiles/accesscontrol_large.shard_configuration @@ -0,0 +1,36 @@ +4 +uuid +email +name +platformId +2 +IS_FOR_IDENTITY +IS_FOR_FILE +3 +File +1 +uuid +int +1 +[1] +Identity +1 +uuid +int +1 +[10001] +Permission +1 +uuid +int +10 +[20001] +[10020000] +[20002000] +[30002000] +[40002000] +[50002000] +[60002000] +[70002000] +[80002000] +[90002000] diff --git a/tests/mgbench/splitfiles/accesscontrol_medium.shard_configuration b/tests/mgbench/splitfiles/accesscontrol_medium.shard_configuration new file mode 100644 index 000000000..a807e783f --- /dev/null +++ b/tests/mgbench/splitfiles/accesscontrol_medium.shard_configuration @@ -0,0 +1,36 @@ +4 +uuid +email +name +platformId +2 +IS_FOR_IDENTITY +IS_FOR_FILE +3 +File +1 +uuid +int +1 +[1] +Identity +1 +uuid +int +1 +[1001] +Permission +1 +uuid +int +10 +[2001] +[102000] +[202000] +[302000] +[402000] +[502000] +[602000] +[702000] +[802000] +[902000] diff --git a/tests/mgbench/splitfiles/accesscontrol_small.shard_configuration b/tests/mgbench/splitfiles/accesscontrol_small.shard_configuration new file mode 100644 index 000000000..9c11b6258 --- /dev/null +++ b/tests/mgbench/splitfiles/accesscontrol_small.shard_configuration @@ -0,0 +1,36 @@ +4 +uuid +email +name +platformId +2 +IS_FOR_IDENTITY +IS_FOR_FILE +3 +File +1 +uuid +int +1 +[1] +Identity +1 +uuid +int +1 +[11] +Permission +1 +uuid +int +10 +[21] +[31] +[41] +[51] +[61] +[71] +[81] +[91] +[100] +[110]