diff --git a/Program.cs b/Program.cs index 4f5bd83..2d4e48a 100644 --- a/Program.cs +++ b/Program.cs @@ -1,12 +1,14 @@ using System; using System.Buffers.Binary; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Net.Sockets; using System.Numerics; using System.Security.Cryptography; +using System.Runtime.InteropServices; using System.Text; using System.Text.Json; using System.Threading; @@ -85,10 +87,11 @@ internal static class Program password = GetArg(args, "-p") ?? "x"; var threadText = GetArg(args, "-t"); + var threadsAuto = threadText == null; - threadCount = threadText != null - ? int.Parse(threadText, CultureInfo.InvariantCulture) - : Environment.ProcessorCount; + threadCount = threadsAuto + ? GetPhysicalCoreCount() + : int.Parse(threadText!, CultureInfo.InvariantCulture); if(threadCount < 1) throw new ArgumentException("Thread count must be at least 1"); @@ -125,9 +128,12 @@ internal static class Program }; PrintBanner(); - Console.WriteLine($"Pool : {host}:{port}"); - Console.WriteLine($"Worker : {worker}"); - Console.WriteLine($"Threads : {threadCount}"); + PrintStartupDiagnostics(); + Console.WriteLine($"Pool : {host}:{port}"); + Console.WriteLine($"Worker : {worker}"); + Console.WriteLine( + $"Mining Threads : {threadCount}" + + (threadsAuto ? " (auto)" : " (manual)")); Console.WriteLine(); var statsTask = StatsLoopAsync(shutdown.Token); @@ -814,6 +820,235 @@ internal static class Program } } + private static void PrintStartupDiagnostics() + { + var logicalCpus = Environment.ProcessorCount; + var physicalCores = GetPhysicalCoreCount(); + var cpuModel = GetCpuModel(); + var aesSupported = GetAesSupport(); + var hugePages = GetHugePagesStatus(); + + string randomXStatus; + string randomXPath; + + if(OperatingSystem.IsLinux()) + { + // Accessing these properties initializes VextaRandomX and + // explicitly loads librandomx.so from the miner directory. + randomXStatus = VextaRandomX.NativeLibraryLoaded + ? "Loaded OK" + : "Not loaded"; + + randomXPath = VextaRandomX.NativeLibraryPath ?? "Unknown"; + } + else + { + randomXStatus = "System loader"; + randomXPath = "Platform default"; + } + + Console.WriteLine($"CPU : {cpuModel}"); + Console.WriteLine($"Physical Cores : {physicalCores}"); + Console.WriteLine($"Logical CPUs : {logicalCpus}"); + Console.WriteLine($"AES : {aesSupported}"); + Console.WriteLine($"Huge Pages : {hugePages}"); + Console.WriteLine($"RandomX Library: {randomXStatus}"); + Console.WriteLine($"Library Path : {randomXPath}"); + Console.WriteLine(); + } + + private static string GetCpuModel() + { + try + { + if(OperatingSystem.IsLinux() && File.Exists("/proc/cpuinfo")) + { + foreach(var line in File.ReadLines("/proc/cpuinfo")) + { + if(line.StartsWith("model name", StringComparison.OrdinalIgnoreCase)) + { + var colon = line.IndexOf(':'); + + if(colon >= 0) + return line[(colon + 1)..].Trim(); + } + } + } + + var identifier = Environment.GetEnvironmentVariable("PROCESSOR_IDENTIFIER"); + + if(!string.IsNullOrWhiteSpace(identifier)) + return identifier.Trim(); + } + catch + { + } + + return "Unknown"; + } + + private static int GetPhysicalCoreCount() + { + try + { + if(OperatingSystem.IsLinux() && File.Exists("/proc/cpuinfo")) + { + var cores = new HashSet(); + string? physicalId = null; + string? coreId = null; + + foreach(var rawLine in File.ReadLines("/proc/cpuinfo")) + { + var line = rawLine.Trim(); + + if(line.Length == 0) + { + if(physicalId != null && coreId != null) + cores.Add($"{physicalId}:{coreId}"); + + physicalId = null; + coreId = null; + continue; + } + + if(line.StartsWith("physical id", StringComparison.OrdinalIgnoreCase)) + { + var colon = line.IndexOf(':'); + + if(colon >= 0) + physicalId = line[(colon + 1)..].Trim(); + } + else if(line.StartsWith("core id", StringComparison.OrdinalIgnoreCase)) + { + var colon = line.IndexOf(':'); + + if(colon >= 0) + coreId = line[(colon + 1)..].Trim(); + } + } + + if(physicalId != null && coreId != null) + cores.Add($"{physicalId}:{coreId}"); + + if(cores.Count > 0) + return cores.Count; + } + + if(OperatingSystem.IsWindows()) + { + var windowsCores = GetWindowsPhysicalCoreCount(); + + if(windowsCores > 0) + return windowsCores; + } + } + catch + { + } + + return Environment.ProcessorCount; + } + + private static int GetWindowsPhysicalCoreCount() + { + uint length = 0; + + GetLogicalProcessorInformation(IntPtr.Zero, ref length); + + if(length == 0) + return 0; + + var buffer = Marshal.AllocHGlobal((int)length); + + try + { + if(!GetLogicalProcessorInformation(buffer, ref length)) + return 0; + + // SYSTEM_LOGICAL_PROCESSOR_INFORMATION is 32 bytes on x64 + // and 24 bytes on x86. + var entrySize = IntPtr.Size == 8 ? 32 : 24; + var relationshipOffset = IntPtr.Size; + var count = 0; + + for(var offset = 0; + offset + entrySize <= length; + offset += entrySize) + { + var relationship = Marshal.ReadInt32( + buffer, + offset + relationshipOffset); + + // RelationProcessorCore = 0 + if(relationship == 0) + count++; + } + + return count; + } + finally + { + Marshal.FreeHGlobal(buffer); + } + } + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetLogicalProcessorInformation( + IntPtr buffer, + ref uint returnedLength); + + private static string GetAesSupport() + { + try + { + if(System.Runtime.Intrinsics.X86.Aes.IsSupported) + return "Supported"; + } + catch + { + } + + return "Not detected"; + } + + private static string GetHugePagesStatus() + { + try + { + if(OperatingSystem.IsLinux() && File.Exists("/proc/meminfo")) + { + foreach(var line in File.ReadLines("/proc/meminfo")) + { + if(!line.StartsWith("HugePages_Total:", StringComparison.Ordinal)) + continue; + + var value = line["HugePages_Total:".Length..].Trim(); + var parts = value.Split( + ' ', + StringSplitOptions.RemoveEmptyEntries); + + if(parts.Length > 0 && + long.TryParse( + parts[0], + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out var total)) + { + return total > 0 + ? $"Configured ({total})" + : "Not configured"; + } + } + } + } + catch + { + } + + return "Unknown"; + } + private static async Task StatsLoopAsync(CancellationToken ct) { var timer = new PeriodicTimer(TimeSpan.FromSeconds(10)); @@ -823,6 +1058,10 @@ internal static class Program var lastTime = sw.Elapsed; var measuring = false; + var averageHistory = new List<(TimeSpan Time, long Hashes)>(); + TimeSpan? averageStartTime = null; + var nextAverageReport = TimeSpan.Zero; + try { while(await timer.WaitForNextTickAsync(ct)) @@ -830,6 +1069,34 @@ internal static class Program var now = sw.Elapsed; var hashes = Interlocked.Read(ref totalHashes); + if(hashes > 0) + { + if(averageStartTime == null) + { + averageStartTime = now; + nextAverageReport = now + TimeSpan.FromMinutes(1); + averageHistory.Add((now, hashes)); + } + else if(now >= nextAverageReport) + { + averageHistory.Add((now, hashes)); + + WriteAverageHashrate( + now, + hashes, + averageHistory, + averageStartTime.Value); + + do + { + nextAverageReport += TimeSpan.FromMinutes(1); + } + while(nextAverageReport <= now); + + TrimAverageHistory(now, averageHistory); + } + } + if(hashes == 0) { lastHashes = hashes; @@ -877,6 +1144,74 @@ internal static class Program } } + private static void WriteAverageHashrate( + TimeSpan now, + long hashes, + List<(TimeSpan Time, long Hashes)> history, + TimeSpan startTime) + { + var avg5 = FormatAverageWindow( + now, hashes, history, startTime, TimeSpan.FromMinutes(5)); + + var avg15 = FormatAverageWindow( + now, hashes, history, startTime, TimeSpan.FromMinutes(15)); + + var avg60 = FormatAverageWindow( + now, hashes, history, startTime, TimeSpan.FromMinutes(60)); + + WriteColor( + $"AVG HASHRATE 5m:{avg5} 15m:{avg15} 60m:{avg60}", + ConsoleColor.DarkCyan); + } + + private static string FormatAverageWindow( + TimeSpan now, + long hashes, + List<(TimeSpan Time, long Hashes)> history, + TimeSpan startTime, + TimeSpan window) + { + if(now - startTime < window) + return "warming up"; + + var cutoff = now - window; + (TimeSpan Time, long Hashes)? sample = null; + + foreach(var item in history) + { + if(item.Time <= cutoff) + sample = item; + else + break; + } + + if(sample == null) + return "warming up"; + + var elapsed = (now - sample.Value.Time).TotalSeconds; + + if(elapsed <= 0) + return "warming up"; + + var deltaHashes = hashes - sample.Value.Hashes; + var rate = deltaHashes / elapsed; + + return FormatHashrate(rate); + } + + private static void TrimAverageHistory( + TimeSpan now, + List<(TimeSpan Time, long Hashes)> history) + { + var keepAfter = now - TimeSpan.FromMinutes(62); + + while(history.Count > 2 && + history[1].Time < keepAfter) + { + history.RemoveAt(0); + } + } + private static string FormatHashrate(double value) { if(value >= 1_000_000) @@ -956,7 +1291,7 @@ internal static class Program Console.ForegroundColor = ConsoleColor.White; Console.WriteLine(); - Console.WriteLine("Vexta CPU Miner 0.1.1"); + Console.WriteLine("Vexta CPU Miner 0.1.2"); Console.ForegroundColor = ConsoleColor.DarkGray; Console.WriteLine("Copyright (c) 2026 Vexta Project"); @@ -996,7 +1331,7 @@ internal static class Program Console.WriteLine(" -u Vexta address / worker name"); Console.WriteLine(" -p Password (default: x)"); Console.WriteLine( - " -t CPU threads (default: logical CPU count)"); + " -t CPU threads (default: auto-detect physical cores)"); Console.WriteLine(); Console.WriteLine("Ctrl+C stops the miner cleanly."); } diff --git a/README.md b/README.md index 0940212..e62caa5 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,11 @@ Prebuilt binaries are available for: - Vexta RandomX mining - RandomX full-memory mode - Windows x64 and Linux x64 support -- Configurable CPU thread count +- Automatic CPU thread selection based on physical cores +- Manual CPU thread override with `-t` +- Startup diagnostics for CPU, cores, AES, Huge Pages, and RandomX library - Local hashrate display +- 5 / 15 / 60 minute average hashrate - Accepted / rejected share statistics - Automatic RandomX seed changes - Daemon-confirmed `BLOCK FOUND` notifications when supported by the pool @@ -59,7 +62,7 @@ Example: | `-o` | Stratum pool address | | `-u` | Vexta wallet address, optionally followed by `.worker` | | `-p` | Pool password, normally `x` | -| `-t` | Number of CPU mining threads | +| `-t` | Number of CPU mining threads (optional; auto-detects physical cores when omitted) | ## Requirements @@ -69,6 +72,11 @@ Example: RandomX full-memory mode is enabled. +On startup, the miner reports detected CPU information, physical and logical CPU counts, +AES support, Huge Pages status, RandomX library status, and the selected mining thread count. + +If `-t` is omitted, the miner automatically uses the detected physical core count. + ## Building from Source ### Requirements diff --git a/VextaRandomX.cs b/VextaRandomX.cs index 7400d88..d00dff0 100644 --- a/VextaRandomX.cs +++ b/VextaRandomX.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Threading; using System.Collections.Concurrent; using System.Collections.Generic; @@ -9,6 +10,51 @@ namespace Vexta.CpuMiner; public static unsafe class VextaRandomX { + private static IntPtr nativeLibraryHandle = IntPtr.Zero; + + public static string? NativeLibraryPath { get; private set; } + + public static bool NativeLibraryLoaded => nativeLibraryHandle != IntPtr.Zero; + + static VextaRandomX() + { + if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + var libraryPath = Path.Combine(AppContext.BaseDirectory, "librandomx.so"); + + if(!File.Exists(libraryPath)) + throw new DllNotFoundException( + $"RandomX library was not found at '{libraryPath}'."); + + try + { + nativeLibraryHandle = NativeLibrary.Load(libraryPath); + NativeLibraryPath = libraryPath; + } + catch(Exception ex) + { + throw new DllNotFoundException( + $"Unable to load RandomX library from '{libraryPath}': {ex.Message}", + ex); + } + + NativeLibrary.SetDllImportResolver( + typeof(VextaRandomX).Assembly, + (libraryName, assembly, searchPath) => + { + if(string.Equals( + libraryName, + "librandomx", + StringComparison.Ordinal)) + { + return nativeLibraryHandle; + } + + return IntPtr.Zero; + }); + } + } + [Flags] public enum RandomXFlags {