}
};
-inline std::optional<std::string> get_system_command_output(const std::string& command) {
- std::unique_ptr<FILE, PipeDeleter> pipe(popen(command.c_str(), "r"));
- if (!pipe)
- return std::nullopt;
-
- std::string result;
- char buffer[1024];
- while (fgets(buffer, sizeof(buffer), pipe.get()) != nullptr)
- result += buffer;
-
- return result;
-}
-
#endif
+// Reads the file as bytes.
+// Returns std::nullopt if the file does not exist.
+std::optional<std::string> read_file_to_string(const std::string& path);
+
void dbg_hit_on(bool cond, int slot = 0);
void dbg_mean_of(int64_t value, int slot = 0);
void dbg_stdev_of(int64_t value, int slot = 0);
}
inline std::vector<std::string> split(const std::string& s, const std::string& delimiter) {
- size_t begin = 0;
std::vector<std::string> res;
+ if (s.empty())
+ return res;
+
+ size_t begin = 0;
for (;;)
{
const size_t end = s.find(delimiter, begin);
return res;
}
+void remove_whitespace(std::string& s);
+
enum SyncCout {
IO_LOCK,
IO_UNLOCK
#include <utility>
#include <vector>
-// We support linux very well, but we explicitly do NOT support Android, partially because
-// there are potential issues with `lscpu`, `popen` availability, and partially because
-// there's no NUMA environments running Android and there probably won't be.
+// We support linux very well, but we explicitly do NOT support Android, because there's
+// no affected systems, not worth maintaining.
#if defined(__linux__) && !defined(__ANDROID__)
#if !defined(_GNU_SOURCE)
#define _GNU_SOURCE
}
// This function queries the system for the mapping of processors to NUMA nodes.
- // On Linux we utilize `lscpu` to avoid libnuma.
+ // On Linux we read from standardized kernel sysfs, with a fallback to single NUMA node.
+ // On Windows we utilize GetNumaProcessorNodeEx, which has its quirks, see
+ // comment for Windows implementation of get_process_affinity
static NumaConfig from_system([[maybe_unused]] bool respectProcessAffinity = true) {
NumaConfig cfg = empty();
// On Linux things are straightforward, since there's no processor groups and
// any thread can be scheduled on all processors.
- // This command produces output in the following form
- // CPU NODE
- // 0 0
- // 1 0
- // 2 1
- // 3 1
- //
- // On some systems it may use '-' to signify no NUMA node, in which case we assume it's in node 0.
- auto lscpuOpt = get_system_command_output("lscpu -e=cpu,node");
- if (lscpuOpt.has_value())
- {
- std::istringstream ss(*lscpuOpt);
+ // We try to gather this information from the sysfs first
+ // https://www.kernel.org/doc/Documentation/ABI/stable/sysfs-devices-node
- // skip the list header
- ss.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
+ bool useFallback = false;
+ auto fallback = [&]() {
+ useFallback = true;
+ cfg = empty();
+ };
- while (true)
+ // /sys/devices/system/node/online contains information about active NUMA nodes
+ auto nodeIdsStr = read_file_to_string("/sys/devices/system/node/online");
+ if (!nodeIdsStr.has_value() || nodeIdsStr->empty())
+ {
+ fallback();
+ }
+ else
+ {
+ remove_whitespace(*nodeIdsStr);
+ for (size_t n : indices_from_shortened_string(*nodeIdsStr))
{
- CpuIndex c;
- NumaIndex n;
-
- ss >> c;
-
- if (!ss)
+ // /sys/devices/system/node/node.../cpulist
+ std::string path =
+ std::string("/sys/devices/system/node/node") + std::to_string(n) + "/cpulist";
+ auto cpuIdsStr = read_file_to_string(path);
+ // Now, we only bail if the file does not exist. Some nodes may be empty, that's fine.
+ // An empty node still has a file that appears to have some whitespace, so we need
+ // to handle that.
+ if (!cpuIdsStr.has_value())
+ {
+ fallback();
break;
-
- ss >> n;
-
- if (!ss)
+ }
+ else
{
- ss.clear();
- std::string dummy;
- ss >> dummy;
- n = 0;
+ remove_whitespace(*cpuIdsStr);
+ for (size_t c : indices_from_shortened_string(*cpuIdsStr))
+ {
+ if (is_cpu_allowed(c))
+ cfg.add_cpu_to_node(n, c);
+ }
}
-
- if (is_cpu_allowed(c))
- cfg.add_cpu_to_node(n, c);
}
}
- else
+
+ if (useFallback)
{
for (CpuIndex c = 0; c < SYSTEM_THREADS_NB; ++c)
if (is_cpu_allowed(c))
NumaIndex n = 0;
for (auto&& nodeStr : split(s, ":"))
{
- bool addedAnyCpuInThisNode = false;
-
- for (const std::string& cpuStr : split(nodeStr, ","))
+ auto indices = indices_from_shortened_string(nodeStr);
+ if (!indices.empty())
{
- if (cpuStr.empty())
- continue;
-
- auto parts = split(cpuStr, "-");
- if (parts.size() == 1)
+ for (auto idx : indices)
{
- const CpuIndex c = CpuIndex{str_to_size_t(parts[0])};
- if (!cfg.add_cpu_to_node(n, c))
+ if (!cfg.add_cpu_to_node(n, CpuIndex(idx)))
std::exit(EXIT_FAILURE);
}
- else if (parts.size() == 2)
- {
- const CpuIndex cfirst = CpuIndex{str_to_size_t(parts[0])};
- const CpuIndex clast = CpuIndex{str_to_size_t(parts[1])};
- if (!cfg.add_cpu_range_to_node(n, cfirst, clast))
- std::exit(EXIT_FAILURE);
- }
- else
- {
- std::exit(EXIT_FAILURE);
- }
-
- addedAnyCpuInThisNode = true;
- }
-
- if (addedAnyCpuInThisNode)
n += 1;
+ }
}
cfg.customAffinity = true;
return true;
}
-
#if defined(__linux__) && !defined(__ANDROID__)
static std::set<CpuIndex> get_process_affinity() {
}
#endif
+
+ static std::vector<size_t> indices_from_shortened_string(const std::string& s) {
+ std::vector<size_t> indices;
+
+ if (s.empty())
+ return indices;
+
+ for (const std::string& ss : split(s, ","))
+ {
+ if (ss.empty())
+ continue;
+
+ auto parts = split(ss, "-");
+ if (parts.size() == 1)
+ {
+ const CpuIndex c = CpuIndex{str_to_size_t(parts[0])};
+ indices.emplace_back(c);
+ }
+ else if (parts.size() == 2)
+ {
+ const CpuIndex cfirst = CpuIndex{str_to_size_t(parts[0])};
+ const CpuIndex clast = CpuIndex{str_to_size_t(parts[1])};
+ for (size_t c = cfirst; c <= clast; ++c)
+ {
+ indices.emplace_back(c);
+ }
+ }
+ }
+
+ return indices;
+ }
};
class NumaReplicationContext;