πŸš€ Performance Guide

This guide provides best practices and optimization techniques for maximizing performance when using CVOYA graph.

πŸ“Š General Performance Principles

1. Query Optimization

2. Connection Management

3. Transaction Efficiency

🎯 Querying Best Practices

Efficient Node Queries

// βœ… Good - specific query with projection
var userEmails = await graph.Nodes<User>()
    .Where(u => u.IsActive && u.CreatedDate > DateTime.Now.AddDays(-30))
    .Select(u => new { u.Id, u.Email })
    .ToListAsync();

// ❌ Avoid - loading full objects when only emails needed
var users = await graph.Nodes<User>()
    .Where(u => u.IsActive)
    .ToListAsync();
var emails = users.Select(u => u.Email).ToList();

Efficient Relationship Traversal

// βœ… Good - depth-limited traversal
var nearbyFriends = await graph.Nodes<User>()
    .Where(u => u.Id == userId)
    .Traverse<FriendOf, Person>(minDepth: 1, maxDepth: 2)  // Limit to 2 degrees of separation
    .Where(friend => friend.City == "Seattle")
    .ToListAsync();

// ❌ Avoid - unlimited depth traversal
var allConnections = await graph.Nodes<User>()
    .Where(u => u.Id == userId)
    .Traverse<FriendOf, Person>(minDepth: 1, maxDepth: 100) // Large depth limit
    .ToListAsync();

Pagination for Large Results

// βœ… Good - paginated results
var pageSize = 100;
var page = 0;

var firstPage = await graph.Nodes<User>()
    .OrderBy(u => u.CreatedDate)
    .Skip(page * pageSize)
    .Take(pageSize)
    .ToListAsync();

// For very large datasets, consider cursor-based pagination
var lastSeenDate = DateTime.UtcNow.AddDays(-1);
var nextPage = await graph.Nodes<User>()
    .Where(u => u.CreatedDate > lastSeenDate)
    .OrderBy(u => u.CreatedDate)
    .Take(pageSize)
    .ToListAsync();

πŸ”§ Neo4j-Specific Optimizations

Connection Configuration

// βœ… Optimized connection settings
var driver = GraphDatabase.Driver(
    "neo4j+s://your-server:7687",
    AuthTokens.Basic("username", "password"),
    config => config
        .WithMaxConnectionLifetime(TimeSpan.FromMinutes(30))
        .WithMaxConnectionPoolSize(100)
        .WithConnectionAcquisitionTimeout(TimeSpan.FromSeconds(30))
        .WithConnectionTimeout(TimeSpan.FromSeconds(30)));

var store = new Neo4jGraphStore(driver);
var graph = store.Graph;

Efficient Batch Operations

// βœ… Good - batch create in single transaction
await using var transaction = await graph.GetTransactionAsync();

var users = new List<User>();
for (int i = 0; i < 1000; i++)
{
    users.Add(new User { Id = Guid.NewGuid().ToString(), Email = $"user{i}@example.com" });
}

foreach (var user in users)
{
    await graph.CreateNodeAsync(user, transaction: transaction);
}
await transaction.CommitAsync();

// ❌ Avoid - individual transactions
foreach (var user in users)
{
    await graph.CreateNodeAsync(user);  // Separate transaction per node!
}

Complex Property Optimization

Complex properties are stored as first-class value nodes, so each value adds a node and relationship. They auto-load recursively for declared read shapes, bounded at five relationship levels. Keep frequently filtered scalar values near the owning node, and use explicit graph entities when a value needs shared identity across owners. Collections preserve order through relationship sequence metadata.

// For large complex objects, consider splitting into separate nodes
public record User : Node
{
    public string Email { get; set; } = string.Empty;

    // βœ… Good - simple properties on main node
    public string Name { get; set; } = string.Empty;
    public DateTime CreatedDate { get; set; }

    // ❌ Avoid - very large auto-loaded complex-property subgraphs
    // public List<VeryLargeObject> ComplexData { get; set; }
}

// βœ… Better - separate related entities
public record UserProfile : Node
{
    public string UserId { get; set; } = string.Empty;
    public List<SomeComplexData> ProfileData { get; set; } = new();
}

πŸ“ˆ Monitoring and Profiling

Query Performance Monitoring

