diff options
-rw-r--r-- | ConfigEditor.hpp | 57 | ||||
-rw-r--r-- | autoAdjust.hpp | 154 | ||||
-rw-r--r-- | autoAdjustHwloc.hpp | 195 | ||||
-rw-r--r-- | cli-miner.cpp | 50 | ||||
-rw-r--r-- | config.txt | 280 | ||||
-rw-r--r-- | console.cpp | 16 | ||||
-rw-r--r-- | console.h | 2 | ||||
-rw-r--r-- | executor.cpp | 47 | ||||
-rw-r--r-- | executor.h | 16 | ||||
-rw-r--r-- | jconf.cpp | 145 | ||||
-rw-r--r-- | jconf.h | 11 | ||||
-rw-r--r-- | jpsock.cpp | 3 | ||||
-rw-r--r-- | minethd.cpp | 555 | ||||
-rw-r--r-- | minethd.h | 144 | ||||
-rw-r--r-- | msgstruct.h | 3 | ||||
-rw-r--r-- | telemetry.cpp | 107 | ||||
-rw-r--r-- | telemetry.h | 23 | ||||
-rw-r--r-- | version.h | 4 | ||||
-rw-r--r-- | webdesign.cpp | 2 |
19 files changed, 374 insertions, 1440 deletions
diff --git a/ConfigEditor.hpp b/ConfigEditor.hpp new file mode 100644 index 0000000..80607ff --- /dev/null +++ b/ConfigEditor.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include <atomic> +#include <string> +#include <fstream> +#include <streambuf> +#include <regex> + + +namespace xmrstak +{ + +struct ConfigEditor +{ + std::string m_fileContent; + + ConfigEditor() + { + + } + + static bool file_exist( const std::string filename) + { + std::ifstream fstream(filename); + return fstream.good(); + } + + void set( const std::string && content) + { + m_fileContent = content; + } + + bool load(const std::string filename) + { + std::ifstream fstream(filename); + m_fileContent = std::string( + (std::istreambuf_iterator<char>(fstream)), + std::istreambuf_iterator<char>() + ); + return fstream.good(); + } + + void write(const std::string filename) + { + std::ofstream out(filename); + out << m_fileContent; + out.close(); + } + + void replace(const std::string search, const std::string substring) + { + m_fileContent = std::regex_replace(m_fileContent, std::regex(search), substring); + } + +}; + +} // namepsace xmrstak diff --git a/autoAdjust.hpp b/autoAdjust.hpp deleted file mode 100644 index 93a88e8..0000000 --- a/autoAdjust.hpp +++ /dev/null @@ -1,154 +0,0 @@ -#pragma once -#include "jconf.h" -#include "console.h" - -#ifdef _WIN32 -#include <windows.h> -#else -#include <unistd.h> -#endif // _WIN32 - -// Mask bits between h and l and return the value -// This enables us to put in values exactly like in the manual -// For example EBX[31:22] is get_masked(cpu_info[1], 31, 22) -inline int32_t get_masked(int32_t val, int32_t h, int32_t l) -{ - val &= (0x7FFFFFFF >> (31-(h-l))) << l; - return val >> l; -} - -class autoAdjust -{ -public: - - autoAdjust() - { - } - - void printConfig() - { - printer::inst()->print_str("The configuration for 'cpu_threads_conf' in your config file is 'null'.\n"); - printer::inst()->print_str("The miner evaluates your system and prints a suggestion for the section `cpu_threads_conf` to the terminal.\n"); - printer::inst()->print_str("The values are not optimal, please try to tweak the values based on notes in config.txt.\n"); - printer::inst()->print_str("Please copy & paste the block within the asterisks to your config.\n\n"); - - if(!detectL3Size() || L3KB_size < 1024 || L3KB_size > 102400) - { - if(L3KB_size < 1024 || L3KB_size > 102400) - printer::inst()->print_msg(L0, "Autoconf failed: L3 size sanity check failed - %u KB.", L3KB_size); - - printer::inst()->print_msg(L0, "Autoconf failed: Printing config for a single thread. Please try to add new ones until the hashrate slows down."); - printer::inst()->print_str("\n**************** Copy&Paste BEGIN ****************\n\n"); - printer::inst()->print_str("\"cpu_threads_conf\" :\n[\n"); - printer::inst()->print_str(" { \"low_power_mode\" : false, \"no_prefetch\" : true, \"affine_to_cpu\" : false },\n"); - printer::inst()->print_str("],\n\n**************** Copy&Paste END ****************\n"); - return; - } - - printer::inst()->print_msg(L0, "Autoconf L3 size detected at %u KB.", L3KB_size); - - detectCPUConf(); - - printer::inst()->print_msg(L0, "Autoconf core count detected as %u on %s.", corecnt, - linux_layout ? "Linux" : "Windows"); - - printer::inst()->print_str("\n**************** Copy&Paste BEGIN ****************\n\n"); - printer::inst()->print_str("\"cpu_threads_conf\" :\n[\n"); - - uint32_t aff_id = 0; - char strbuf[256]; - for(uint32_t i=0; i < corecnt; i++) - { - bool double_mode; - - if(L3KB_size <= 0) - break; - - double_mode = L3KB_size / 2048 > (int32_t)(corecnt-i); - - snprintf(strbuf, sizeof(strbuf), " { \"low_power_mode\" : %s, \"no_prefetch\" : true, \"affine_to_cpu\" : %u },\n", - double_mode ? "true" : "false", aff_id); - printer::inst()->print_str(strbuf); - - if(!linux_layout || old_amd) - { - aff_id += 2; - - if(aff_id >= corecnt) - aff_id = 1; - } - else - aff_id++; - - if(double_mode) - L3KB_size -= 4096; - else - L3KB_size -= 2048; - } - - printer::inst()->print_str("],\n\n**************** Copy&Paste END ****************\n"); - } - -private: - bool detectL3Size() - { - int32_t cpu_info[4]; - char cpustr[13] = {0}; - - jconf::cpuid(0, 0, cpu_info); - memcpy(cpustr, &cpu_info[1], 4); - memcpy(cpustr+4, &cpu_info[3], 4); - memcpy(cpustr+8, &cpu_info[2], 4); - - if(strcmp(cpustr, "GenuineIntel") == 0) - { - jconf::cpuid(4, 3, cpu_info); - - if(get_masked(cpu_info[0], 7, 5) != 3) - { - printer::inst()->print_msg(L0, "Autoconf failed: Couln't find L3 cache page."); - return false; - } - - L3KB_size = ((get_masked(cpu_info[1], 31, 22) + 1) * (get_masked(cpu_info[1], 21, 12) + 1) * - (get_masked(cpu_info[1], 11, 0) + 1) * (cpu_info[2] + 1)) / 1024; - - return true; - } - else if(strcmp(cpustr, "AuthenticAMD") == 0) - { - jconf::cpuid(0x80000006, 0, cpu_info); - - L3KB_size = get_masked(cpu_info[3], 31, 18) * 512; - - jconf::cpuid(1, 0, cpu_info); - if(get_masked(cpu_info[0], 11, 8) < 0x17) //0x17h is Zen - old_amd = true; - - return true; - } - else - { - printer::inst()->print_msg(L0, "Autoconf failed: Unknown CPU type: %s.", cpustr); - return false; - } - } - - void detectCPUConf() - { -#ifdef _WIN32 - SYSTEM_INFO info; - GetSystemInfo(&info); - corecnt = info.dwNumberOfProcessors; - linux_layout = false; -#else - corecnt = sysconf(_SC_NPROCESSORS_ONLN); - linux_layout = true; -#endif // _WIN32 - } - - int32_t L3KB_size = 0; - uint32_t corecnt; - bool old_amd = false; - bool linux_layout; -}; diff --git a/autoAdjustHwloc.hpp b/autoAdjustHwloc.hpp deleted file mode 100644 index 92a668a..0000000 --- a/autoAdjustHwloc.hpp +++ /dev/null @@ -1,195 +0,0 @@ -#pragma once - -#include "console.h" -#include <hwloc.h> -#include <stdio.h> - -#ifdef _WIN32 -#include <windows.h> -#else -#include <unistd.h> -#endif // _WIN32 - -class autoAdjust -{ -public: - - autoAdjust() - { - } - - void printConfig() - { - printer::inst()->print_str("The configuration for 'cpu_threads_conf' in your config file is 'null'.\n"); - printer::inst()->print_str("The miner evaluates your system and prints a suggestion for the section `cpu_threads_conf` to the terminal.\n"); - printer::inst()->print_str("The values are not optimal, please try to tweak the values based on notes in config.txt.\n"); - printer::inst()->print_str("Please copy & paste the block within the asterisks to your config.\n\n"); - - hwloc_topology_t topology; - hwloc_topology_init(&topology); - hwloc_topology_load(topology); - - try - { - std::vector<hwloc_obj_t> tlcs; - tlcs.reserve(16); - results.reserve(16); - - findChildrenCaches(hwloc_get_root_obj(topology), - [&tlcs](hwloc_obj_t found) { tlcs.emplace_back(found); } ); - - if(tlcs.size() == 0) - throw(std::runtime_error("The CPU doesn't seem to have a cache.")); - - for(hwloc_obj_t obj : tlcs) - proccessTopLevelCache(obj); - - printer::inst()->print_str("\n**************** Copy&Paste BEGIN ****************\n\n"); - printer::inst()->print_str("\"cpu_threads_conf\" :\n[\n"); - - for(uint32_t id : results) - { - char str[128]; - snprintf(str, sizeof(str), " { \"low_power_mode\" : %s, \"no_prefetch\" : true, \"affine_to_cpu\" : %u },\n", - (id & 0x8000000) != 0 ? "true" : "false", id & 0x7FFFFFF); - printer::inst()->print_str(str); - } - - printer::inst()->print_str("],\n\n**************** Copy&Paste END ****************\n"); - } - catch(const std::runtime_error& err) - { - printer::inst()->print_msg(L0, "Autoconf FAILED: %s", err.what()); - printer::inst()->print_str("\nPrinting config for a single thread. Please try to add new ones until the hashrate slows down.\n"); - printer::inst()->print_str("\n**************** FAILURE Copy&Paste BEGIN ****************\n\n"); - printer::inst()->print_str("\"cpu_threads_conf\" :\n[\n"); - printer::inst()->print_str(" { \"low_power_mode\" : false, \"no_prefetch\" : true, \"affine_to_cpu\" : false },\n"); - printer::inst()->print_str("],\n\n**************** FAILURE Copy&Paste END ****************\n"); - } - - /* Destroy topology object. */ - hwloc_topology_destroy(topology); - } - -private: - static constexpr size_t hashSize = 2 * 1024 * 1024; - std::vector<uint32_t> results; - - template<typename func> - inline void findChildrenByType(hwloc_obj_t obj, hwloc_obj_type_t type, func lambda) - { - for(size_t i=0; i < obj->arity; i++) - { - if(obj->children[i]->type == type) - lambda(obj->children[i]); - else - findChildrenByType(obj->children[i], type, lambda); - } - } - - inline bool isCacheObject(hwloc_obj_t obj) - { -#if HWLOC_API_VERSION >= 0x20000 - return hwloc_obj_type_is_cache(obj->type); -#else - return obj->type == HWLOC_OBJ_CACHE; -#endif // HWLOC_API_VERSION - } - - template<typename func> - inline void findChildrenCaches(hwloc_obj_t obj, func lambda) - { - for(size_t i=0; i < obj->arity; i++) - { - if(isCacheObject(obj->children[i])) - lambda(obj->children[i]); - else - findChildrenCaches(obj->children[i], lambda); - } - } - - inline bool isCacheExclusive(hwloc_obj_t obj) - { - const char* value = hwloc_obj_get_info_by_name(obj, "Inclusive"); - return value == nullptr || value[0] != '1'; - } - - // Top level cache isn't shared with other cores on the same package - // This will usually be 1 x L3, but can be 2 x L2 per package - void proccessTopLevelCache(hwloc_obj_t obj) - { - if(obj->attr == nullptr) - throw(std::runtime_error("Cache object hasn't got attributes.")); - - size_t PUs = 0; - findChildrenByType(obj, HWLOC_OBJ_PU, [&PUs](hwloc_obj_t found) { PUs++; } ); - - //Strange case, but we will handle it silently, surely there must be one PU somewhere? - if(PUs == 0) - return; - - if(obj->attr->cache.size == 0) - { - //We will always have one child if PUs > 0 - if(!isCacheObject(obj->children[0])) - throw(std::runtime_error("The CPU doesn't seem to have a cache.")); - - //Try our luck with lower level caches - for(size_t i=0; i < obj->arity; i++) - proccessTopLevelCache(obj->children[i]); - return; - } - - size_t cacheSize = obj->attr->cache.size; - if(isCacheExclusive(obj)) - { - for(size_t i=0; i < obj->arity; i++) - { - hwloc_obj_t l2obj = obj->children[i]; - //If L2 is exclusive and greater or equal to 2MB add room for one more hash - if(isCacheObject(l2obj) && l2obj->attr != nullptr && l2obj->attr->cache.size >= hashSize) - cacheSize += hashSize; - } - } - - std::vector<hwloc_obj_t> cores; - cores.reserve(16); - findChildrenByType(obj, HWLOC_OBJ_CORE, [&cores](hwloc_obj_t found) { cores.emplace_back(found); } ); - - size_t cacheHashes = (cacheSize + hashSize/2) / hashSize; - - //Firstly allocate PU 0 of every CORE, then PU 1 etc. - size_t pu_id = 0; - while(cacheHashes > 0 && PUs > 0) - { - bool allocated_pu = false; - for(hwloc_obj_t core : cores) - { - if(core->arity <= pu_id || core->children[pu_id]->type != HWLOC_OBJ_PU) - continue; - - size_t os_id = core->children[pu_id]->os_index; - - if(cacheHashes > PUs) - { - cacheHashes -= 2; - os_id |= 0x8000000; //double hash marker bit - } - else - cacheHashes--; - PUs--; - - allocated_pu = true; - results.emplace_back(os_id); - - if(cacheHashes == 0) - break; - } - - if(!allocated_pu) - throw(std::runtime_error("Failed to allocate a PU.")); - - pu_id++; - } - } -}; diff --git a/cli-miner.cpp b/cli-miner.cpp index 45d2c16..d7d2781 100644 --- a/cli-miner.cpp +++ b/cli-miner.cpp @@ -22,15 +22,13 @@ */ #include "executor.h" -#include "minethd.h" +#include "backend/miner_work.h" +#include "backend/GlobalStates.hpp" +#include "backend/BackendConnector.hpp" #include "jconf.h" #include "console.h" #include "donate-level.h" -#ifndef CONF_NO_HWLOC -# include "autoAdjustHwloc.hpp" -#else -# include "autoAdjust.hpp" -#endif + #include "version.h" #ifndef CONF_NO_HTTPD @@ -48,19 +46,9 @@ #include <openssl/err.h> #endif -//Do a press any key for the windows folk. *insert any key joke here* -#ifdef _WIN32 -void win_exit() -{ - printer::inst()->print_str("Press any key to exit."); - get_key(); - return; -} - -#define strcasecmp _stricmp -#else -void win_exit() { return; } +#ifdef _WIN32 +# define strcasecmp _stricmp #endif // _WIN32 void do_benchmark(); @@ -109,15 +97,7 @@ int main(int argc, char *argv[]) return 0; } - if(jconf::inst()->NeedsAutoconf()) - { - autoAdjust adjust; - adjust.printConfig(); - win_exit(); - return 0; - } - - if (!minethd::self_test()) + if (!xmrstak::BackendConnector::self_test()) { win_exit(); return 0; @@ -144,6 +124,12 @@ int main(int argc, char *argv[]) printer::inst()->print_str("-------------------------------------------------------------------\n"); printer::inst()->print_str( XMR_STAK_NAME" " XMR_STAK_VERSION " mining software, CPU Version.\n"); printer::inst()->print_str("Based on CPU mining code by wolf9466 (heavily optimized by fireice_uk).\n"); +#ifndef CONF_NO_CUDA + printer::inst()->print_str("NVIDIA mining code was written by KlausT and psychocrypt.\n"); +#endif +#ifndef CONF_NO_OPENCL + printer::inst()->print_str("AMD mining code was written by wolf9466.\n"); +#endif printer::inst()->print_str("Brought to you by fireice_uk and psychocrypt under GPLv3.\n\n"); char buffer[64]; snprintf(buffer, sizeof(buffer), "Configurable dev donation level is set to %.1f %%\n\n", fDevDonationLevel * 100.0); @@ -196,20 +182,20 @@ int main(int argc, char *argv[]) void do_benchmark() { using namespace std::chrono; - std::vector<minethd*>* pvThreads; + std::vector<xmrstak::IBackend*>* pvThreads; printer::inst()->print_msg(L0, "Running a 60 second benchmark..."); uint8_t work[76] = {0}; - minethd::miner_work oWork = minethd::miner_work("", work, sizeof(work), 0, 0, false, 0); - pvThreads = minethd::thread_starter(oWork); + xmrstak::miner_work oWork = xmrstak::miner_work("", work, sizeof(work), 0, 0, 0); + pvThreads = xmrstak::BackendConnector::thread_starter(oWork); uint64_t iStartStamp = time_point_cast<milliseconds>(high_resolution_clock::now()).time_since_epoch().count(); std::this_thread::sleep_for(std::chrono::seconds(60)); - oWork = minethd::miner_work(); - minethd::switch_work(oWork); + oWork = xmrstak::miner_work(); + xmrstak::GlobalStates::switch_work(oWork); double fTotalHps = 0.0; for (uint32_t i = 0; i < pvThreads->size(); i++) @@ -1,184 +1,96 @@ -/*
- * Thread configuration for each thread. Make sure it matches the number above.
- * low_power_mode - This mode will double the cache usage, and double the single thread performance. It will
- * consume much less power (as less cores are working), but will max out at around 80-85% of
- * the maximum performance.
- *
- * no_prefetch - Some sytems can gain up to extra 5% here, but sometimes it will have no difference or make
- * things slower.
- *
- * affine_to_cpu - This can be either false (no affinity), or the CPU core number. Note that on hyperthreading
- * systems it is better to assign threads to physical cores. On Windows this usually means selecting
- * even or odd numbered cpu numbers. For Linux it will be usually the lower CPU numbers, so for a 4
- * physical core CPU you should select cpu numbers 0-3.
- *
- * On the first run the miner will look at your system and suggest a basic configuration that will work,
- * you can try to tweak it from there to get the best performance.
- *
- * A filled out configuration should look like this:
- * "cpu_threads_conf" :
- * [
- * { "low_power_mode" : false, "no_prefetch" : true, "affine_to_cpu" : 0 },
- * { "low_power_mode" : false, "no_prefetch" : true, "affine_to_cpu" : 1 },
- * ],
- */
-"cpu_threads_conf" :
-null,
-
-/*
- * LARGE PAGE SUPPORT
- * Large pages need a properly set up OS. It can be difficult if you are not used to systems administration,
- * but the performance results are worth the trouble - you will get around 20% boost. Slow memory mode is
- * meant as a backup, you won't get stellar results there. If you are running into trouble, especially
- * on Windows, please read the common issues in the README.
- *
- * By default we will try to allocate large pages. This means you need to "Run As Administrator" on Windows.
- * You need to edit your system's group policies to enable locking large pages. Here are the steps from MSDN
- *
- * 1. On the Start menu, click Run. In the Open box, type gpedit.msc.
- * 2. On the Local Group Policy Editor console, expand Computer Configuration, and then expand Windows Settings.
- * 3. Expand Security Settings, and then expand Local Policies.
- * 4. Select the User Rights Assignment folder.
- * 5. The policies will be displayed in the details pane.
- * 6. In the pane, double-click Lock pages in memory.
- * 7. In the Local Security Setting – Lock pages in memory dialog box, click Add User or Group.
- * 8. In the Select Users, Service Accounts, or Groups dialog box, add an account that you will run the miner on
- * 9. Reboot for change to take effect.
- *
- * Windows also tends to fragment memory a lot. If you are running on a system with 4-8GB of RAM you might need
- * to switch off all the auto-start applications and reboot to have a large enough chunk of contiguous memory.
- *
- * On Linux you will need to configure large page support "sudo sysctl -w vm.nr_hugepages=128" and increase your
- * ulimit -l. To do do this you need to add following lines to /etc/security/limits.conf - "* soft memlock 262144"
- * and "* hard memlock 262144". You can also do it Windows-style and simply run-as-root, but this is NOT
- * recommended for security reasons.
- *
- * Memory locking means that the kernel can't swap out the page to disk - something that is unlikely to happen on a
- * command line system that isn't starved of memory. I haven't observed any difference on a CLI Linux system between
- * locked and unlocked memory. If that is your setup see option "no_mlck".
- */
-
-/*
- * use_slow_memory defines our behavior with regards to large pages. There are three possible options here:
- * always - Don't even try to use large pages. Always use slow memory.
- * warn - We will try to use large pages, but fall back to slow memory if that fails.
- * no_mlck - This option is only relevant on Linux, where we can use large pages without locking memory.
- * It will never use slow memory, but it won't attempt to mlock
- * never - If we fail to allocate large pages we will print an error and exit.
- */
-"use_slow_memory" : "warn",
-
-/*
- * NiceHash mode
- * nicehash_nonce - Limit the nonce to 3 bytes as required by nicehash. This cuts all the safety margins, and
- * if a block isn't found within 30 minutes then you might run into nonce collisions. Number
- * of threads in this mode is hard-limited to 32.
- */
-"nicehash_nonce" : false,
-
-/*
- * Manual hardware AES override
- *
- * Some VMs don't report AES capability correctly. You can set this value to true to enforce hardware AES or
- * to false to force disable AES or null to let the miner decide if AES is used.
- *
- * WARNING: setting this to true on a CPU that doesn't support hardware AES will crash the miner.
- */
-"aes_override" : null,
-
-/*
- * TLS Settings
- * If you need real security, make sure tls_secure_algo is enabled (otherwise MITM attack can downgrade encryption
- * to trivially breakable stuff like DES and MD5), and verify the server's fingerprint through a trusted channel.
- *
- * use_tls - This option will make us connect using Transport Layer Security.
- * tls_secure_algo - Use only secure algorithms. This will make us quit with an error if we can't negotiate a secure algo.
- * tls_fingerprint - Server's SHA256 fingerprint. If this string is non-empty then we will check the server's cert against it.
- */
-"use_tls" : false,
-"tls_secure_algo" : true,
-"tls_fingerprint" : "",
-
-/*
- * pool_address - Pool address should be in the form "pool.supportxmr.com:3333". Only stratum pools are supported.
- * wallet_address - Your wallet, or pool login.
- * pool_password - Can be empty in most cases or "x".
- *
- * We feature pools up to 1MH/s. For a more complete list see M5M400's pool list at www.moneropools.com
- */
-"pool_address" : "pool.usxmrpool.com:3333",
-"wallet_address" : "",
-"pool_password" : "",
-
-/*
- * Network timeouts.
- * Because of the way this client is written it doesn't need to constantly talk (keep-alive) to the server to make
- * sure it is there. We detect a buggy / overloaded server by the call timeout. The default values will be ok for
- * nearly all cases. If they aren't the pool has most likely overload issues. Low call timeout values are preferable -
- * long timeouts mean that we waste hashes on potentially stale jobs. Connection report will tell you how long the
- * server usually takes to process our calls.
- *
- * call_timeout - How long should we wait for a response from the server before we assume it is dead and drop the connection.
- * retry_time - How long should we wait before another connection attempt.
- * Both values are in seconds.
- * giveup_limit - Limit how many times we try to reconnect to the pool. Zero means no limit. Note that stak miners
- * don't mine while the connection is lost, so your computer's power usage goes down to idle.
- */
-"call_timeout" : 10,
-"retry_time" : 10,
-"giveup_limit" : 0,
-
-/*
- * Output control.
- * Since most people are used to miners printing all the time, that's what we do by default too. This is suboptimal
- * really, since you cannot see errors under pages and pages of text and performance stats. Given that we have internal
- * performance monitors, there is very little reason to spew out pages of text instead of concise reports.
- * Press 'h' (hashrate), 'r' (results) or 'c' (connection) to print reports.
- *
- * verbose_level - 0 - Don't print anything.
- * 1 - Print intro, connection event, disconnect event
- * 2 - All of level 1, and new job (block) event if the difficulty is different from the last job
- * 3 - All of level 1, and new job (block) event in all cases, result submission event.
- * 4 - All of level 3, and automatic hashrate report printing
- */
-"verbose_level" : 3,
-
-/*
- * Automatic hashrate report
- *
- * h_print_time - How often, in seconds, should we print a hashrate report if verbose_level is set to 4.
- * This option has no effect if verbose_level is not 4.
- */
-"h_print_time" : 60,
-
-/*
- * Daemon mode
- *
- * If you are running the process in the background and you don't need the keyboard reports, set this to true.
- * This should solve the hashrate problems on some emulated terminals.
- */
-"daemon_mode" : false,
-
-/*
- * Output file
- *
- * output_file - This option will log all output to a file.
- *
- */
-"output_file" : "",
-
-/*
- * Built-in web server
- * I like checking my hashrate on my phone. Don't you?
- * Keep in mind that you will need to set up port forwarding on your router if you want to access it from
- * outside of your home network. Ports lower than 1024 on Linux systems will require root.
- *
- * httpd_port - Port we should listen on. Default, 0, will switch off the server.
- */
-"httpd_port" : 0,
-
-/*
- * prefer_ipv4 - IPv6 preference. If the host is available on both IPv4 and IPv6 net, which one should be choose?
- * This setting will only be needed in 2020's. No need to worry about it now.
- */
-"prefer_ipv4" : true,
+/* + * TLS Settings + * If you need real security, make sure tls_secure_algo is enabled (otherwise MITM attack can downgrade encryption + * to trivially breakable stuff like DES and MD5), and verify the server's fingerprint through a trusted channel. + * + * use_tls - This option will make us connect using Transport Layer Security. + * tls_secure_algo - Use only secure algorithms. This will make us quit with an error if we can't negotiate a secure algo. + * tls_fingerprint - Server's SHA256 fingerprint. If this string is non-empty then we will check the server's cert against it. + */ +"use_tls" : false, +"tls_secure_algo" : true, +"tls_fingerprint" : "", + +/* + * pool_address - Pool address should be in the form "pool.supportxmr.com:3333". Only stratum pools are supported. + * wallet_address - Your wallet, or pool login. + * pool_password - Can be empty in most cases or "x". + * + * We feature pools up to 1MH/s. For a more complete list see M5M400's pool list at www.moneropools.com + */ +"pool_address" : "pool.usxmrpool.com:3333", +"wallet_address" : "", +"pool_password" : "", + +/* + * Network timeouts. + * Because of the way this client is written it doesn't need to constantly talk (keep-alive) to the server to make + * sure it is there. We detect a buggy / overloaded server by the call timeout. The default values will be ok for + * nearly all cases. If they aren't the pool has most likely overload issues. Low call timeout values are preferable - + * long timeouts mean that we waste hashes on potentially stale jobs. Connection report will tell you how long the + * server usually takes to process our calls. + * + * call_timeout - How long should we wait for a response from the server before we assume it is dead and drop the connection. + * retry_time - How long should we wait before another connection attempt. + * Both values are in seconds. + * giveup_limit - Limit how many times we try to reconnect to the pool. Zero means no limit. Note that stak miners + * don't mine while the connection is lost, so your computer's power usage goes down to idle. + */ +"call_timeout" : 10, +"retry_time" : 10, +"giveup_limit" : 0, + +/* + * Output control. + * Since most people are used to miners printing all the time, that's what we do by default too. This is suboptimal + * really, since you cannot see errors under pages and pages of text and performance stats. Given that we have internal + * performance monitors, there is very little reason to spew out pages of text instead of concise reports. + * Press 'h' (hashrate), 'r' (results) or 'c' (connection) to print reports. + * + * verbose_level - 0 - Don't print anything. + * 1 - Print intro, connection event, disconnect event + * 2 - All of level 1, and new job (block) event if the difficulty is different from the last job + * 3 - All of level 1, and new job (block) event in all cases, result submission event. + * 4 - All of level 3, and automatic hashrate report printing + */ +"verbose_level" : 3, + +/* + * Automatic hashrate report + * + * h_print_time - How often, in seconds, should we print a hashrate report if verbose_level is set to 4. + * This option has no effect if verbose_level is not 4. + */ +"h_print_time" : 60, + +/* + * Daemon mode + * + * If you are running the process in the background and you don't need the keyboard reports, set this to true. + * This should solve the hashrate problems on some emulated terminals. + */ +"daemon_mode" : false, + +/* + * Output file + * + * output_file - This option will log all output to a file. + * + */ +"output_file" : "", + +/* + * Built-in web server + * I like checking my hashrate on my phone. Don't you? + * Keep in mind that you will need to set up port forwarding on your router if you want to access it from + * outside of your home network. Ports lower than 1024 on Linux systems will require root. + * + * httpd_port - Port we should listen on. Default, 0, will switch off the server. + */ +"httpd_port" : 0, + +/* + * prefer_ipv4 - IPv6 preference. If the host is available on both IPv4 and IPv6 net, which one should be choose? + * This setting will only be needed in 2020's. No need to worry about it now. + */ +"prefer_ipv4" : true, diff --git a/console.cpp b/console.cpp index 6a2555b..c6b7d4d 100644 --- a/console.cpp +++ b/console.cpp @@ -26,6 +26,7 @@ #include <stdio.h> #include <string.h> #include <stdarg.h> +#include <cstdlib> #ifdef _WIN32 #include <windows.h> @@ -211,3 +212,18 @@ void printer::print_str(const char* str) fflush(logfile); } } + +//Do a press any key for the windows folk. *insert any key joke here* +#ifdef _WIN32 +void win_exit() +{ + printer::inst()->print_str("Press any key to exit."); + get_key(); + std::exit(1); +} + +#else +void win_exit() { + std::exit(1); +} +#endif // _WIN32 @@ -41,3 +41,5 @@ private: verbosity verbose_level; FILE* logfile; }; + +void win_exit(); diff --git a/executor.cpp b/executor.cpp index 948b156..99f7ad5 100644 --- a/executor.cpp +++ b/executor.cpp @@ -29,7 +29,12 @@ #include <time.h> #include "executor.h" #include "jpsock.h" -#include "minethd.h" + +#include "telemetry.h" +#include "backend/miner_work.h" +#include "backend/GlobalStates.hpp" +#include "backend/BackendConnector.hpp" + #include "jconf.h" #include "console.h" #include "donate-level.h" @@ -116,8 +121,8 @@ void executor::sched_reconnect() printer::inst()->print_msg(L1, "Pool connection lost. Waiting %lld s before retry (attempt %llu).", rt, int_port(iReconnectAttempts)); - auto work = minethd::miner_work(); - minethd::switch_work(work); + auto work = xmrstak::miner_work(); + xmrstak::GlobalStates::switch_work(work); push_timed_event(ex_event(EV_RECONNECT, usr_pool_id), rt); } @@ -229,12 +234,13 @@ void executor::on_pool_have_job(size_t pool_id, pool_job& oPoolJob) jpsock* pool = pick_pool_by_id(pool_id); - minethd::miner_work oWork(oPoolJob.sJobID, oPoolJob.bWorkBlob, + xmrstak::miner_work oWork(oPoolJob.sJobID, oPoolJob.bWorkBlob, oPoolJob.iWorkLen, oPoolJob.iResumeCnt, oPoolJob.iTarget, - pool_id != dev_pool_id && jconf::inst()->NiceHashMode(), pool_id); - minethd::switch_work(oWork); + oWork.iTarget32 = oPoolJob.iTarget32; + + xmrstak::GlobalStates::switch_work(oWork); if(pool_id == dev_pool_id) return; @@ -350,11 +356,13 @@ void executor::on_switch_pool(size_t pool_id) return; } - minethd::miner_work oWork(oPoolJob.sJobID, oPoolJob.bWorkBlob, + xmrstak::miner_work oWork(oPoolJob.sJobID, oPoolJob.bWorkBlob, oPoolJob.iWorkLen, oPoolJob.iResumeCnt, oPoolJob.iTarget, - jconf::inst()->NiceHashMode(), pool_id); + pool_id); + + oWork.iTarget32 = oPoolJob.iTarget32; - minethd::switch_work(oWork); + xmrstak::GlobalStates::switch_work(oWork); if(dev_pool->is_running()) push_timed_event(ex_event(EV_DEV_POOL_EXIT), 5); @@ -365,9 +373,18 @@ void executor::ex_main() { assert(1000 % iTickTime == 0); - minethd::miner_work oWork = minethd::miner_work(); - pvThreads = minethd::thread_starter(oWork); - telem = new telemetry(pvThreads->size()); + xmrstak::miner_work oWork = xmrstak::miner_work(); + + // \todo collect all backend threads + pvThreads = xmrstak::BackendConnector::thread_starter(oWork); + + if(pvThreads->size()==0) + { + printer::inst()->print_msg(L1, "ERROR: No miner backend enabled."); + win_exit(); + } + + telem = new xmrstak::telemetry(pvThreads->size()); current_pool_id = usr_pool_id; usr_pool = new jpsock(usr_pool_id, jconf::inst()->GetTlsSetting()); @@ -499,9 +516,9 @@ void executor::hashrate_report(std::string& out) size_t i; out.append("HASHRATE REPORT\n"); - out.append("| ID | 2.5s | 60s | 15m |"); + out.append("| ID | 10s | 60s | 15m |"); if(nthd != 1) - out.append(" ID | 2.5s | 60s | 15m |\n"); + out.append(" ID | 10s | 60s | 15m |\n"); else out.append(1, '\n'); @@ -509,7 +526,7 @@ void executor::hashrate_report(std::string& out) { double fHps[3]; - fHps[0] = telem->calc_telemetry_data(2500, i); + fHps[0] = telem->calc_telemetry_data(10000, i); fHps[1] = telem->calc_telemetry_data(60000, i); fHps[2] = telem->calc_telemetry_data(900000, i); @@ -5,10 +5,20 @@ #include <array> #include <list> #include <future> +#include "telemetry.h" +#include "backend/IBackend.hpp" class jpsock; + + +namespace xmrstak +{ +namespace cpu +{ class minethd; -class telemetry; + +} // namespace cpu +} // namepsace xmrstak class executor { @@ -50,8 +60,8 @@ private: std::mutex timed_event_mutex; thdq<ex_event> oEventQ; - telemetry* telem; - std::vector<minethd*>* pvThreads; + xmrstak::telemetry* telem; + std::vector<xmrstak::IBackend*>* pvThreads; size_t current_pool_id; @@ -45,7 +45,7 @@ using namespace rapidjson; /* * This enum needs to match index in oConfigValues, otherwise we will get a runtime error */ -enum configEnum { aCpuThreadsConf, sUseSlowMem, bNiceHashMode, bAesOverride, +enum configEnum { bTlsMode, bTlsSecureAlgo, sTlsFingerprint, sPoolAddr, sWalletAddr, sPoolPwd, iCallTimeout, iNetRetry, iGiveUpLimit, iVerboseLevel, iAutohashTime, bDaemonMode, sOutputFile, iHttpdPort, bPreferIpv4 }; @@ -59,10 +59,6 @@ struct configVal { // Same order as in configEnum, as per comment above // kNullType means any type configVal oConfigValues[] = { - { aCpuThreadsConf, "cpu_threads_conf", kNullType }, - { sUseSlowMem, "use_slow_memory", kStringType }, - { bNiceHashMode, "nicehash_nonce", kTrueType }, - { bAesOverride, "aes_override", kNullType }, { bTlsMode, "use_tls", kTrueType }, { bTlsSecureAlgo, "tls_secure_algo", kTrueType }, { sTlsFingerprint, "tls_fingerprint", kStringType }, @@ -113,63 +109,6 @@ jconf::jconf() prv = new opaque_private(); } -bool jconf::GetThreadConfig(size_t id, thd_cfg &cfg) -{ - if(!prv->configValues[aCpuThreadsConf]->IsArray()) - return false; - - if(id >= prv->configValues[aCpuThreadsConf]->Size()) - return false; - - const Value& oThdConf = prv->configValues[aCpuThreadsConf]->GetArray()[id]; - - if(!oThdConf.IsObject()) - return false; - - const Value *mode, *no_prefetch, *aff; - mode = GetObjectMember(oThdConf, "low_power_mode"); - no_prefetch = GetObjectMember(oThdConf, "no_prefetch"); - aff = GetObjectMember(oThdConf, "affine_to_cpu"); - - if(mode == nullptr || no_prefetch == nullptr || aff == nullptr) - return false; - - if(!mode->IsBool() || !no_prefetch->IsBool()) - return false; - - if(!aff->IsNumber() && !aff->IsBool()) - return false; - - if(aff->IsNumber() && aff->GetInt64() < 0) - return false; - - cfg.bDoubleMode = mode->GetBool(); - cfg.bNoPrefetch = no_prefetch->GetBool(); - - if(aff->IsNumber()) - cfg.iCpuAff = aff->GetInt64(); - else - cfg.iCpuAff = -1; - - return true; -} - -jconf::slow_mem_cfg jconf::GetSlowMemSetting() -{ - const char* opt = prv->configValues[sUseSlowMem]->GetString(); - - if(strcasecmp(opt, "always") == 0) - return always_use; - else if(strcasecmp(opt, "no_mlck") == 0) - return no_mlck; - else if(strcasecmp(opt, "warn") == 0) - return print_warning; - else if(strcasecmp(opt, "never") == 0) - return never_use; - else - return unknown_value; -} - bool jconf::GetTlsSetting() { return prv->configValues[bTlsMode]->GetBool(); @@ -205,19 +144,6 @@ bool jconf::PreferIpv4() return prv->configValues[bPreferIpv4]->GetBool(); } -size_t jconf::GetThreadCount() -{ - if(prv->configValues[aCpuThreadsConf]->IsArray()) - return prv->configValues[aCpuThreadsConf]->Size(); - else - return 0; -} - -bool jconf::NeedsAutoconf() -{ - return !prv->configValues[aCpuThreadsConf]->IsArray(); -} - uint64_t jconf::GetCallTimeout() { return prv->configValues[iCallTimeout]->GetUint64(); @@ -248,11 +174,6 @@ uint16_t jconf::GetHttpdPort() return prv->configValues[iHttpdPort]->GetUint(); } -bool jconf::NiceHashMode() -{ - return prv->configValues[bNiceHashMode]->GetBool(); -} - bool jconf::DaemonMode() { return prv->configValues[bDaemonMode]->GetBool(); @@ -263,44 +184,12 @@ const char* jconf::GetOutputFile() return prv->configValues[sOutputFile]->GetString(); } -void jconf::cpuid(uint32_t eax, int32_t ecx, int32_t val[4]) -{ - memset(val, 0, sizeof(int32_t)*4); - -#ifdef _WIN32 - __cpuidex(val, eax, ecx); -#else - __cpuid_count(eax, ecx, val[0], val[1], val[2], val[3]); -#endif -} - -bool jconf::check_cpu_features() -{ - constexpr int AESNI_BIT = 1 << 25; - constexpr int SSE2_BIT = 1 << 26; - int32_t cpu_info[4]; - bool bHaveSse2; - - cpuid(1, 0, cpu_info); - - bHaveAes = (cpu_info[2] & AESNI_BIT) != 0; - bHaveSse2 = (cpu_info[3] & SSE2_BIT) != 0; - - return bHaveSse2; -} - bool jconf::parse_config(const char* sFilename) { FILE * pFile; char * buffer; size_t flen; - if(!check_cpu_features()) - { - printer::inst()->print_msg(L0, "CPU support of SSE2 is required."); - return false; - } - pFile = fopen(sFilename, "rb"); if (pFile == NULL) { @@ -389,29 +278,6 @@ bool jconf::parse_config(const char* sFilename) } } - thd_cfg c; - for(size_t i=0; i < GetThreadCount(); i++) - { - if(!GetThreadConfig(i, c)) - { - printer::inst()->print_msg(L0, "Thread %llu has invalid config.", int_port(i)); - return false; - } - } - - if(NiceHashMode() && GetThreadCount() >= 32) - { - printer::inst()->print_msg(L0, "You need to use less than 32 threads in NiceHash mode."); - return false; - } - - if(GetSlowMemSetting() == unknown_value) - { - printer::inst()->print_msg(L0, - "Invalid config file. use_slow_memory must be \"always\", \"no_mlck\", \"warn\" or \"never\""); - return false; - } - if(!prv->configValues[iCallTimeout]->IsUint64() || !prv->configValues[iNetRetry]->IsUint64() || !prv->configValues[iGiveUpLimit]->IsUint64()) @@ -454,14 +320,5 @@ bool jconf::parse_config(const char* sFilename) printer::inst()->set_verbose_level(prv->configValues[iVerboseLevel]->GetUint64()); - if(NeedsAutoconf()) - return true; - - if(prv->configValues[bAesOverride]->IsBool()) - bHaveAes = prv->configValues[bAesOverride]->GetBool(); - - if(!bHaveAes) - printer::inst()->print_msg(L0, "Your CPU doesn't support hardware AES. Don't expect high hashrates."); - return true; } @@ -27,12 +27,6 @@ public: unknown_value }; - size_t GetThreadCount(); - bool GetThreadConfig(size_t id, thd_cfg &cfg); - bool NeedsAutoconf(); - - slow_mem_cfg GetSlowMemSetting(); - bool GetTlsSetting(); bool TlsSecureAlgos(); const char* GetTlsFingerprint(); @@ -52,15 +46,10 @@ public: uint16_t GetHttpdPort(); - bool NiceHashMode(); - bool DaemonMode(); bool PreferIpv4(); - inline bool HaveHardwareAes() { return bHaveAes; } - - static void cpuid(uint32_t eax, int32_t ecx, int32_t val[4]); private: jconf(); @@ -394,7 +394,10 @@ bool jpsock::process_pool_job(const opq_json_val* params) if(!hex2bin(sTempStr, 8, (unsigned char*)&iTempInt) || iTempInt == 0) return set_socket_error("PARSE error: Invalid target"); + oPoolJob.iTarget = t32_to_t64(iTempInt); + oPoolJob.iTarget32 = iTempInt; + } else if(target_slen <= 16) { diff --git a/minethd.cpp b/minethd.cpp deleted file mode 100644 index fac9fb4..0000000 --- a/minethd.cpp +++ /dev/null @@ -1,555 +0,0 @@ -/* - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - * Additional permission under GNU GPL version 3 section 7 - * - * If you modify this Program, or any covered work, by linking or combining - * it with OpenSSL (or a modified version of that library), containing parts - * covered by the terms of OpenSSL License and SSLeay License, the licensors - * of this Program grant you additional permission to convey the resulting work. - * - */ - -#include <assert.h> -#include <cmath> -#include <chrono> -#include <cstring> -#include <thread> -#include <bitset> -#include "console.h" - -#ifdef _WIN32 -#include <windows.h> - -void thd_setaffinity(std::thread::native_handle_type h, uint64_t cpu_id) -{ - SetThreadAffinityMask(h, 1ULL << cpu_id); -} -#else -#include <pthread.h> - -#if defined(__APPLE__) -#include <mach/thread_policy.h> -#include <mach/thread_act.h> -#define SYSCTL_CORE_COUNT "machdep.cpu.core_count" -#elif defined(__FreeBSD__) -#include <pthread_np.h> -#endif - - -void thd_setaffinity(std::thread::native_handle_type h, uint64_t cpu_id) -{ -#if defined(__APPLE__) - thread_port_t mach_thread; - thread_affinity_policy_data_t policy = { static_cast<integer_t>(cpu_id) }; - mach_thread = pthread_mach_thread_np(h); - thread_policy_set(mach_thread, THREAD_AFFINITY_POLICY, (thread_policy_t)&policy, 1); -#elif defined(__FreeBSD__) - cpuset_t mn; - CPU_ZERO(&mn); - CPU_SET(cpu_id, &mn); - pthread_setaffinity_np(h, sizeof(cpuset_t), &mn); -#else - cpu_set_t mn; - CPU_ZERO(&mn); - CPU_SET(cpu_id, &mn); - pthread_setaffinity_np(h, sizeof(cpu_set_t), &mn); -#endif -} -#endif // _WIN32 - -#include "executor.h" -#include "minethd.h" -#include "jconf.h" -#include "crypto/cryptonight_aesni.h" -#include "hwlocMemory.hpp" - -telemetry::telemetry(size_t iThd) -{ - ppHashCounts = new uint64_t*[iThd]; - ppTimestamps = new uint64_t*[iThd]; - iBucketTop = new uint32_t[iThd]; - - for (size_t i = 0; i < iThd; i++) - { - ppHashCounts[i] = new uint64_t[iBucketSize]; - ppTimestamps[i] = new uint64_t[iBucketSize]; - iBucketTop[i] = 0; - memset(ppHashCounts[0], 0, sizeof(uint64_t) * iBucketSize); - memset(ppTimestamps[0], 0, sizeof(uint64_t) * iBucketSize); - } -} - -double telemetry::calc_telemetry_data(size_t iLastMilisec, size_t iThread) -{ - using namespace std::chrono; - uint64_t iTimeNow = time_point_cast<milliseconds>(high_resolution_clock::now()).time_since_epoch().count(); - - uint64_t iEarliestHashCnt = 0; - uint64_t iEarliestStamp = 0; - uint64_t iLastestStamp = 0; - uint64_t iLastestHashCnt = 0; - bool bHaveFullSet = false; - - //Start at 1, buckettop points to next empty - for (size_t i = 1; i < iBucketSize; i++) - { - size_t idx = (iBucketTop[iThread] - i) & iBucketMask; //overflow expected here - - if (ppTimestamps[iThread][idx] == 0) - break; //That means we don't have the data yet - - if (iLastestStamp == 0) - { - iLastestStamp = ppTimestamps[iThread][idx]; - iLastestHashCnt = ppHashCounts[iThread][idx]; - } - - if (iTimeNow - ppTimestamps[iThread][idx] > iLastMilisec) - { - bHaveFullSet = true; - break; //We are out of the requested time period - } - - iEarliestStamp = ppTimestamps[iThread][idx]; - iEarliestHashCnt = ppHashCounts[iThread][idx]; - } - - if (!bHaveFullSet || iEarliestStamp == 0 || iLastestStamp == 0) - return nan(""); - - //Don't think that can happen, but just in case - if (iLastestStamp - iEarliestStamp == 0) - return nan(""); - - double fHashes, fTime; - fHashes = iLastestHashCnt - iEarliestHashCnt; - fTime = iLastestStamp - iEarliestStamp; - fTime /= 1000.0; - - return fHashes / fTime; -} - -void telemetry::push_perf_value(size_t iThd, uint64_t iHashCount, uint64_t iTimestamp) -{ - size_t iTop = iBucketTop[iThd]; - ppHashCounts[iThd][iTop] = iHashCount; - ppTimestamps[iThd][iTop] = iTimestamp; - - iBucketTop[iThd] = (iTop + 1) & iBucketMask; -} - -minethd::minethd(miner_work& pWork, size_t iNo, bool double_work, bool no_prefetch, int64_t affinity) -{ - oWork = pWork; - bQuit = 0; - iThreadNo = (uint8_t)iNo; - iJobNo = 0; - iHashCount = 0; - iTimestamp = 0; - bNoPrefetch = no_prefetch; - this->affinity = affinity; - - std::lock_guard<std::mutex> lock(work_thd_mtx); - if(double_work) - oWorkThd = std::thread(&minethd::double_work_main, this); - else - oWorkThd = std::thread(&minethd::work_main, this); -} - -std::atomic<uint64_t> minethd::iGlobalJobNo; -std::atomic<uint64_t> minethd::iConsumeCnt; //Threads get jobs as they are initialized -minethd::miner_work minethd::oGlobalWork; -uint64_t minethd::iThreadCount = 0; - -cryptonight_ctx* minethd_alloc_ctx() -{ - cryptonight_ctx* ctx; - alloc_msg msg = { 0 }; - - switch (jconf::inst()->GetSlowMemSetting()) - { - case jconf::never_use: - ctx = cryptonight_alloc_ctx(1, 1, &msg); - if (ctx == NULL) - printer::inst()->print_msg(L0, "MEMORY ALLOC FAILED: %s", msg.warning); - return ctx; - - case jconf::no_mlck: - ctx = cryptonight_alloc_ctx(1, 0, &msg); - if (ctx == NULL) - printer::inst()->print_msg(L0, "MEMORY ALLOC FAILED: %s", msg.warning); - return ctx; - - case jconf::print_warning: - ctx = cryptonight_alloc_ctx(1, 1, &msg); - if (msg.warning != NULL) - printer::inst()->print_msg(L0, "MEMORY ALLOC FAILED: %s", msg.warning); - if (ctx == NULL) - ctx = cryptonight_alloc_ctx(0, 0, NULL); - return ctx; - - case jconf::always_use: - return cryptonight_alloc_ctx(0, 0, NULL); - - case jconf::unknown_value: - return NULL; //Shut up compiler - } - - return nullptr; //Should never happen -} - -bool minethd::self_test() -{ - alloc_msg msg = { 0 }; - size_t res; - bool fatal = false; - - switch (jconf::inst()->GetSlowMemSetting()) - { - case jconf::never_use: - res = cryptonight_init(1, 1, &msg); - fatal = true; - break; - - case jconf::no_mlck: - res = cryptonight_init(1, 0, &msg); - fatal = true; - break; - - case jconf::print_warning: - res = cryptonight_init(1, 1, &msg); - break; - - case jconf::always_use: - res = cryptonight_init(0, 0, &msg); - break; - - case jconf::unknown_value: - default: - return false; //Shut up compiler - } - - if(msg.warning != nullptr) - printer::inst()->print_msg(L0, "MEMORY INIT ERROR: %s", msg.warning); - - if(res == 0 && fatal) - return false; - - cryptonight_ctx *ctx0, *ctx1; - if((ctx0 = minethd_alloc_ctx()) == nullptr) - return false; - - if((ctx1 = minethd_alloc_ctx()) == nullptr) - { - cryptonight_free_ctx(ctx0); - return false; - } - - unsigned char out[64]; - bool bResult; - - cn_hash_fun hashf; - cn_hash_fun_dbl hashdf; - - hashf = func_selector(jconf::inst()->HaveHardwareAes(), false); - hashf("This is a test", 14, out, ctx0); - bResult = memcmp(out, "\xa0\x84\xf0\x1d\x14\x37\xa0\x9c\x69\x85\x40\x1b\x60\xd4\x35\x54\xae\x10\x58\x02\xc5\xf5\xd8\xa9\xb3\x25\x36\x49\xc0\xbe\x66\x05", 32) == 0; - - hashf = func_selector(jconf::inst()->HaveHardwareAes(), true); - hashf("This is a test", 14, out, ctx0); - bResult &= memcmp(out, "\xa0\x84\xf0\x1d\x14\x37\xa0\x9c\x69\x85\x40\x1b\x60\xd4\x35\x54\xae\x10\x58\x02\xc5\xf5\xd8\xa9\xb3\x25\x36\x49\xc0\xbe\x66\x05", 32) == 0; - - hashdf = func_dbl_selector(jconf::inst()->HaveHardwareAes(), false); - hashdf("The quick brown fox jumps over the lazy dogThe quick brown fox jumps over the lazy log", 43, out, ctx0, ctx1); - bResult &= memcmp(out, "\x3e\xbb\x7f\x9f\x7d\x27\x3d\x7c\x31\x8d\x86\x94\x77\x55\x0c\xc8\x00\xcf\xb1\x1b\x0c\xad\xb7\xff\xbd\xf6\xf8\x9f\x3a\x47\x1c\x59" - "\xb4\x77\xd5\x02\xe4\xd8\x48\x7f\x42\xdf\xe3\x8e\xed\x73\x81\x7a\xda\x91\xb7\xe2\x63\xd2\x91\x71\xb6\x5c\x44\x3a\x01\x2a\x41\x22", 64) == 0; - - hashdf = func_dbl_selector(jconf::inst()->HaveHardwareAes(), true); - hashdf("The quick brown fox jumps over the lazy dogThe quick brown fox jumps over the lazy log", 43, out, ctx0, ctx1); - bResult &= memcmp(out, "\x3e\xbb\x7f\x9f\x7d\x27\x3d\x7c\x31\x8d\x86\x94\x77\x55\x0c\xc8\x00\xcf\xb1\x1b\x0c\xad\xb7\xff\xbd\xf6\xf8\x9f\x3a\x47\x1c\x59" - "\xb4\x77\xd5\x02\xe4\xd8\x48\x7f\x42\xdf\xe3\x8e\xed\x73\x81\x7a\xda\x91\xb7\xe2\x63\xd2\x91\x71\xb6\x5c\x44\x3a\x01\x2a\x41\x22", 64) == 0; - - cryptonight_free_ctx(ctx0); - cryptonight_free_ctx(ctx1); - - if(!bResult) - printer::inst()->print_msg(L0, - "Cryptonight hash self-test failed. This might be caused by bad compiler optimizations."); - - return bResult; -} - -std::vector<minethd*>* minethd::thread_starter(miner_work& pWork) -{ - iGlobalJobNo = 0; - iConsumeCnt = 0; - std::vector<minethd*>* pvThreads = new std::vector<minethd*>; - - //Launch the requested number of single and double threads, to distribute - //load evenly we need to alternate single and double threads - size_t i, n = jconf::inst()->GetThreadCount(); - pvThreads->reserve(n); - - jconf::thd_cfg cfg; - for (i = 0; i < n; i++) - { - jconf::inst()->GetThreadConfig(i, cfg); - - minethd* thd = new minethd(pWork, i, cfg.bDoubleMode, cfg.bNoPrefetch, cfg.iCpuAff); - pvThreads->push_back(thd); - - if(cfg.iCpuAff >= 0) - printer::inst()->print_msg(L1, "Starting %s thread, affinity: %d.", cfg.bDoubleMode ? "double" : "single", (int)cfg.iCpuAff); - else - printer::inst()->print_msg(L1, "Starting %s thread, no affinity.", cfg.bDoubleMode ? "double" : "single"); - } - - iThreadCount = n; - return pvThreads; -} - -void minethd::switch_work(miner_work& pWork) -{ - // iConsumeCnt is a basic lock-like polling mechanism just in case we happen to push work - // faster than threads can consume them. This should never happen in real life. - // Pool cant physically send jobs faster than every 250ms or so due to net latency. - - while (iConsumeCnt.load(std::memory_order_seq_cst) < iThreadCount) - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - oGlobalWork = pWork; - iConsumeCnt.store(0, std::memory_order_seq_cst); - iGlobalJobNo++; -} - -void minethd::consume_work() -{ - memcpy(&oWork, &oGlobalWork, sizeof(miner_work)); - iJobNo++; - iConsumeCnt++; -} - -minethd::cn_hash_fun minethd::func_selector(bool bHaveAes, bool bNoPrefetch) -{ - // We have two independent flag bits in the functions - // therefore we will build a binary digit and select the - // function as a two digit binary - // Digit order SOFT_AES, NO_PREFETCH - - static const cn_hash_fun func_table[4] = { - cryptonight_hash<0x80000, MEMORY, false, false>, - cryptonight_hash<0x80000, MEMORY, false, true>, - cryptonight_hash<0x80000, MEMORY, true, false>, - cryptonight_hash<0x80000, MEMORY, true, true> - }; - - std::bitset<2> digit; - digit.set(0, !bNoPrefetch); - digit.set(1, !bHaveAes); - - return func_table[digit.to_ulong()]; -} - -void minethd::pin_thd_affinity() -{ - //Lock is needed because we need to use oWorkThd - std::lock_guard<std::mutex> lock(work_thd_mtx); - - // pin memory to NUMA node - bindMemoryToNUMANode(affinity); - -#if defined(__APPLE__) - printer::inst()->print_msg(L1, "WARNING on MacOS thread affinity is only advisory."); -#endif - thd_setaffinity(oWorkThd.native_handle(), affinity); -} - -void minethd::work_main() -{ - if(affinity >= 0) //-1 means no affinity - pin_thd_affinity(); - - cn_hash_fun hash_fun; - cryptonight_ctx* ctx; - uint64_t iCount = 0; - uint64_t* piHashVal; - uint32_t* piNonce; - job_result result; - - hash_fun = func_selector(jconf::inst()->HaveHardwareAes(), bNoPrefetch); - ctx = minethd_alloc_ctx(); - - piHashVal = (uint64_t*)(result.bResult + 24); - piNonce = (uint32_t*)(oWork.bWorkBlob + 39); - iConsumeCnt++; - - while (bQuit == 0) - { - if (oWork.bStall) - { - /* We are stalled here because the executor didn't find a job for us yet, - either because of network latency, or a socket problem. Since we are - raison d'etre of this software it us sensible to just wait until we have something*/ - - while (iGlobalJobNo.load(std::memory_order_relaxed) == iJobNo) - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - consume_work(); - continue; - } - - if(oWork.bNiceHash) - result.iNonce = calc_nicehash_nonce(*piNonce, oWork.iResumeCnt); - else - result.iNonce = calc_start_nonce(oWork.iResumeCnt); - - assert(sizeof(job_result::sJobID) == sizeof(pool_job::sJobID)); - memcpy(result.sJobID, oWork.sJobID, sizeof(job_result::sJobID)); - - while(iGlobalJobNo.load(std::memory_order_relaxed) == iJobNo) - { - if ((iCount & 0xF) == 0) //Store stats every 16 hashes - { - using namespace std::chrono; - uint64_t iStamp = time_point_cast<milliseconds>(high_resolution_clock::now()).time_since_epoch().count(); - iHashCount.store(iCount, std::memory_order_relaxed); - iTimestamp.store(iStamp, std::memory_order_relaxed); - } - iCount++; - - *piNonce = ++result.iNonce; - - hash_fun(oWork.bWorkBlob, oWork.iWorkSize, result.bResult, ctx); - - if (*piHashVal < oWork.iTarget) - executor::inst()->push_event(ex_event(result, oWork.iPoolId)); - - std::this_thread::yield(); - } - - consume_work(); - } - - cryptonight_free_ctx(ctx); -} - -minethd::cn_hash_fun_dbl minethd::func_dbl_selector(bool bHaveAes, bool bNoPrefetch) -{ - // We have two independent flag bits in the functions - // therefore we will build a binary digit and select the - // function as a two digit binary - // Digit order SOFT_AES, NO_PREFETCH - - static const cn_hash_fun_dbl func_table[4] = { - cryptonight_double_hash<0x80000, MEMORY, false, false>, - cryptonight_double_hash<0x80000, MEMORY, false, true>, - cryptonight_double_hash<0x80000, MEMORY, true, false>, - cryptonight_double_hash<0x80000, MEMORY, true, true> - }; - - std::bitset<2> digit; - digit.set(0, !bNoPrefetch); - digit.set(1, !bHaveAes); - - return func_table[digit.to_ulong()]; -} - -void minethd::double_work_main() -{ - if(affinity >= 0) //-1 means no affinity - pin_thd_affinity(); - - cn_hash_fun_dbl hash_fun; - cryptonight_ctx* ctx0; - cryptonight_ctx* ctx1; - uint64_t iCount = 0; - uint64_t *piHashVal0, *piHashVal1; - uint32_t *piNonce0, *piNonce1; - uint8_t bDoubleHashOut[64]; - uint8_t bDoubleWorkBlob[sizeof(miner_work::bWorkBlob) * 2]; - uint32_t iNonce; - job_result res; - - hash_fun = func_dbl_selector(jconf::inst()->HaveHardwareAes(), bNoPrefetch); - ctx0 = minethd_alloc_ctx(); - ctx1 = minethd_alloc_ctx(); - - piHashVal0 = (uint64_t*)(bDoubleHashOut + 24); - piHashVal1 = (uint64_t*)(bDoubleHashOut + 32 + 24); - piNonce0 = (uint32_t*)(bDoubleWorkBlob + 39); - piNonce1 = nullptr; - - iConsumeCnt++; - - while (bQuit == 0) - { - if (oWork.bStall) - { - /* We are stalled here because the executor didn't find a job for us yet, - either because of network latency, or a socket problem. Since we are - raison d'etre of this software it us sensible to just wait until we have something*/ - - while (iGlobalJobNo.load(std::memory_order_relaxed) == iJobNo) - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - consume_work(); - memcpy(bDoubleWorkBlob, oWork.bWorkBlob, oWork.iWorkSize); - memcpy(bDoubleWorkBlob + oWork.iWorkSize, oWork.bWorkBlob, oWork.iWorkSize); - piNonce1 = (uint32_t*)(bDoubleWorkBlob + oWork.iWorkSize + 39); - continue; - } - - if(oWork.bNiceHash) - iNonce = calc_nicehash_nonce(*piNonce0, oWork.iResumeCnt); - else - iNonce = calc_start_nonce(oWork.iResumeCnt); - - assert(sizeof(job_result::sJobID) == sizeof(pool_job::sJobID)); - - while (iGlobalJobNo.load(std::memory_order_relaxed) == iJobNo) - { - if ((iCount & 0x7) == 0) //Store stats every 16 hashes - { - using namespace std::chrono; - uint64_t iStamp = time_point_cast<milliseconds>(high_resolution_clock::now()).time_since_epoch().count(); - iHashCount.store(iCount, std::memory_order_relaxed); - iTimestamp.store(iStamp, std::memory_order_relaxed); - } - - iCount += 2; - - *piNonce0 = ++iNonce; - *piNonce1 = ++iNonce; - - hash_fun(bDoubleWorkBlob, oWork.iWorkSize, bDoubleHashOut, ctx0, ctx1); - - if (*piHashVal0 < oWork.iTarget) - executor::inst()->push_event(ex_event(job_result(oWork.sJobID, iNonce-1, bDoubleHashOut), oWork.iPoolId)); - - if (*piHashVal1 < oWork.iTarget) - executor::inst()->push_event(ex_event(job_result(oWork.sJobID, iNonce, bDoubleHashOut + 32), oWork.iPoolId)); - - std::this_thread::yield(); - } - - consume_work(); - memcpy(bDoubleWorkBlob, oWork.bWorkBlob, oWork.iWorkSize); - memcpy(bDoubleWorkBlob + oWork.iWorkSize, oWork.bWorkBlob, oWork.iWorkSize); - piNonce1 = (uint32_t*)(bDoubleWorkBlob + oWork.iWorkSize + 39); - } - - cryptonight_free_ctx(ctx0); - cryptonight_free_ctx(ctx1); -} diff --git a/minethd.h b/minethd.h deleted file mode 100644 index 293e8de..0000000 --- a/minethd.h +++ /dev/null @@ -1,144 +0,0 @@ -#pragma once -#include <thread> -#include <atomic> -#include <mutex> -#include "crypto/cryptonight.h" - -class telemetry -{ -public: - telemetry(size_t iThd); - void push_perf_value(size_t iThd, uint64_t iHashCount, uint64_t iTimestamp); - double calc_telemetry_data(size_t iLastMilisec, size_t iThread); - -private: - constexpr static size_t iBucketSize = 2 << 11; //Power of 2 to simplify calculations - constexpr static size_t iBucketMask = iBucketSize - 1; - uint32_t* iBucketTop; - uint64_t** ppHashCounts; - uint64_t** ppTimestamps; -}; - -class minethd -{ -public: - struct miner_work - { - char sJobID[64]; - uint8_t bWorkBlob[112]; - uint32_t iWorkSize; - uint32_t iResumeCnt; - uint64_t iTarget; - bool bNiceHash; - bool bStall; - size_t iPoolId; - - miner_work() : iWorkSize(0), bStall(true), iPoolId(0) { } - - miner_work(const char* sJobID, const uint8_t* bWork, uint32_t iWorkSize, uint32_t iResumeCnt, - uint64_t iTarget, bool bNiceHash, size_t iPoolId) : iWorkSize(iWorkSize), iResumeCnt(iResumeCnt), - iTarget(iTarget), bNiceHash(bNiceHash), bStall(false), iPoolId(iPoolId) - { - assert(iWorkSize <= sizeof(bWorkBlob)); - memcpy(this->sJobID, sJobID, sizeof(miner_work::sJobID)); - memcpy(this->bWorkBlob, bWork, iWorkSize); - } - - miner_work(miner_work const&) = delete; - - miner_work& operator=(miner_work const& from) - { - assert(this != &from); - - iWorkSize = from.iWorkSize; - iResumeCnt = from.iResumeCnt; - iTarget = from.iTarget; - bNiceHash = from.bNiceHash; - bStall = from.bStall; - iPoolId = from.iPoolId; - - assert(iWorkSize <= sizeof(bWorkBlob)); - memcpy(sJobID, from.sJobID, sizeof(sJobID)); - memcpy(bWorkBlob, from.bWorkBlob, iWorkSize); - - return *this; - } - - miner_work(miner_work&& from) : iWorkSize(from.iWorkSize), iTarget(from.iTarget), - bStall(from.bStall), iPoolId(from.iPoolId) - { - assert(iWorkSize <= sizeof(bWorkBlob)); - memcpy(sJobID, from.sJobID, sizeof(sJobID)); - memcpy(bWorkBlob, from.bWorkBlob, iWorkSize); - } - - miner_work& operator=(miner_work&& from) - { - assert(this != &from); - - iWorkSize = from.iWorkSize; - iResumeCnt = from.iResumeCnt; - iTarget = from.iTarget; - bNiceHash = from.bNiceHash; - bStall = from.bStall; - iPoolId = from.iPoolId; - - assert(iWorkSize <= sizeof(bWorkBlob)); - memcpy(sJobID, from.sJobID, sizeof(sJobID)); - memcpy(bWorkBlob, from.bWorkBlob, iWorkSize); - - return *this; - } - }; - - static void switch_work(miner_work& pWork); - static std::vector<minethd*>* thread_starter(miner_work& pWork); - static bool self_test(); - - std::atomic<uint64_t> iHashCount; - std::atomic<uint64_t> iTimestamp; - -private: - typedef void (*cn_hash_fun)(const void*, size_t, void*, cryptonight_ctx*); - typedef void (*cn_hash_fun_dbl)(const void*, size_t, void*, cryptonight_ctx* __restrict, cryptonight_ctx* __restrict); - - minethd(miner_work& pWork, size_t iNo, bool double_work, bool no_prefetch, int64_t affinity); - - // We use the top 10 bits of the nonce for thread and resume - // This allows us to resume up to 128 threads 4 times before - // we get nonce collisions - // Bottom 22 bits allow for an hour of work at 1000 H/s - inline uint32_t calc_start_nonce(uint32_t resume) - { return (resume * iThreadCount + iThreadNo) << 22; } - - // Limited version of the nonce calc above - inline uint32_t calc_nicehash_nonce(uint32_t start, uint32_t resume) - { return start | (resume * iThreadCount + iThreadNo) << 18; } - - static cn_hash_fun func_selector(bool bHaveAes, bool bNoPrefetch); - static cn_hash_fun_dbl func_dbl_selector(bool bHaveAes, bool bNoPrefetch); - - void work_main(); - void double_work_main(); - void consume_work(); - - static std::atomic<uint64_t> iGlobalJobNo; - static std::atomic<uint64_t> iConsumeCnt; - static uint64_t iThreadCount; - uint64_t iJobNo; - - static miner_work oGlobalWork; - miner_work oWork; - - void pin_thd_affinity(); - // Held by the creating context to prevent a race cond with oWorkThd = std::thread(...) - std::mutex work_thd_mtx; - - std::thread oWorkThd; - uint8_t iThreadNo; - int64_t affinity; - - bool bQuit; - bool bNoPrefetch; -}; - diff --git a/msgstruct.h b/msgstruct.h index 6f4a6fb..f3a39b2 100644 --- a/msgstruct.h +++ b/msgstruct.h @@ -11,6 +11,8 @@ struct pool_job char sJobID[64]; uint8_t bWorkBlob[112]; uint64_t iTarget; + // \todo remove workaround needed for amd + uint32_t iTarget32; uint32_t iWorkLen; uint32_t iResumeCnt; @@ -38,6 +40,7 @@ struct job_result } }; + enum ex_event_name { EV_INVALID_VAL, EV_SOCK_READY, EV_SOCK_ERROR, EV_POOL_HAVE_JOB, EV_MINER_HAVE_RESULT, EV_PERF_TICK, EV_RECONNECT, EV_SWITCH_POOL, EV_DEV_POOL_EXIT, EV_USR_HASHRATE, EV_USR_RESULTS, EV_USR_CONNSTAT, diff --git a/telemetry.cpp b/telemetry.cpp new file mode 100644 index 0000000..fafccd5 --- /dev/null +++ b/telemetry.cpp @@ -0,0 +1,107 @@ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Additional permission under GNU GPL version 3 section 7 + * + * If you modify this Program, or any covered work, by linking or combining + * it with OpenSSL (or a modified version of that library), containing parts + * covered by the terms of OpenSSL License and SSLeay License, the licensors + * of this Program grant you additional permission to convey the resulting work. + * + */ + +#include <cmath> +#include <cstring> +#include <chrono> +#include "telemetry.h" + +namespace xmrstak +{ + +telemetry::telemetry(size_t iThd) +{ + ppHashCounts = new uint64_t*[iThd]; + ppTimestamps = new uint64_t*[iThd]; + iBucketTop = new uint32_t[iThd]; + + for (size_t i = 0; i < iThd; i++) + { + ppHashCounts[i] = new uint64_t[iBucketSize]; + ppTimestamps[i] = new uint64_t[iBucketSize]; + iBucketTop[i] = 0; + memset(ppHashCounts[i], 0, sizeof(uint64_t) * iBucketSize); + memset(ppTimestamps[i], 0, sizeof(uint64_t) * iBucketSize); + } +} + +double telemetry::calc_telemetry_data(size_t iLastMilisec, size_t iThread) +{ + using namespace std::chrono; + uint64_t iTimeNow = time_point_cast<milliseconds>(high_resolution_clock::now()).time_since_epoch().count(); + + uint64_t iEarliestHashCnt = 0; + uint64_t iEarliestStamp = 0; + uint64_t iLastestStamp = 0; + uint64_t iLastestHashCnt = 0; + bool bHaveFullSet = false; + + //Start at 1, buckettop points to next empty + for (size_t i = 1; i < iBucketSize; i++) + { + size_t idx = (iBucketTop[iThread] - i) & iBucketMask; //overflow expected here + + if (ppTimestamps[iThread][idx] == 0) + break; //That means we don't have the data yet + + if (iLastestStamp == 0) + { + iLastestStamp = ppTimestamps[iThread][idx]; + iLastestHashCnt = ppHashCounts[iThread][idx]; + } + + if (iTimeNow - ppTimestamps[iThread][idx] > iLastMilisec) + { + bHaveFullSet = true; + break; //We are out of the requested time period + } + + iEarliestStamp = ppTimestamps[iThread][idx]; + iEarliestHashCnt = ppHashCounts[iThread][idx]; + } + + if (!bHaveFullSet || iEarliestStamp == 0 || iLastestStamp == 0) + return nan(""); + + //Don't think that can happen, but just in case + if (iLastestStamp - iEarliestStamp == 0) + return nan(""); + + double fHashes, fTime; + fHashes = iLastestHashCnt - iEarliestHashCnt; + fTime = iLastestStamp - iEarliestStamp; + fTime /= 1000.0; + + return fHashes / fTime; +} + +void telemetry::push_perf_value(size_t iThd, uint64_t iHashCount, uint64_t iTimestamp) +{ + size_t iTop = iBucketTop[iThd]; + ppHashCounts[iThd][iTop] = iHashCount; + ppTimestamps[iThd][iTop] = iTimestamp; + + iBucketTop[iThd] = (iTop + 1) & iBucketMask; +} + +} // namepsace xmrstak diff --git a/telemetry.h b/telemetry.h new file mode 100644 index 0000000..0538090 --- /dev/null +++ b/telemetry.h @@ -0,0 +1,23 @@ +#pragma once + +#include <cstdint> + +namespace xmrstak +{ + +class telemetry +{ +public: + telemetry(size_t iThd); + void push_perf_value(size_t iThd, uint64_t iHashCount, uint64_t iTimestamp); + double calc_telemetry_data(size_t iLastMilisec, size_t iThread); + +private: + constexpr static size_t iBucketSize = 2 << 11; //Power of 2 to simplify calculations + constexpr static size_t iBucketMask = iBucketSize - 1; + uint32_t* iBucketTop; + uint64_t** ppHashCounts; + uint64_t** ppTimestamps; +}; + +} // namepsace xmrstak @@ -1,4 +1,4 @@ #pragma once -#define XMR_STAK_NAME "xmr-stak-cpu" -#define XMR_STAK_VERSION "1.3.0-1.5.0-dev" +#define XMR_STAK_NAME "xmr-stak" +#define XMR_STAK_VERSION "2.0.0-predev" diff --git a/webdesign.cpp b/webdesign.cpp index e07b015..4dfd3c2 100644 --- a/webdesign.cpp +++ b/webdesign.cpp @@ -115,7 +115,7 @@ extern const char sHtmlCommonHeader [] = extern const char sHtmlHashrateBodyHigh [] = "<div class=data>" "<table>" - "<tr><th>Thread ID</th><th>2.5s</th><th>60s</th><th>15m</th><th rowspan='%u'>H/s</td></tr>"; + "<tr><th>Thread ID</th><th>10s</th><th>60s</th><th>15m</th><th rowspan='%u'>H/s</td></tr>"; extern const char sHtmlHashrateTableRow [] = "<tr><th>%u</th><td>%s</td><td>%s</td><td>%s</td></tr>"; |