Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use a struct to store config #647

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open

Conversation

lvxnull
Copy link
Contributor

@lvxnull lvxnull commented Oct 17, 2023

Unlike the previous hashmap based approach, this ensures there are no hash collisions(by not utilizing any sort of hashing function) on read and write. Dynamic access to fields will still utilize hashmaps to look up the offset and type of the field. By using a struct, your IDE can now suggest config fields, and access to a non-existent field will result in a compile time error.

@imwints
Copy link
Contributor

imwints commented Oct 18, 2023

I really like this and thought about this as well, but the implementation with a macro is really clever.

Just FYI from another issue from @aristocratos:

I have a replacement for threading ready, from another project based on btop where I rewrote large parts of the threading model, configuration model and more. (The project is not public yet).

Since the next release of 1.3.0 will feature GPU support every other big change will probably be on hold. We might want to create a dev branch for things like this (??) and worry about merge conflicts later 😆

@imwints
Copy link
Contributor

imwints commented Oct 18, 2023

Also I have some ideas for this

src/btop_config.cpp Outdated Show resolved Hide resolved
src/btop_config.cpp Outdated Show resolved Hide resolved
src/btop_config.cpp Outdated Show resolved Hide resolved
src/btop_config.hpp Outdated Show resolved Hide resolved
@lvxnull lvxnull force-pushed the config branch 7 times, most recently from 2352d98 to 86f1953 Compare October 19, 2023 11:25
src/btop_config.hpp Outdated Show resolved Hide resolved
src/btop_config.hpp Show resolved Hide resolved
@aristocratos
Copy link
Owner

aristocratos commented Oct 19, 2023

@lvxnull that is some nifty macro magic :)
However as @nobounce mentioned I have some planned backporting of the threading model and configuration model from another project into btop.

The new config model uses a vector of ConfigEntry structs, which brings together all the current dispersed maps and lists of values and descriptions in to one entity.

Will create a PR when I've adapted it back to btop, so it can be compared and either discarded or merged with ideas from this.
I'm sure your code will probably more efficient, probably the biggest issue with heavy use of macros would be the readability (speaking for myself).

One issue regarding the configuration is handling the config in the secondary thread, since it needs to be the same between "collection" and "drawing".
One idea could be to make the "main" config lock free and instead have a function that creates a vector of "small_config" structs which creates copies of the current values and a "changed" bool. (Since the collection could have the need to change values that are no longer valid).

This "small_config" then gets passed along to the secondary thread, the draw function calls gets moved in to the main thread and only after drawing is done, will any changed values from the "small_config" get synced to the main config.

