1
0
mirror of https://github.com/mpv-player/mpv synced 2024-12-22 06:42:03 +00:00

m_config: introduce basic mechanism to synchronize global option updates

The way option runtime changes are handled is pretty bad in the current
codebase. There's a big option struct (MPOpts), which contains almost
everything, and for which no synchronization mechanism exists. This was
handled by either making some options read-only after initialization,
duplicating the option struct, using sub-options (in the VO), and so on.

Introduce a mechanism that creates a copy of the global options (or
parts of it), and provides a well-defined way to update them in a
thread-safe way.

Most code can remain the same, just that all the component glue code has
to explicitly make use of it first.

There is still lots of room for improvement. For example, the update
mechanism could be better.
This commit is contained in:
wm4 2016-09-02 15:45:22 +02:00
parent f2e25e9e1f
commit 423e53ba0b
5 changed files with 264 additions and 5 deletions

View File

@ -5,9 +5,13 @@
// The only purpose of this is to make mpv library-safe.
// Think hard before adding new members.
struct mpv_global {
struct MPOpts *opts;
struct mp_log *log;
struct m_config_shadow *config;
struct mp_client_api *client_api;
// Using this is deprecated and should be avoided (missing synchronization).
// Use m_config_cache to access mpv_global.config instead.
struct MPOpts *opts;
};
#endif

View File

