Mining/cmd/mining/cmd/simulate.go
Virgil 895f9da069
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run
docs: align mining code with AX naming
2026-04-04 08:15:43 +00:00

183 lines
7 KiB
Go

package cmd
import (
"context"
"fmt"
"math/rand"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"forge.lthn.ai/Snider/Mining/pkg/mining"
"github.com/spf13/cobra"
)
var (
simulatedMinerCount int
simulationPreset string
simulationHashrate int
simulationAlgorithm string
)
// Example: mining simulate --count 3 --preset cpu-medium starts the API with fake miners.
var simulateCmd = &cobra.Command{
Use: "simulate",
Short: "Start the service with simulated miners for UI testing",
Long: `Start the mining service with simulated miners that generate realistic
hashrate data and statistics. This is useful for UI development and testing
without requiring actual mining hardware.
Examples:
# Start with 3 medium-hashrate CPU miners
mining simulate --count 3 --preset cpu-medium
# Start with custom hashrate
mining simulate --count 2 --hashrate 8000 --algorithm rx/0
# Start with a mix of presets
mining simulate --count 1 --preset gpu-ethash
Available presets:
cpu-low - Low-end CPU (500 H/s, rx/0)
cpu-medium - Medium CPU (5 kH/s, rx/0)
cpu-high - High-end CPU (15 kH/s, rx/0)
gpu-ethash - GPU mining ETH (30 MH/s, ethash)
gpu-kawpow - GPU mining RVN (15 MH/s, kawpow)`,
RunE: func(_ *cobra.Command, arguments []string) error {
runContext, cancel := context.WithCancel(context.Background())
defer cancel()
displayHost := serveHost
if displayHost == "0.0.0.0" {
var err error
displayHost, err = getLocalIP()
if err != nil {
displayHost = "localhost"
}
}
displayAddress := fmt.Sprintf("%s:%d", displayHost, servePort)
listenAddress := fmt.Sprintf("%s:%d", serveHost, servePort)
// Example: simulatedManager := mining.NewManagerForSimulation() keeps fake miners isolated from the real autostart state.
simulatedManager := mining.NewManagerForSimulation()
// Example: getSimulatedConfig(0) returns a config such as sim-cpu-medium-001.
for i := 0; i < simulatedMinerCount; i++ {
simulatedConfig := getSimulatedConfig(i)
simulatedMiner := mining.NewSimulatedMiner(simulatedConfig)
// Example: simulatedMiner.Start(&mining.Config{}) starts the fake miner lifecycle without launching a real binary.
if err := simulatedMiner.Start(&mining.Config{}); err != nil {
return fmt.Errorf("failed to start simulated miner %d: %w", i, err)
}
// Example: simulatedManager.RegisterMiner(simulatedMiner) makes the simulated miner visible to `mining serve`.
if err := simulatedManager.RegisterMiner(simulatedMiner); err != nil {
return fmt.Errorf("failed to register simulated miner %d: %w", i, err)
}
fmt.Printf("Started simulated miner: %s (%s, ~%d H/s)\n",
simulatedConfig.Name, simulatedConfig.Algorithm, simulatedConfig.BaseHashrate)
}
// Example: service, err := mining.NewService(simulatedManager, listenAddress, displayAddress, apiBasePath) serves the simulator on http://127.0.0.1:9090.
service, err := mining.NewService(simulatedManager, listenAddress, displayAddress, apiBasePath)
if err != nil {
return fmt.Errorf("failed to create new service: %w", err)
}
// Example: service.ServiceStartup(runContext) starts the API server while the simulation loop keeps running.
go func() {
if err := service.ServiceStartup(runContext); err != nil {
fmt.Fprintf(os.Stderr, "Failed to start service: %v\n", err)
cancel()
}
}()
fmt.Printf("\n=== SIMULATION MODE ===\n")
fmt.Printf("Mining service started on http://%s:%d\n", displayHost, servePort)
fmt.Printf("Swagger documentation is available at http://%s:%d%s/swagger/index.html\n", displayHost, servePort, apiBasePath)
fmt.Printf("\nSimulating %d miner(s). Press Ctrl+C to stop.\n", simulatedMinerCount)
fmt.Printf("Note: All data is simulated - no actual mining is occurring.\n\n")
// Example: shutdownSignal := make(chan os.Signal, 1) captures Ctrl+C and SIGTERM for graceful shutdown.
shutdownSignal := make(chan os.Signal, 1)
signal.Notify(shutdownSignal, syscall.SIGINT, syscall.SIGTERM)
select {
case <-shutdownSignal:
fmt.Println("\nReceived shutdown signal, stopping simulation...")
cancel()
case <-runContext.Done():
}
// Example: for _, miner := range simulatedManager.ListMiners() { simulatedManager.StopMiner(context.Background(), miner.GetName()) } stops every simulated miner before exit.
for _, miner := range simulatedManager.ListMiners() {
simulatedManager.StopMiner(context.Background(), miner.GetName())
}
fmt.Println("Simulation stopped.")
return nil
},
}
// getSimulatedConfig(0) returns a simulated miner such as `sim-cpu-medium-001` with preset-driven hashrate and algorithm defaults.
func getSimulatedConfig(index int) mining.SimulatedMinerConfig {
// Example: name := fmt.Sprintf("sim-cpu-medium-001", index+1) keeps simulated miner names predictable.
name := fmt.Sprintf("sim-%s-%03d", simulationPreset, index+1)
var simulatedConfig mining.SimulatedMinerConfig
if preset, ok := mining.SimulatedMinerPresets[simulationPreset]; ok {
simulatedConfig = preset
} else {
simulatedConfig = mining.SimulatedMinerPresets["cpu-medium"]
}
simulatedConfig.Name = name
// Example: simulationHashrate = 8000 overrides the preset with a custom 8 kH/s baseline.
if simulationHashrate > 0 {
simulatedConfig.BaseHashrate = simulationHashrate
}
// Example: simulationAlgorithm = "rx/0" swaps the preset algorithm before the miner starts.
if simulationAlgorithm != "" {
simulatedConfig.Algorithm = simulationAlgorithm
}
variance := 0.1 + rand.Float64()*0.1 // 10-20% variance
simulatedConfig.BaseHashrate = int(float64(simulatedConfig.BaseHashrate) * (0.9 + rand.Float64()*0.2))
simulatedConfig.Variance = variance
return simulatedConfig
}
func init() {
rand.Seed(time.Now().UnixNano())
simulateCmd.Flags().IntVarP(&simulatedMinerCount, "count", "c", 1, "Number of simulated miners to create")
simulateCmd.Flags().StringVar(&simulationPreset, "preset", "cpu-medium", "Miner preset (cpu-low, cpu-medium, cpu-high, gpu-ethash, gpu-kawpow)")
simulateCmd.Flags().IntVar(&simulationHashrate, "hashrate", 0, "Custom base hashrate (overrides preset)")
simulateCmd.Flags().StringVar(&simulationAlgorithm, "algorithm", "", "Custom algorithm (overrides preset)")
simulateCmd.Flags().StringVar(&serveHost, "host", "127.0.0.1", "Host to bind the simulation API server, for example 127.0.0.1 or 0.0.0.0")
simulateCmd.Flags().IntVarP(&servePort, "port", "p", 9090, "Port to bind the simulation API server, for example 9090")
simulateCmd.Flags().StringVarP(&apiBasePath, "namespace", "n", "/api/v1/mining", "Simulation API base path, for example /api/v1/mining")
rootCmd.AddCommand(simulateCmd)
}
// formatHashrate(1250) // returns "1.25 kH/s" for display in the simulation summary.
func formatHashrate(h int) string {
if h >= 1000000000 {
return strconv.FormatFloat(float64(h)/1000000000, 'f', 2, 64) + " GH/s"
}
if h >= 1000000 {
return strconv.FormatFloat(float64(h)/1000000, 'f', 2, 64) + " MH/s"
}
if h >= 1000 {
return strconv.FormatFloat(float64(h)/1000, 'f', 2, 64) + " kH/s"
}
return strconv.Itoa(h) + " H/s"
}