Some examples of how the code from new config model looks:

	enum class Types {
		Bool,
		Int,
		String
	};

	struct ConfigEntry {
		string name{};
		string category{};
		Types type{};
		string cfg_description{};
		vector<string> menu_description{};
		std::variant<int64_t, bool, string> value{};
		vector<string> valid_values{};
		std::array<int64_t, 2> valid_range = { 0, 0 };
		std::variant<int64_t, bool, string> real_value{};
		bool has_override = false;
	};

	extern vector<ConfigEntry> configuration;

	//* Return value for config key <name>
	template <typename T>
	T get(const string& name) requires (std::is_same_v<T, int64_t> or std::is_same_v<T, bool> or std::is_same_v<T, string>) {
		auto e = std::ranges::find_if(configuration, [&name](const ConfigEntry& entry) { return entry.name == name; });
		if (e == configuration.end()) {
			throw std::runtime_error("Config::get() -> key: [" + name + "] not found!");
		}
		try {
			return std::get<T>(e->value);
		}
		catch(const std::bad_variant_access&) {
			throw std::runtime_error("Config::get() -> requesting wrong type from key: [" + name + "]!");
		}
	}

	//* Return value as string for key <name>
	inline string getAsString(const string& name) {
		auto e = std::ranges::find_if(configuration, [&name](const ConfigEntry& entry) { return entry.name == name; });
		if (e == configuration.end()) {
			throw std::runtime_error("Config::getAsString() -> key: [" + name + "] not found!");
		}
		if (e->type == Types::String)
			return std::get<string>(e->value);
		else if (e->type == Types::Bool)
			return std::get<bool>(e->value) ? "True" : "False";
		else if (e->type == Types::Int)
			return std::to_string(std::get<int64_t>(e->value));

		return "";
	}

	inline ConfigEntry& getRawEntry(const string& name) {
		auto e = std::ranges::find_if(configuration, [&name](const ConfigEntry& entry) { return entry.name == name; });
		if (e == configuration.end()) {
			throw std::runtime_error("Config::getRawEntry() -> key: [" + name + "] not found!");
		}
		return *e;
	}

	//* Set config key <name> to <value>
	template <typename T>
	inline void set(const string& name, const T& value) requires (std::is_convertible_v<T, int64_t> or std::is_same_v<T, bool> or std::is_convertible_v<T, string>) {
		auto e = std::ranges::find_if(configuration, [&name](const ConfigEntry& entry) { return entry.name == name; });
		if (e == configuration.end()) {
			throw std::runtime_error("Config::set() -> key: [" + name + "] not found!");
		}
		write_new = true;
		e->has_override = false;
		e->value = value;
	}

	//* Set override for config key <name> to <value>
	template <typename T>
	inline void setOverride(const string& name, const T& value) requires (std::is_convertible_v<T, int64_t> or std::is_same_v<T, bool> or std::is_convertible_v<T, string>) {
		auto e = std::ranges::find_if(configuration, [&name](const ConfigEntry& entry) { return entry.name == name; });
		if (e == configuration.end()) {
			throw std::runtime_error("Config::set() -> key: [" + name + "] not found!");
		}
		e->real_value = e->value;
		e->value = value;
		e->has_override = true;
	}

	//* Flip bool value for config key <name>
	inline void flip(const string& name) {
		auto e = std::ranges::find_if(configuration, [&name](const ConfigEntry& entry) { return entry.name == name; });
		if (e == configuration.end()) {
			throw std::runtime_error("Config::flip() -> key: [" + name + "] not found!");
		}
		if (e->type != Types::Bool) {
			throw std::runtime_error("Config::flip() -> key: [" + name + "] is not a bool!");
		}
		write_new = true;
		e->value = not std::get<bool>(e->value);
	}

Example of ConfigEntry:

vector<ConfigEntry> configuration =	{
		//* General
		{
			.name = "server_address",
			.category = "general",
			.type = Types::String,
			.cfg_description = "#* xxx http server address including port, defaults to: \"http://localhost:2020\". Only accepts \"http://[hostname][:port]\".\n"
							   "#* Will use environment variable \"xxx_SERVER_ADDRESS\" if set.",
			.menu_description = {"Server address",
				"xxx http server address including",
				"port.",
				"",
				"Defaults to: \"http://localhost:2020\"",
				"",
				"Only accepts \"http://[hostname][:port]\".",
				"",
				"Will use environment variable",
				"\"xxx_SERVER_ADDRESS\" if set."},
			.value = "http://localhost:2020"
		},
		{
			.name = "update_ms",
			.category = "general",
			.type = Types::Int,
			.cfg_description = "#* Update time in milliseconds, recommended 2000 ms or above for better sample times for graphs.",
			.menu_description = {"Update time",
				"Update time in milliseconds.",
				"",
				"Recommended 2000 ms or above for better",
				"sample times for graphs.",
				"",
				"Min value: 100 ms",
				"Max value: 86400000 ms = 24 hours."},
			.value = 2000,
			.valid_range = { 100, 86400000 }
		},
		{
			.name = "theme_background",
			.category = "general",
			.type = Types::Bool,
			.cfg_description = "#* If the theme set background should be shown, set to False if you want terminal background transparency.",
			.menu_description = { "Theme background",
				"Set theme background.",
				"",
				"Set to False if you want terminal",
				"background transparency." },
			.value = true
		},
		{
			.name = "log_level",
			.category = "general",
			.type = Types::String,
			.cfg_description = "#* Set loglevel for \"~/.config/xxx/xxx.log\" levels are: \"ERROR\" \"WARNING\" \"INFO\" \"DEBUG\".\n"
								"#* The level set includes all lower levels, i.e. \"DEBUG\" will show all logging info.",
			.menu_description = {"Log level",
				"Set loglevel for \"xxx.log\".",
				"",
				"Levels are: \"ERROR\" \"WARNING\" \"INFO\" \"DEBUG\".",
				"",
				"The level set includes all lower levels,",
				"i.e. \"DEBUG\" will show all logging info.",
				"",
				"Log file is usually located in:",
				"\"~/.config/xxx/xxx.log\""},
			.value = "INFO",
			.valid_values = Logger::log_levels
		}
	}