@ -27,6 +27,7 @@
#include <strings.h>
#include <assert.h>
#include <stdbool.h>
#include <pthread.h>
#include "libmpv/client.h"
@ -34,9 +35,11 @@
#include "m_config.h"
#include "options/m_option.h"
#include "common/global.h"
#include "common/msg.h"
#include "common/msg_control.h"
#include "misc/node.h"
#include "osdep/atomics.h"
static const union m_option_value default_value;
@ -46,6 +49,21 @@ static const union m_option_value default_value;
// Maximal include depth.
#define MAX_RECURSION_DEPTH 8
// For use with m_config_cache.
struct m_config_shadow {
pthread_mutex_t lock;
struct m_config *root;
char *data;
};
// Represents a sub-struct (OPT_SUBSTRUCT()).
struct m_config_group {
const struct m_sub_options *group; // or NULL for top-level options
int parent_group; // index of parent group in m_config.groups
void *opts; // pointer to group user option struct
atomic_llong ts; // incremented on every write access
};
struct m_profile {
struct m_profile *next;
char *name;
@ -183,8 +201,17 @@ static void config_destroy(void *p)
{
struct m_config *config = p;
m_config_restore_backups(config);
for (int n = 0; n < config->num_opts; n++)
m_option_free(config->opts[n].opt, config->opts[n].data);
for (int n = 0; n < config->num_opts; n++) {
struct m_config_option *co = &config->opts[n];
m_option_free(co->opt, co->data);
if (config->shadow && co->shadow_offset >= 0)
m_option_free(co->opt, config->shadow->data + co->shadow_offset);
}
if (config->shadow)
pthread_mutex_destroy(&config->shadow->lock);
}
struct m_config *m_config_new(void *talloc_ctx, struct mp_log *log,
@ -195,12 +222,21 @@ struct m_config *m_config_new(void *talloc_ctx, struct mp_log *log,
talloc_set_destructor(config, config_destroy);
*config = (struct m_config)
{.log = log, .size = size, .defaults = defaults, .options = options};
// size==0 means a dummy object is created
if (size) {
config->optstruct = talloc_zero_size(config, size);
if (defaults)
memcpy(config->optstruct, defaults, size);
}
config->num_groups = 1;
MP_TARRAY_GROW(config, config->groups, 1);
config->groups[0] = (struct m_config_group){
.parent_group = -1,
.opts = config->optstruct,
};
if (options)
add_options(config, NULL, config->optstruct, defaults, options);
return config;
@ -356,6 +392,8 @@ static void m_config_add_option(struct m_config *config,
struct m_config_option co = {
.opt = arg,
.name = arg->name,
.shadow_offset = -1,
.group = parent ? parent->group : 0,
};
if (arg->offset >= 0) {
@ -363,6 +401,14 @@ static void m_config_add_option(struct m_config *config,
co.data = (char *)optstruct + arg->offset;
if (optstruct_def)
co.default_data = (char *)optstruct_def + arg->offset;
int size = arg->type->size;
if (optstruct && size) {
// The required alignment is unknown, so go with the minimum C
// could require. Slightly wasteful, but not that much.
int align = (size - config->shadow_size % size) % size;
co.shadow_offset = config->shadow_size + align;
config->shadow_size = co.shadow_offset + size;
}
}
if (arg->defval)
@ -385,6 +431,10 @@ static void m_config_add_option(struct m_config *config,
if (arg->type->flags & M_OPT_TYPE_HAS_CHILD) {
const struct m_sub_options *subopts = arg->priv;
// Can't be used multiple times.
for (int n = 0; n < config->num_groups; n++)
assert(config->groups[n].group != subopts);
void *new_optstruct = NULL;
if (co.data) {
new_optstruct = m_config_alloc_struct(config, subopts);
@ -395,6 +445,16 @@ static void m_config_add_option(struct m_config *config,
if (!new_optstruct_def)
new_optstruct_def = subopts->defaults;
int parent_group = co.group;
co.group = config->num_groups++;
MP_TARRAY_GROW(config, config->groups, co.group);
struct m_config_group *group = &config->groups[co.group];
*group = (struct m_config_group){
.group = subopts,
.parent_group = parent_group,
.opts = new_optstruct,
};
add_options(config, &co, new_optstruct, new_optstruct_def, subopts->opts);
} else {
// Initialize options
@ -531,8 +591,7 @@ static void handle_on_set(struct m_config *config, struct m_config_option *co,
if (flags & M_SETOPT_FROM_CMDLINE)
co->is_set_from_cmdline = true;
if (config->global && (co->opt->flags & M_OPT_TERM))
mp_msg_update_msglevels(config->global);
m_config_notify_change_co(config, co);
}
// The type data points to is as in: co->opt
@ -949,6 +1008,146 @@ struct mpv_node m_config_get_profiles(struct m_config *config)
return root;
}
void m_config_create_shadow(struct m_config *config)
{
assert(config->global && config->options && config->size);
assert(!config->shadow && !config->global->config);
config->shadow = talloc_zero(config, struct m_config_shadow);
config->shadow->data = talloc_zero_size(config->shadow, config->shadow_size);
config->shadow->root = config;
pthread_mutex_init(&config->shadow->lock, NULL);
config->global->config = config->shadow;
for (int n = 0; n < config->num_opts; n++) {
struct m_config_option *co = &config->opts[n];
if (co->shadow_offset < 0)
continue;
m_option_copy(co->opt, config->shadow->data + co->shadow_offset, co->data);
}
}
// Return whether parent is a parent of group. Also returns true if they're equal.
static bool is_group_included(struct m_config *config, int group, int parent)
{
for (;;) {
if (group == parent)
return true;
if (group < 0)
break;
group = config->groups[group].parent_group;
}
return false;
}
struct m_config_cache *m_config_cache_alloc(void *ta_parent,
struct mpv_global *global,
const struct m_sub_options *group)
{
struct m_config_shadow *shadow = global->config;
struct m_config *root = shadow->root;
struct m_config_cache *cache = talloc_zero(ta_parent, struct m_config_cache);
cache->shadow = shadow;
cache->shadow_config = m_config_new(cache, mp_null_log, root->size,
root->defaults, root->options);
struct m_config *config = cache->shadow_config;
assert(config->num_opts == root->num_opts);
for (int n = 0; n < root->num_opts; n++) {
assert(config->opts[n].opt->type == root->opts[n].opt->type);
assert(config->opts[n].shadow_offset == root->opts[n].shadow_offset);
}
cache->ts = -1;
cache->group = -1;
for (int n = 0; n < config->num_groups; n++) {
if (config->groups[n].group == group) {
cache->opts = config->groups[n].opts;
cache->group = n;
break;
}
}
assert(cache->group >= 0);
assert(cache->opts);
// If we're not on the top-level, restrict set of options to the sub-group
// to reduce update costs. (It would be better not to add them in the first
// place.)
if (cache->group > 0) {
int num_opts = config->num_opts;
config->num_opts = 0;
for (int n = 0; n < num_opts; n++) {
struct m_config_option *co = &config->opts[n];
if (is_group_included(config, co->group, cache->group)) {
config->opts[config->num_opts++] = *co;
} else {
m_option_free(co->opt, co->data);
}
}
for (int n = 0; n < config->num_groups; n++) {
if (!is_group_included(config, n, cache->group))
TA_FREEP(&config->groups[n].opts);
}
}
m_config_cache_update(cache);
return cache;
}
bool m_config_cache_update(struct m_config_cache *cache)
{
struct m_config_shadow *shadow = cache->shadow;
// Using atomics and checking outside of the lock - it's unknown whether
// this makes it faster or slower. Just cargo culting it.
if (atomic_load(&shadow->root->groups[cache->group].ts) <= cache->ts)
return false;
pthread_mutex_lock(&shadow->lock);
cache->ts = atomic_load(&shadow->root->groups[cache->group].ts);
for (int n = 0; n < cache->shadow_config->num_opts; n++) {
struct m_config_option *co = &cache->shadow_config->opts[n];
if (co->shadow_offset >= 0)
m_option_copy(co->opt, co->data, shadow->data + co->shadow_offset);
}
pthread_mutex_unlock(&shadow->lock);
return true;
}
void m_config_notify_change_co(struct m_config *config,
struct m_config_option *co)
{
struct m_config_shadow *shadow = config->shadow;
if (shadow) {
pthread_mutex_lock(&shadow->lock);
if (co->shadow_offset >= 0)
m_option_copy(co->opt, shadow->data + co->shadow_offset, co->data);
pthread_mutex_unlock(&shadow->lock);
int group = co->group;
while (group >= 0) {
atomic_fetch_add(&config->groups[group].ts, 1);
group = config->groups[group].parent_group;
}
}
if (config->global && (co->opt->flags & M_OPT_TERM))
mp_msg_update_msglevels(config->global);
}
struct m_config *mp_get_root_config(struct mpv_global *global)
{
return global->config->root;
}
void *m_config_alloc_struct(void *talloc_ctx,
const struct m_sub_options *subopts)
{

View File

@ -19,6 +19,7 @@
#define MPLAYER_M_CONFIG_H
#include <stddef.h>
#include <stdint.h>
#include <stdbool.h>
#include "misc/bstr.h"
@ -41,6 +42,8 @@ struct m_config_option {
bool is_set_from_cmdline : 1; // Set by user from command line
bool is_set_locally : 1; // Has a backup entry
bool warning_was_printed : 1;
int16_t shadow_offset; // Offset into m_config_shadow.data
int16_t group; // Index into m_config.groups
const char *name; // Full name (ie option-subopt)
const struct m_option *opt; // Option description
void *data; // Raw value of the option
@ -80,6 +83,16 @@ typedef struct m_config {
bool subopt_deprecation_warning;
void *optstruct; // struct mpopts or other
int shadow_size;
// List of m_sub_options instances.
// Index 0 is the top-level and is always present.
struct m_config_group *groups;
int num_groups;
// Thread-safe shadow memory; only set for the main m_config.
struct m_config_shadow *shadow;
} m_config_t;
// Create a new config object.
@ -96,6 +109,10 @@ struct m_config *m_config_new(void *talloc_ctx, struct mp_log *log,
size_t size, const void *defaults,
const struct m_option *options);
// Creates "backup" shadow memory for use with m_config_cache. Sets it on
// mpv_global. Expected to be called at early init on the main m_config.
void m_config_create_shadow(struct m_config *config);
// (Warning: new object references config->log and others.)
struct m_config *m_config_dup(void *talloc_ctx, struct m_config *config);
@ -187,6 +204,10 @@ const char *m_config_get_positional_option(const struct m_config *config, int n)
// Returns: error code (<0), or number of expected params (0, 1)
int m_config_option_requires_param(struct m_config *config, bstr name);
// Notify m_config_cache users that the option has (probably) changed its value.
void m_config_notify_change_co(struct m_config *config,
struct m_config_option *co);
// Return all (visible) option names as NULL terminated string list.
char **m_config_list_options(void *ta_parent, const struct m_config *config);
@ -252,4 +273,37 @@ void *m_config_alloc_struct(void *talloc_ctx,
void *m_sub_options_copy(void *talloc_ctx, const struct m_sub_options *opts,
const void *ptr);
// This can be used to create and synchronize per-thread option structs,
// which then can be read without synchronization. No concurrent access to
// the cache itself is allowed.
struct m_config_cache {
// The struct as indicated by m_config_cache_alloc's group parameter.
void *opts;
// Internal.
struct m_config_shadow *shadow;
struct m_config *shadow_config;
long long ts;
int group;
};
// Create a mirror copy from the global options.
// ta_parent: parent for the returned allocation
// global: option data source
// group: the option group to return. This can be NULL for the global option
// struct (MPOpts), or m_sub_options used in a certain OPT_SUBSTRUCT()
// item.
struct m_config_cache *m_config_cache_alloc(void *ta_parent,
struct mpv_global *global,
const struct m_sub_options *group);
// Update the options in cache->opts to current global values. Return whether
// there was an update notification at all (which may or may not indicate that
// some options have changed).
// Keep in mind that while the cache->opts pointer does not change, the option
// data itself will (e.g. string options might be reallocated).
bool m_config_cache_update(struct m_config_cache *cache);
struct m_config *mp_get_root_config(struct mpv_global *global);
#endif /* MPLAYER_M_CONFIG_H */

View File

@ -364,6 +364,7 @@ struct MPContext *mp_create(void)
mpctx->mconfig->is_toplevel = true;
mpctx->mconfig->global = mpctx->global;
m_config_parse(mpctx->mconfig, "", bstr0(def_config), NULL, 0);
m_config_create_shadow(mpctx->mconfig);
mpctx->global->opts = mpctx->opts;

View File

@ -261,6 +261,7 @@ struct mpv_global *create_sub_global(struct MPContext *mpctx)
struct m_config *new_config = m_config_dup(new, mpctx->mconfig);
*new = (struct mpv_global){
.log = mpctx->global->log,
.config = mpctx->global->config,
.opts = new_config->optstruct,
.client_api = mpctx->clients,
};