Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ build-legacy:
@echo "Legacy build completed"

# Run the application (new CLI)
run:
run: docker-up
@echo "Waiting for Neo4j to be ready..."
@sleep 15
@echo "Starting application..."
go run cmd/sql-graph-visualizer/main.go serve

Expand All @@ -98,7 +100,7 @@ clean:
# Start Docker services
docker-up:
@echo "Starting Docker services..."
docker-compose up -d neo4j-test
docker-compose up -d neo4j-test mysql-test
@echo "Docker services started"

# Stop Docker services
Expand Down
118 changes: 106 additions & 12 deletions internal/application/bootstrap/servers.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,12 @@ func initPerformanceServices(cfg *models.Config, db *sql.DB) *PerformanceService
realtimeMonitor := performance.NewRealtimePerformanceMonitor(logger, realtimeConfig, psAdapter, performanceAnalyzer, graphMapper)

benchmarkConfig := createBenchmarkConfig(cfg)
// The source/graph repositories are intentionally nil here: the current
// benchmark execution path runs sysbench, which connects to the database
// directly via the configured DatabaseURL. Repositories will be wired in when
// benchmark-result persistence is added.
benchmarkService := performance.NewBenchmarkService(nil, nil, nil, performanceAnalyzer, logger, benchmarkConfig)
registerBenchmarkTools(benchmarkService, cfg, logger)

if cfg.Performance.Realtime != nil && cfg.Performance.Realtime.Enabled {
ctx := context.Background()
Expand Down Expand Up @@ -431,19 +436,108 @@ func createRealtimeConfig(cfg *models.Config) *performance.RealtimeMonitorConfig
}