And then in use something like this:

const auto single_graph = Config::get<bool>("overview_single_graph");
const auto graph_up_field = Config::get<string>("overview_graph_upper");
Config::set("update_ms", new_val);
Config::setOverride("tty_mode", true);

auto old_val = Config::get<int64_t>("update_ms");

if (old_val + 100l > Config::getRawEntry("update_ms").valid_range.back())
	return;
int64_t new_val = old_val + (old_val >= 2000 and last_press >= time_ms() - 200 and rng::all_of(Input::history, [](const auto& str){ return str == "+"; }) ? 1000 : 100);
Config::set("update_ms", new_val);

@lvxnull
Copy link
Contributor Author

lvxnull commented Oct 20, 2023

I don't like the idea of storing range and possible values alongside data. These are only useful in certain places and needlessly bloat the structure. Instead, I would use a generic function that takes some value unique to a field(e.g name, with my implementation it could be the offset) and the value itself and then did validation.

template<typename T>
inline bool validate(const size_t offset, const T& value) {
    return true;
}

template<>
inline bool validate<int>(const size_t offset, const int& value) {
    switch(offset) {
        case offsetof(ConfigSet, update_ms):
            return value >= 100 && value <= 86400000;
    }

    return true;
}

template<>
inline bool validate<string>(const size_t offset, const string& value) {
    switch(offset) {
        //...
    }

    return true;
}

In it's current form, your structure requires 256 bytes of memory. And that's for each field, so 256 * 77 = 19712 bytes of memory, just for these structures for every option. It might be even more if the vector pre-allocates more of these structures than necessary. And for each read or write, it must do 77 string comparisons in the worst case.

I also think that the menu should be defined separately from the config entries. The config and the menu present their contents in different order, if you don't want to reshuffle either then you would have to hard code the order of one of them. I'd rather change the current implementation of menu to work based on offsets rather than string names.

@aristocratos
Copy link
Owner

aristocratos commented Oct 20, 2023

I don't like the idea of storing range and possible values alongside data. These are only useful in certain places and needlessly bloat the structure.
In it's current form, your structure requires 256 bytes of memory. And that's for each field, so 256 * 77 = 19712 bytes of memory, just for these structures for every option.

I would argue it's the opposite of bloat since it's all values related to specific config entry.
And it makes it really easy to add or change the properties of config entries since you don't have to add constraints and checks in multiple different places.
Instead the functions that checks for value validity could be just generic functions that takes a config struct reference and returns a bool.

If there is a worry about keeping the struct small, couldn't this just be fixed by having the bigger and less frequently accessed values be either reference_wrappers or actual pointers to data stored somewhere else?
For example the category name could just as well be an enum class value.

It might be even more if the vector pre-allocates more of these structures than necessary.

It could be switched to a std::array since the size won't be changing.
Regardless, the code I posted above was just an example, not a suggestion that you change the PR to using a less efficient container.

And for each read or write, it must do 77 string comparisons in the worst case.

As I mentioned: "I'm sure your code will probably more efficient [...]" :)

I also think that the menu should be defined separately from the config entries. The config and the menu present their contents in different order, if you don't want to reshuffle either then you would have to hard code the order of one of them.

Where the menu is defined isn't really important, that just how it ended up in the other project (which also has alot less options than btop). The ordering of the config file doesn't really matter, but it would probably not be a negative if it was ordered and categorized like the menu is.

The main point of the example code above is ease of use. Just add a new config entry and you're done. And less likely to get unintended bugs if you see the struct have members to define constraints which you might not have thought about otherwise.

@lvxnull
Copy link
Contributor Author

lvxnull commented Oct 22, 2023

I removed string based access to options from the codebase except the config loader. Now that I think of it, maybe themes could be rewritten like this as well?

@lvxnull
Copy link
Contributor Author

lvxnull commented Oct 22, 2023

@aristocratos should I open another pull request for the theme rewrite or use this one?

@aristocratos
Copy link
Owner

@aristocratos should I open another pull request for the theme rewrite or use this one?

Depends, if any code from this PR will be reused it's probably better to have both in the same PR, otherwise create a new one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants