From c21c3b0befeb46a51b6bf3758ffa30813bea0ff0 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 9 Mar 2024 14:19:22 +0100 Subject: Adding upstream version 1.44.3. Signed-off-by: Daniel Baumann --- libnetdata/facets/facets.c | 625 ++++++++++++++++++++++++++++++--------------- libnetdata/facets/facets.h | 10 + 2 files changed, 436 insertions(+), 199 deletions(-) (limited to 'libnetdata/facets') diff --git a/libnetdata/facets/facets.c b/libnetdata/facets/facets.c index e72cb7321..4a5f5442b 100644 --- a/libnetdata/facets/facets.c +++ b/libnetdata/facets/facets.c @@ -1,12 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "facets.h" -#define HISTOGRAM_COLUMNS 150 // the target number of points in a histogram +#define FACETS_HISTOGRAM_COLUMNS 150 // the target number of points in a histogram #define FACETS_KEYS_WITH_VALUES_MAX 200 // the max number of keys that can be facets #define FACETS_KEYS_IN_ROW_MAX 500 // the max number of keys in a row -#define FACETS_KEYS_HASHTABLE_ENTRIES 127 -#define FACETS_VALUES_HASHTABLE_ENTRIES 31 +#define FACETS_KEYS_HASHTABLE_ENTRIES 15 +#define FACETS_VALUES_HASHTABLE_ENTRIES 15 + +static inline void facets_reset_key(FACET_KEY *k); // ---------------------------------------------------------------------------- @@ -30,14 +32,12 @@ static const uint8_t id_encoding_characters_reverse[256] = { ['6'] = 60, ['7'] = 61, ['8'] = 62, ['9'] = 63 }; -__attribute__((constructor)) void initialize_facets_id_encoding_characters_reverse(void) { - -} - #define FACET_STRING_HASH_SIZE 12 #define FACETS_HASH XXH64_hash_t #define FACETS_HASH_FUNCTION(src, len) XXH3_64bits(src, len) #define FACETS_HASH_ZERO (FACETS_HASH)0 +#define FACETS_HASH_UNSAMPLED (FACETS_HASH)(UINT64_MAX - 1) +#define FACETS_HASH_ESTIMATED (FACETS_HASH)UINT64_MAX static inline void facets_hash_to_str(FACETS_HASH num, char *out) { out[11] = '\0'; @@ -99,76 +99,47 @@ static inline bool is_valid_string_hash(const char *s) { } // ---------------------------------------------------------------------------- +// hashtable for FACET_VALUE -typedef uint64_t SIMPLE_HASHTABLE_HASH; -#define SIMPLE_HASHTABLE_HASH_SECOND_HASH_SHIFTS 32 - -typedef struct simple_hashtable_slot { - SIMPLE_HASHTABLE_HASH hash; - void *data; -} SIMPLE_HASHTABLE_SLOT; +// cleanup hashtable defines +#undef SIMPLE_HASHTABLE_SORT_FUNCTION +#undef SIMPLE_HASHTABLE_VALUE_TYPE +#undef SIMPLE_HASHTABLE_NAME +#undef NETDATA_SIMPLE_HASHTABLE_H -typedef struct simple_hashtable { - size_t size; - SIMPLE_HASHTABLE_SLOT *hashtable; -} SIMPLE_HASHTABLE; - -static void simple_hashtable_init(SIMPLE_HASHTABLE *ht, size_t size) { - ht->size = size; - ht->hashtable = callocz(ht->size, sizeof(*ht->hashtable)); -} - -static void simple_hashtable_free(SIMPLE_HASHTABLE *ht) { - freez(ht->hashtable); - ht->hashtable = NULL; - ht->size = 0; -} - -static inline SIMPLE_HASHTABLE_SLOT *simple_hashtable_get_slot(SIMPLE_HASHTABLE *ht, SIMPLE_HASHTABLE_HASH hash) { - // IMPORTANT: - // If the hashtable supported deletions, we would need to have a special slot.data value - // to mark deleted values and assume they are occupied during lookup, but empty during insert. - // But for our case, we don't need it, since we never delete items from the hashtable. - - size_t slot = hash % ht->size; - if(!ht->hashtable[slot].data || ht->hashtable[slot].hash == hash) - return &ht->hashtable[slot]; - - slot = ((hash >> SIMPLE_HASHTABLE_HASH_SECOND_HASH_SHIFTS) + 1) % ht->size; - // Linear probing until we find it - while (ht->hashtable[slot].data && ht->hashtable[slot].hash != hash) - slot = (slot + 1) % ht->size; // Wrap around if necessary - - return &ht->hashtable[slot]; -} - -static void simple_hashtable_resize_double(SIMPLE_HASHTABLE *ht) { - SIMPLE_HASHTABLE_SLOT *old = ht->hashtable; - size_t old_size = ht->size; - - ht->size = (ht->size * 2) + 1; - ht->hashtable = callocz(ht->size, sizeof(*ht->hashtable)); - for(size_t i = 0 ; i < old_size ; i++) { - if(!old[i].data) - continue; +struct facet_value; +// #define SIMPLE_HASHTABLE_SORT_FUNCTION compare_facet_value +#define SIMPLE_HASHTABLE_VALUE_TYPE struct facet_value +#define SIMPLE_HASHTABLE_NAME _VALUE +#include "../simple_hashtable.h" - SIMPLE_HASHTABLE_SLOT *slot = simple_hashtable_get_slot(ht, old[i].hash); - *slot = old[i]; - } +// ---------------------------------------------------------------------------- +// hashtable for FACET_KEY - freez(old); -} +// cleanup hashtable defines +#undef SIMPLE_HASHTABLE_SORT_FUNCTION +#undef SIMPLE_HASHTABLE_VALUE_TYPE +#undef SIMPLE_HASHTABLE_NAME +#undef NETDATA_SIMPLE_HASHTABLE_H +struct facet_key; +// #define SIMPLE_HASHTABLE_SORT_FUNCTION compare_facet_key +#define SIMPLE_HASHTABLE_VALUE_TYPE struct facet_key +#define SIMPLE_HASHTABLE_NAME _KEY +#include "../simple_hashtable.h" // ---------------------------------------------------------------------------- typedef struct facet_value { FACETS_HASH hash; const char *name; + const char *color; uint32_t name_len; bool selected; bool empty; + bool unsampled; + bool estimated; uint32_t rows_matching_facet_value; uint32_t final_facet_value_counter; @@ -181,14 +152,19 @@ typedef struct facet_value { } FACET_VALUE; typedef enum { - FACET_KEY_VALUE_NONE = 0, - FACET_KEY_VALUE_UPDATED = (1 << 0), - FACET_KEY_VALUE_EMPTY = (1 << 1), - FACET_KEY_VALUE_COPIED = (1 << 2), + FACET_KEY_VALUE_NONE = 0, + FACET_KEY_VALUE_UPDATED = (1 << 0), + FACET_KEY_VALUE_EMPTY = (1 << 1), + FACET_KEY_VALUE_UNSAMPLED = (1 << 2), + FACET_KEY_VALUE_ESTIMATED = (1 << 3), + FACET_KEY_VALUE_COPIED = (1 << 4), } FACET_KEY_VALUE_FLAGS; #define facet_key_value_updated(k) ((k)->current_value.flags & FACET_KEY_VALUE_UPDATED) #define facet_key_value_empty(k) ((k)->current_value.flags & FACET_KEY_VALUE_EMPTY) +#define facet_key_value_unsampled(k) ((k)->current_value.flags & FACET_KEY_VALUE_UNSAMPLED) +#define facet_key_value_estimated(k) ((k)->current_value.flags & FACET_KEY_VALUE_ESTIMATED) +#define facet_key_value_empty_or_unsampled_or_estimated(k) ((k)->current_value.flags & (FACET_KEY_VALUE_EMPTY|FACET_KEY_VALUE_UNSAMPLED|FACET_KEY_VALUE_ESTIMATED)) #define facet_key_value_copied(k) ((k)->current_value.flags & FACET_KEY_VALUE_COPIED) struct facet_key { @@ -210,7 +186,7 @@ struct facet_key { bool enabled; uint32_t used; FACET_VALUE *ll; - SIMPLE_HASHTABLE ht; + SIMPLE_HASHTABLE_VALUE ht; } values; struct { @@ -226,6 +202,14 @@ struct facet_key { FACET_VALUE *v; } empty_value; + struct { + FACET_VALUE *v; + } unsampled_value; + + struct { + FACET_VALUE *v; + } estimated_value; + struct { facet_dynamic_row_t cb; void *data; @@ -261,7 +245,7 @@ struct facets { struct { size_t count; FACET_KEY *ll; - SIMPLE_HASHTABLE ht; + SIMPLE_HASHTABLE_KEY ht; } keys; struct { @@ -324,6 +308,8 @@ struct facets { struct { size_t evaluated; size_t matched; + size_t unsampled; + size_t estimated; size_t created; size_t reused; } rows; @@ -331,7 +317,6 @@ struct facets { struct { size_t registered; size_t unique; - size_t hashtable_increases; } keys; struct { @@ -339,16 +324,21 @@ struct facets { size_t transformed; size_t dynamic; size_t empty; + size_t unsampled; + size_t estimated; size_t indexed; size_t inserts; size_t conflicts; - size_t hashtable_increases; } values; struct { size_t searches; } fts; } operations; + + struct { + DICTIONARY *used_hashes_registry; + } report; }; usec_t facets_row_oldest_ut(FACETS *facets) { @@ -386,7 +376,7 @@ static inline bool facets_key_is_facet(FACETS *facets, FACET_KEY *k); static inline void FACETS_VALUES_INDEX_CREATE(FACET_KEY *k) { k->values.ll = NULL; k->values.used = 0; - simple_hashtable_init(&k->values.ht, FACETS_VALUES_HASHTABLE_ENTRIES); + simple_hashtable_init_VALUE(&k->values.ht, FACETS_VALUES_HASHTABLE_ENTRIES); } static inline void FACETS_VALUES_INDEX_DESTROY(FACET_KEY *k) { @@ -402,7 +392,7 @@ static inline void FACETS_VALUES_INDEX_DESTROY(FACET_KEY *k) { k->values.used = 0; k->values.enabled = false; - simple_hashtable_free(&k->values.ht); + simple_hashtable_destroy_VALUE(&k->values.ht); } static inline const char *facets_key_get_value(FACET_KEY *k) { @@ -449,17 +439,17 @@ static inline void FACET_VALUE_ADD_CONFLICT(FACET_KEY *k, FACET_VALUE *v, const } static inline FACET_VALUE *FACET_VALUE_GET_FROM_INDEX(FACET_KEY *k, FACETS_HASH hash) { - SIMPLE_HASHTABLE_SLOT *slot = simple_hashtable_get_slot(&k->values.ht, hash); - return slot->data; + SIMPLE_HASHTABLE_SLOT_VALUE *slot = simple_hashtable_get_slot_VALUE(&k->values.ht, hash, true); + return SIMPLE_HASHTABLE_SLOT_DATA(slot); } static inline FACET_VALUE *FACET_VALUE_ADD_TO_INDEX(FACET_KEY *k, const FACET_VALUE * const tv) { - SIMPLE_HASHTABLE_SLOT *slot = simple_hashtable_get_slot(&k->values.ht, tv->hash); + SIMPLE_HASHTABLE_SLOT_VALUE *slot = simple_hashtable_get_slot_VALUE(&k->values.ht, tv->hash, true); - if(slot->data) { + if(SIMPLE_HASHTABLE_SLOT_DATA(slot)) { // already exists - FACET_VALUE *v = slot->data; + FACET_VALUE *v = SIMPLE_HASHTABLE_SLOT_DATA(slot); FACET_VALUE_ADD_CONFLICT(k, v, tv); return v; } @@ -467,12 +457,21 @@ static inline FACET_VALUE *FACET_VALUE_ADD_TO_INDEX(FACET_KEY *k, const FACET_VA // we have to add it FACET_VALUE *v = mallocz(sizeof(*v)); - slot->hash = tv->hash; - slot->data = v; + simple_hashtable_set_slot_VALUE(&k->values.ht, slot, tv->hash, v); memcpy(v, tv, sizeof(*v)); - DOUBLE_LINKED_LIST_APPEND_ITEM_UNSAFE(k->values.ll, v, prev, next); + if(v->estimated || v->unsampled) { + if(k->values.ll && k->values.ll->estimated) { + FACET_VALUE *estimated = k->values.ll; + DOUBLE_LINKED_LIST_INSERT_ITEM_AFTER_UNSAFE(k->values.ll, estimated, v, prev, next); + } + else + DOUBLE_LINKED_LIST_PREPEND_ITEM_UNSAFE(k->values.ll, v, prev, next); + } + else + DOUBLE_LINKED_LIST_APPEND_ITEM_UNSAFE(k->values.ll, v, prev, next); + k->values.used++; if(!v->selected) @@ -490,12 +489,53 @@ static inline FACET_VALUE *FACET_VALUE_ADD_TO_INDEX(FACET_KEY *k, const FACET_VA k->facets->operations.values.inserts++; - if(unlikely(k->values.used > k->values.ht.size / 2)) { - simple_hashtable_resize_double(&k->values.ht); - k->facets->operations.values.hashtable_increases++; + return v; +} + +static inline void FACET_VALUE_ADD_UNSAMPLED_VALUE_TO_INDEX(FACET_KEY *k) { + static const FACET_VALUE tv = { + .hash = FACETS_HASH_UNSAMPLED, + .name = FACET_VALUE_UNSAMPLED, + .name_len = sizeof(FACET_VALUE_UNSAMPLED) - 1, + .unsampled = true, + .color = "offline", + }; + + k->current_value.hash = FACETS_HASH_UNSAMPLED; + + if(k->unsampled_value.v) { + FACET_VALUE_ADD_CONFLICT(k, k->unsampled_value.v, &tv); + k->current_value.v = k->unsampled_value.v; } + else { + FACET_VALUE *v = FACET_VALUE_ADD_TO_INDEX(k, &tv); + v->unsampled = true; + k->unsampled_value.v = v; + k->current_value.v = v; + } +} - return v; +static inline void FACET_VALUE_ADD_ESTIMATED_VALUE_TO_INDEX(FACET_KEY *k) { + static const FACET_VALUE tv = { + .hash = FACETS_HASH_ESTIMATED, + .name = FACET_VALUE_ESTIMATED, + .name_len = sizeof(FACET_VALUE_ESTIMATED) - 1, + .estimated = true, + .color = "generic", + }; + + k->current_value.hash = FACETS_HASH_ESTIMATED; + + if(k->estimated_value.v) { + FACET_VALUE_ADD_CONFLICT(k, k->estimated_value.v, &tv); + k->current_value.v = k->estimated_value.v; + } + else { + FACET_VALUE *v = FACET_VALUE_ADD_TO_INDEX(k, &tv); + v->estimated = true; + k->estimated_value.v = v; + k->current_value.v = v; + } } static inline void FACET_VALUE_ADD_EMPTY_VALUE_TO_INDEX(FACET_KEY *k) { @@ -503,6 +543,7 @@ static inline void FACET_VALUE_ADD_EMPTY_VALUE_TO_INDEX(FACET_KEY *k) { .hash = FACETS_HASH_ZERO, .name = FACET_VALUE_UNSET, .name_len = sizeof(FACET_VALUE_UNSET) - 1, + .empty = true, }; k->current_value.hash = FACETS_HASH_ZERO; @@ -527,6 +568,9 @@ static inline void FACET_VALUE_ADD_CURRENT_VALUE_TO_INDEX(FACET_KEY *k) { tv.name = facets_key_get_value(k); tv.name_len = facets_key_get_value_length(k); tv.hash = FACETS_HASH_FUNCTION(tv.name, tv.name_len); + tv.empty = false; + tv.estimated = false; + tv.unsampled = false; k->current_value.v = FACET_VALUE_ADD_TO_INDEX(k, &tv); k->facets->operations.values.indexed++; @@ -567,7 +611,7 @@ static inline void FACETS_KEYS_INDEX_CREATE(FACETS *facets) { facets->keys.count = 0; facets->keys_with_values.used = 0; - simple_hashtable_init(&facets->keys.ht, FACETS_KEYS_HASHTABLE_ENTRIES); + simple_hashtable_init_KEY(&facets->keys.ht, FACETS_KEYS_HASHTABLE_ENTRIES); } static inline void FACETS_KEYS_INDEX_DESTROY(FACETS *facets) { @@ -586,12 +630,12 @@ static inline void FACETS_KEYS_INDEX_DESTROY(FACETS *facets) { facets->keys.count = 0; facets->keys_with_values.used = 0; - simple_hashtable_free(&facets->keys.ht); + simple_hashtable_destroy_KEY(&facets->keys.ht); } static inline FACET_KEY *FACETS_KEY_GET_FROM_INDEX(FACETS *facets, FACETS_HASH hash) { - SIMPLE_HASHTABLE_SLOT *slot = simple_hashtable_get_slot(&facets->keys.ht, hash); - return slot->data; + SIMPLE_HASHTABLE_SLOT_KEY *slot = simple_hashtable_get_slot_KEY(&facets->keys.ht, hash, true); + return SIMPLE_HASHTABLE_SLOT_DATA(slot); } bool facets_key_name_value_length_is_selected(FACETS *facets, const char *key, size_t key_length, const char *value, size_t value_length) { @@ -670,26 +714,20 @@ static inline FACET_KEY *FACETS_KEY_CREATE(FACETS *facets, FACETS_HASH hash, con static inline FACET_KEY *FACETS_KEY_ADD_TO_INDEX(FACETS *facets, FACETS_HASH hash, const char *name, size_t name_length, FACET_KEY_OPTIONS options) { facets->operations.keys.registered++; - SIMPLE_HASHTABLE_SLOT *slot = simple_hashtable_get_slot(&facets->keys.ht, hash); + SIMPLE_HASHTABLE_SLOT_KEY *slot = simple_hashtable_get_slot_KEY(&facets->keys.ht, hash, true); - if(unlikely(!slot->data)) { + if(unlikely(!SIMPLE_HASHTABLE_SLOT_DATA(slot))) { // we have to add it FACET_KEY *k = FACETS_KEY_CREATE(facets, hash, name, name_length, options); - slot->hash = hash; - slot->data = k; - - if(facets->keys.count > facets->keys.ht.size / 2) { - simple_hashtable_resize_double(&facets->keys.ht); - facets->operations.keys.hashtable_increases++; - } + simple_hashtable_set_slot_KEY(&facets->keys.ht, slot, hash, k); return k; } // already in the index - FACET_KEY *k = slot->data; + FACET_KEY *k = SIMPLE_HASHTABLE_SLOT_DATA(slot); facet_key_set_name(k, name, name_length); @@ -716,6 +754,10 @@ bool facets_key_name_is_facet(FACETS *facets, const char *key) { // ---------------------------------------------------------------------------- +size_t facets_histogram_slots(FACETS *facets) { + return facets->histogram.slots; +} + static usec_t calculate_histogram_bar_width(usec_t after_ut, usec_t before_ut) { // Array of valid durations in seconds static time_t valid_durations_s[] = { @@ -731,7 +773,7 @@ static usec_t calculate_histogram_bar_width(usec_t after_ut, usec_t before_ut) { usec_t bar_width_ut = 1 * USEC_PER_SEC; for (int i = array_size - 1; i >= 0; --i) { - if (duration_ut / (valid_durations_s[i] * USEC_PER_SEC) >= HISTOGRAM_COLUMNS) { + if (duration_ut / (valid_durations_s[i] * USEC_PER_SEC) >= FACETS_HISTOGRAM_COLUMNS) { bar_width_ut = valid_durations_s[i] * USEC_PER_SEC; break; } @@ -788,17 +830,7 @@ void facets_set_timeframe_and_histogram_by_name(FACETS *facets, const char *key_ facets_set_timeframe_and_histogram_by_id(facets, hash_str, after_ut, before_ut); } -static inline void facets_histogram_update_value(FACETS *facets, usec_t usec) { - if(!facets->histogram.enabled || - !facets->histogram.key || - !facets->histogram.key->values.enabled || - !facet_key_value_updated(facets->histogram.key) || - usec < facets->histogram.after_ut || - usec > facets->histogram.before_ut) - return; - - FACET_VALUE *v = facets->histogram.key->current_value.v; - +static inline uint32_t facets_histogram_slot_at_time_ut(FACETS *facets, usec_t usec, FACET_VALUE *v) { if(unlikely(!v->histogram)) v->histogram = callocz(facets->histogram.slots, sizeof(*v->histogram)); @@ -815,11 +847,177 @@ static inline void facets_histogram_update_value(FACETS *facets, usec_t usec) { if(unlikely(slot >= facets->histogram.slots)) slot = facets->histogram.slots - 1; + return slot; +} + +static inline void facets_histogram_update_value_slot(FACETS *facets, usec_t usec, FACET_VALUE *v) { + uint32_t slot = facets_histogram_slot_at_time_ut(facets, usec, v); v->histogram[slot]++; } +static inline void facets_histogram_update_value(FACETS *facets, usec_t usec) { + if(!facets->histogram.enabled || + !facets->histogram.key || + !facets->histogram.key->values.enabled || + !facet_key_value_updated(facets->histogram.key) || + usec < facets->histogram.after_ut || + usec > facets->histogram.before_ut) + return; + + FACET_VALUE *v = facets->histogram.key->current_value.v; + facets_histogram_update_value_slot(facets, usec, v); +} + +static usec_t overlap_duration_ut(usec_t start1, usec_t end1, usec_t start2, usec_t end2) { + usec_t overlap_start = MAX(start1, start2); + usec_t overlap_end = MIN(end1, end2); + + if (overlap_start < overlap_end) + return overlap_end - overlap_start; + else + return 0; // No overlap +} + +void facets_update_estimations(FACETS *facets, usec_t from_ut, usec_t to_ut, size_t entries) { + if(unlikely(!facets->histogram.enabled)) + return; + + if(unlikely(!overlap_duration_ut(facets->histogram.after_ut, facets->histogram.before_ut, from_ut, to_ut))) + return; + + facets->operations.rows.evaluated += entries; + facets->operations.rows.matched += entries; + facets->operations.rows.estimated += entries; + + if (!facets->histogram.enabled || + !facets->histogram.key || + !facets->histogram.key->values.enabled) + return; + + if (from_ut < facets->histogram.after_ut) + from_ut = facets->histogram.after_ut; + + if (to_ut > facets->histogram.before_ut) + to_ut = facets->histogram.before_ut; + + if (!facets->histogram.key->estimated_value.v) + FACET_VALUE_ADD_ESTIMATED_VALUE_TO_INDEX(facets->histogram.key); + + FACET_VALUE *v = facets->histogram.key->estimated_value.v; + + size_t slots = 0; + size_t total_ut = to_ut - from_ut; + ssize_t remaining_entries = (ssize_t)entries; + size_t slot = facets_histogram_slot_at_time_ut(facets, from_ut, v); + for(; slot < facets->histogram.slots ;slot++) { + usec_t slot_start_ut = facets->histogram.after_ut + slot * facets->histogram.slot_width_ut; + usec_t slot_end_ut = slot_start_ut + facets->histogram.slot_width_ut; + + if(slot_start_ut > to_ut) + break; + + usec_t overlap_ut = overlap_duration_ut(from_ut, to_ut, slot_start_ut, slot_end_ut); + + size_t slot_entries = (overlap_ut * entries) / total_ut; + v->histogram[slot] += slot_entries; + remaining_entries -= (ssize_t)slot_entries; + slots++; + } + + // Check if all entries are assigned + // This should always be true if the distribution is correct + internal_fatal(remaining_entries < 0 || remaining_entries >= (ssize_t)(slots), + "distribution of estimations is not accurate - there are %zd remaining entries", + remaining_entries); +} + +void facets_row_finished_unsampled(FACETS *facets, usec_t usec) { + facets->operations.rows.evaluated++; + facets->operations.rows.matched++; + facets->operations.rows.unsampled++; + + if(!facets->histogram.enabled || + !facets->histogram.key || + !facets->histogram.key->values.enabled || + usec < facets->histogram.after_ut || + usec > facets->histogram.before_ut) + return; + + if(!facets->histogram.key->unsampled_value.v) + FACET_VALUE_ADD_UNSAMPLED_VALUE_TO_INDEX(facets->histogram.key); + + FACET_VALUE *v = facets->histogram.key->unsampled_value.v; + facets_histogram_update_value_slot(facets, usec, v); + + facets_reset_key(facets->histogram.key); +} + +static const char *facets_key_name_cached(FACET_KEY *k, DICTIONARY *used_hashes_registry) { + if(k->name) { + if(used_hashes_registry && !k->default_selected_for_values) { + char hash_str[FACET_STRING_HASH_SIZE]; + facets_hash_to_str(k->hash, hash_str); + dictionary_set(used_hashes_registry, hash_str, (void *)k->name, strlen(k->name) + 1); + } + + return k->name; + } + + // key has no name + const char *name = "[UNAVAILABLE_FIELD]"; + + if(used_hashes_registry) { + char hash_str[FACET_STRING_HASH_SIZE]; + facets_hash_to_str(k->hash, hash_str); + const char *s = dictionary_get(used_hashes_registry, hash_str); + if(s) name = s; + } + + return name; +} + +static const char *facets_key_value_cached(FACET_KEY *k, FACET_VALUE *v, DICTIONARY *used_hashes_registry) { + if(v->empty || v->estimated || v->unsampled) + return v->name; + + if(v->name && v->name_len) { + if(used_hashes_registry && !k->default_selected_for_values && v->selected) { + char hash_str[FACET_STRING_HASH_SIZE]; + facets_hash_to_str(v->hash, hash_str); + dictionary_set(used_hashes_registry, hash_str, (void *)v->name, v->name_len + 1); + } + + return v->name; + } + + // key has no name + const char *name = "[unavailable field]"; + + if(used_hashes_registry) { + char hash_str[FACET_STRING_HASH_SIZE]; + facets_hash_to_str(v->hash, hash_str); + const char *s = dictionary_get(used_hashes_registry, hash_str); + if(s) name = s; + } + + return name; +} + +static inline void facets_key_value_transformed(FACETS *facets, FACET_KEY *k, FACET_VALUE *v, BUFFER *dst, FACETS_TRANSFORMATION_SCOPE scope) { + buffer_flush(dst); + + if(v->empty || v->unsampled || v->estimated) + buffer_strcat(dst, v->name); + else if(k->transform.cb && k->transform.view_only) { + buffer_contents_replace(dst, v->name, v->name_len); + k->transform.cb(facets, dst, scope, k->transform.data); + } + else + buffer_strcat(dst, facets_key_value_cached(k, v, facets->report.used_hashes_registry)); +} + static inline void facets_histogram_value_names(BUFFER *wb, FACETS *facets __maybe_unused, FACET_KEY *k, const char *key, const char *first_key) { - BUFFER *tb = NULL; + CLEAN_BUFFER *tb = buffer_create(0, NULL); buffer_json_member_add_array(wb, key); { @@ -832,23 +1030,30 @@ static inline void facets_histogram_value_names(BUFFER *wb, FACETS *facets __may if (unlikely(!v->histogram)) continue; - if(!v->empty && k->transform.cb && k->transform.view_only) { - if(!tb) - tb = buffer_create(0, NULL); - - buffer_contents_replace(tb, v->name, v->name_len); - k->transform.cb(facets, tb, FACETS_TRANSFORM_HISTOGRAM, k->transform.data); - buffer_json_add_array_item_string(wb, buffer_tostring(tb)); - } - else - buffer_json_add_array_item_string(wb, v->name); + facets_key_value_transformed(facets, k, v, tb, FACETS_TRANSFORM_HISTOGRAM); + buffer_json_add_array_item_string(wb, buffer_tostring(tb)); } foreach_value_in_key_done(v); } } buffer_json_array_close(wb); // key +} - buffer_free(tb); +static inline void facets_histogram_value_colors(BUFFER *wb, FACETS *facets __maybe_unused, FACET_KEY *k, const char *key) { + buffer_json_member_add_array(wb, key); + { + if(k && k->values.enabled) { + FACET_VALUE *v; + foreach_value_in_key(k, v) { + if (unlikely(!v->histogram)) + continue; + + buffer_json_add_array_item_string(wb, v->color); + } + foreach_value_in_key_done(v); + } + } + buffer_json_array_close(wb); // key } static inline void facets_histogram_value_units(BUFFER *wb, FACETS *facets __maybe_unused, FACET_KEY *k, const char *key) { @@ -954,6 +1159,8 @@ static inline void facets_histogram_value_con(BUFFER *wb, FACETS *facets __maybe } static void facets_histogram_generate(FACETS *facets, FACET_KEY *k, BUFFER *wb) { + CLEAN_BUFFER *tmp = buffer_create(0, NULL); + size_t dimensions = 0; uint32_t min = UINT32_MAX, max = 0, sum = 0, count = 0; @@ -995,6 +1202,7 @@ static void facets_histogram_generate(FACETS *facets, FACET_KEY *k, BUFFER *wb) buffer_json_member_add_object(wb, "summary"); { + // summary.nodes buffer_json_member_add_array(wb, "nodes"); { buffer_json_add_array_item_object(wb); // node @@ -1041,6 +1249,7 @@ static void facets_histogram_generate(FACETS *facets, FACET_KEY *k, BUFFER *wb) } buffer_json_array_close(wb); // nodes + // summary.contexts buffer_json_member_add_array(wb, "contexts"); { buffer_json_add_array_item_object(wb); // context @@ -1078,6 +1287,7 @@ static void facets_histogram_generate(FACETS *facets, FACET_KEY *k, BUFFER *wb) } buffer_json_array_close(wb); // contexts + // summary.instances buffer_json_member_add_array(wb, "instances"); { buffer_json_add_array_item_object(wb); // instance @@ -1109,17 +1319,20 @@ static void facets_histogram_generate(FACETS *facets, FACET_KEY *k, BUFFER *wb) } buffer_json_array_close(wb); // instances + // summary.dimensions buffer_json_member_add_array(wb, "dimensions"); if(dimensions && k && k->values.enabled) { size_t pri = 0; FACET_VALUE *v; + foreach_value_in_key(k, v) { if(unlikely(!v->histogram)) continue; buffer_json_add_array_item_object(wb); // dimension { - buffer_json_member_add_string(wb, "id", v->name); + facets_key_value_transformed(facets, k, v, tmp, FACETS_TRANSFORM_HISTOGRAM); + buffer_json_member_add_string(wb, "id", buffer_tostring(tmp)); buffer_json_member_add_object(wb, "ds"); { buffer_json_member_add_uint64(wb, "sl", 1); @@ -1275,7 +1488,7 @@ static void facets_histogram_generate(FACETS *facets, FACET_KEY *k, BUFFER *wb) char title[1024 + 1] = "Events Distribution"; FACET_KEY *kt = FACETS_KEY_GET_FROM_INDEX(facets, facets->histogram.hash); if(kt && kt->name) - snprintfz(title, 1024, "Events Distribution by %s", kt->name); + snprintfz(title, sizeof(title) - 1, "Events Distribution by %s", kt->name); buffer_json_member_add_string(wb, "title", title); buffer_json_member_add_time_t(wb, "update_every", facets->histogram.slot_width_ut / USEC_PER_SEC); @@ -1293,6 +1506,7 @@ static void facets_histogram_generate(FACETS *facets, FACET_KEY *k, BUFFER *wb) facets_histogram_value_names(wb, facets, k, "ids", NULL); facets_histogram_value_names(wb, facets, k, "names", NULL); + facets_histogram_value_colors(wb, facets, k, "colors"); facets_histogram_value_units(wb, facets, k, "units"); buffer_json_member_add_object(wb, "sts"); @@ -1525,6 +1739,30 @@ void facets_set_additional_options(FACETS *facets, FACETS_OPTIONS options) { // ---------------------------------------------------------------------------- +static inline void facets_key_set_unsampled_value(FACETS *facets, FACET_KEY *k) { + if(likely(!facet_key_value_updated(k) && facets->keys_in_row.used < FACETS_KEYS_IN_ROW_MAX)) + facets->keys_in_row.array[facets->keys_in_row.used++] = k; + + k->current_value.flags |= FACET_KEY_VALUE_UPDATED | FACET_KEY_VALUE_UNSAMPLED; + + facets->operations.values.registered++; + facets->operations.values.unsampled++; + + // no need to copy the UNSET value + // empty values are exported as empty + k->current_value.raw = NULL; + k->current_value.raw_len = 0; + k->current_value.b->len = 0; + k->current_value.flags &= ~FACET_KEY_VALUE_COPIED; + + if(unlikely(k->values.enabled)) + FACET_VALUE_ADD_UNSAMPLED_VALUE_TO_INDEX(k); + else { + k->key_found_in_row++; + k->key_values_selected_in_row++; + } +} + static inline void facets_key_set_empty_value(FACETS *facets, FACET_KEY *k) { if(likely(!facet_key_value_updated(k) && facets->keys_in_row.used < FACETS_KEYS_IN_ROW_MAX)) facets->keys_in_row.array[facets->keys_in_row.used++] = k; @@ -1554,7 +1792,7 @@ static inline void facets_key_check_value(FACETS *facets, FACET_KEY *k) { facets->keys_in_row.array[facets->keys_in_row.used++] = k; k->current_value.flags |= FACET_KEY_VALUE_UPDATED; - k->current_value.flags &= ~FACET_KEY_VALUE_EMPTY; + k->current_value.flags &= ~(FACET_KEY_VALUE_EMPTY|FACET_KEY_VALUE_UNSAMPLED|FACET_KEY_VALUE_ESTIMATED); facets->operations.values.registered++; @@ -1568,7 +1806,7 @@ static inline void facets_key_check_value(FACETS *facets, FACET_KEY *k) { // if(strstr(buffer_tostring(k->current_value), "fprintd") != NULL) // found = true; - if(facets->query && !facet_key_value_empty(k) && ((k->options & FACET_KEY_OPTION_FTS) || facets->options & FACETS_OPTION_ALL_KEYS_FTS)) { + if(facets->query && !facet_key_value_empty_or_unsampled_or_estimated(k) && ((k->options & FACET_KEY_OPTION_FTS) || facets->options & FACETS_OPTION_ALL_KEYS_FTS)) { facets->operations.fts.searches++; facets_key_value_copy_to_buffer(k); switch(simple_pattern_matches_extract(facets->query, buffer_tostring(k->current_value.b), NULL, 0)) { @@ -1679,7 +1917,7 @@ static FACET_ROW *facets_row_create(FACETS *facets, usec_t usec, FACET_ROW *into .empty = true, }; - if(facet_key_value_updated(k) && !facet_key_value_empty(k)) { + if(facet_key_value_updated(k) && !facet_key_value_empty_or_unsampled_or_estimated(k)) { t.tmp = facets_key_get_value(k); t.tmp_len = facets_key_get_value_length(k); t.empty = false; @@ -1758,6 +1996,12 @@ static inline bool facets_is_entry_within_anchor(FACETS *facets, usec_t usec) { return true; } +bool facets_row_candidate_to_keep(FACETS *facets, usec_t usec) { + return !facets->base || + (usec >= facets->base->prev->usec && usec <= facets->base->usec && facets_is_entry_within_anchor(facets, usec)) || + facets->items_to_return < facets->max_items_to_return; +} + static void facets_row_keep(FACETS *facets, usec_t usec) { facets->operations.rows.matched++; @@ -1885,9 +2129,10 @@ bool facets_row_finished(FACETS *facets, usec_t usec) { for(size_t p = 0; p < entries ;p++) { FACET_KEY *k = facets->keys_with_values.array[p]; - if(!facet_key_value_updated(k)) + if(!facet_key_value_updated(k)) { // put the FACET_VALUE_UNSET value into it facets_key_set_empty_value(facets, k); + } total_keys++; @@ -1935,7 +2180,7 @@ bool facets_row_finished(FACETS *facets, usec_t usec) { // ---------------------------------------------------------------------------- // output -static const char *facets_severity_to_string(FACET_ROW_SEVERITY severity) { +const char *facets_severity_to_string(FACET_ROW_SEVERITY severity) { switch(severity) { default: case FACET_ROW_SEVERITY_NORMAL: @@ -2104,7 +2349,7 @@ static uint32_t facets_sort_and_reorder_values(FACET_KEY *k) { if(!k->values.enabled || !k->values.ll || !k->values.used) return 0; - if(!k->transform.cb || !(k->facets->options & FACETS_OPTION_SORT_FACETS_ALPHABETICALLY)) + if(!k->transform.cb || !k->transform.view_only || !(k->facets->options & FACETS_OPTION_SORT_FACETS_ALPHABETICALLY)) return facets_sort_and_reorder_values_internal(k); // we have a transformation and has to be sorted alphabetically @@ -2128,8 +2373,7 @@ static uint32_t facets_sort_and_reorder_values(FACET_KEY *k) { values[used].name_len = v->name_len; used++; - buffer_contents_replace(tb, v->name, v->name_len); - k->transform.cb(k->facets, tb, FACETS_TRANSFORM_FACET_SORT, k->transform.data); + facets_key_value_transformed(k->facets, k, v, tb, FACETS_TRANSFORM_FACET_SORT); v->name = strdupz(buffer_tostring(tb)); v->name_len = buffer_strlen(tb); } @@ -2167,55 +2411,9 @@ void facets_table_config(BUFFER *wb) { buffer_json_object_close(wb); // pagination } -static const char *facets_json_key_name_string(FACET_KEY *k, DICTIONARY *used_hashes_registry) { - if(k->name) { - if(used_hashes_registry && !k->default_selected_for_values) { - char hash_str[FACET_STRING_HASH_SIZE]; - facets_hash_to_str(k->hash, hash_str); - dictionary_set(used_hashes_registry, hash_str, (void *)k->name, strlen(k->name) + 1); - } - - return k->name; - } - - // key has no name - const char *name = "[UNAVAILABLE_FIELD]"; - - if(used_hashes_registry) { - char hash_str[FACET_STRING_HASH_SIZE]; - facets_hash_to_str(k->hash, hash_str); - const char *s = dictionary_get(used_hashes_registry, hash_str); - if(s) name = s; - } - - return name; -} - -static const char *facets_json_key_value_string(FACET_KEY *k, FACET_VALUE *v, DICTIONARY *used_hashes_registry) { - if(v->name && v->name_len) { - if(used_hashes_registry && !k->default_selected_for_values && v->selected) { - char hash_str[FACET_STRING_HASH_SIZE]; - facets_hash_to_str(v->hash, hash_str); - dictionary_set(used_hashes_registry, hash_str, (void *)v->name, v->name_len + 1); - } - - return v->name; - } - - // key has no name - const char *name = "[unavailable field]"; - - if(used_hashes_registry) { - char hash_str[FACET_STRING_HASH_SIZE]; - facets_hash_to_str(v->hash, hash_str); - const char *s = dictionary_get(used_hashes_registry, hash_str); - if(s) name = s; - } - - return name; -} - void facets_report(FACETS *facets, BUFFER *wb, DICTIONARY *used_hashes_registry) { + facets->report.used_hashes_registry = used_hashes_registry; + if(!(facets->options & FACETS_OPTION_DATA_ONLY)) { facets_table_config(wb); facets_accepted_parameters_to_json_array(facets, wb, true); @@ -2239,7 +2437,7 @@ void facets_report(FACETS *facets, BUFFER *wb, DICTIONARY *used_hashes_registry) } if(show_facets) { - BUFFER *tb = NULL; + CLEAN_BUFFER *tb = buffer_create(0, NULL); FACET_KEY *k; foreach_key_in_facets(facets, k) { if(!k->values.enabled) @@ -2252,7 +2450,9 @@ void facets_report(FACETS *facets, BUFFER *wb, DICTIONARY *used_hashes_registry) buffer_json_add_array_item_object(wb); // key { buffer_json_member_add_string(wb, "id", hash_to_static_string(k->hash)); - buffer_json_member_add_string(wb, "name", facets_json_key_name_string(k, used_hashes_registry)); + buffer_json_member_add_string(wb, "name", facets_key_name_cached(k + , facets->report.used_hashes_registry + )); if(!k->order) k->order = facets->order++; buffer_json_member_add_uint64(wb, "order", k->order); @@ -2264,21 +2464,15 @@ void facets_report(FACETS *facets, BUFFER *wb, DICTIONARY *used_hashes_registry) if((facets->options & FACETS_OPTION_DONT_SEND_EMPTY_VALUE_FACETS) && v->empty) continue; + if(v->unsampled || v->estimated) + continue; + buffer_json_add_array_item_object(wb); { buffer_json_member_add_string(wb, "id", hash_to_static_string(v->hash)); - if(!v->empty && k->transform.cb && k->transform.view_only) { - if(!tb) - tb = buffer_create(0, NULL); - - buffer_contents_replace(tb, v->name, v->name_len); - k->transform.cb(facets, tb, FACETS_TRANSFORM_FACET, k->transform.data); - buffer_json_member_add_string(wb, "name", buffer_tostring(tb)); - } - else - buffer_json_member_add_string(wb, "name", facets_json_key_value_string(k, v, used_hashes_registry)); - + facets_key_value_transformed(facets, k, v, tb, FACETS_TRANSFORM_FACET); + buffer_json_member_add_string(wb, "name", buffer_tostring(tb)); buffer_json_member_add_uint64(wb, "count", v->final_facet_value_counter); buffer_json_member_add_uint64(wb, "order", v->order); } @@ -2291,7 +2485,6 @@ void facets_report(FACETS *facets, BUFFER *wb, DICTIONARY *used_hashes_registry) buffer_json_object_close(wb); // key } foreach_key_in_facets_done(k); - buffer_free(tb); buffer_json_array_close(wb); // facets } } @@ -2325,7 +2518,7 @@ void facets_report(FACETS *facets, BUFFER *wb, DICTIONARY *used_hashes_registry) NULL, RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_NONE, - RRDR_FIELD_OPTS_DUMMY, + RRDF_FIELD_OPTS_DUMMY, NULL); FACET_KEY *k; @@ -2345,6 +2538,9 @@ void facets_report(FACETS *facets, BUFFER *wb, DICTIONARY *used_hashes_registry) if (k->options & FACET_KEY_OPTION_MAIN_TEXT) options |= RRDF_FIELD_OPTS_FULL_WIDTH | RRDF_FIELD_OPTS_WRAP; + if (k->options & FACET_KEY_OPTION_EXPANDED_FILTER) + options |= RRDF_FIELD_OPTS_EXPANDED_FILTER; + const char *hash_str = hash_to_static_string(k->hash); buffer_rrdf_table_add_field( @@ -2356,8 +2552,7 @@ void facets_report(FACETS *facets, BUFFER *wb, DICTIONARY *used_hashes_registry) RRDF_FIELD_SORT_FIXED, NULL, RRDF_FIELD_SUMMARY_COUNT, - (k->options & FACET_KEY_OPTION_NEVER_FACET) ? RRDF_FIELD_FILTER_NONE - : RRDF_FIELD_FILTER_FACET, + (k->options & FACET_KEY_OPTION_NEVER_FACET) ? RRDF_FIELD_FILTER_NONE : RRDF_FIELD_FILTER_FACET, options, FACET_VALUE_UNSET); } foreach_key_in_facets_done(k); @@ -2503,6 +2698,8 @@ void facets_report(FACETS *facets, BUFFER *wb, DICTIONARY *used_hashes_registry) if(show_items) { buffer_json_member_add_uint64(wb, "evaluated", facets->operations.rows.evaluated); buffer_json_member_add_uint64(wb, "matched", facets->operations.rows.matched); + buffer_json_member_add_uint64(wb, "unsampled", facets->operations.rows.unsampled); + buffer_json_member_add_uint64(wb, "estimated", facets->operations.rows.estimated); buffer_json_member_add_uint64(wb, "returned", facets->items_to_return); buffer_json_member_add_uint64(wb, "max_to_return", facets->max_items_to_return); buffer_json_member_add_uint64(wb, "before", facets->operations.skips_before); @@ -2533,21 +2730,51 @@ void facets_report(FACETS *facets, BUFFER *wb, DICTIONARY *used_hashes_registry) buffer_json_object_close(wb); // rows buffer_json_member_add_object(wb, "keys"); { + size_t resizes = 0, searches = 0, collisions = 0, used = 0, size = 0, count = 0; + count++; + used += facets->keys.ht.used; + size += facets->keys.ht.size; + resizes += facets->keys.ht.resizes; + searches += facets->keys.ht.searches; + collisions += facets->keys.ht.collisions; + buffer_json_member_add_uint64(wb, "registered", facets->operations.keys.registered); buffer_json_member_add_uint64(wb, "unique", facets->operations.keys.unique); - buffer_json_member_add_uint64(wb, "hashtable_increases", facets->operations.keys.hashtable_increases); + buffer_json_member_add_uint64(wb, "hashtables", count); + buffer_json_member_add_uint64(wb, "hashtable_used", used); + buffer_json_member_add_uint64(wb, "hashtable_size", size); + buffer_json_member_add_uint64(wb, "hashtable_searches", searches); + buffer_json_member_add_uint64(wb, "hashtable_collisions", collisions); + buffer_json_member_add_uint64(wb, "hashtable_resizes", resizes); } buffer_json_object_close(wb); // keys buffer_json_member_add_object(wb, "values"); { + size_t resizes = 0, searches = 0, collisions = 0, used = 0, size = 0, count = 0; + for(FACET_KEY *k = facets->keys.ll; k ; k = k->next) { + count++; + used += k->values.ht.used; + size += k->values.ht.size; + resizes += k->values.ht.resizes; + searches += k->values.ht.searches; + collisions += k->values.ht.collisions; + } + buffer_json_member_add_uint64(wb, "registered", facets->operations.values.registered); buffer_json_member_add_uint64(wb, "transformed", facets->operations.values.transformed); buffer_json_member_add_uint64(wb, "dynamic", facets->operations.values.dynamic); buffer_json_member_add_uint64(wb, "empty", facets->operations.values.empty); + buffer_json_member_add_uint64(wb, "unsampled", facets->operations.values.unsampled); + buffer_json_member_add_uint64(wb, "estimated", facets->operations.values.estimated); buffer_json_member_add_uint64(wb, "indexed", facets->operations.values.indexed); buffer_json_member_add_uint64(wb, "inserts", facets->operations.values.inserts); buffer_json_member_add_uint64(wb, "conflicts", facets->operations.values.conflicts); - buffer_json_member_add_uint64(wb, "hashtable_increases", facets->operations.values.hashtable_increases); + buffer_json_member_add_uint64(wb, "hashtables", count); + buffer_json_member_add_uint64(wb, "hashtable_used", used); + buffer_json_member_add_uint64(wb, "hashtable_size", size); + buffer_json_member_add_uint64(wb, "hashtable_searches", searches); + buffer_json_member_add_uint64(wb, "hashtable_collisions", collisions); + buffer_json_member_add_uint64(wb, "hashtable_resizes", resizes); } buffer_json_object_close(wb); // values buffer_json_member_add_object(wb, "fts"); diff --git a/libnetdata/facets/facets.h b/libnetdata/facets/facets.h index 759725617..8364d8612 100644 --- a/libnetdata/facets/facets.h +++ b/libnetdata/facets/facets.h @@ -6,6 +6,8 @@ #include "../libnetdata.h" #define FACET_VALUE_UNSET "-" +#define FACET_VALUE_UNSAMPLED "[unsampled]" +#define FACET_VALUE_ESTIMATED "[estimated]" typedef enum __attribute__((packed)) { FACETS_ANCHOR_DIRECTION_FORWARD, @@ -31,6 +33,7 @@ typedef enum __attribute__((packed)) { FACET_KEY_OPTION_RICH_TEXT = (1 << 7), FACET_KEY_OPTION_REORDER = (1 << 8), // give the key a new order id on first encounter FACET_KEY_OPTION_TRANSFORM_VIEW = (1 << 9), // when registering the transformation, do it only at the view, not on all data + FACET_KEY_OPTION_EXPANDED_FILTER = (1 << 10), // the presentation should have this filter expanded by default } FACET_KEY_OPTIONS; typedef enum __attribute__((packed)) { @@ -84,11 +87,16 @@ void facets_accepted_param(FACETS *facets, const char *param); void facets_rows_begin(FACETS *facets); bool facets_row_finished(FACETS *facets, usec_t usec); +void facets_row_finished_unsampled(FACETS *facets, usec_t usec); +void facets_update_estimations(FACETS *facets, usec_t from_ut, usec_t to_ut, size_t entries); +size_t facets_histogram_slots(FACETS *facets); + FACET_KEY *facets_register_key_name(FACETS *facets, const char *key, FACET_KEY_OPTIONS options); void facets_set_query(FACETS *facets, const char *query); void facets_set_items(FACETS *facets, uint32_t items); void facets_set_anchor(FACETS *facets, usec_t start_ut, usec_t stop_ut, FACETS_ANCHOR_DIRECTION direction); void facets_enable_slice_mode(FACETS *facets); +bool facets_row_candidate_to_keep(FACETS *facets, usec_t usec); FACET_KEY *facets_register_facet_id(FACETS *facets, const char *key_id, FACET_KEY_OPTIONS options); void facets_register_facet_id_filter(FACETS *facets, const char *key_id, char *value_id, FACET_KEY_OPTIONS options); @@ -115,4 +123,6 @@ uint32_t facets_rows(FACETS *facets); void facets_table_config(BUFFER *wb); +const char *facets_severity_to_string(FACET_ROW_SEVERITY severity); + #endif -- cgit v1.2.3