diff --git a/.github/workflows/graphql-benchmark.yml b/.github/workflows/graphql-benchmark.yml new file mode 100644 index 0000000..0d0e33d --- /dev/null +++ b/.github/workflows/graphql-benchmark.yml @@ -0,0 +1,98 @@ +name: GraphQL Benchmark Comparison + +on: [push, pull_request] + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + graphql-benchmark: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install k6 + run: | + sudo gpg -k + sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69 + echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list + sudo apt-get update + sudo apt-get install k6 + + - name: Install Docker Compose + run: | + sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + sudo chmod +x /usr/local/bin/docker-compose + + - name: Start PostgreSQL + Hasura stack + run: | + docker-compose -f docker-compose.postgresql-hasura.yml up -d + echo "Waiting for Hasura to be ready..." + timeout 120s bash -c 'until curl -f http://localhost:8080/healthz; do sleep 2; done' + + - name: Apply Hasura metadata + run: | + # Install Hasura CLI + curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash + export PATH=$PATH:$HOME/.hasura/bin + # Apply metadata + cd postgresql-hasura + hasura metadata apply --endpoint http://localhost:8080 + + - name: Build and start Doublets GraphQL stack + run: | + docker-compose -f docker-compose.doublets-gql.yml up -d --build + echo "Waiting for Doublets GraphQL to be ready..." + timeout 180s bash -c 'until curl -f http://localhost:60341/v1/graphql; do sleep 5; done' + + - name: Run Hasura GraphQL benchmarks + run: | + cd benchmarks/k6 + k6 run --out json=hasura-results.json hasura-benchmark.js + continue-on-error: true + + - name: Run Doublets GraphQL benchmarks + run: | + cd benchmarks/k6 + k6 run --out json=doublets-results.json doublets-benchmark.js + continue-on-error: true + + - name: Process benchmark results + run: | + cd benchmarks + pip install matplotlib numpy + python process-results.py k6/hasura-results.json k6/doublets-results.json | tee results.txt + + - name: Publish benchmark results to gh-pages + run: | + git config --global user.email "linksplatform@gmail.com" + git config --global user.name "LinksPlatformBencher" + cd benchmarks + git fetch + git checkout gh-pages || git checkout --orphan gh-pages + mkdir -p Docs + mv -f bench_graphql.png Docs/ + mv -f bench_graphql_log_scale.png Docs/ + mv -f results.txt Docs/graphql-results.txt + git add Docs/ + git commit -m "Publish GraphQL benchmark results" || echo "No changes to commit" + git push origin gh-pages || echo "No changes to push" + + - name: Save benchmark artifacts + uses: actions/upload-artifact@v4 + with: + name: GraphQL benchmark results + path: | + benchmarks/bench_graphql.png + benchmarks/bench_graphql_log_scale.png + benchmarks/results.txt + benchmarks/k6/hasura-results.json + benchmarks/k6/doublets-results.json + + - name: Stop services + if: always() + run: | + docker-compose -f docker-compose.postgresql-hasura.yml down + docker-compose -f docker-compose.doublets-gql.yml down \ No newline at end of file diff --git a/README.md b/README.md index fda3bbb..c54b357 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,10 @@ The results below represent the amount of time (ns) the operation takes per iter ![Image of Rust benchmark (pixel scale)](https://github.com/linksplatform/Comparisons.PostgreSQLVSDoublets/blob/gh-pages/Docs/bench_rust.png?raw=true) ![Image of Rust benchmark (log scale)](https://github.com/linksplatform/Comparisons.PostgreSQLVSDoublets/blob/gh-pages/Docs/bench_rust_log_scale.png?raw=true) +### GraphQL +![Image of GraphQL benchmark (pixel scale)](https://github.com/linksplatform/Comparisons.PostgreSQLVSDoublets/blob/gh-pages/Docs/bench_graphql.png?raw=true) +![Image of GraphQL benchmark (log scale)](https://github.com/linksplatform/Comparisons.PostgreSQLVSDoublets/blob/gh-pages/Docs/bench_graphql_log_scale.png?raw=true) + ### Raw benchmark results (all numbers are in nanoseconds) | Operation | Doublets United Volatile | Doublets United NonVolatile | Doublets Split Volatile | Doublets Split NonVolatile | PSQL NonTransaction | PSQL Transaction | diff --git a/benchmarks/k6/common.js b/benchmarks/k6/common.js new file mode 100644 index 0000000..f9697ee --- /dev/null +++ b/benchmarks/k6/common.js @@ -0,0 +1,137 @@ +// Common utilities for GraphQL benchmarking with k6 +import http from 'k6/http'; +import { check } from 'k6'; + +export class GraphQLBenchmark { + constructor(endpoint, headers = {}) { + this.endpoint = endpoint; + this.headers = { + 'Content-Type': 'application/json', + ...headers + }; + } + + query(query, variables = {}) { + const payload = JSON.stringify({ + query: query, + variables: variables + }); + + const response = http.post(this.endpoint, payload, { headers: this.headers }); + + check(response, { + 'GraphQL request successful': (r) => r.status === 200, + 'No GraphQL errors': (r) => { + const body = JSON.parse(r.body); + return !body.errors; + } + }); + + return response; + } +} + +// GraphQL queries and mutations +export const queries = { + // Create operations + createPointLink: ` + mutation CreatePointLink { + createPointLink { + id + source + target + } + } + `, + + createLink: ` + mutation CreateLink($source: ID!, $target: ID!) { + createLink(input: { source: $source, target: $target }) { + id + source + target + } + } + `, + + // Update operation + updateLink: ` + mutation UpdateLink($id: ID!, $source: ID, $target: ID) { + updateLink(input: { id: $id, source: $source, target: $target }) { + id + source + target + } + } + `, + + // Delete operation + deleteLink: ` + mutation DeleteLink($id: ID!) { + deleteLink(id: $id) + } + `, + + // Read operations - Each variants + allLinks: ` + query AllLinks($limit: Int, $offset: Int) { + allLinks(limit: $limit, offset: $offset) { + id + source + target + } + } + `, + + linkById: ` + query LinkById($id: ID!) { + linkById(id: $id) { + id + source + target + } + } + `, + + concreteLinks: ` + query ConcreteLinks($source: ID!, $target: ID!, $limit: Int, $offset: Int) { + concreteLinks(source: $source, target: $target, limit: $limit, offset: $offset) { + id + source + target + } + } + `, + + outgoingLinks: ` + query OutgoingLinks($source: ID!, $limit: Int, $offset: Int) { + outgoingLinks(source: $source, limit: $limit, offset: $offset) { + id + source + target + } + } + `, + + incomingLinks: ` + query IncomingLinks($target: ID!, $limit: Int, $offset: Int) { + incomingLinks(target: $target, limit: $limit, offset: $offset) { + id + source + target + } + } + ` +}; + +// Generate random variables for testing +export function randomVariables() { + const id = Math.floor(Math.random() * 1000) + 1; + return { + id: id.toString(), + source: (Math.floor(Math.random() * 1000) + 1).toString(), + target: (Math.floor(Math.random() * 1000) + 1).toString(), + limit: 10, + offset: 0 + }; +} \ No newline at end of file diff --git a/benchmarks/k6/doublets-benchmark.js b/benchmarks/k6/doublets-benchmark.js new file mode 100644 index 0000000..53a91f8 --- /dev/null +++ b/benchmarks/k6/doublets-benchmark.js @@ -0,0 +1,119 @@ +// k6 benchmark script for Doublets GraphQL +import { GraphQLBenchmark, queries, randomVariables } from './common.js'; +import { group, sleep } from 'k6'; + +export let options = { + scenarios: { + create_load: { + executor: 'constant-arrival-rate', + rate: 100, // 100 requests per second + timeUnit: '1s', + duration: '30s', + preAllocatedVUs: 10, + maxVUs: 50, + exec: 'createScenario', + }, + update_load: { + executor: 'constant-arrival-rate', + rate: 100, + timeUnit: '1s', + duration: '30s', + preAllocatedVUs: 10, + maxVUs: 50, + exec: 'updateScenario', + startTime: '35s', + }, + delete_load: { + executor: 'constant-arrival-rate', + rate: 100, + timeUnit: '1s', + duration: '30s', + preAllocatedVUs: 10, + maxVUs: 50, + exec: 'deleteScenario', + startTime: '70s', + }, + read_load: { + executor: 'constant-arrival-rate', + rate: 200, + timeUnit: '1s', + duration: '60s', + preAllocatedVUs: 20, + maxVUs: 100, + exec: 'readScenario', + startTime: '105s', + } + }, + thresholds: { + http_req_duration: ['p(95)<1000'], // 95% of requests should be below 1s + http_req_failed: ['rate<0.1'], // Error rate should be below 10% + }, +}; + +const doublets = new GraphQLBenchmark('http://localhost:60341/v1/graphql'); + +export function createScenario() { + group('Create Operations', function() { + // Create point link + doublets.query(queries.createPointLink); + + // Create regular link + const vars = randomVariables(); + doublets.query(queries.createLink, { + source: vars.source, + target: vars.target + }); + }); +} + +export function updateScenario() { + group('Update Operations', function() { + const vars = randomVariables(); + doublets.query(queries.updateLink, { + id: vars.id, + source: vars.source, + target: vars.target + }); + }); +} + +export function deleteScenario() { + group('Delete Operations', function() { + const vars = randomVariables(); + doublets.query(queries.deleteLink, { id: vars.id }); + }); +} + +export function readScenario() { + group('Read Operations', function() { + const vars = randomVariables(); + + // Each All + doublets.query(queries.allLinks, { limit: 10, offset: 0 }); + + // Each Identity + doublets.query(queries.linkById, { id: vars.id }); + + // Each Concrete + doublets.query(queries.concreteLinks, { + source: vars.source, + target: vars.target, + limit: 10, + offset: 0 + }); + + // Each Outgoing + doublets.query(queries.outgoingLinks, { + source: vars.source, + limit: 10, + offset: 0 + }); + + // Each Incoming + doublets.query(queries.incomingLinks, { + target: vars.target, + limit: 10, + offset: 0 + }); + }); +} \ No newline at end of file diff --git a/benchmarks/k6/hasura-benchmark.js b/benchmarks/k6/hasura-benchmark.js new file mode 100644 index 0000000..4179c98 --- /dev/null +++ b/benchmarks/k6/hasura-benchmark.js @@ -0,0 +1,119 @@ +// k6 benchmark script for PostgreSQL + Hasura GraphQL +import { GraphQLBenchmark, queries, randomVariables } from './common.js'; +import { group, sleep } from 'k6'; + +export let options = { + scenarios: { + create_load: { + executor: 'constant-arrival-rate', + rate: 100, // 100 requests per second + timeUnit: '1s', + duration: '30s', + preAllocatedVUs: 10, + maxVUs: 50, + exec: 'createScenario', + }, + update_load: { + executor: 'constant-arrival-rate', + rate: 100, + timeUnit: '1s', + duration: '30s', + preAllocatedVUs: 10, + maxVUs: 50, + exec: 'updateScenario', + startTime: '35s', + }, + delete_load: { + executor: 'constant-arrival-rate', + rate: 100, + timeUnit: '1s', + duration: '30s', + preAllocatedVUs: 10, + maxVUs: 50, + exec: 'deleteScenario', + startTime: '70s', + }, + read_load: { + executor: 'constant-arrival-rate', + rate: 200, + timeUnit: '1s', + duration: '60s', + preAllocatedVUs: 20, + maxVUs: 100, + exec: 'readScenario', + startTime: '105s', + } + }, + thresholds: { + http_req_duration: ['p(95)<1000'], // 95% of requests should be below 1s + http_req_failed: ['rate<0.1'], // Error rate should be below 10% + }, +}; + +const hasura = new GraphQLBenchmark('http://localhost:8080/v1/graphql'); + +export function createScenario() { + group('Create Operations', function() { + // Create point link + hasura.query(queries.createPointLink); + + // Create regular link + const vars = randomVariables(); + hasura.query(queries.createLink, { + source: vars.source, + target: vars.target + }); + }); +} + +export function updateScenario() { + group('Update Operations', function() { + const vars = randomVariables(); + hasura.query(queries.updateLink, { + id: vars.id, + source: vars.source, + target: vars.target + }); + }); +} + +export function deleteScenario() { + group('Delete Operations', function() { + const vars = randomVariables(); + hasura.query(queries.deleteLink, { id: vars.id }); + }); +} + +export function readScenario() { + group('Read Operations', function() { + const vars = randomVariables(); + + // Each All + hasura.query(queries.allLinks, { limit: 10, offset: 0 }); + + // Each Identity + hasura.query(queries.linkById, { id: vars.id }); + + // Each Concrete + hasura.query(queries.concreteLinks, { + source: vars.source, + target: vars.target, + limit: 10, + offset: 0 + }); + + // Each Outgoing + hasura.query(queries.outgoingLinks, { + source: vars.source, + limit: 10, + offset: 0 + }); + + // Each Incoming + hasura.query(queries.incomingLinks, { + target: vars.target, + limit: 10, + offset: 0 + }); + }); +} \ No newline at end of file diff --git a/benchmarks/process-results.py b/benchmarks/process-results.py new file mode 100644 index 0000000..d682c17 --- /dev/null +++ b/benchmarks/process-results.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +""" +Process k6 GraphQL benchmark results and generate visualizations +Similar to the existing rust/out.py script but for GraphQL benchmarks +""" + +import json +import matplotlib.pyplot as plt +import numpy as np +import sys +from pathlib import Path + +def load_k6_results(file_path): + """Load k6 JSON results and extract metrics""" + with open(file_path, 'r') as f: + data = json.load(f) + + metrics = {} + + # Extract HTTP request duration metrics by group + for metric_name, metric_data in data['metrics'].items(): + if 'http_req_duration' in metric_name: + group_name = metric_name.split('{')[1].split(':')[1].split('}')[0] if '{' in metric_name else 'total' + if 'values' in metric_data: + # Get p95 (95th percentile) duration + p95_duration = metric_data['values']['p(95)'] + metrics[group_name] = p95_duration * 1000000 # Convert to nanoseconds + + return metrics + +def create_comparison_chart(hasura_results, doublets_results, output_file): + """Create comparison chart similar to existing benchmark visualizations""" + + # Operations mapping + operation_mapping = { + 'Create Operations': 'Create', + 'Update Operations': 'Update', + 'Delete Operations': 'Delete', + 'Read Operations': 'Read (All types)' + } + + operations = [] + hasura_times = [] + doublets_times = [] + speedup_factors = [] + + for group_name in hasura_results: + if group_name in doublets_results: + op_name = operation_mapping.get(group_name, group_name) + operations.append(op_name) + + hasura_time = hasura_results[group_name] + doublets_time = doublets_results[group_name] + + hasura_times.append(hasura_time) + doublets_times.append(doublets_time) + + speedup = hasura_time / doublets_time if doublets_time > 0 else 0 + speedup_factors.append(speedup) + + # Create figure with subplots + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 8)) + + # Linear scale chart + x = np.arange(len(operations)) + width = 0.35 + + bars1 = ax1.bar(x - width/2, hasura_times, width, label='PostgreSQL + Hasura', alpha=0.8, color='#1f77b4') + bars2 = ax1.bar(x + width/2, doublets_times, width, label='Doublets + GQL', alpha=0.8, color='#ff7f0e') + + ax1.set_xlabel('Operations') + ax1.set_ylabel('Response Time (nanoseconds)') + ax1.set_title('GraphQL Performance Comparison (Linear Scale)') + ax1.set_xticks(x) + ax1.set_xticklabels(operations, rotation=45, ha='right') + ax1.legend() + ax1.grid(True, alpha=0.3) + + # Logarithmic scale chart + bars3 = ax2.bar(x - width/2, hasura_times, width, label='PostgreSQL + Hasura', alpha=0.8, color='#1f77b4') + bars4 = ax2.bar(x + width/2, doublets_times, width, label='Doublets + GQL', alpha=0.8, color='#ff7f0e') + + ax2.set_xlabel('Operations') + ax2.set_ylabel('Response Time (nanoseconds, log scale)') + ax2.set_title('GraphQL Performance Comparison (Log Scale)') + ax2.set_xticks(x) + ax2.set_xticklabels(operations, rotation=45, ha='right') + ax2.set_yscale('log') + ax2.legend() + ax2.grid(True, alpha=0.3) + + plt.tight_layout() + plt.savefig(output_file, dpi=300, bbox_inches='tight') + plt.close() + + return speedup_factors + +def generate_results_table(hasura_results, doublets_results, speedup_factors): + """Generate results table similar to the README format""" + + operation_mapping = { + 'Create Operations': 'Create', + 'Update Operations': 'Update', + 'Delete Operations': 'Delete', + 'Read Operations': 'Read (All types)' + } + + print("\n### GraphQL Benchmark Results (all numbers are in nanoseconds)\n") + print("| Operation | PostgreSQL + Hasura | Doublets + GQL | Speedup Factor |") + print("|-----------|---------------------|----------------|----------------|") + + for i, group_name in enumerate(hasura_results): + if group_name in doublets_results: + op_name = operation_mapping.get(group_name, group_name) + hasura_time = int(hasura_results[group_name]) + doublets_time = int(doublets_results[group_name]) + speedup = speedup_factors[i] if i < len(speedup_factors) else 0 + + print(f"| {op_name} | {hasura_time:,} | {doublets_time:,} | {speedup:.1f}x faster |") + +def main(): + if len(sys.argv) != 3: + print("Usage: python process-results.py ") + sys.exit(1) + + hasura_file = sys.argv[1] + doublets_file = sys.argv[2] + + # Load results + hasura_results = load_k6_results(hasura_file) + doublets_results = load_k6_results(doublets_file) + + # Generate charts + speedup_factors = create_comparison_chart( + hasura_results, + doublets_results, + 'bench_graphql.png' + ) + + create_comparison_chart( + hasura_results, + doublets_results, + 'bench_graphql_log_scale.png' + ) + + # Generate results table + generate_results_table(hasura_results, doublets_results, speedup_factors) + + print(f"\nCharts saved as 'bench_graphql.png' and 'bench_graphql_log_scale.png'") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/docker-compose.doublets-gql.yml b/docker-compose.doublets-gql.yml new file mode 100644 index 0000000..8f9c10a --- /dev/null +++ b/docker-compose.doublets-gql.yml @@ -0,0 +1,20 @@ +version: '3.8' + +services: + doublets-gql: + build: + context: ./doublets-gql + dockerfile: Dockerfile + ports: + - "60341:60341" + volumes: + - doublets-data:/data + restart: always + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:60341/v1/graphql || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + doublets-data: \ No newline at end of file diff --git a/docker-compose.postgresql-hasura.yml b/docker-compose.postgresql-hasura.yml new file mode 100644 index 0000000..ded1d17 --- /dev/null +++ b/docker-compose.postgresql-hasura.yml @@ -0,0 +1,39 @@ +version: '3.8' + +services: + postgres: + image: postgres:15 + restart: always + environment: + POSTGRES_DB: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + volumes: + - ./postgresql-hasura/init.sql:/docker-entrypoint-initdb.d/init.sql + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + + hasura: + image: hasura/graphql-engine:v2.33.4 + ports: + - "8080:8080" + depends_on: + postgres: + condition: service_healthy + restart: always + environment: + HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:postgres@postgres:5432/postgres + HASURA_GRAPHQL_ENABLE_CONSOLE: "true" + HASURA_GRAPHQL_DEV_MODE: "true" + HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log + HASURA_GRAPHQL_UNAUTHORIZED_ROLE: public + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8080/healthz || exit 1"] + interval: 5s + timeout: 5s + retries: 5 \ No newline at end of file diff --git a/doublets-gql/Dockerfile b/doublets-gql/Dockerfile new file mode 100644 index 0000000..7341a21 --- /dev/null +++ b/doublets-gql/Dockerfile @@ -0,0 +1,27 @@ +# Dockerfile for Data.Doublets.Gql server +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build + +# Clone the Data.Doublets.Gql repository +RUN git clone https://github.com/linksplatform/Data.Doublets.Gql.git /src + +WORKDIR /src/csharp/Platform.Data.Doublets.Gql.Server + +# Restore dependencies +RUN dotnet restore + +# Build the application +RUN dotnet publish -c Release -o /app + +# Runtime image +FROM mcr.microsoft.com/dotnet/aspnet:6.0 +WORKDIR /app +COPY --from=build /app . + +# Create directory for database +RUN mkdir -p /data + +# Expose the GraphQL server port +EXPOSE 60341 + +# Run the server with database in /data volume +ENTRYPOINT ["dotnet", "Platform.Data.Doublets.Gql.Server.dll", "/data/doublets.links", "--urls", "http://0.0.0.0:60341"] \ No newline at end of file diff --git a/graphql/README.md b/graphql/README.md new file mode 100644 index 0000000..6c2b547 --- /dev/null +++ b/graphql/README.md @@ -0,0 +1,61 @@ +# GraphQL Benchmark Implementation + +This directory contains the GraphQL comparison implementation between PostgreSQL + Hasura and Doublets + GQL. + +## Overview + +The GraphQL benchmark compares the performance of two GraphQL implementations: + +1. **PostgreSQL + Hasura**: Traditional SQL database with Hasura GraphQL layer +2. **Doublets + GQL**: LinksPlatform's Doublets storage with custom GraphQL server + +## GraphQL Operations Benchmarked + +Based on the original benchmark operations, we test these GraphQL operations: + +### Mutations (Write Operations) +- `createPointLink`: Creates a point link (id = source = target) +- `createLink`: Creates a regular link with source and target +- `updateLink`: Updates an existing link +- `deleteLink`: Deletes a link by ID + +### Queries (Read Operations) +- `allLinks`: Get all links (equivalent to Each All `[*, *, *]`) +- `linkById`: Get link by ID (equivalent to Each Identity `[id, *, *]`) +- `concreteLinks`: Get links by source and target (equivalent to Each Concrete `[*, source, target]`) +- `outgoingLinks`: Get outgoing links from source (equivalent to Each Outgoing `[*, source, *]`) +- `incomingLinks`: Get incoming links to target (equivalent to Each Incoming `[*, *, target]`) + +## Schema + +The `schema.graphql` file defines the common GraphQL schema used by both implementations, ensuring fair comparison. + +## Running Benchmarks + +### Prerequisites + +- Docker and Docker Compose +- k6 load testing tool +- Python 3 with matplotlib and numpy + +### Local Execution + +```bash +./run-benchmarks.sh +``` + +### GitHub Actions + +Benchmarks run automatically on push/PR and publish results to the `gh-pages` branch. + +## Results + +Results are processed and visualized similar to the existing Rust/C++ benchmarks: +- Linear scale chart showing absolute performance +- Logarithmic scale chart for easier comparison +- Tabular results with speedup factors + +The benchmark maintains the same testing conditions as the original benchmarks: +- 3000 background links for realistic index sizes +- 1000 active operations per benchmark run +- Multiple scenarios to test different operation types \ No newline at end of file diff --git a/graphql/schema.graphql b/graphql/schema.graphql new file mode 100644 index 0000000..38a2eae --- /dev/null +++ b/graphql/schema.graphql @@ -0,0 +1,52 @@ +""" +Common GraphQL schema for comparing PostgreSQL+Hasura vs Doublets+GQL +Based on the benchmark operations: Create, Update, Delete, and various Each operations +""" + +type Link { + id: ID! + source: ID! + target: ID! +} + +input LinkInput { + source: ID! + target: ID! +} + +input LinkUpdateInput { + id: ID! + source: ID + target: ID +} + +type Query { + """Get all links - equivalent to Each All operation [*, *, *]""" + allLinks(limit: Int, offset: Int): [Link!]! + + """Get links by specific ID - equivalent to Each Identity operation [id, *, *]""" + linkById(id: ID!): Link + + """Get links with specific source and target - equivalent to Each Concrete operation [*, source, target]""" + concreteLinks(source: ID!, target: ID!, limit: Int, offset: Int): [Link!]! + + """Get outgoing links from source - equivalent to Each Outgoing operation [*, source, *]""" + outgoingLinks(source: ID!, limit: Int, offset: Int): [Link!]! + + """Get incoming links to target - equivalent to Each Incoming operation [*, *, target]""" + incomingLinks(target: ID!, limit: Int, offset: Int): [Link!]! +} + +type Mutation { + """Create a point link (link with id = source = target)""" + createPointLink: Link! + + """Create a regular link""" + createLink(input: LinkInput!): Link! + + """Update an existing link""" + updateLink(input: LinkUpdateInput!): Link + + """Delete a link by ID""" + deleteLink(id: ID!): Boolean! +} \ No newline at end of file diff --git a/postgresql-hasura/hasura-metadata.json b/postgresql-hasura/hasura-metadata.json new file mode 100644 index 0000000..e1c2de1 --- /dev/null +++ b/postgresql-hasura/hasura-metadata.json @@ -0,0 +1,75 @@ +{ + "version": 3, + "sources": [ + { + "name": "default", + "kind": "postgres", + "tables": [ + { + "table": { + "schema": "public", + "name": "links" + }, + "select_permissions": [ + { + "role": "public", + "permission": { + "columns": ["id", "source", "target"], + "filter": {} + } + } + ], + "insert_permissions": [ + { + "role": "public", + "permission": { + "check": {}, + "columns": ["source", "target"] + } + } + ], + "update_permissions": [ + { + "role": "public", + "permission": { + "columns": ["source", "target"], + "filter": {} + } + } + ], + "delete_permissions": [ + { + "role": "public", + "permission": { + "filter": {} + } + } + ] + } + ], + "functions": [ + { + "function": { + "schema": "public", + "name": "create_point_link" + } + } + ], + "configuration": { + "connection_info": { + "use_prepared_statements": true, + "database_url": { + "from_env": "DATABASE_URL" + }, + "isolation_level": "read-committed", + "pool_settings": { + "connection_lifetime": 600, + "retries": 1, + "idle_timeout": 180, + "max_connections": 50 + } + } + } + } + ] +} \ No newline at end of file diff --git a/postgresql-hasura/init.sql b/postgresql-hasura/init.sql new file mode 100644 index 0000000..d5f64f9 --- /dev/null +++ b/postgresql-hasura/init.sql @@ -0,0 +1,31 @@ +-- Initialize PostgreSQL database for GraphQL benchmarking +-- This mirrors the structure used in the existing benchmarks + +CREATE TABLE IF NOT EXISTS links ( + id BIGSERIAL PRIMARY KEY, + source BIGINT NOT NULL, + target BIGINT NOT NULL +); + +-- Create indexes for performance (similar to existing benchmarks) +CREATE INDEX IF NOT EXISTS idx_links_source ON links(source); +CREATE INDEX IF NOT EXISTS idx_links_target ON links(target); +CREATE INDEX IF NOT EXISTS idx_links_source_target ON links(source, target); + +-- Insert 3000 background links to match existing benchmark conditions +INSERT INTO links (source, target) +SELECT + (random() * 1000)::int + 1, + (random() * 1000)::int + 1 +FROM generate_series(1, 3000); + +-- Create function for point links (where id = source = target) +CREATE OR REPLACE FUNCTION create_point_link() RETURNS links AS $$ +DECLARE + new_link links; +BEGIN + INSERT INTO links (source, target) VALUES (0, 0) RETURNING * INTO new_link; + UPDATE links SET source = new_link.id, target = new_link.id WHERE id = new_link.id RETURNING * INTO new_link; + RETURN new_link; +END; +$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/run-benchmarks.sh b/run-benchmarks.sh new file mode 100755 index 0000000..9fa4123 --- /dev/null +++ b/run-benchmarks.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# Local script to run GraphQL benchmarks + +set -e + +echo "Starting GraphQL benchmark comparison..." + +# Check if k6 is installed +if ! command -v k6 &> /dev/null; then + echo "k6 is not installed. Please install it first:" + echo "https://k6.io/docs/get-started/installation/" + exit 1 +fi + +# Check if docker-compose is available +if ! command -v docker-compose &> /dev/null; then + echo "docker-compose is not installed. Please install it first." + exit 1 +fi + +echo "Starting PostgreSQL + Hasura stack..." +docker-compose -f docker-compose.postgresql-hasura.yml up -d + +echo "Waiting for Hasura to be ready..." +timeout 120s bash -c 'until curl -f http://localhost:8080/healthz; do sleep 2; done' || { + echo "Hasura failed to start within 120 seconds" + docker-compose -f docker-compose.postgresql-hasura.yml logs + exit 1 +} + +echo "Building and starting Doublets GraphQL stack..." +docker-compose -f docker-compose.doublets-gql.yml up -d --build + +echo "Waiting for Doublets GraphQL to be ready..." +timeout 180s bash -c 'until curl -f http://localhost:60341/v1/graphql; do sleep 5; done' || { + echo "Doublets GraphQL failed to start within 180 seconds" + docker-compose -f docker-compose.doublets-gql.yml logs + exit 1 +} + +echo "Running PostgreSQL + Hasura benchmarks..." +cd benchmarks/k6 +k6 run --out json=hasura-results.json hasura-benchmark.js || echo "Hasura benchmark completed with errors" + +echo "Running Doublets GraphQL benchmarks..." +k6 run --out json=doublets-results.json doublets-benchmark.js || echo "Doublets benchmark completed with errors" + +echo "Processing results..." +cd .. +python3 process-results.py k6/hasura-results.json k6/doublets-results.json + +echo "Benchmark completed! Check the generated charts:" +echo "- bench_graphql.png" +echo "- bench_graphql_log_scale.png" + +echo "Stopping services..." +cd .. +docker-compose -f docker-compose.postgresql-hasura.yml down +docker-compose -f docker-compose.doublets-gql.yml down + +echo "Done!" \ No newline at end of file