On 1 Jul 2024, at 13:02, Grigorii Nazarov wrote:
Hi Grigorii, This is not an in-depth review, but find some comments below. Cheers, Eelco > Currently serialization is performed by first converting > the internal data representation into JSON objects, followed by > serializing these objects by jsonrpc. This process results in lots of > allocations for these intermediate objects. Consequently, this > not only increases peak memory consumption, but also > demands significantly more CPU work. > > By forming row-update JSONs directly in `ds`, which is then used > to create 'serialized object' JSONs, the overall speed increased > by a factor of 2.3. > > A local benchmark was run on a proprietary Southbound backup. > Both versions, before and after applying the patch, were measured. > For each version, there were two runs with 10 parallel clients, > and two runs with 30 parallel clients. CPU time was recorded > after startup (before clients started running) and after > all clients received all updates. Clients were essentially running > `ovsdb-client monitor-cond unix:pipe OVN_Southbound > '[[\"actions\",\"!=\",\"sdfdfsd\"]]' Logical_Flow`. > Similar results were obtained with other requests > that required a significant amount of data transfer. > The backup size is about 600 MB. Results are measured in seconds. > > Before After > Baseline x10: 9.53 108.54 > Baseline x10: 9.62 108.67 > Baseline x30: 9.69 307.04 > Baseline x30: 9.65 303.32 > Patch x10: 9.67 52.57 > Patch x10: 9.57 53.12 > Patch x30: 9.53 136.33 > Patch x30: 9.63 135.88 Are before and after times reversed? I assume a shorter time is better? Also, how does this relate to the 2.3x speed improvement? > Signed-off-by: Grigorii Nazarov <whitecrow...@gmail.com> > --- > v2: updated title > v3: fixed bracing > v4: changed patch number from 4/4 to 3/3 > > include/openvswitch/json.h | 1 + > lib/json.c | 8 ++ > lib/ovsdb-data.c | 105 +++++++++++++++++++ > lib/ovsdb-data.h | 3 + > ovsdb/monitor.c | 210 ++++++++++++++++++++++++------------- > 5 files changed, 254 insertions(+), 73 deletions(-) > > diff --git a/include/openvswitch/json.h b/include/openvswitch/json.h > index 555440760..80b9479c7 100644 > --- a/include/openvswitch/json.h > +++ b/include/openvswitch/json.h > @@ -81,6 +81,7 @@ struct json *json_boolean_create(bool); > struct json *json_string_create(const char *); > struct json *json_string_create_nocopy(char *); > struct json *json_serialized_object_create(const struct json *); > +struct json *json_serialized_object_create_from_string(const char *); > struct json *json_integer_create(long long int); > struct json *json_real_create(double); > > diff --git a/lib/json.c b/lib/json.c > index d40e93857..66b1f571f 100644 > --- a/lib/json.c > +++ b/lib/json.c > @@ -199,6 +199,14 @@ json_serialized_object_create(const struct json *src) > return json; > } > > +struct json * > +json_serialized_object_create_from_string(const char *s) > +{ > + struct json *json = json_create(JSON_SERIALIZED_OBJECT); > + json->string = xstrdup(s); > + return json; > +} > + > struct json * > json_serialized_object_create_with_yield(const struct json *src) > { > diff --git a/lib/ovsdb-data.c b/lib/ovsdb-data.c > index defb048d7..f32b7975a 100644 > --- a/lib/ovsdb-data.c > +++ b/lib/ovsdb-data.c > @@ -492,6 +492,45 @@ ovsdb_atom_to_json__(const union ovsdb_atom *atom, enum > ovsdb_atomic_type type, > } > } > > +/* Serializes 'atom', of the specified 'type', into 'ds' in JSON format. > + * > + * Refer to RFC 7047 for the format of the JSON that this function produces. > */ > +static void > +ovsdb_atom_to_json_ds(const union ovsdb_atom *atom, > + enum ovsdb_atomic_type type, struct ds *ds) Isn’t this a duplicate of ovsdb_atom_to_string()? > +{ > + switch (type) { > + case OVSDB_TYPE_VOID: > + OVS_NOT_REACHED(); > + > + case OVSDB_TYPE_INTEGER: > + ds_put_format(ds, "%lld", (long long) atom->integer); Rather than typecast this one, just use: ds_put_format(ds, "%"PRId64, atom->integer); > + return; > + > + case OVSDB_TYPE_REAL: > + ds_put_format(ds, "%.*g", DBL_DIG, atom->real); > + return; > + > + case OVSDB_TYPE_BOOLEAN: > + ds_put_cstr(ds, atom->boolean ? "true" : "false"); > + return; > + > + case OVSDB_TYPE_STRING: > + json_to_ds(atom->s, JSSF_SORT, ds); > + return; > + > + case OVSDB_TYPE_UUID: > + ds_put_cstr(ds, "[\"uuid\",\""); > + ds_put_format(ds, UUID_FMT, UUID_ARGS(&atom->uuid)); > + ds_put_cstr(ds, "\"]"); > + return; > + > + case OVSDB_N_TYPES: > + default: > + OVS_NOT_REACHED(); > + } > +} > + > struct json * > ovsdb_atom_to_json(const union ovsdb_atom *atom, enum ovsdb_atomic_type type) > { > @@ -1445,6 +1484,23 @@ ovsdb_base_to_json(const union ovsdb_atom *atom, > } > } > > +static void > +ovsdb_base_to_json_ds(const union ovsdb_atom *atom, > + const struct ovsdb_base_type *base, > + bool use_row_names, > + struct ds *ds) > +{ Not sure what all this logic is trying to do, as it’s not straightforward. If I reverse the logic it could be the following: if (base->type == OVSDB_TYPE_UUID) { } else { } But I’m not sure this is what you want. > + if (!use_row_names > + || base->type != OVSDB_TYPE_UUID > + || !base->uuid.refTableName) { > + ovsdb_atom_to_json_ds(atom, base->type, ds); > + } else { > + ds_put_cstr(ds, "[\"named-uuid\",\""); > + ds_put_format(ds, UUID_ROW_FMT, UUID_ARGS(&atom->uuid)); > + ds_put_cstr(ds, "\"]"); > + } > +} > + > static struct json * > ovsdb_datum_to_json__(const struct ovsdb_datum *datum, > const struct ovsdb_type *type, > @@ -1482,6 +1538,47 @@ ovsdb_datum_to_json__(const struct ovsdb_datum *datum, > } > } > > +static void > +ovsdb_datum_to_json_ds__(const struct ovsdb_datum *datum, > + const struct ovsdb_type *type, > + bool use_row_names, > + struct ds *ds) > +{ > + if (ovsdb_type_is_map(type)) { > + size_t i; > + > + ds_put_cstr(ds, "[\"map\",["); > + for (i = 0; i < datum->n; i++) { > + if (i != 0) { > + ds_put_char(ds, ','); > + } > + ds_put_char(ds, '['); > + ovsdb_base_to_json_ds(&datum->keys[i], &type->key, > + use_row_names, ds); > + ds_put_char(ds, ','); > + ovsdb_base_to_json_ds(&datum->values[i], &type->value, > + use_row_names, ds); > + ds_put_char(ds, ']'); > + } > + ds_put_cstr(ds, "]]"); > + } else if (datum->n == 1) { > + ovsdb_base_to_json_ds(&datum->keys[0], &type->key, > + use_row_names, ds); > + } else { > + size_t i; > + > + ds_put_cstr(ds, "[\"set\",["); > + for (i = 0; i < datum->n; i++) { > + if (i != 0) { > + ds_put_char(ds, ','); > + } > + ovsdb_base_to_json_ds(&datum->keys[i], &type->key, > + use_row_names, ds); > + } > + ds_put_cstr(ds, "]]"); > + } > +} > + > /* Converts 'datum', of the specified 'type', to JSON format, and returns the > * JSON. The caller is responsible for freeing the returned JSON. > * > @@ -1509,6 +1606,14 @@ ovsdb_datum_to_json_with_row_names(const struct > ovsdb_datum *datum, > return ovsdb_datum_to_json__(datum, type, true, true); > } > > +void > +ovsdb_datum_to_json_ds(const struct ovsdb_datum *datum, > + const struct ovsdb_type *type, > + struct ds *ds) > +{ > + ovsdb_datum_to_json_ds__(datum, type, false, ds); > +} > + > static const char * > skip_spaces(const char *p) > { > diff --git a/lib/ovsdb-data.h b/lib/ovsdb-data.h > index c0408ee49..5bc47f0a1 100644 > --- a/lib/ovsdb-data.h > +++ b/lib/ovsdb-data.h > @@ -197,6 +197,9 @@ struct json *ovsdb_datum_to_json(const struct ovsdb_datum > *, > const struct ovsdb_type *); > struct json *ovsdb_datum_to_json_deep(const struct ovsdb_datum *, > const struct ovsdb_type *); > +void ovsdb_datum_to_json_ds(const struct ovsdb_datum *, > + const struct ovsdb_type *, > + struct ds *); > > char *ovsdb_datum_from_string(struct ovsdb_datum *, > const struct ovsdb_type *, const char *, > diff --git a/ovsdb/monitor.c b/ovsdb/monitor.c > index c3bfae3d2..8355b8da7 100644 > --- a/ovsdb/monitor.c > +++ b/ovsdb/monitor.c > @@ -25,6 +25,7 @@ > #include "openvswitch/json.h" > #include "json.h" > #include "jsonrpc.h" > +#include "ovsdb-data.h" > #include "ovsdb-error.h" > #include "ovsdb-parser.h" > #include "ovsdb.h" > @@ -197,14 +198,15 @@ enum ovsdb_monitor_row_type { > OVSDB_MONITOR_ROW > }; > > -typedef struct json * > +typedef bool > (*compose_row_update_cb_func) > (const struct ovsdb_monitor_table *mt, > const struct ovsdb_monitor_session_condition * condition, > enum ovsdb_monitor_row_type row_type, > const void *, > bool initial, unsigned long int *changed, > - size_t n_columns); > + size_t n_columns, > + struct ds *); > > static void ovsdb_monitor_destroy(struct ovsdb_monitor *); > static struct ovsdb_monitor_change_set * ovsdb_monitor_add_change_set( > @@ -963,28 +965,27 @@ ovsdb_monitor_row_skip_update(const struct > ovsdb_monitor_table *mt, > return false; > } > > -/* Returns JSON for a <row-update> (as described in RFC 7047) for 'row' > within > - * 'mt', or NULL if no row update should be sent. > +/* Serializes a <row-update> (as described in RFC 7047) for 'row' within > + * 'mt' into 'ds'. Returns true unless no row update should be sent. > * > - * The caller should specify 'initial' as true if the returned JSON is going > to > - * be used as part of the initial reply to a "monitor" request, false if it > is > - * going to be used as part of an "update" notification. > + * The caller should specify 'initial' as true if the serialized JSON is > + * going to be used as part of the initial reply to a "monitor" request, > + * false if it is going to be used as part of an "update" notification. > * > * 'changed' must be a scratch buffer for internal use that is at least > * bitmap_n_bytes(n_columns) bytes long. */ > -static struct json * > +static bool > ovsdb_monitor_compose_row_update( > const struct ovsdb_monitor_table *mt, > const struct ovsdb_monitor_session_condition *condition OVS_UNUSED, > enum ovsdb_monitor_row_type row_type OVS_UNUSED, > const void *_row, > bool initial, unsigned long int *changed, > - size_t n_columns OVS_UNUSED) > + size_t n_columns OVS_UNUSED, > + struct ds *ds) > { > const struct ovsdb_monitor_row *row = _row; > enum ovsdb_monitor_selection type; > - struct json *old_json, *new_json; > - struct json *row_json; > size_t i; > > ovs_assert(row_type == OVSDB_MONITOR_ROW); > @@ -992,65 +993,95 @@ ovsdb_monitor_compose_row_update( > if (ovsdb_monitor_row_skip_update(mt, row_type, row->old, > row->new, type, changed, > mt->n_columns)) { > - return NULL; > + return false; > } > > - row_json = json_object_create(); > - old_json = new_json = NULL; > + ds_put_char(ds, '{'); > + > + bool has_old = false; > if (type & (OJMS_DELETE | OJMS_MODIFY)) { > - old_json = json_object_create(); > - json_object_put(row_json, "old", old_json); > - } > - if (type & (OJMS_INITIAL | OJMS_INSERT | OJMS_MODIFY)) { > - new_json = json_object_create(); > - json_object_put(row_json, "new", new_json); > - } > - for (i = 0; i < mt->n_monitored_columns; i++) { > - const struct ovsdb_monitor_column *c = &mt->columns[i]; > + ds_put_cstr(ds, "\"old\":{"); > There is now a lot of duplicate code with only the old/new string being different. Can we create a function to remove the duplication? > - if (!c->monitored || !(type & c->select)) { > - /* We don't care about this type of change for this > - * particular column (but we will care about it for some > - * other column). */ > - continue; > + has_old = true; > + bool empty = true; > + for (i = 0; i < mt->n_monitored_columns; i++) { > + const struct ovsdb_monitor_column *c = &mt->columns[i]; > + > + if (!c->monitored || !(type & c->select)) { > + /* We don't care about this type of change for this > + * particular column (but we will care about it for some > + * other column). */ > + continue; > + } > + > + if (type == OJMS_DELETE || bitmap_is_set(changed, i)) { > + if (!empty) { > + ds_put_char(ds, ','); > + } > + empty = false; > + > + json_string_escape(c->column->name, ds); > + ds_put_char(ds, ':'); > + ovsdb_datum_to_json_ds(&row->old[i], &c->column->type, ds); > + } > } > + ds_put_char(ds, '}'); > + } > > - if ((type == OJMS_MODIFY && bitmap_is_set(changed, i)) > - || type == OJMS_DELETE) { > - json_object_put(old_json, c->column->name, > - ovsdb_datum_to_json(&row->old[i], > - &c->column->type)); > + if (type & (OJMS_INITIAL | OJMS_INSERT | OJMS_MODIFY)) { > + if (has_old) { > + ds_put_char(ds, ','); > } > - if (type & (OJMS_INITIAL | OJMS_INSERT | OJMS_MODIFY)) { > - json_object_put(new_json, c->column->name, > - ovsdb_datum_to_json(&row->new[i], > - &c->column->type)); > + ds_put_cstr(ds, "\"new\":{"); > + > + bool empty = true; > + for (i = 0; i < mt->n_monitored_columns; i++) { > + const struct ovsdb_monitor_column *c = &mt->columns[i]; > + > + if (!c->monitored || !(type & c->select)) { > + /* We don't care about this type of change for this > + * particular column (but we will care about it for some > + * other column). */ > + continue; > + } > + > + if (!empty) { > + ds_put_char(ds, ','); > + } > + empty = false; > + > + json_string_escape(c->column->name, ds); > + ds_put_char(ds, ':'); > + ovsdb_datum_to_json_ds(&row->new[i], &c->column->type, ds); > } > + ds_put_char(ds, '}'); > } > + ds_put_char(ds, '}'); > > - return row_json; > + return true; > } > > -/* Returns JSON for a <row-update2> (as described in ovsdb-server(1) mapage) > - * for 'row' within * 'mt', or NULL if no row update should be sent. > +/* Serializes a <row-update2> (as described in ovsdb-server(1) mapage) > + * for 'row' within * 'mt' into 'ds'. > + * Returns true unless no row update should be sent. > * > - * The caller should specify 'initial' as true if the returned JSON is > + * The caller should specify 'initial' as true if the serialized JSON is > * going to be used as part of the initial reply to a "monitor_cond" request, > * false if it is going to be used as part of an "update2" notification. > * > * 'changed' must be a scratch buffer for internal use that is at least > * bitmap_n_bytes(n_columns) bytes long. */ > -static struct json * > +static bool > ovsdb_monitor_compose_row_update2( > const struct ovsdb_monitor_table *mt, > const struct ovsdb_monitor_session_condition *condition, > enum ovsdb_monitor_row_type row_type, > const void *_row, > bool initial, unsigned long int *changed, > - size_t n_columns) > + size_t n_columns, > + struct ds *ds) > { > enum ovsdb_monitor_selection type; > - struct json *row_update2, *diff_json; > const struct ovsdb_datum *old, *new; > size_t i; > > @@ -1065,15 +1096,18 @@ ovsdb_monitor_compose_row_update2( > row_type, old, new); > if (ovsdb_monitor_row_skip_update(mt, row_type, old, new, type, changed, > n_columns)) { > - return NULL; > + return false; > } > > - row_update2 = json_object_create(); > + ds_put_char(ds, '{'); > if (type == OJMS_DELETE) { > - json_object_put(row_update2, "delete", json_null_create()); > + ds_put_cstr(ds, "\"delete\":null"); > } else { > - diff_json = json_object_create(); > const char *op; > + op = type == OJMS_INITIAL ? "initial" > + : type == OJMS_MODIFY ? "modify" : > "insert"; > + ds_put_format(ds, "\"%s\":{", op); > + bool empty = true; > > for (i = 0; i < mt->n_monitored_columns; i++) { > const struct ovsdb_monitor_column *c = &mt->columns[i]; > @@ -1085,33 +1119,43 @@ ovsdb_monitor_compose_row_update2( > continue; > } > > - if (type == OJMS_MODIFY) { > - struct ovsdb_datum diff; > + struct ovsdb_datum diff; > + const struct ovsdb_datum *column_update = NULL; > > + if (type == OJMS_MODIFY) { > if (!bitmap_is_set(changed, i)) { > continue; > } > > - ovsdb_datum_diff(&diff ,&old[index], &new[index], > + ovsdb_datum_diff(&diff, &old[index], &new[index], > &c->column->type); > - json_object_put(diff_json, c->column->name, > - ovsdb_datum_to_json(&diff, > &c->column->type)); > - ovsdb_datum_destroy(&diff, &c->column->type); > + column_update = &diff; > } else { > - if (!ovsdb_datum_is_default(&new[index], &c->column->type)) { > - json_object_put(diff_json, c->column->name, > - ovsdb_datum_to_json(&new[index], > - &c->column->type)); > + if (ovsdb_datum_is_default(&new[index], &c->column->type)) { > + continue; > } > + > + column_update = &new[index]; > + } > + > + if (!empty) { > + ds_put_char(ds, ','); > + } > + empty = false; > + json_string_escape(c->column->name, ds); > + ds_put_char(ds, ':'); > + ovsdb_datum_to_json_ds(column_update, &c->column->type, ds); > + > + if (type == OJMS_MODIFY) { > + ovsdb_datum_destroy(&diff, &c->column->type); > } > } > > - op = type == OJMS_INITIAL ? "initial" > - : type == OJMS_MODIFY ? "modify" : > "insert"; > - json_object_put(row_update2, op, diff_json); > + ds_put_char(ds, '}'); > } > + ds_put_char(ds, '}'); > > - return row_update2; > + return true; > } > > static size_t > @@ -1166,6 +1210,8 @@ ovsdb_monitor_compose_update( Same comments here, as for ovsdb_monitor_compose_cond_change_update() below (sorry I started with the wrong function). > size_t max_columns = ovsdb_monitor_max_columns(dbmon); > unsigned long int *changed = xmalloc(bitmap_n_bytes(max_columns)); > > + struct ds ds; > + ds_init(&ds); > json = NULL; > struct ovsdb_monitor_change_set_for_table *mcst; > LIST_FOR_EACH (mcst, list_in_change_set, &mcs->change_set_for_tables) { > @@ -1176,16 +1222,24 @@ ovsdb_monitor_compose_update( > HMAP_FOR_EACH_SAFE (row, hmap_node, &mcst->rows) { > cooperative_multitasking_yield(); > > - struct json *row_json; > - row_json = (*row_update)(mt, condition, OVSDB_MONITOR_ROW, row, > - initial, changed, mcst->n_columns); > - if (row_json) { > + bool have_update; > + have_update = (*row_update)(mt, condition, OVSDB_MONITOR_ROW, > row, > + initial, changed, mcst->n_columns, > + &ds); > + if (have_update) { > + struct json *row_json; > + /* row_json content is only used for serialization */ > + row_json = > + > json_serialized_object_create_from_string(ds_cstr_ro(&ds)); > + ds_clear(&ds); > + > ovsdb_monitor_add_json_row(&json, mt->table->schema->name, > &table_json, row_json, > &row->uuid); > } > } > } > + ds_destroy(&ds); > free(changed); > > return json; > @@ -1201,6 +1255,9 @@ ovsdb_monitor_compose_cond_change_update( > size_t max_columns = ovsdb_monitor_max_columns(dbmon); > unsigned long int *changed = xmalloc(bitmap_n_bytes(max_columns)); > > + struct ds ds; > + ds_init(&ds); Maybe do ‘struct ds ds = DS_EMPTY_INITIALIZER;’ and move it higher at the right place (reverse Christmas tree order) with the other definitions. > + > SHASH_FOR_EACH (node, &dbmon->tables) { > struct ovsdb_condition *old_condition, *new_condition, > *diff_condition; > struct ovsdb_monitor_table *mt = node->data; > @@ -1219,15 +1276,21 @@ ovsdb_monitor_compose_cond_change_update( > > /* Iterate over all rows in table */ > HMAP_FOR_EACH (row, hmap_node, &mt->table->rows) { > - struct json *row_json; > - > cooperative_multitasking_yield(); > > - row_json = ovsdb_monitor_compose_row_update2(mt, condition, > - OVSDB_ROW, row, > - false, changed, > - mt->n_columns); > - if (row_json) { > + bool have_update; > + have_update = ovsdb_monitor_compose_row_update2(mt, condition, > + OVSDB_ROW, row, > + false, changed, > + mt->n_columns, > + &ds); > + if (have_update) { > + struct json *row_json; > + /* row_json content is only used for serialization */ > + row_json = > + > json_serialized_object_create_from_string(ds_cstr_ro(&ds)); Maybe create a function in the likes of json_serialized_object_create_from_string_nocopy() (see json_string_create_nocopy()), and use ds_steal_cstr() so we do not need to allocate and copy the memory. This should improve your results even more ;) > + ds_clear(&ds); Not needed if you use ds_steal_cstr() > + > ovsdb_monitor_add_json_row(&json, mt->table->schema->name, > &table_json, row_json, > ovsdb_row_get_uuid(row)); > @@ -1235,6 +1298,7 @@ ovsdb_monitor_compose_cond_change_update( > } > ovsdb_monitor_table_condition_updated(mt, condition); > } > + ds_destroy(&ds); > free(changed); > > return json; > -- > 2.45.2 > > _______________________________________________ > dev mailing list > d...@openvswitch.org > https://mail.openvswitch.org/mailman/listinfo/ovs-dev _______________________________________________ dev mailing list d...@openvswitch.org https://mail.openvswitch.org/mailman/listinfo/ovs-dev