func createBenchmarkConfig(cfg *models.Config) *performance.BenchmarkServiceConfig {
config := &performance.BenchmarkServiceConfig{}
if cfg.Performance.Benchmarks != nil {
defaultDuration, _ := time.ParseDuration(cfg.Performance.Benchmarks.DefaultDuration)
maxDuration, _ := time.ParseDuration(cfg.Performance.Benchmarks.MaxDuration)
resultsRetention, _ := time.ParseDuration(cfg.Performance.Benchmarks.ResultsRetention)
config.DefaultTimeout = defaultDuration
config.MaxDuration = maxDuration
config.RetainResults = resultsRetention
config.CleanupInterval = 15 * time.Minute
if cfg.Performance.Benchmarks.Limits != nil {
config.MaxConcurrentRuns = cfg.Performance.Benchmarks.Limits.MaxConcurrentBenchmarks
config.MaxResultsInMemory = cfg.Performance.Benchmarks.Limits.MemoryLimitMB
// Start from sane defaults so safety limits and execution defaults are always
// populated, then override with user configuration where provided.
config := performance.DefaultBenchmarkServiceConfig()

// Default the benchmark target to the configured source database so that
// benchmark requests work without explicitly specifying a connection.
if url, dbType := benchmarkDatabaseURL(cfg); url != "" {
config.DefaultDatabaseURL = url
config.DefaultDatabaseType = dbType
}

if b := cfg.Performance.Benchmarks; b != nil {
if d, err := time.ParseDuration(b.DefaultDuration); err == nil && d > 0 {
config.DefaultTestDuration = d
}
if d, err := time.ParseDuration(b.MaxDuration); err == nil && d > 0 {
config.MaxDuration = d
}
if d, err := time.ParseDuration(b.ResultsRetention); err == nil && d > 0 {
config.RetainResults = d
}
if b.Limits != nil {
if b.Limits.MaxConcurrentBenchmarks > 0 {
config.MaxConcurrentRuns = b.Limits.MaxConcurrentBenchmarks
}
if b.Limits.MemoryLimitMB > 0 {
config.MaxResultsInMemory = b.Limits.MemoryLimitMB
}
}
if b.Sysbench != nil && b.Sysbench.Defaults != nil {
if b.Sysbench.Defaults.Threads > 0 {
config.DefaultThreads = b.Sysbench.Defaults.Threads
}
if b.Sysbench.Defaults.TableSize > 0 {
config.DefaultTableSize = b.Sysbench.Defaults.TableSize
}
if b.Sysbench.Defaults.Time > 0 {
config.DefaultTestDuration = time.Duration(b.Sysbench.Defaults.Time) * time.Second
}
}
}

// The execution context must comfortably outlast a default-length run.
if config.DefaultTimeout <= config.DefaultTestDuration {
config.DefaultTimeout = config.DefaultTestDuration + 5*time.Minute
}
if config.MaxDuration < config.DefaultTestDuration {
config.MaxDuration = config.DefaultTestDuration
}

return config
}

// registerBenchmarkTools wires the available benchmark tools (currently
// sysbench) into the benchmark service. Unavailable tools are logged and
// skipped so the application keeps running without benchmarking support.
func registerBenchmarkTools(svc *performance.BenchmarkService, cfg *models.Config, logger *logrus.Logger) {
if b := cfg.Performance.Benchmarks; b != nil && !b.Enabled {
logrus.Info("Benchmarking disabled in configuration; skipping benchmark tool registration")
return
}

sysbenchAdapter := performance.NewSysbenchAdapter(logger, createSysbenchConfig(cfg))
if err := svc.RegisterBenchmarkTool("sysbench", sysbenchAdapter); err != nil {
logrus.Warnf("Sysbench benchmark tool unavailable; sysbench benchmarks disabled: %v", err)
return
}
logrus.Info("Registered sysbench benchmark tool")
}

// createSysbenchConfig maps user configuration onto the sysbench adapter config,
// starting from the adapter defaults.
func createSysbenchConfig(cfg *models.Config) *performance.SysbenchConfig {
sbConfig := performance.DefaultSysbenchConfig()

if b := cfg.Performance.Benchmarks; b != nil && b.Sysbench != nil {
if b.Sysbench.ExecutablePath != "" {
sbConfig.BinaryPath = b.Sysbench.ExecutablePath
}
if d := b.Sysbench.Defaults; d != nil && d.TableSize > 0 {
sbConfig.DefaultTableSize = d.TableSize
}
}

return sbConfig
}

// benchmarkDatabaseURL builds a sysbench-compatible database URL and driver type
// from the active source database configuration.
func benchmarkDatabaseURL(cfg *models.Config) (string, string) {
dbCfg := cfg.GetDatabaseConfig()
switch dbCfg.Type {
case models.DatabaseTypePostgreSQL:
if pg := dbCfg.PostgreSQL; pg != nil {
return fmt.Sprintf("postgresql://%s:%s@%s:%d/%s",
pg.GetUsername(), pg.GetPassword(), pg.GetHost(), pg.GetPort(), pg.GetDatabase()), "postgresql"
}
case models.DatabaseTypeMySQL:
if my := dbCfg.MySQL; my != nil {
return fmt.Sprintf("mysql://%s:%s@%s:%d/%s",
my.GetUsername(), my.GetPassword(), my.GetHost(), my.GetPort(), my.GetDatabase()), "mysql"
}
}
return "", ""
}
80 changes: 68 additions & 12 deletions internal/application/services/performance/benchmark_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ type BenchmarkServiceConfig struct {
MaxDuration time.Duration `yaml:"max_duration" json:"max_duration"`
MaxThreads int `yaml:"max_threads" json:"max_threads"`

// Default execution parameters (applied when a request omits them)
DefaultDatabaseURL string `yaml:"default_database_url" json:"default_database_url"`
DefaultDatabaseType string `yaml:"default_database_type" json:"default_database_type"`
DefaultTestType string `yaml:"default_test_type" json:"default_test_type"`
DefaultTestDuration time.Duration `yaml:"default_test_duration" json:"default_test_duration"`
DefaultThreads int `yaml:"default_threads" json:"default_threads"`
DefaultTables int `yaml:"default_tables" json:"default_tables"`
DefaultTableSize int `yaml:"default_table_size" json:"default_table_size"`

// Tool configurations
EnabledTools []string `yaml:"enabled_tools" json:"enabled_tools"`
ToolConfigurations map[string]interface{} `yaml:"tool_configurations" json:"tool_configurations"`
Expand Down Expand Up @@ -142,6 +151,10 @@ func (s *BenchmarkService) GetAvailableTools() []string {

// ExecuteBenchmark runs a benchmark with the specified configuration
func (s *BenchmarkService) ExecuteBenchmark(ctx context.Context, config ports.BenchmarkConfig, toolName string) (string, error) {
// Fill in any unset fields from the service defaults (database URL, threads,
// table sizing, duration, ...) so callers can submit a minimal request.
s.applyConfigDefaults(&config)

// Validate configuration
if err := s.validateConfig(config); err != nil {
return "", fmt.Errorf("invalid configuration: %w", err)
Expand All @@ -163,7 +176,10 @@ func (s *BenchmarkService) ExecuteBenchmark(ctx context.Context, config ports.Be
}

executionID := uuid.New().String()
executionCtx, cancel := context.WithTimeout(ctx, s.config.DefaultTimeout)
// Detach from the caller's context: benchmarks run asynchronously and must
// outlive the originating HTTP request. Cancellation is handled explicitly
// through CancelBenchmark/StopBenchmark via the stored cancel function.
executionCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), s.config.DefaultTimeout)

execution := &BenchmarkExecution{
ID: executionID,
Expand Down Expand Up @@ -357,6 +373,33 @@ func (s *BenchmarkService) validateConfig(config ports.BenchmarkConfig) error {
return nil
}

// applyConfigDefaults fills unset benchmark configuration fields from the
// service defaults so that minimal requests (e.g. only a tool name) still
// produce a runnable configuration.
func (s *BenchmarkService) applyConfigDefaults(config *ports.BenchmarkConfig) {
if config.TestType == "" {
config.TestType = s.config.DefaultTestType
}
if config.DatabaseURL == "" {
config.DatabaseURL = s.config.DefaultDatabaseURL
}
if config.DatabaseType == "" {
config.DatabaseType = s.config.DefaultDatabaseType
}
if config.Threads <= 0 {
config.Threads = s.config.DefaultThreads
}
if config.Tables <= 0 {
config.Tables = s.config.DefaultTables
}
if config.TableSize <= 0 {
config.TableSize = s.config.DefaultTableSize
}
if config.Duration <= 0 {
config.Duration = s.config.DefaultTestDuration
}
}

func (s *BenchmarkService) getBenchmarkTool(name string) (ports.BenchmarkToolPort, error) {
s.toolsMutex.RLock()
defer s.toolsMutex.RUnlock()
Expand Down Expand Up @@ -473,17 +516,30 @@ func (s *BenchmarkService) cleanupOldExecutions() {
// defaultBenchmarkServiceConfig returns default configuration
func defaultBenchmarkServiceConfig() *BenchmarkServiceConfig {
return &BenchmarkServiceConfig{
MaxConcurrentRuns: 5,
DefaultTimeout: 30 * time.Minute,
CleanupInterval: 15 * time.Minute,
RetainResults: 2 * time.Hour,
MaxResultsInMemory: 100,
MaxTableSize: 1000000, // 1M rows
MaxDuration: 1 * time.Hour,
MaxThreads: 64,
EnabledTools: []string{"sysbench", "custom"},
ToolConfigurations: make(map[string]interface{}),
}
MaxConcurrentRuns: 5,
DefaultTimeout: 30 * time.Minute,
CleanupInterval: 15 * time.Minute,
RetainResults: 2 * time.Hour,
MaxResultsInMemory: 100,
MaxTableSize: 1000000, // 1M rows
MaxDuration: 1 * time.Hour,
MaxThreads: 64,
EnabledTools: []string{"sysbench", "custom"},
ToolConfigurations: make(map[string]interface{}),
DefaultDatabaseType: "mysql",
DefaultTestType: "oltp_read_write",
DefaultTestDuration: 60 * time.Second,
DefaultThreads: 4,
DefaultTables: 4,
DefaultTableSize: 10000,
}
}

// DefaultBenchmarkServiceConfig returns the default benchmark service
// configuration. Exported so callers (e.g. bootstrap) can start from sane
// defaults and override individual fields from user configuration.
func DefaultBenchmarkServiceConfig() *BenchmarkServiceConfig {
return defaultBenchmarkServiceConfig()
}

// Additional methods for integration with existing graph services
Expand Down
7 changes: 7 additions & 0 deletions internal/application/services/performance/sysbench_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,13 @@ func (s *SysbenchAdapter) classifyPerformanceImpact(avgLatency float64) string {
return "HIGH"
}

// DefaultSysbenchConfig returns the default sysbench adapter configuration.
// Exported so callers (e.g. bootstrap) can start from sane defaults and
// override individual fields from user configuration.
func DefaultSysbenchConfig() *SysbenchConfig {
return defaultSysbenchConfig()
}

// defaultSysbenchConfig returns default sysbench configuration
func defaultSysbenchConfig() *SysbenchConfig {
return &SysbenchConfig{
Expand Down
2 changes: 1 addition & 1 deletion internal/domain/models/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ func (c *Config) GetDatabaseType() DatabaseType {

// PerformanceConfig represents the main performance .monitoring configuration
type PerformanceConfig struct {
Monitoring *MonitoringConfig `yaml:".monitoring,omitempty"`
Monitoring *MonitoringConfig `yaml:"monitoring,omitempty"`
Realtime *RealtimeConfig `yaml:"realtime,omitempty"`
Benchmarks *BenchmarksConfig `yaml:"benchmarks,omitempty"`
Visualization *VisualizationConfig `yaml:"visualization,omitempty"`
Expand Down
Loading
Loading