// Enable query logging for development
using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
var store = new Neo4jGraphStore(connectionString, username, password, loggerFactory: loggerFactory);
var graph = store.Graph;

// Time critical operations
var stopwatch = Stopwatch.StartNew();
var results = await graph.Nodes<User>().Where(u => u.Email == email).ToListAsync();
stopwatch.Stop();

if (stopwatch.ElapsedMilliseconds > 1000)
{
    _logger.LogWarning($"Slow query detected: {stopwatch.ElapsedMilliseconds}ms");
}

Memory Usage Optimization

// βœ… Good - dispose resources properly
await using var store = new Neo4jGraphStore(connectionString, username, password);
var graph = store.Graph;

// βœ… Good - use streaming for large datasets
await foreach (var user in graph.Nodes<User>())
{
    // Process one at a time instead of loading all into memory
    ProcessUser(user);
}

// ❌ Avoid - loading huge datasets into memory
var allUsers = await graph.Nodes<User>().ToListAsync();  // Could use lots of RAM

πŸŽ›οΈ Configuration Tuning

Neo4j Driver Settings

Pseudo-code:
Configure connection lifetime, pool size, acquisition timeout, connection timeout,
TLS trust strategy, and logging in the Neo4j driver configuration callback used
when creating the driver.

Application-Level Caching

// Consider caching frequently accessed, rarely changing data
private readonly Dictionary<string, User> _cache = new();

public async Task<User> GetUserAsync(string userId)
{
    if (_cache.TryGetValue($"user:{userId}", out User cachedUser))
    {
        return cachedUser;
    }

    var user = await graph.Nodes<User>()
        .Where(u => u.Id == userId)
        .FirstOrDefaultAsync();

    if (user != null)
    {
        _cache[$"user:{userId}"] = user;
    }

    return user;
}

πŸ“‹ Performance Checklist

Before Going to Production

Common Performance Issues

πŸ”§ Troubleshooting Slow Queries

1. Enable Query Logging

using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
var store = new Neo4jGraphStore(connectionString, username, password, loggerFactory: loggerFactory);
var graph = store.Graph;

2. Analyze Generated Cypher

Review the generated Cypher queries in logs:

3. Use Neo4j Query Profiling

PROFILE MATCH (u:User) RETURN u
WHERE u.email = $p0
RETURN ...

4. Common Query Patterns to Avoid

// ❌ Avoid - Loading related data in loops (N+1 problem)
foreach (var user in users)
{
    user.Friends = await graph.Relationships<FriendOf>()
        .Where(f => f.StartNodeId == user.Id)
        .ToListAsync();
}

// βœ… Better - Load related data in batch
var userIds = users.Select(u => u.Id).ToList();
var friendships = await graph.Relationships<FriendOf>()
    .Where(f => userIds.Contains(f.StartNodeId))
    .ToListAsync();

πŸ“Š Benchmarking

Create benchmarks for critical operations:

[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net10_0)]
public class CVOYA graphBenchmarks
{
    private Neo4jGraphStore _store = null!;
    private IGraph _graph = null!;

    private const string ConnectionString = "neo4j://localhost:7687";
    private const string Username = "neo4j";
    private const string Password = "password";

    [GlobalSetup]
    public void Setup()
    {
        _store = new Neo4jGraphStore(ConnectionString, Username, Password);
        _graph = _store.Graph;
    }

    [Benchmark]
    public async Task<List<User>> QueryUsers()
    {
        return await _graph.Nodes<User>()
            .Where(u => u.IsActive)
            .Take(1000)
            .ToListAsync();
    }

    [GlobalCleanup]
    public async Task Cleanup()
    {
        await _store.DisposeAsync();
    }
}

Remember: Measure first, optimize second. Use profiling tools to identify actual bottlenecks rather than guessing.

Performance Testing and Benchmarking

CVOYA graph includes comprehensive performance testing infrastructure using BenchmarkDotNet to ensure consistent performance across releases and detect regressions.

πŸ“Š Available Benchmarks

CRUD Operations Benchmark

Relationship Benchmark

πŸš€ Running Benchmarks Locally

Quick Start

Use the provided scripts for easy benchmark execution:

PowerShell (Windows)

# Run all benchmarks
./scripts/run-benchmarks.ps1

