From 5b2053bbb8c00b5b5eae4a9a30a627aef36f2c66 Mon Sep 17 00:00:00 2001 From: Matej Kafka Date: Thu, 22 Jul 2021 17:25:35 +0200 Subject: [PATCH] config: Add config validation, including per-process frequency feasibility --- src/config.cpp | 148 ++++++++++++++++++++++++++++++++++++++++++++++++- src/config.hpp | 3 + 2 files changed, 150 insertions(+), 1 deletion(-) diff --git a/src/config.cpp b/src/config.cpp index 5b55bd02..acd52208 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -290,6 +290,145 @@ Node Config::normalize_window(const Node &win, // in: window to normalize return norm_win; } +/** + * Check if per-process frequencies for the i.MX8 are feasible; that is, if each process + * is guaranteed to have the correct frequency set and there won't ever be 2 processes + * with different per-process frequency running on the same CPU cluster at the same time. + * + * FIXME: currently, this whole section is i.MX8-specific; if it was moved somewhere where + * the available cpufreq policies are already known, it could be generic for any CPU; + * however, it would be system-dependent and could not be checked ahead-of-time on another machine + */ +void Config::validate_per_process_freq_feasibility() +{ + // i.MX8 frequency bitmap (4 possible frequencies) + using Imx8FreqMap = bitset<4>; + const std::array imx8_cpus{ 0b1111, 0b110000 }; + + // for each partition, find out which frequencies the processes inside are requesting + map> requested_freqs{}; + for (const auto &part : config["partitions"]) { + Imx8FreqMap part_freqs_a53(0); + Imx8FreqMap part_freqs_a72(0); + for (const auto &proc : part["processes"]) { + if (proc["_a53_freq"]) part_freqs_a53.set(proc["_a53_freq"].as()); + if (proc["_a72_freq"]) part_freqs_a72.set(proc["_a72_freq"].as()); + } + requested_freqs[part["name"].as()] = { part_freqs_a53, part_freqs_a72 }; + } + + // find all requested frequencies for each window (separately for SC and BE + // partitions, as they run separately), and check if there are any collisions + // it is OK if there are multiple frequencies, all from the same partition; + // it is also OK if multiple partitions require the same single frequency; + // otherwise, a runtime collision may potentially occur and we throw an error + int window_i = -1; + bool collision_found = false; + for (const auto &win : config["windows"]) { + window_i++; + for (const string &part_key : { "sc_partition", "be_partition" }) { + std::array win_freqs{ 0, 0 }; + for (const auto &slice : win["slices"]) { + if (!slice[part_key]) continue; // no SC/BE partition + + size_t i = 0; + // iterate in parallel over CPU clusters and corresponding requested frequencies + // we already checked that all partition references are valid, this lookup is safe + for (Imx8FreqMap req_freq : requested_freqs[slice[part_key].as()]) { + Imx8FreqMap &win_freq = win_freqs[i]; + cpu_set cluster_cpu = imx8_cpus[i]; + i++; + + if (req_freq.none()) continue; // no freq constraints + // check for overlap with the cluster + if (!(cluster_cpu & slice["cpu"].as())) continue; + + if (win_freq.none() || (win_freq | req_freq).count() == 1) { + // no previous constraints or a single compatible frequency, feasible so far + win_freq |= req_freq; + } else { + // there are at least 2 different freqs, and as both sets are non-empty, + // that's a potential runtime collision + // TODO: find the offending process tuple and report it in the error message + collision_found = true; + // don't throw the error, as that would only show the first one; + // instead, log it and then throw error after all checks are done + logger->error("Unfeasible combination of fixed per-process frequencies was " + "specified in the configuration." + "\n\tThe collision occurs between processes in the window " + "#{}, between {} partitions on CPU cluster '{}'." + "\n\tA process from the partition '{}' collides with a " + "process from one of the previous partitions " + "scheduled inside the window.", + window_i, + part_key == "sc_partition" ? "SC" : "BE", + cluster_cpu.as_list(), + slice[part_key].as()); + } + } + } + } + } + + if (collision_found) { + throw runtime_error("Unfeasible per-process frequencies in configuration; " + "see above for more detailed information"); + } +} + +/** + * Validates that all referenced partitions exist, slices don't have overlapping cpusets, + * and that per-process frequencies are feasible, if specified. + * + * It would be simpler, and significantly to do the validation using the created scheduler objects + * rather than the YAML config, but I want the validation to take place even when + * only dumping normalized config, and creating an intermediate struct-based config + * format is probably currently too much work for too little benefit. + */ +void Config::validate_config() +{ + // check if all references to partition names are valid + int window_i = -1; + for (const auto &win : config["windows"]) { + window_i++; + int slice_i = -1; + for (const auto &slice : win["slices"]) { + slice_i++; + for (const auto &part_key : { "sc_partition", "be_partition" }) { + if (!slice[part_key]) continue; + // FIXME: slow, create a std::set first for the partition names + auto searched_name = slice[part_key].as(); + for (const auto &part : config["partitions"]) { + if (searched_name == part["name"].as()) { + goto found; + } + } + throw runtime_error("Reference to unknown partition '" + searched_name + + "' in window #" + to_string(window_i) + ", slice #" + + to_string(slice_i)); + found:; + } + } + } + + // check if slices in each window have any CPU set overlaps + window_i = -1; + for (const auto &win : config["windows"]) { + window_i++; + cpu_set acc{}; + for (const auto &slice : win["slices"]) { + auto slice_cpu = slice["cpu"].as(); + if (acc & slice_cpu) { + throw runtime_error("Slices in window #" + to_string(window_i) + + " have overlapping CPU sets"); + } + acc |= slice_cpu; + } + } + + validate_per_process_freq_feasibility(); +} + void Config::normalize() { if (!config.IsMap()) { @@ -298,6 +437,11 @@ void Config::normalize() throw runtime_error("Config must be YAML mapping node"); } + // TODO: allow setting power policy in the config file + + // TODO: change `partitions` from list of definitions to a map from partition + // name to process list + // clang-format off bool set_cwd = config["set_cwd"] ? config_file_path @@ -342,6 +486,7 @@ void Config::normalize() norm_config["windows"] = norm_windows; config = norm_config; + validate_config(); } static Partition *find_partition(const string &name, Partitions &partitions) @@ -349,7 +494,8 @@ static Partition *find_partition(const string &name, Partitions &partitions) for (auto &p : partitions) { if (p.get_name() == name) return &p; } - throw runtime_error("Could not find partition: " + name); + // already checked during config validation above + throw runtime_error("not reachable"); } // TODO: Move this out of Config to new DemosSched class diff --git a/src/config.hpp b/src/config.hpp index 23874e4d..b40b1793 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -35,6 +35,9 @@ class Config std::optional config_file_path{}; int anonymous_partition_counter = 0; + void validate_config(); + void validate_per_process_freq_feasibility(); + YAML::Node normalize_window(const YAML::Node &win, YAML::Node &partitions); YAML::Node normalize_partition(const YAML::Node &part, float total_budget); YAML::Node normalize_slice(const YAML::Node &slice, float win_length, YAML::Node &partitions);