Performance Optimization Guide

May 13, 2025 ยท View on GitHub

Introduction

This document describes the performance optimizations implemented in Agent Rules Kit and provides recommendations for contributors who want to maintain a high level of performance in future versions.

Implemented Optimizations

1. Dynamic Service Loading

Instead of loading all stack services at startup, they are now dynamically loaded on demand:

// Before: Static loading of all services
import { AngularService } from './services/angular-service.js';
import { LaravelService } from './services/laravel-service.js';
import { NextjsService } from './services/nextjs-service.js';
import { ReactService } from './services/react-service.js';

// After: Dynamic loading with cache
async function loadStackService(stack) {
	// Return from cache if already loaded
	if (stackServices.has(stack)) {
		return stackServices.get(stack);
	}

	// Dynamically import the required service
	const servicePath = `./services/${stack}-service.js`;
	const serviceModule = await import(servicePath);

	// Instantiate and cache the service
	const ServiceClass =
		serviceModule[
			`${stack.charAt(0).toUpperCase() + stack.slice(1)}Service`
		];
	// ...
}

Benefits:

  • Faster startup time
  • Lower memory usage when only one service is needed
  • Faster loading on systems with limited resources

2. Template Caching System

A caching system was implemented to avoid repeatedly loading the same templates:

class TemplateCache {
	constructor(options = {}) {
		this.cache = new Map();
		this.maxSize = options.maxSize || 100;
		this.ttl = options.ttl || 300000; // 5 minutes
	}

	get(key) {
		const item = this.cache.get(key);
		if (item && Date.now() < item.expiry) {
			return item.value;
		}
		return null;
	}

	// ...
}

Benefits:

  • Reduction of repetitive I/O operations
  • Faster generation time for rules with common templates
  • Memory control through size limits and TTL

3. Asynchronous File Operations

Synchronous file operations were replaced with asynchronous ones:

// Before
const files = this.getFilesInDirectory(tmplDir);
files.forEach((f) => {
	// Synchronous operations
});

// After
const files = await this.getFilesInDirectoryAsync(tmplDir);
await Promise.all(
	batch.map(async (f) => {
		// Asynchronous operations
	})
);

Benefits:

  • Better performance on systems with slow I/O
  • Doesn't block the main thread
  • Better error handling

4. Batch Processing

To improve memory handling, files are processed in batches:

async processBatch(items, processFn, batchSize = 10) {
    const results = [];

    for (let i = 0; i < items.length; i += batchSize) {
        const batch = items.slice(i, i + batchSize);
        const batchResults = await Promise.all(
            batch.map(item => processFn(item))
        );
        results.push(...batchResults);

        // Allow event loop to handle other tasks
        if (i + batchSize < items.length) {
            await new Promise(resolve => setTimeout(resolve, 0));
        }
    }

    return results;
}

Benefits:

  • Lower memory peak in large projects
  • Improved responsiveness during generation
  • Avoids "memory exhaustion" issues

5. Incremental Updates

A system was implemented to regenerate only rules that have changed:

async needsUpdate(srcFile, destFile) {
    try {
        if (!await fs.pathExists(destFile)) return true;

        const srcStat = await fsPromises.stat(srcFile);
        const destStat = await fsPromises.stat(destFile);

        return srcStat.mtime > destStat.mtime;
    } catch (error) {
        return true; // When in doubt, update
    }
}

Benefits:

  • Faster generation during updates
  • Fewer I/O operations when only some rules change
  • Better experience for iterative development

6. Optimization for Large Files

For large files, we use an optimized approach:

async readFileOptimized(filePath) {
    // Check size to determine strategy
    const stats = await fsPromises.stat(filePath);

    // For large files (>1MB), use streaming
    if (stats.size > 1024 * 1024) {
        return new Promise((resolve, reject) => {
            let data = '';
            const stream = fs.createReadStream(filePath, { encoding: 'utf8' });

            stream.on('data', (chunk) => { data += chunk; });
            stream.on('end', () => { resolve(data); });
            stream.on('error', reject);
        });
    }

    // For small files, read at once
    return await fsPromises.readFile(filePath, 'utf8');
}

Benefits:

  • Lower memory usage for large templates
  • Better performance when handling large files
  • Avoids blocking when loading extensive files

Best Practices for Contributors

When contributing to the project, keep these recommendations in mind to maintain good performance:

  1. Prioritize asynchronous operations: Always use async methods for I/O operations.

  2. Avoid unnecessary loading: Don't load resources (like templates or services) until they are needed.

  3. Use batch processing: For large operations, divide the work into manageable batches.

  4. Cache frequent results: Use the cache system for results that are queried repeatedly.

  5. Check for redundant operations: Avoid reading/writing the same file multiple times.

  6. Monitor memory usage: Be careful with the accumulation of large objects in memory.

  7. Measure performance: Before and after significant changes, conduct performance tests.

Performance Measurement

To evaluate the project's performance, you can use the following techniques:

  1. Time logging: Use Date.now() to measure the time of critical operations:
const startTime = Date.now();
// Operation to measure
const endTime = Date.now();
console.log(`Operation completed in ${endTime - startTime}ms`);
  1. Memory monitoring: Use process.memoryUsage() to check memory usage:
console.log(process.memoryUsage());
  1. Profiling: For more detailed analysis, use tools such as:
    • Node.js Profiler
    • Chrome DevTools when running with --inspect
    • Tools like clinic.js

Future Optimizations

For future versions, the following optimizations could be considered:

  1. Parallel workers: Use worker threads for CPU-intensive tasks.

  2. Template compilation: Pre-compile frequent templates for faster substitution.

  3. Deferred configuration loading: Load configuration sections only when needed.

  4. Template indexing: Create template indexes for faster searches.

  5. Cache compression: Store compressed templates in memory to reduce RAM usage.

Considerations for Large Projects

For projects with thousands of rule files:

  1. Low memory mode: Implement a mode that sacrifices speed for lower memory usage.

  2. Rule fragmentation: Split large rule sets into smaller groups.

  3. On-demand rules: Load rules only when the agent requests them, not all at startup.

  4. Adaptive configuration: Automatically adjust performance parameters based on project size.


By following these practices and considerations, we can keep Agent Rules Kit efficient and responsive, even for very large projects and environments with limited resources.