# Run specific benchmark types
./scripts/run-benchmarks.ps1 -Mode crud
./scripts/run-benchmarks.ps1 -Mode relationships

# Interactive selection (choose specific benchmarks)
./scripts/run-benchmarks.ps1 -Mode interactive

# Custom output directory
./scripts/run-benchmarks.ps1 -OutputDir "./my-benchmarks"

Bash (macOS/Linux)

# Run all benchmarks
./scripts/run-benchmarks.sh

# Run specific benchmark types
./scripts/run-benchmarks.sh --mode crud
./scripts/run-benchmarks.sh --mode relationships

# Interactive selection
./scripts/run-benchmarks.sh --mode interactive

# Custom output directory
./scripts/run-benchmarks.sh --output "./my-benchmarks"

Manual Execution

You can also run benchmarks directly:

# Build the performance test project
dotnet build tests/Cvoya.Graph.Performance.Tests --configuration Benchmark

# Run all benchmarks (non-interactive)
dotnet run --project tests/Cvoya.Graph.Performance.Tests --configuration Benchmark -- --all

# Run specific benchmark class
dotnet run --project tests/Cvoya.Graph.Performance.Tests --configuration Benchmark -- --filter "*CrudOperations*"

# Interactive mode (local development only)
dotnet run --project tests/Cvoya.Graph.Performance.Tests --configuration Benchmark

πŸ”„ CI/CD Integration

Automated Performance Testing

The GitHub Actions workflow (.github/workflows/performance.yml) automatically:

  1. Runs on every push to main and pull requests
  2. Uses non-interactive mode with --all parameter
  3. Generates multiple report formats:
    • HTML reports for human readability
    • JSON reports for programmatic analysis
    • Markdown summaries for PR comments
  4. Stores artifacts for 30 days
  5. Compares performance against baseline when available

Non-Interactive Mode

The performance tests are configured to run automatically in CI environments:

Benchmark Configuration

CVOYA graph uses a special Benchmark build configuration that combines:

This configuration is ideal for:

Workflow Configuration

The performance workflow runs:

πŸ“ˆ Interpreting Results

BenchmarkDotNet Output

Results include:

HTML Reports

Generated HTML reports provide:

Performance Baselines

Key performance indicators to monitor:

🎯 Adding New Benchmarks

Creating a Benchmark Class

[MemoryDiagnoser]
[HtmlExporter]
[MarkdownExporter]
public class MyCustomBenchmark
{
    private IGraph _graph = null!;
    private Neo4jGraphStore _store = null!;

    [GlobalSetup]
    public void Setup()
    {
        // Initialize test data
        _store = CreateTestGraphStore();
        _graph = _store.Graph;
    }

    [Benchmark]
    public async Task<int> MyOperation()
    {
        // Your benchmark code here
        return await _graph.Nodes<MyNode>().CountAsync();
    }

    [GlobalCleanup]
    public async Task Cleanup()
    {
        await _store.DisposeAsync();
    }

    private static Neo4jGraphStore CreateTestGraphStore()
        => new("neo4j://localhost:7687", "neo4j", "password");
}

Benchmark Attributes

Best Practices

  1. Realistic data: Use representative dataset sizes
  2. Proper setup/cleanup: Initialize in [GlobalSetup], cleanup in [GlobalCleanup]
  3. Avoid optimization: Don’t let compiler optimize away your benchmark
  4. Consistent environment: Use same configuration across runs
  5. Multiple iterations: Let BenchmarkDotNet determine optimal iteration count

πŸ”§ Configuration

BenchmarkDotNet Configuration

The performance tests use a custom configuration optimized for CI:

var config = DefaultConfig.Instance
    .AddJob(Job.Default.WithToolchain(InProcessEmitToolchain.Instance))
    .WithOptions(ConfigOptions.DisableOptimizationsValidator);

Output Formats

Multiple export formats are generated:

Artifacts

Benchmark results are stored as GitHub artifacts:

🚨 Performance Regression Detection

Monitoring

Thresholds

Performance regression is flagged when:

Investigation

When regressions are detected:

  1. Compare reports: Review before/after performance data
  2. Profile locally: Use detailed profiling tools for root cause analysis
  3. Isolate changes: Identify specific commits causing regression
  4. Optimize: Apply targeted performance improvements