@smartsamurai/krapi-sdk
Version:
KRAPI TypeScript SDK - Easy-to-use client SDK for connecting to self-hosted KRAPI servers (like Appwrite SDK)
773 lines (693 loc) • 23.6 kB
text/typescript
/**
* Performance Monitor
*
* Monitors and tracks performance metrics for database operations.
* Provides performance logging, query analysis, and load testing capabilities.
*
* @module performance-monitor
* @example
* const monitor = new PerformanceMonitor(dbConnection, console);
* await monitor.initializePerformanceTables();
* await monitor.startOperation('query');
* // ... perform operation
* await monitor.endOperation('query', { success: true });
*/
import { Logger } from "./core";
/**
* Performance Metric Interface
*
* @interface PerformanceMetric
* @property {string} id - Metric ID
* @property {string} operation - Operation name
* @property {string} [collection_name] - Collection name
* @property {number} duration_ms - Operation duration in milliseconds
* @property {number} memory_usage_mb - Memory usage in MB
* @property {number} cpu_usage_percent - CPU usage percentage
* @property {number} records_processed - Number of records processed
* @property {boolean} success - Whether operation succeeded
* @property {string} [error_message] - Error message if failed
* @property {Record<string, unknown>} [metadata] - Additional metadata
* @property {Date} timestamp - Metric timestamp
*/
export interface PerformanceMetric {
id: string;
operation: string;
collection_name?: string;
duration_ms: number;
memory_usage_mb: number;
cpu_usage_percent: number;
records_processed: number;
success: boolean;
error_message?: string;
metadata?: Record<string, unknown>;
timestamp: Date;
}
/**
* Load Test Result Interface
*
* @interface LoadTestResult
* @property {number} total_operations - Total operations performed
* @property {number} successful_operations - Successful operations
* @property {number} failed_operations - Failed operations
* @property {number} average_duration_ms - Average duration in milliseconds
* @property {number} min_duration_ms - Minimum duration
* @property {number} max_duration_ms - Maximum duration
* @property {number} total_duration_ms - Total duration
* @property {number} operations_per_second - Operations per second
* @property {number} memory_peak_mb - Peak memory usage in MB
* @property {number} cpu_peak_percent - Peak CPU usage percentage
* @property {string[]} errors - Error messages
*/
export interface LoadTestResult {
total_operations: number;
successful_operations: number;
failed_operations: number;
average_duration_ms: number;
min_duration_ms: number;
max_duration_ms: number;
total_duration_ms: number;
operations_per_second: number;
memory_peak_mb: number;
cpu_peak_percent: number;
errors: string[];
}
/**
* Query Performance Interface
*
* @interface QueryPerformance
* @property {string} query - SQL query
* @property {number} execution_time_ms - Execution time in milliseconds
* @property {number} rows_returned - Number of rows returned
* @property {number} rows_scanned - Number of rows scanned
* @property {boolean} index_usage - Whether index was used
* @property {string[]} optimization_suggestions - Optimization suggestions
*/
export interface QueryPerformance {
query: string;
execution_time_ms: number;
rows_returned: number;
rows_scanned: number;
index_usage: boolean;
optimization_suggestions: string[];
}
/**
* Performance Monitor Class
*
* Monitors and tracks performance metrics for database operations.
*
* @class PerformanceMonitor
* @example
* const monitor = new PerformanceMonitor(dbConnection, console);
* await monitor.startOperation('query');
* // ... perform operation
* await monitor.endOperation('query', { success: true });
*/
export class PerformanceMonitor {
private metrics: PerformanceMetric[] = [];
private startTime = 0;
private memoryStart = 0;
private initialized = false;
constructor(
private dbConnection: {
query: (sql: string, params?: unknown[]) => Promise<{ rows?: unknown[] }>;
},
private logger: Logger = console
) {
// Don't initialize in constructor - use lazy initialization
}
/**
* Initialize performance monitoring tables
*/
private async initializePerformanceTables(): Promise<void> {
if (this.initialized) {
return;
}
try {
// Wait for essential tables to exist first
let attempts = 0;
const maxAttempts = 30;
while (attempts < maxAttempts) {
try {
// Check if admin_users and projects tables exist
const tablesCheck = await this.dbConnection.query(`
SELECT 1 FROM information_schema.tables
WHERE table_name IN ('admin_users', 'projects') AND table_schema = 'public'
`);
if (tablesCheck.rows && tablesCheck.rows.length >= 2) {
break; // Both tables exist, proceed
}
// Wait a bit and try again
await new Promise((resolve) => setTimeout(resolve, 1000));
attempts++;
} catch {
// Table check failed, wait and try again
await new Promise((resolve) => setTimeout(resolve, 1000));
attempts++;
}
}
if (attempts >= maxAttempts) {
this.logger.warn(
"Essential tables not found after waiting, skipping performance table creation"
);
return;
}
// Create performance_metrics table (SQLite-compatible)
await this.dbConnection.query(`
CREATE TABLE IF NOT EXISTS performance_metrics (
id TEXT PRIMARY KEY,
operation_name TEXT NOT NULL,
operation_type TEXT,
duration_ms INTEGER NOT NULL,
memory_usage_mb REAL,
cpu_usage_percent REAL,
rows_affected INTEGER,
success INTEGER DEFAULT 1,
error_message TEXT,
metadata TEXT DEFAULT '{}',
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`);
// Create query_performance table (SQLite-compatible)
await this.dbConnection.query(`
CREATE TABLE IF NOT EXISTS query_performance (
id TEXT PRIMARY KEY,
query_hash TEXT NOT NULL,
query_text TEXT NOT NULL,
execution_time_ms INTEGER NOT NULL,
rows_returned INTEGER,
rows_scanned INTEGER,
index_usage INTEGER,
cache_hit INTEGER,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`);
// Create load_test_results table (SQLite-compatible)
await this.dbConnection.query(`
CREATE TABLE IF NOT EXISTS load_test_results (
id TEXT PRIMARY KEY,
test_name TEXT NOT NULL,
concurrent_users INTEGER NOT NULL,
total_requests INTEGER NOT NULL,
successful_requests INTEGER NOT NULL,
failed_requests INTEGER NOT NULL,
average_response_time_ms REAL,
p95_response_time_ms REAL,
p99_response_time_ms REAL,
throughput_rps REAL,
memory_peak_mb REAL,
cpu_peak_percent REAL,
test_duration_seconds INTEGER,
metadata TEXT DEFAULT '{}',
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`);
// Create indexes
await this.dbConnection.query(`
CREATE INDEX IF NOT EXISTS idx_performance_metrics_operation ON performance_metrics(operation_name)
`);
await this.dbConnection.query(`
CREATE INDEX IF NOT EXISTS idx_performance_metrics_created ON performance_metrics(created_at)
`);
await this.dbConnection.query(`
CREATE INDEX IF NOT EXISTS idx_query_performance_hash ON query_performance(query_hash)
`);
await this.dbConnection.query(`
CREATE INDEX IF NOT EXISTS idx_load_test_results_name ON load_test_results(test_name)
`);
this.initialized = true;
this.logger.info("Performance monitoring tables initialized");
} catch (error) {
this.logger.error("Failed to initialize performance tables:", error);
// Don't throw error, just log it - this service is not critical for basic functionality
}
}
/**
* Ensure tables are initialized before any operation
*/
private async ensureInitialized(): Promise<void> {
if (!this.initialized) {
await this.initializePerformanceTables();
}
}
/**
* Start performance monitoring
*/
startMonitoring(): void {
this.startTime = Date.now();
this.memoryStart = this.getMemoryUsage();
this.logger.info("Performance monitoring started");
}
/**
* Stop performance monitoring and get results
*/
stopMonitoring(): {
total_duration_ms: number;
memory_used_mb: number;
cpu_usage_percent: number;
} {
const totalDuration = Date.now() - this.startTime;
const memoryUsed = this.getMemoryUsage() - this.memoryStart;
const cpuUsage = this.getCPUUsage();
this.logger.info(
`Performance monitoring stopped. Duration: ${totalDuration}ms, Memory: ${memoryUsed}MB`
);
return {
total_duration_ms: totalDuration,
memory_used_mb: memoryUsed,
cpu_usage_percent: cpuUsage,
};
}
/**
* Measure operation performance
*/
async measureOperation<T>(
operation: string,
collectionName: string | undefined,
operationFn: () => Promise<T>,
metadata?: Record<string, unknown>
): Promise<T> {
await this.ensureInitialized();
const startTime = Date.now();
const startMemory = this.getMemoryUsage();
const startCPU = this.getCPUUsage();
try {
const result = await operationFn();
const duration = Date.now() - startTime;
const memoryUsed = this.getMemoryUsage() - startMemory;
const cpuUsed = this.getCPUUsage() - startCPU;
const metric: PerformanceMetric = {
id: `metric_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
operation,
duration_ms: duration,
memory_usage_mb: memoryUsed,
cpu_usage_percent: cpuUsed,
records_processed: 0, // This would need to be calculated based on the operation
success: true,
timestamp: new Date(),
};
if (collectionName !== undefined) {
metric.collection_name = collectionName;
}
if (metadata !== undefined) {
metric.metadata = metadata;
}
await this.saveMetric(metric);
this.metrics.push(metric);
this.logger.info(`Operation '${operation}' completed in ${duration}ms`);
return result;
} catch (error) {
const duration = Date.now() - startTime;
const memoryUsed = this.getMemoryUsage() - startMemory;
const cpuUsed = this.getCPUUsage() - startCPU;
const metric: PerformanceMetric = {
id: `metric_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
operation,
duration_ms: duration,
memory_usage_mb: memoryUsed,
cpu_usage_percent: cpuUsed,
records_processed: 0,
success: false,
error_message: error instanceof Error ? error.message : "Unknown error",
timestamp: new Date(),
};
if (collectionName !== undefined) {
metric.collection_name = collectionName;
}
if (metadata !== undefined) {
metric.metadata = metadata;
}
await this.saveMetric(metric);
this.metrics.push(metric);
this.logger.error(
`Operation '${operation}' failed after ${duration}ms:`,
error
);
throw error;
}
}
/**
* Run load test with multiple concurrent operations
*/
async runLoadTest(
operation: string,
collectionName: string | undefined,
operationFn: () => Promise<unknown>,
concurrency = 10,
totalOperations = 100
): Promise<LoadTestResult> {
await this.ensureInitialized();
this.startMonitoring();
const results: PerformanceMetric[] = [];
const errors: string[] = [];
this.logger.info(
`Starting load test: ${totalOperations} operations with concurrency ${concurrency}`
);
// Create operation batches
const batches: (() => Promise<void>)[] = [];
for (let i = 0; i < totalOperations; i++) {
batches.push(async () => {
try {
const result = await this.measureOperation(
operation,
collectionName,
operationFn
);
results.push(result as PerformanceMetric);
} catch (error) {
errors.push(error instanceof Error ? error.message : "Unknown error");
}
});
}
// Execute batches with concurrency control
const batchSize = Math.ceil(batches.length / concurrency);
const batchPromises: Promise<void>[] = [];
for (let i = 0; i < concurrency; i++) {
const start = i * batchSize;
const end = Math.min(start + batchSize, batches.length);
const batch = batches.slice(start, end);
batchPromises.push(
Promise.all(batch.map((operation) => operation())).then(() => {})
);
}
await Promise.all(batchPromises);
// Monitoring results available for analysis
void this.stopMonitoring();
const successfulOperations = results.filter((r) => r.success).length;
const failedOperations = results.filter((r) => !r.success).length;
const durations = results.map((r) => r.duration_ms);
const averageDuration =
durations.reduce((a, b) => a + b, 0) / durations.length;
const minDuration = Math.min(...durations);
const maxDuration = Math.max(...durations);
const totalDuration = results.reduce((sum, r) => sum + r.duration_ms, 0);
const operationsPerSecond = successfulOperations / (totalDuration / 1000);
const memoryPeak = Math.max(...results.map((r) => r.memory_usage_mb));
const cpuPeak = Math.max(...results.map((r) => r.cpu_usage_percent));
const loadTestResult: LoadTestResult = {
total_operations: totalOperations,
successful_operations: successfulOperations,
failed_operations: failedOperations,
average_duration_ms: averageDuration,
min_duration_ms: minDuration,
max_duration_ms: maxDuration,
total_duration_ms: totalDuration,
operations_per_second: operationsPerSecond,
memory_peak_mb: memoryPeak,
cpu_peak_percent: cpuPeak,
errors,
};
this.logger.info("Load test completed:", loadTestResult);
return loadTestResult;
}
/**
* Analyze query performance
*/
async analyzeQueryPerformance(
query: string,
params: unknown[] = []
): Promise<QueryPerformance> {
await this.ensureInitialized();
const startTime = Date.now();
try {
// Execute the query
const result = await this.dbConnection.query(query, params);
const executionTime = Date.now() - startTime;
const rowsReturned = result.rows?.length || 0;
const rowsScanned = this.estimateRowsScanned(query);
const indexUsage = this.detectIndexUsage(query);
const optimizationSuggestions = this.generateOptimizationSuggestions(
query,
executionTime,
rowsReturned,
rowsScanned
);
return {
query,
execution_time_ms: executionTime,
rows_returned: rowsReturned,
rows_scanned: rowsScanned,
index_usage: indexUsage,
optimization_suggestions: optimizationSuggestions,
};
} catch (error) {
this.logger.error("Failed to analyze query performance:", error);
throw error;
}
}
/**
* Get performance statistics
*/
async getPerformanceStats(
days = 30,
operation?: string,
collectionName?: string
): Promise<{
total_operations: number;
average_duration_ms: number;
success_rate: number;
top_slow_operations: Array<{ operation: string; avg_duration: number }>;
memory_usage_trend: Array<{ date: string; avg_memory: number }>;
cpu_usage_trend: Array<{ date: string; avg_cpu: number }>;
}> {
await this.ensureInitialized();
try {
// Calculate the cutoff date in JavaScript (SQLite doesn't support INTERVAL)
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
let whereClause = "WHERE created_at >= $1";
const params: unknown[] = [cutoffDate.toISOString()];
let paramIndex = 1;
if (operation) {
paramIndex++;
whereClause += ` AND operation_name = $${paramIndex}`;
params.push(operation);
}
if (collectionName) {
paramIndex++;
whereClause += ` AND operation_name = $${paramIndex}`;
params.push(collectionName);
}
// Total operations and success rate
const statsResult = await this.dbConnection.query(
`
SELECT
COUNT(*) as total_operations,
AVG(duration_ms) as avg_duration,
COUNT(CASE WHEN success THEN 1 END) as successful_operations
FROM performance_metrics ${whereClause}
`,
params
);
const stats = statsResult.rows?.[0] as Record<string, unknown>;
const totalOperations = parseInt(
(stats.total_operations as string) || "0"
);
const averageDuration = parseFloat(stats.avg_duration as string) || 0;
const successRate =
totalOperations > 0
? (parseInt((stats.successful_operations as string) || "0") /
totalOperations) *
100
: 0;
// Top slow operations
const slowOpsResult = await this.dbConnection.query(
`
SELECT operation_name, AVG(duration_ms) as avg_duration
FROM performance_metrics ${whereClause}
GROUP BY operation_name
ORDER BY avg_duration DESC
LIMIT 10
`,
params
);
const topSlowOperations = (slowOpsResult.rows || [] as unknown[]).map(
(row: unknown) => {
const rowData = row as Record<string, unknown>;
return {
operation: rowData.operation_name as string,
avg_duration: parseFloat(rowData.avg_duration as string),
};
}
);
// Memory usage trend
const memoryResult = await this.dbConnection.query(
`
SELECT
DATE(created_at) as date,
AVG(memory_usage_mb) as avg_memory
FROM performance_metrics ${whereClause}
GROUP BY DATE(created_at)
ORDER BY date
`,
params
);
const memoryUsageTrend = (memoryResult.rows || [] as unknown[]).map(
(row: unknown) => {
const rowData = row as Record<string, unknown>;
return {
date: rowData.date as string,
avg_memory: parseFloat(rowData.avg_memory as string),
};
}
);
// CPU usage trend
const cpuResult = await this.dbConnection.query(
`
SELECT
DATE(created_at) as date,
AVG(cpu_usage_percent) as avg_cpu
FROM performance_metrics ${whereClause}
GROUP BY DATE(created_at)
ORDER BY date
`,
params
);
const cpuUsageTrend = (cpuResult.rows || [] as unknown[]).map(
(row: unknown) => {
const rowData = row as Record<string, unknown>;
return {
date: rowData.date as string,
avg_cpu: parseFloat(rowData.avg_cpu as string),
};
}
);
return {
total_operations: totalOperations,
average_duration_ms: averageDuration,
success_rate: successRate,
top_slow_operations: topSlowOperations,
memory_usage_trend: memoryUsageTrend,
cpu_usage_trend: cpuUsageTrend,
};
} catch (error) {
this.logger.error("Failed to get performance statistics:", error);
throw error;
}
}
/**
* Clean old performance metrics
*/
async cleanOldMetrics(daysToKeep = 90): Promise<number> {
await this.ensureInitialized();
try {
// Calculate the cutoff date in JavaScript (SQLite doesn't support INTERVAL)
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
const result = await this.dbConnection.query(
`
DELETE FROM performance_metrics
WHERE created_at < $1
`,
[cutoffDate.toISOString()]
);
const deletedCount = (result as { rowCount?: number }).rowCount || 0;
this.logger.info(`Cleaned ${deletedCount} old performance metrics`);
return deletedCount;
} catch (error) {
this.logger.error("Failed to clean old performance metrics:", error);
throw error;
}
}
/**
* Save performance metric to database
*/
private async saveMetric(metric: PerformanceMetric): Promise<void> {
try {
await this.dbConnection.query(
`
INSERT INTO performance_metrics (
id, operation_name, duration_ms, memory_usage_mb,
cpu_usage_percent, success, error_message, metadata
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`,
[
metric.id,
metric.operation,
metric.duration_ms,
metric.memory_usage_mb,
metric.cpu_usage_percent,
metric.success,
metric.error_message,
JSON.stringify(metric.metadata || {}),
]
);
} catch (error) {
this.logger.error("Failed to save performance metric:", error);
}
}
/**
* Get current memory usage in MB
*/
private getMemoryUsage(): number {
if (typeof process !== "undefined" && process.memoryUsage) {
return (
Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) / 100
);
}
return 0;
}
/**
* Get current CPU usage percentage
*/
private getCPUUsage(): number {
// This is a simplified CPU usage calculation
// In a real implementation, you'd want to use system monitoring tools
return Math.random() * 100; // Placeholder
}
/**
* Estimate rows scanned based on query complexity
*/
private estimateRowsScanned(query: string): number {
// This is a simplified estimation
// In a real implementation, you'd analyze the query plan
const complexity = query.toLowerCase().includes("where") ? 2 : 1;
return Math.floor(Math.random() * 1000 * complexity);
}
/**
* Detect if query uses indexes
*/
private detectIndexUsage(query: string): boolean {
// This is a simplified detection
// In a real implementation, you'd analyze the query plan
return (
query.toLowerCase().includes("where") &&
(query.toLowerCase().includes("id") ||
query.toLowerCase().includes("name"))
);
}
/**
* Generate optimization suggestions
*/
private generateOptimizationSuggestions(
query: string,
executionTime: number,
rowsReturned: number,
rowsScanned: number
): string[] {
const suggestions: string[] = [];
if (executionTime > 100) {
suggestions.push(
"Query execution time is high. Consider adding indexes on frequently queried columns."
);
}
if (rowsScanned > rowsReturned * 10) {
suggestions.push(
"Query scans many more rows than returned. Consider adding WHERE clauses or indexes."
);
}
if (query.toLowerCase().includes("select *")) {
suggestions.push(
"Avoid SELECT *. Specify only needed columns to reduce data transfer."
);
}
if (
query.toLowerCase().includes("order by") &&
!query.toLowerCase().includes("limit")
) {
suggestions.push(
"ORDER BY without LIMIT can be expensive. Consider adding LIMIT clause."
);
}
return suggestions;
}
}