4
#include "KolmogorovSmirnovDist.h"
5
6
#define MAX_POINTS 10000
7
-int enable_metric_correlations = CONFIG_BOOLEAN_NO;
7
+int enable_metric_correlations = CONFIG_BOOLEAN_YES;
8
int metric_correlations_version = 1;
9
+METRIC_CORRELATIONS_METHOD default_metric_correlations_method = METRIC_CORRELATIONS_VOLUME;
10
+
11
+typedef struct mc_stats {
12
+ size_t db_points;
13
+ size_t result_points;
14
+ size_t db_queries;
15
+ size_t binary_searches;
16
+} MC_STATS;
17
+
18
+// ----------------------------------------------------------------------------
19
+// parse and render metric correlations methods
20
+
21
+static struct {
22
+ const char *name;
23
+ METRIC_CORRELATIONS_METHOD value;
24
+} metric_correlations_methods[] = {
25
+ { "ks2" , METRIC_CORRELATIONS_KS2 }
26
+ , { "volume" , METRIC_CORRELATIONS_VOLUME }
27
+ , { NULL , 0 }
28
+};
29
+
30
+METRIC_CORRELATIONS_METHOD mc_string_to_method(const char *method) {
31
+ for(int i = 0; metric_correlations_methods[i].name ;i++)
32
+ if(strcmp(method, metric_correlations_methods[i].name) == 0)
33
+ return metric_correlations_methods[i].value;
34
+
35
+ return default_metric_correlations_method;
36
+}
37
+
38
+const char *mc_method_to_string(METRIC_CORRELATIONS_METHOD method) {
39
+ for(int i = 0; metric_correlations_methods[i].name ;i++)
40
+ if(metric_correlations_methods[i].value == method)
41
+ return metric_correlations_methods[i].name;
42
10
-struct charts {
43
+ return "unknown";
44
+}
45
+
46
+// ----------------------------------------------------------------------------
47
+// The results per dimension are aggregated into a dictionary
48
+
49
+struct register_result {
50
RRDSET *st;
12
- struct charts *next;
51
+ const char *chart_id;
52
+ const char *context;
53
+ const char *dim_name;
54
+ calculated_number value;
55
};
56
15
-struct per_dim {
16
- char *dimension;
17
- calculated_number baseline[MAX_POINTS];
18
- calculated_number highlight[MAX_POINTS];
57
+static void register_result_insert_callback(const char *name, void *value, void *data) {
58
+ (void)name;
59
+ (void)data;
60
20
- double baseline_diffs[MAX_POINTS];
21
- double highlight_diffs[MAX_POINTS];
22
-};
61
+ struct register_result *t = (struct register_result *)value;
62
24
-int find_index(double arr[], long int n, double K, long int start)
25
-{
26
- for (long int i = start; i < n; i++) {
27
- if (K<arr[i]){
28
- return i;
29
- }
30
- }
31
- return n;
63
+ if(t->chart_id) t->chart_id = strdupz(t->chart_id);
64
+ if(t->context) t->context = strdupz(t->context);
65
+ if(t->dim_name) t->dim_name = strdupz(t->dim_name);
66
}
67
34
-int compare(const void *left, const void *right) {
35
- double lt = *(double *)left;
36
- double rt = *(double *)right;
68
+static void register_result_delete_callback(const char *name, void *value, void *data) {
69
+ (void)name;
70
+ (void)data;
71
+ struct register_result *t = (struct register_result *)value;
72
38
- if(unlikely(lt < rt)) return -1;
39
- if(unlikely(lt > rt)) return 1;
40
- return 0;
73
+ freez((void *)t->chart_id);
74
+ freez((void *)t->context);
75
+ freez((void *)t->dim_name);
76
}
77
43
-void kstwo(double data1[], long int n1, double data2[], long int n2, double *d, double *prob)
44
-{
45
- double en1, en2, en, data_all[MAX_POINTS*2], cdf1[MAX_POINTS], cdf2[MAX_POINTS], cddiffs[MAX_POINTS];
46
- double min = 0.0, max = 0.0;
47
- qsort(data1, n1, sizeof(double), compare);
48
- qsort(data2, n2, sizeof(double), compare);
78
+static DICTIONARY *register_result_init() {
79
+ DICTIONARY *results = dictionary_create(DICTIONARY_FLAG_SINGLE_THREADED);
80
+ dictionary_register_insert_callback(results, register_result_insert_callback, results);
81
+ dictionary_register_delete_callback(results, register_result_delete_callback, results);
82
+ return results;
83
+}
84
50
- for (int i = 0; i < n1; i++)
51
- data_all[i] = data1[i];
52
- for (int i = 0; i < n2; i++)
53
- data_all[n1 + i] = data2[i];
85
+static void register_result_destroy(DICTIONARY *results) {
86
+ dictionary_destroy(results);
87
+}
88
55
- en1 = (double)n1;
56
- en2 = (double)n2;
57
- *d = 0.0;
58
- cddiffs[0]=0; //for uninitialized warning
89
+static void register_result(DICTIONARY *results, RRDSET *st, RRDDIM *d, calculated_number value) {
90
+ struct register_result t = {
91
+ .st = st,
92
+ .chart_id = st->id,
93
+ .context = st->context,
94
+ .dim_name = d->name,
95
+ .value = value
96
+ };
97
+
98
+ char buf[5000 + 1];
99
+ snprintfz(buf, 5000, "%s:%s", st->id, d->name);
100
+ dictionary_set(results, buf, &t, sizeof(struct register_result));
101
+}
102
60
- for (int i=0; i<n1+n2;i++)
61
- cdf1[i] = find_index(data1, n1, data_all[i], 0) / en1; //TODO, use the start to reduce loops
103
+// ----------------------------------------------------------------------------
104
+// Generation of JSON output for the results
105
+
106
+static size_t registered_results_to_json(DICTIONARY *results, BUFFER *wb,
107
+ long long after, long long before,
108
+ long long baseline_after, long long baseline_before,
109
+ long points, METRIC_CORRELATIONS_METHOD method,
110
+ RRDR_GROUPING group, RRDR_OPTIONS options, uint32_t shifts,
111
+ size_t correlated_dimensions, usec_t duration, MC_STATS *stats) {
112
+
113
+ buffer_sprintf(wb, "{\n"
114
+ "\t\"after\": %lld,\n"
115
+ "\t\"before\": %lld,\n"
116
+ "\t\"duration\": %lld,\n"
117
+ "\t\"points\": %ld,\n"
118
+ "\t\"baseline_after\": %lld,\n"
119
+ "\t\"baseline_before\": %lld,\n"
120
+ "\t\"baseline_duration\": %lld,\n"
121
+ "\t\"baseline_points\": %ld,\n"
122
+ "\t\"statistics\": {\n"
123
+ "\t\t\"query_time_ms\": %f,\n"
124
+ "\t\t\"db_queries\": %zu,\n"
125
+ "\t\t\"db_points_read\": %zu,\n"
126
+ "\t\t\"query_result_points\": %zu,\n"
127
+ "\t\t\"binary_searches\": %zu\n"
128
+ "\t},\n"
129
+ "\t\"group\": \"%s\",\n"
130
+ "\t\"method\": \"%s\",\n"
131
+ "\t\"options\": \"",
132
+ after,
133
+ before,
134
+ before - after,
135
+ points,
136
+ baseline_after,
137
+ baseline_before,
138
+ baseline_before - baseline_after,
139
+ points << shifts,
140
+ (double)duration / (double)USEC_PER_MS,
141
+ stats->db_queries,
142
+ stats->db_points,
143
+ stats->result_points,
144
+ stats->binary_searches,
145
+ web_client_api_request_v1_data_group_to_string(group),
146
+ mc_method_to_string(method));
147
+
148
+ web_client_api_request_v1_data_options_to_string(wb, options);
149
+ buffer_strcat(wb, "\",\n\t\"correlated_charts\": {\n");
150
+
151
+ size_t charts = 0, chart_dims = 0, total_dimensions = 0;
152
+ struct register_result *t;
153
+ RRDSET *last_st = NULL; // never access this - we use it only for comparison
154
+ dfe_start_read(results, t) {
155
+ if(!last_st || t->st != last_st) {
156
+ last_st = t->st;
157
+
158
+ if(charts) buffer_strcat(wb, "\n\t\t\t}\n\t\t},\n");
159
+ buffer_strcat(wb, "\t\t\"");
160
+ buffer_strcat(wb, t->chart_id);
161
+ buffer_strcat(wb, "\": {\n");
162
+ buffer_strcat(wb, "\t\t\t\"context\": \"");
163
+ buffer_strcat(wb, t->context);
164
+ buffer_strcat(wb, "\",\n\t\t\t\"dimensions\": {\n");
165
+ charts++;
166
+ chart_dims = 0;
167
+ }
168
+ if (chart_dims) buffer_sprintf(wb, ",\n");
169
+ buffer_sprintf(wb, "\t\t\t\t\"%s\": " CALCULATED_NUMBER_FORMAT, t->dim_name, t->value);
170
+ chart_dims++;
171
+ total_dimensions++;
172
+ }
173
+ dfe_done(t);
174
+
175
+ // close dimensions and chart
176
+ if (total_dimensions)
177
+ buffer_strcat(wb, "\n\t\t\t}\n\t\t}\n");
178
+
179
+ // close correlated_charts
180
+ buffer_sprintf(wb, "\t},\n"
181
+ "\t\"correlated_dimensions\": %zu,\n"
182
+ "\t\"total_dimensions_count\": %zu\n"
183
+ "}\n",
184
+ total_dimensions,
185
+ correlated_dimensions // yes, we flip them
186
+ );
187
+
188
+ return total_dimensions;
189
+}
190
63
- for (int i=0; i<n1+n2;i++)
64
- cdf2[i] = find_index(data2, n2, data_all[i], 0) / en2;
191
+// ----------------------------------------------------------------------------
192
+// KS2 algorithm functions
193
66
- for ( int i=0;i<n2+n1;i++)
67
- cddiffs[i] = cdf1[i] - cdf2[i];
194
+typedef long int DIFFS_NUMBERS;
195
+#define DOUBLE_TO_INT_MULTIPLIER 100000
196
69
- min = cddiffs[0];
70
- for ( int i=0;i<n2+n1;i++) {
71
- if (cddiffs[i] < min)
72
- min = cddiffs[i];
73
- }
197
+static inline int binary_search_bigger_than(const DIFFS_NUMBERS arr[], int left, int size, DIFFS_NUMBERS K) {
198
+ // binary search to find the index the smallest index
199
+ // of the first value in the array that is greater than K
200
75
- //clip min
76
- if (fabs(min) < 0) min = 0;
77
- else if (fabs(min) > 1) min = 1;
201
+ int right = size;
202
+ while(left < right) {
203
+ int middle = (int)(((unsigned int)(left + right)) >> 1);
204
79
- max = fabs(cddiffs[0]);
80
- for ( int i=0;i<n2+n1;i++)
81
- if (cddiffs[i] >= max) max = cddiffs[i];
205
+ if(arr[middle] > K)
206
+ right = middle;
207
83
- if (fabs(min) < max)
84
- *d = max;
85
- else
86
- *d = fabs(min);
208
+ else
209
+ left = middle + 1;
210
+ }
211
88
-
89
-
90
- en = (en1*en2 / (en1 + en2));
91
- *prob = KSfbar(round(en), *d);
212
+ return left;
213
}
214
94
-void fill_nan (struct per_dim *d, long int hp, long int bp)
95
-{
96
- int k;
215
+int compare_diffs(const void *left, const void *right) {
216
+ DIFFS_NUMBERS lt = *(DIFFS_NUMBERS *)left;
217
+ DIFFS_NUMBERS rt = *(DIFFS_NUMBERS *)right;
218
+
219
+ // https://stackoverflow.com/a/3886497/1114110
220
+ return (lt > rt) - (lt < rt);
221
+}
222
+
223
+static size_t calculate_pairs_diff(DIFFS_NUMBERS *diffs, calculated_number *arr, size_t size) {
224
+ calculated_number *last = &arr[size - 1];
225
+ size_t added = 0;
226
+
227
+ while(last > arr) {
228
+ calculated_number second = *last--;
229
+ calculated_number first = *last;
230
+ *diffs++ = (DIFFS_NUMBERS)((first - second) * (calculated_number)DOUBLE_TO_INT_MULTIPLIER);
231
+ added++;
232
+ }
233
+
234
+ return added;
235
+}
236
98
- for (k = 0; k < bp; k++) {
99
- if (isnan(d->baseline[k])) {
100
- d->baseline[k] = 0.0;
237
+static double ks_2samp(DIFFS_NUMBERS baseline_diffs[], int base_size, DIFFS_NUMBERS highlight_diffs[], int high_size, uint32_t base_shifts) {
238
+
239
+ qsort(baseline_diffs, base_size, sizeof(DIFFS_NUMBERS), compare_diffs);
240
+ qsort(highlight_diffs, high_size, sizeof(DIFFS_NUMBERS), compare_diffs);
241
+
242
+ // Now we should be calculating this:
243
+ //
244
+ // For each number in the diffs arrays, we should find the index of the
245
+ // number bigger than them in both arrays and calculate the % of this index
246
+ // vs the total array size. Once we have the 2 percentages, we should find
247
+ // the min and max across the delta of all of them.
248
+ //
249
+ // It should look like this:
250
+ //
251
+ // base_pcent = binary_search_bigger_than(...) / base_size;
252
+ // high_pcent = binary_search_bigger_than(...) / high_size;
253
+ // delta = base_pcent - high_pcent;
254
+ // if(delta < min) min = delta;
255
+ // if(delta > max) max = delta;
256
+ //
257
+ // This would require a lot of multiplications and divisions.
258
+ //
259
+ // To speed it up, we do the binary search to find the index of each number
260
+ // but then we divide the base index by the power of two number (shifts) it
261
+ // is bigger than high index. So the 2 indexes are now comparable.
262
+ // We also keep track of the original indexes with min and max, to properly
263
+ // calculate their percentages once the loops finish.
264
+
265
+
266
+ // initialize min and max using the first number of baseline_diffs
267
+ DIFFS_NUMBERS K = baseline_diffs[0];
268
+ int base_idx = binary_search_bigger_than(baseline_diffs, 1, base_size, K);
269
+ int high_idx = binary_search_bigger_than(highlight_diffs, 0, high_size, K);
270
+ int delta = base_idx - (high_idx << base_shifts);
271
+ int min = delta, max = delta;
272
+ int base_min_idx = base_idx;
273
+ int base_max_idx = base_idx;
274
+ int high_min_idx = high_idx;
275
+ int high_max_idx = high_idx;
276
+
277
+ // do the baseline_diffs starting from 1 (we did position 0 above)
278
+ for(int i = 1; i < base_size; i++) {
279
+ K = baseline_diffs[i];
280
+ base_idx = binary_search_bigger_than(baseline_diffs, i + 1, base_size, K); // starting from i, since data1 is sorted
281
+ high_idx = binary_search_bigger_than(highlight_diffs, 0, high_size, K);
282
+
283
+ delta = base_idx - (high_idx << base_shifts);
284
+ if(delta < min) {
285
+ min = delta;
286
+ base_min_idx = base_idx;
287
+ high_min_idx = high_idx;
288
+ }
289
+ else if(delta > max) {
290
+ max = delta;
291
+ base_max_idx = base_idx;
292
+ high_max_idx = high_idx;
293
}
294
}
295
104
- for (k = 0; k < hp; k++) {
105
- if (isnan(d->highlight[k])) {
106
- d->highlight[k] = 0.0;
296
+ // do the highlight_diffs starting from 0
297
+ for(int i = 0; i < high_size; i++) {
298
+ K = highlight_diffs[i];
299
+ base_idx = binary_search_bigger_than(baseline_diffs, 0, base_size, K);
300
+ high_idx = binary_search_bigger_than(highlight_diffs, i + 1, high_size, K); // starting from i, since data2 is sorted
301
+
302
+ delta = base_idx - (high_idx << base_shifts);
303
+ if(delta < min) {
304
+ min = delta;
305
+ base_min_idx = base_idx;
306
+ high_min_idx = high_idx;
307
+ }
308
+ else if(delta > max) {
309
+ max = delta;
310
+ base_max_idx = base_idx;
311
+ high_max_idx = high_idx;
312
}
313
}
314
+
315
+ // now we have the min, max and their indexes
316
+ // properly calculate min and max as dmin and dmax
317
+ double dbase_size = (double)base_size;
318
+ double dhigh_size = (double)high_size;
319
+ double dmin = ((double)base_min_idx / dbase_size) - ((double)high_min_idx / dhigh_size);
320
+ double dmax = ((double)base_max_idx / dbase_size) - ((double)high_max_idx / dhigh_size);
321
+
322
+ dmin = -dmin;
323
+ if(islessequal(dmin, 0.0)) dmin = 0.0;
324
+ else if(isgreaterequal(dmin, 1.0)) dmin = 1.0;
325
+
326
+ double d;
327
+ if(isgreaterequal(dmin, dmax)) d = dmin;
328
+ else d = dmax;
329
+
330
+ double en = round(dbase_size * dhigh_size / (dbase_size + dhigh_size));
331
+
332
+ // under these conditions, KSfbar() crashes
333
+ if(unlikely(isnan(en) || isinf(en) || en == 0.0 || isnan(d) || isinf(d)))
334
+ return NAN;
335
+
336
+ return KSfbar((int)en, d);
337
}
338
111
-//TODO check counters
112
-void run_diffs_and_rev (struct per_dim *d, long int hp, long int bp)
113
-{
114
- int k, j;
339
+static double kstwo(calculated_number baseline[], int baseline_points, calculated_number highlight[], int highlight_points, uint32_t base_shifts) {
340
+ // -1 in size, since the calculate_pairs_diffs() returns one less point
341
+ DIFFS_NUMBERS baseline_diffs[baseline_points - 1];
342
+ DIFFS_NUMBERS highlight_diffs[highlight_points - 1];
343
116
- for (k = 0, j = bp; k < bp - 1; k++, j--)
117
- d->baseline_diffs[k] = (double)d->baseline[j - 2] - (double)d->baseline[j - 1];
118
- for (k = 0, j = hp; k < hp - 1; k++, j--) {
119
- d->highlight_diffs[k] = (double)d->highlight[j - 2] - (double)d->highlight[j - 1];
344
+ int base_size = (int)calculate_pairs_diff(baseline_diffs, baseline, baseline_points);
345
+ int high_size = (int)calculate_pairs_diff(highlight_diffs, highlight, highlight_points);
346
+
347
+ if(unlikely(!base_size || !high_size))
348
+ return NAN;
349
+
350
+ if(unlikely(base_size != baseline_points - 1 || high_size != highlight_points - 1)) {
351
+ error("Metric correlations: internal error - calculate_pairs_diff() returns the wrong number of entries");
352
+ return NAN;
353
}
354
+
355
+ return ks_2samp(baseline_diffs, base_size, highlight_diffs, high_size, base_shifts);
356
}
357
123
-int run_metric_correlations (BUFFER *wb, RRDSET *st, long long baseline_after, long long baseline_before, long long highlight_after, long long highlight_before, long long max_points)
124
-{
125
- uint32_t options = 0x00000000;
126
- int group_method = RRDR_GROUPING_AVERAGE;
358
+
359
+static int rrdset_metric_correlations_ks2(RRDSET *st, DICTIONARY *results,
360
+ long long baseline_after, long long baseline_before,
361
+ long long after, long long before,
362
+ long long points, RRDR_OPTIONS options, RRDR_GROUPING group,
363
+ uint32_t shifts, int timeout, MC_STATS *stats) {
364
long group_time = 0;
365
struct context_param *context_param_list = NULL;
129
- long c;
130
- int i=0, j=0;
131
- int b_dims = 0;
132
- long int baseline_points = 0, highlight_points = 0;
366
134
- struct per_dim *pd = NULL;
367
+ int correlated_dimensions = 0;
368
+
369
+ RRDR *high_rrdr = NULL;
370
+ RRDR *base_rrdr = NULL;
371
136
- //TODO get everything in one go, when baseline is right before highlight
137
- //get baseline
372
+ // get first the highlight to find the number of points available
373
+ stats->db_queries++;
374
+ usec_t started_usec = now_realtime_usec();
375
ONEWAYALLOC *owa = onewayalloc_create(0);
139
- RRDR *rb = rrd2rrdr(owa, st, max_points, baseline_after, baseline_before, group_method, group_time, options, NULL, context_param_list, 0);
140
- if(!rb) {
141
- info("Cannot generate metric correlations output with these parameters on this chart.");
142
- onewayalloc_destroy(owa);
143
- return 0;
144
- } else {
145
- baseline_points = rrdr_rows(rb);
146
- pd = mallocz(sizeof(struct per_dim) * rb->d);
147
- b_dims = rb->d;
148
- for (c = 0; c != rrdr_rows(rb) ; ++c) {
149
- RRDDIM *d;
150
- for (j = 0, d = rb->st->dimensions ; d && j < rb->d ; ++j, d = d->next) {
151
- calculated_number *cn = &rb->v[ c * rb->d ];
152
- if (!c) {
153
- //TODO use points from query
154
- pd[j].dimension = strdupz (d->name);
155
- pd[j].baseline[c] = cn[j];
156
- } else {
157
- pd[j].baseline[c] = cn[j];
158
- }
376
+ high_rrdr = rrd2rrdr(owa, st, points,
377
+ after, before, group,
378
+ group_time, options, NULL, context_param_list, timeout);
379
+ if(!high_rrdr) {
380
+ info("Metric correlations: rrd2rrdr() failed for the highlighted window on chart '%s'.", st->name);
381
+ goto cleanup;
382
+ }
383
+ stats->db_points += high_rrdr->internal.db_points_read;
384
+ stats->result_points += high_rrdr->internal.result_points_generated;
385
+ if(!high_rrdr->d) {
386
+ info("Metric correlations: rrd2rrdr() did not return any dimensions on chart '%s'.", st->name);
387
+ goto cleanup;
388
+ }
389
+ if(high_rrdr->result_options & RRDR_RESULT_OPTION_CANCEL) {
390
+ info("Metric correlations: rrd2rrdr() on highlighted window timed out '%s'.", st->name);
391
+ goto cleanup;
392
+ }
393
+ int high_points = rrdr_rows(high_rrdr);
394
+
395
+ usec_t now_usec = now_realtime_usec();
396
+ if(now_usec - started_usec > timeout * USEC_PER_MS)
397
+ goto cleanup;
398
+
399
+ // get the baseline, requesting the same number of points as the highlight
400
+ stats->db_queries++;
401
+ base_rrdr = rrd2rrdr(owa, st,high_points << shifts,
402
+ baseline_after, baseline_before, group,
403
+ group_time, options, NULL, context_param_list,
404
+ (int)(timeout - ((now_usec - started_usec) / USEC_PER_MS)));
405
+ if(!base_rrdr) {
406
+ info("Metric correlations: rrd2rrdr() failed for the baseline window on chart '%s'.", st->name);
407
+ goto cleanup;
408
+ }
409
+ stats->db_points += base_rrdr->internal.db_points_read;
410
+ stats->result_points += base_rrdr->internal.result_points_generated;
411
+ if(!base_rrdr->d) {
412
+ info("Metric correlations: rrd2rrdr() did not return any dimensions on chart '%s'.", st->name);
413
+ goto cleanup;
414
+ }
415
+ if (base_rrdr->d != high_rrdr->d) {
416
+ info("Cannot generate metric correlations for chart '%s' when the baseline and the highlight have different number of dimensions.", st->name);
417
+ goto cleanup;
418
+ }
419
+ if(base_rrdr->result_options & RRDR_RESULT_OPTION_CANCEL) {
420
+ info("Metric correlations: rrd2rrdr() on baseline window timed out '%s'.", st->name);
421
+ goto cleanup;
422
+ }
423
+ int base_points = rrdr_rows(base_rrdr);
424
+
425
+ now_usec = now_realtime_usec();
426
+ if(now_usec - started_usec > timeout * USEC_PER_MS)
427
+ goto cleanup;
428
+
429
+ // we need at least 2 points to do the job
430
+ if(base_points < 2 || high_points < 2)
431
+ goto cleanup;
432
+
433
+ // for each dimension
434
+ RRDDIM *d;
435
+ int i;
436
+ for(i = 0, d = base_rrdr->st->dimensions ; d && i < base_rrdr->d; i++, d = d->next) {
437
+
438
+ // skip the not evaluated ones
439
+ if(unlikely(base_rrdr->od[i] & RRDR_DIMENSION_HIDDEN) || (high_rrdr->od[i] & RRDR_DIMENSION_HIDDEN))
440
+ continue;
441
+
442
+ correlated_dimensions++;
443
+
444
+ // skip the dimensions that are just zero for both the baseline and the highlight
445
+ if(unlikely(!(base_rrdr->od[i] & RRDR_DIMENSION_NONZERO) && !(high_rrdr->od[i] & RRDR_DIMENSION_NONZERO)))
446
+ continue;
447
+
448
+ // copy the baseline points of the dimension to a contiguous array
449
+ // there is no need to check for empty values, since empty are already zero
450
+ calculated_number baseline[base_points];
451
+ for(int c = 0; c < base_points; c++)
452
+ baseline[c] = base_rrdr->v[ c * base_rrdr->d + i ];
453
+
454
+ // copy the highlight points of the dimension to a contiguous array
455
+ // there is no need to check for empty values, since empty values are already zero
456
+ // https://github.com/netdata/netdata/blob/6e3144683a73a2024d51425b20ecfd569034c858/web/api/queries/average/average.c#L41-L43
457
+ calculated_number highlight[high_points];
458
+ for(int c = 0; c < high_points; c++)
459
+ highlight[c] = high_rrdr->v[ c * high_rrdr->d + i ];
460
+
461
+ stats->binary_searches += 2 * (base_points - 1) + 2 * (high_points - 1);
462
+
463
+ double prob = kstwo(baseline, base_points, highlight, high_points, shifts);
464
+ if(!isnan(prob) && !isinf(prob)) {
465
+
466
+ // these conditions should never happen, but still let's check
467
+ if(unlikely(prob < 0.0)) {
468
+ error("Metric correlations: kstwo() returned a negative number: %f", prob);
469
+ prob = -prob;
470
+ }
471
+ if(unlikely(prob > 1.0)) {
472
+ error("Metric correlations: kstwo() returned a number above 1.0: %f", prob);
473
+ prob = 1.0;
474
}
475
+
476
+ // to spread the results evenly, 0.0 needs to be the less correlated and 1.0 the most correlated
477
+ // so we flip the result of kstwo()
478
+ register_result(results, base_rrdr->st, d, 1.0 - prob);
479
}
480
}
162
- rrdr_free(owa, rb);
481
+
482
+cleanup:
483
+ rrdr_free(owa, high_rrdr);
484
+ rrdr_free(owa, base_rrdr);
485
onewayalloc_destroy(owa);
164
- if (!pd)
165
- return 0;
166
-
167
- //get highlight
168
- owa = onewayalloc_create(0);
169
- RRDR *rh = rrd2rrdr(owa, st, max_points, highlight_after, highlight_before, group_method, group_time, options, NULL, context_param_list, 0);
170
- if(!rh) {
171
- info("Cannot generate metric correlations output with these parameters on this chart.");
172
- freez(pd);
173
- onewayalloc_destroy(owa);
174
- return 0;
175
- } else {
176
- if (rh->d != b_dims) {
177
- //TODO handle different dims
178
- rrdr_free(owa, rh);
179
- onewayalloc_destroy(owa);
180
- freez(pd);
181
- return 0;
486
+ return correlated_dimensions;
487
+}
488
+
489
+// ----------------------------------------------------------------------------
490
+// VOLUME algorithm functions
491
+
492
+static int rrdset_metric_correlations_volume(RRDSET *st, DICTIONARY *results,
493
+ long long baseline_after, long long baseline_before,
494
+ long long after, long long before,
495
+ RRDR_OPTIONS options, RRDR_GROUPING group, int timeout, MC_STATS *stats) {
496
+ options |= RRDR_OPTION_MATCH_IDS;
497
+ long group_time = 0;
498
+
499
+ int correlated_dimensions = 0;
500
+ int ret, value_is_null;
501
+ usec_t started_usec = now_realtime_usec();
502
+
503
+ RRDDIM *d;
504
+ for(d = st->dimensions; d ; d = d->next) {
505
+ usec_t now_usec = now_realtime_usec();
506
+ if(now_usec - started_usec > timeout * USEC_PER_MS)
507
+ return correlated_dimensions;
508
+
509
+ // we count how many metrics we evaluated
510
+ correlated_dimensions++;
511
+
512
+ // there is no point to pass a timeout to these queries
513
+ // since the query engine checks for a timeout between
514
+ // dimensions, and we query a single dimension at a time.
515
+
516
+ stats->db_queries++;
517
+ calculated_number highlight_average = NAN;
518
+ value_is_null = 1;
519
+ ret = rrdset2value_api_v1(st, NULL, &highlight_average, d->id, 1,
520
+ after, before,
521
+ group, group_time, options,
522
+ NULL, NULL,
523
+ &stats->db_points, &stats->result_points,
524
+ &value_is_null, 0);
525
+
526
+ if(ret != HTTP_RESP_OK || value_is_null || !calculated_number_isnumber(highlight_average)) {
527
+ // error("Metric correlations: cannot query highlight duration of dimension '%s' of chart '%s', %d %s %s %s", st->name, d->name, ret, (ret != HTTP_RESP_OK)?"response failed":"", (value_is_null)?"value is null":"", (!calculated_number_isnumber(highlight_average))?"result is NAN":"");
528
+ // this means no data for the highlighted duration - so skip it
529
+ continue;
530
}
183
- highlight_points = rrdr_rows(rh);
184
- for (c = 0; c != rrdr_rows(rh) ; ++c) {
185
- RRDDIM *d;
186
- for (j = 0, d = rh->st->dimensions ; d && j < rh->d ; ++j, d = d->next) {
187
- calculated_number *cn = &rh->v[ c * rh->d ];
188
- pd[j].highlight[c] = cn[j];
189
- }
531
+
532
+ stats->db_queries++;
533
+ calculated_number baseline_average = NAN;
534
+ value_is_null = 1;
535
+ ret = rrdset2value_api_v1(st, NULL, &baseline_average, d->id, 1,
536
+ baseline_after, baseline_before,
537
+ group, group_time, options,
538
+ NULL, NULL,
539
+ &stats->db_points, &stats->result_points,
540
+ &value_is_null, 0);
541
+
542
+ if(ret != HTTP_RESP_OK || value_is_null || !calculated_number_isnumber(baseline_average)) {
543
+ // error("Metric correlations: cannot query baseline duration of dimension '%s' of chart '%s', %d %s %s %s", st->name, d->name, ret, (ret != HTTP_RESP_OK)?"response failed":"", (value_is_null)?"value is null":"", (!calculated_number_isnumber(baseline_average))?"result is NAN":"");
544
+ // continue;
545
+ // this means no data for the baseline window, but we have data for the highlighted one - assume zero
546
+ baseline_average = 0.0;
547
}
548
+
549
+ calculated_number pcent = NAN;
550
+ if(isgreater(baseline_average, 0.0) || isless(baseline_average, 0.0))
551
+ pcent = (highlight_average - baseline_average) / baseline_average;
552
+
553
+ else if(isgreater(highlight_average, 0.0) || isless(highlight_average, 0.0))
554
+ pcent = highlight_average;
555
+
556
+ if(!isnan(pcent))
557
+ register_result(results, st, d, pcent);
558
}
192
- rrdr_free(owa, rh);
193
- onewayalloc_destroy(owa);
559
195
- for (i = 0; i < b_dims; i++) {
196
- fill_nan(&pd[i], highlight_points, baseline_points);
560
+ return correlated_dimensions;
561
+}
562
+
563
+int compare_calculated_numbers(const void *left, const void *right) {
564
+ calculated_number lt = *(calculated_number *)left;
565
+ calculated_number rt = *(calculated_number *)right;
566
+
567
+ // https://stackoverflow.com/a/3886497/1114110
568
+ return (lt > rt) - (lt < rt);
569
+}
570
+
571
+static inline int binary_search_bigger_than_calculated_number(const calculated_number arr[], int left, int size, calculated_number K) {
572
+ // binary search to find the index the smallest index
573
+ // of the first value in the array that is greater than K
574
+
575
+ int right = size;
576
+ while(left < right) {
577
+ int middle = (int)(((unsigned int)(left + right)) >> 1);
578
+
579
+ if(arr[middle] > K)
580
+ right = middle;
581
+
582
+ else
583
+ left = middle + 1;
584
}
585
199
- for (i = 0; i < b_dims; i++) {
200
- run_diffs_and_rev(&pd[i], highlight_points, baseline_points);
586
+ return left;
587
+}
588
+
589
+// ----------------------------------------------------------------------------
590
+// spread the results evenly according to their value
591
+
592
+static size_t spread_results_evenly(DICTIONARY *results) {
593
+ struct register_result *t;
594
+
595
+ // count the dimensions
596
+ size_t dimensions = dictionary_stats_entries(results);
597
+ if(!dimensions) return 0;
598
+
599
+ // create an array of the right size and copy all the values in it
600
+ calculated_number slots[dimensions];
601
+ dimensions = 0;
602
+ dfe_start_read(results, t) {
603
+ t->value = calculated_number_fabs(t->value);
604
+ slots[dimensions++] = t->value;
605
}
606
+ dfe_done(t);
607
203
- double d=0, prob=0;
204
- for (i=0;i < j ;i++) {
205
- if (baseline_points && highlight_points) {
206
- kstwo(pd[i].baseline_diffs, baseline_points-1, pd[i].highlight_diffs, highlight_points-1, &d, &prob);
207
- buffer_sprintf(wb, "\t\t\t\t\"%s\": %f", pd[i].dimension, prob);
208
- if (i != j-1)
209
- buffer_sprintf(wb, ",\n");
210
- else
211
- buffer_sprintf(wb, "\n");
212
- }
608
+ // sort the array with the values of all dimensions
609
+ qsort(slots, dimensions, sizeof(calculated_number), compare_calculated_numbers);
610
+
611
+ // skip the duplicates in the sorted array
612
+ calculated_number last_value = NAN;
613
+ size_t unique_values = 0;
614
+ for(size_t i = 0; i < dimensions ;i++) {
615
+ if(likely(slots[i] != last_value))
616
+ slots[unique_values++] = last_value = slots[i];
617
}
618
215
- freez(pd);
216
- return j;
619
+ // calculate the weight of each slot, using the number of unique values
620
+ calculated_number slot_weight = 1.0 / (calculated_number)unique_values;
621
+
622
+ dfe_start_read(results, t) {
623
+ int slot = binary_search_bigger_than_calculated_number(slots, 0, (int)unique_values, t->value);
624
+ calculated_number v = slot * slot_weight;
625
+ if(unlikely(v > 1.0)) v = 1.0;
626
+ v = 1.0 - v;
627
+ t->value = v;
628
+ }
629
+ dfe_done(t);
630
+
631
+ return dimensions;
632
}
633
219
-void metric_correlations (RRDHOST *host, BUFFER *wb, long long baseline_after, long long baseline_before, long long highlight_after, long long highlight_before, long long max_points)
220
-{
221
- info ("Running metric correlations, highlight_after: %lld, highlight_before: %lld, baseline_after: %lld, baseline_before: %lld, max_points: %lld", highlight_after, highlight_before, baseline_after, baseline_before, max_points);
634
+// ----------------------------------------------------------------------------
635
+// The main function
636
+
637
+int metric_correlations(RRDHOST *host, BUFFER *wb, METRIC_CORRELATIONS_METHOD method, RRDR_GROUPING group,
638
+ long long baseline_after, long long baseline_before,
639
+ long long after, long long before,
640
+ long long points, RRDR_OPTIONS options, int timeout) {
641
223
- if (!enable_metric_correlations) {
224
- error("Metric correlations functionality is not enabled.");
642
+ // method = METRIC_CORRELATIONS_VOLUME;
643
+ // options |= RRDR_OPTION_ANOMALY_BIT;
644
+
645
+ MC_STATS stats = {};
646
+
647
+ if (enable_metric_correlations == CONFIG_BOOLEAN_NO) {
648
buffer_strcat(wb, "{\"error\": \"Metric correlations functionality is not enabled.\" }");
226
- return;
649
+ return HTTP_RESP_FORBIDDEN;
650
}
651
229
- if (highlight_before <= highlight_after || baseline_before <= baseline_after) {
230
- error("Invalid baseline or highlight ranges.");
652
+ // if the user didn't give a timeout
653
+ // assume 60 seconds
654
+ if(!timeout)
655
+ timeout = 60 * MSEC_PER_SEC;
656
+
657
+ // if the timeout is less than 1 second
658
+ // make it at least 1 second
659
+ if(timeout < (long)(1 * MSEC_PER_SEC))
660
+ timeout = 1 * MSEC_PER_SEC;
661
+
662
+ usec_t timeout_usec = timeout * USEC_PER_MS;
663
+ usec_t started_usec = now_realtime_usec();
664
+
665
+ if(!points) points = 500;
666
+
667
+ rrdr_relative_window_to_absolute(&after, &before, default_rrd_update_every, points);
668
+
669
+ if(baseline_before <= API_RELATIVE_TIME_MAX)
670
+ baseline_before += after;
671
+
672
+ rrdr_relative_window_to_absolute(&baseline_after, &baseline_before, default_rrd_update_every, points * 4);
673
+
674
+ if (before <= after || baseline_before <= baseline_after) {
675
buffer_strcat(wb, "{\"error\": \"Invalid baseline or highlight ranges.\" }");
232
- return;
676
+ return HTTP_RESP_BAD_REQUEST;
677
}
678
235
- long long dims = 0, total_dims = 0;
236
- RRDSET *st;
237
- size_t c = 0;
238
- BUFFER *wdims = buffer_create(1000);
679
+ DICTIONARY *results = register_result_init();
680
+ DICTIONARY *charts = dictionary_create(DICTIONARY_FLAG_SINGLE_THREADED|DICTIONARY_FLAG_VALUE_LINK_DONT_CLONE);;
681
+
682
+ char *error = NULL;
683
+ int resp = HTTP_RESP_OK;
684
+
685
+ // baseline should be a power of two multiple of highlight
686
+ uint32_t shifts = 0;
687
+ {
688
+ long long base_delta = baseline_before - baseline_after;
689
+ long long high_delta = before - after;
690
+ uint32_t multiplier = (uint32_t)round((double)base_delta / (double)high_delta);
691
+
692
+ // check if the multiplier is a power of two
693
+ // https://stackoverflow.com/a/600306/1114110
694
+ if((multiplier & (multiplier - 1)) != 0) {
695
+ // it is not power of two
696
+ // let's find the closest power of two
697
+ // https://stackoverflow.com/a/466242/1114110
698
+ multiplier--;
699
+ multiplier |= multiplier >> 1;
700
+ multiplier |= multiplier >> 2;
701
+ multiplier |= multiplier >> 4;
702
+ multiplier |= multiplier >> 8;
703
+ multiplier |= multiplier >> 16;
704
+ multiplier++;
705
+ }
706
+
707
+ // convert the multiplier to the number of shifts
708
+ // we need to do, to divide baseline numbers to match
709
+ // the highlight ones
710
+ while(multiplier > 1) {
711
+ shifts++;
712
+ multiplier = multiplier >> 1;
713
+ }
714
+
715
+ // if the baseline size will not comply to MAX_POINTS
716
+ // lower the window of the baseline
717
+ while(shifts && (points << shifts) > MAX_POINTS)
718
+ shifts--;
719
240
- if (!max_points || max_points > MAX_POINTS)
241
- max_points = MAX_POINTS;
720
+ // if the baseline size still does not comply to MAX_POINTS
721
+ // lower the resolution of the highlight and the baseline
722
+ while((points << shifts) > MAX_POINTS)
723
+ points = points >> 1;
724
+
725
+ if(points < 100) {
726
+ // error = "cannot comply to at least 100 points";
727
+ resp = HTTP_RESP_BAD_REQUEST;
728
+ goto cleanup;
729
+ }
730
+
731
+ // adjust the baseline to be multiplier times bigger than the highlight
732
+ baseline_after = baseline_before - (high_delta << shifts);
733
+ }
734
243
- //dont lock here and wait for results
244
- //get the charts and run mc after
245
- //should not be a problem for the query
246
- struct charts *charts = NULL;
735
+ // dont lock here and wait for results
736
+ // get the charts and run mc after
737
+ RRDSET *st;
738
rrdhost_rdlock(host);
739
rrdset_foreach_read(st, host) {
249
- if (rrdset_is_available_for_viewers(st)) {
250
- rrdset_rdlock(st);
251
- struct charts *chart = callocz(1, sizeof(struct charts));
252
- chart->st = st;
253
- chart->next = NULL;
254
- if (charts) {
255
- chart->next = charts;
256
- }
257
- charts = chart;
258
- }
740
+ if (rrdset_is_available_for_viewers(st))
741
+ dictionary_set(charts, st->name, "", 1);
742
}
743
rrdhost_unlock(host);
744
262
- buffer_strcat(wb, "{\n\t\"correlated_charts\": {");
745
+ size_t correlated_dimensions = 0;
746
+ void *ptr;
747
264
- for (struct charts *ch = charts; ch; ch = ch->next) {
265
- buffer_flush(wdims);
266
- dims = run_metric_correlations(wdims, ch->st, baseline_after, baseline_before, highlight_after, highlight_before, max_points);
267
- if (dims) {
268
- if (c)
269
- buffer_strcat(wb, "\t\t},");
270
- buffer_strcat(wb, "\n\t\t\"");
271
- buffer_strcat(wb, ch->st->id);
272
- buffer_strcat(wb, "\": {\n");
273
- buffer_strcat(wb, "\t\t\t\"context\": \"");
274
- buffer_strcat(wb, ch->st->context);
275
- buffer_strcat(wb, "\",\n\t\t\t\"dimensions\": {\n");
276
- buffer_sprintf(wb, "%s", buffer_tostring(wdims));
277
- buffer_strcat(wb, "\t\t\t}\n");
278
- total_dims += dims;
279
- c++;
748
+ // for every chart in the dictionary
749
+ dfe_start_read(charts, ptr) {
750
+ usec_t now_usec = now_realtime_usec();
751
+ if(now_usec - started_usec > timeout_usec) {
752
+ error = "timed out";
753
+ resp = HTTP_RESP_GATEWAY_TIMEOUT;
754
+ goto cleanup;
755
+ }
756
+
757
+ st = rrdset_find_byname(host, ptr_name);
758
+ if(!st) continue;
759
+
760
+ rrdset_rdlock(st);
761
+
762
+ switch(method) {
763
+ case METRIC_CORRELATIONS_VOLUME:
764
+ correlated_dimensions += rrdset_metric_correlations_volume(st, results,
765
+ baseline_after, baseline_before,
766
+ after, before,
767
+ options, group,
768
+ (int)(timeout - ((now_usec - started_usec) / USEC_PER_MS)),
769
+ &stats);
770
+ break;
771
+
772
+ default:
773
+ case METRIC_CORRELATIONS_KS2:
774
+ correlated_dimensions += rrdset_metric_correlations_ks2(st, results,
775
+ baseline_after, baseline_before,
776
+ after, before,
777
+ points, options, group, shifts,
778
+ (int)(timeout - ((now_usec - started_usec) / USEC_PER_MS)),
779
+ &stats);
780
+ break;
781
}
782
+
783
+ rrdset_unlock(st);
784
}
282
- buffer_strcat(wb, "\t\t}\n");
283
- buffer_sprintf(wb, "\t},\n\t\"total_dimensions_count\": %lld\n}", total_dims);
785
+ dfe_done(ptr);
786
285
- if (!total_dims) {
286
- buffer_flush(wb);
287
- buffer_strcat(wb, "{\"error\": \"No results from metric correlations.\" }");
787
+ if(!(options & RRDR_OPTION_RETURN_RAW))
788
+ spread_results_evenly(results);
789
+
790
+ usec_t ended_usec = now_realtime_usec();
791
+
792
+ // generate the json output we need
793
+ buffer_flush(wb);
794
+ size_t added_dimensions = registered_results_to_json(results, wb,
795
+ after, before,
796
+ baseline_after, baseline_before,
797
+ points, method, group, options, shifts, correlated_dimensions,
798
+ ended_usec - started_usec, &stats);
799
+
800
+ if(!added_dimensions) {
801
+ error = "no results produced from correlations";
802
+ resp = HTTP_RESP_NOT_FOUND;
803
}
804
290
- struct charts* ch;
291
- while(charts){
292
- ch = charts;
293
- charts = charts->next;
294
- rrdset_unlock(ch->st);
295
- free(ch);
805
+cleanup:
806
+ if(charts) dictionary_destroy(charts);
807
+ if(results) register_result_destroy(results);
808
+
809
+ if(error) {
810
+ buffer_flush(wb);
811
+ buffer_sprintf(wb, "{\"error\": \"%s\" }", error);
812
}
813
298
- buffer_free(wdims);
299
- info ("Done running metric correlations");
814
+ return resp;
815
+}
816
+
817
+
818
+
819
+// ----------------------------------------------------------------------------
820
+// unittest
821
+
822
+/*
823
+
824
+Unit tests against the output of this:
825
+
826
+https://github.com/scipy/scipy/blob/4cf21e753cf937d1c6c2d2a0e372fbc1dbbeea81/scipy/stats/_stats_py.py#L7275-L7449
827
+
828
+import matplotlib.pyplot as plt
829
+import pandas as pd
830
+import numpy as np
831
+import scipy as sp
832
+from scipy import stats
833
+
834
+data1 = np.array([ 1111, -2222, 33, 100, 100, 15555, -1, 19999, 888, 755, -1, -730 ])
835
+data2 = np.array([365, -123, 0])
836
+data1 = np.sort(data1)
837
+data2 = np.sort(data2)
838
+n1 = data1.shape[0]
839
+n2 = data2.shape[0]
840
+data_all = np.concatenate([data1, data2])
841
+cdf1 = np.searchsorted(data1, data_all, side='right') / n1
842
+cdf2 = np.searchsorted(data2, data_all, side='right') / n2
843
+print(data_all)
844
+print("\ndata1", data1, cdf1)
845
+print("\ndata2", data2, cdf2)
846
+cddiffs = cdf1 - cdf2
847
+print("\ncddiffs", cddiffs)
848
+minS = np.clip(-np.min(cddiffs), 0, 1)
849
+maxS = np.max(cddiffs)
850
+print("\nmin", minS)
851
+print("max", maxS)
852
+m, n = sorted([float(n1), float(n2)], reverse=True)
853
+en = m * n / (m + n)
854
+d = max(minS, maxS)
855
+prob = stats.distributions.kstwo.sf(d, np.round(en))
856
+print("\nprob", prob)
857
+
858
+*/
859
+
860
+static int double_expect(double v, const char *str, const char *descr) {
861
+ char buf[100 + 1];
862
+ snprintfz(buf, 100, "%0.6f", v);
863
+ int ret = strcmp(buf, str) ? 1 : 0;
864
+
865
+ fprintf(stderr, "%s %s, expected %s, got %s\n", ret?"FAILED":"OK", descr, str, buf);
866
+ return ret;
867
+}
868
+
869
+static int mc_unittest1(void) {
870
+ int bs = 3, hs = 3;
871
+ DIFFS_NUMBERS base[3] = { 1, 2, 3 };
872
+ DIFFS_NUMBERS high[3] = { 3, 4, 6 };
873
+
874
+ double prob = ks_2samp(base, bs, high, hs, 0);
875
+ return double_expect(prob, "0.222222", "3x3");
876
+}
877
+
878
+static int mc_unittest2(void) {
879
+ int bs = 6, hs = 3;
880
+ DIFFS_NUMBERS base[6] = { 1, 2, 3, 10, 10, 15 };
881
+ DIFFS_NUMBERS high[3] = { 3, 4, 6 };
882
+
883
+ double prob = ks_2samp(base, bs, high, hs, 1);
884
+ return double_expect(prob, "0.500000", "6x3");
885
+}
886
+
887
+static int mc_unittest3(void) {
888
+ int bs = 12, hs = 3;
889
+ DIFFS_NUMBERS base[12] = { 1, 2, 3, 10, 10, 15, 111, 19999, 8, 55, -1, -73 };
890
+ DIFFS_NUMBERS high[3] = { 3, 4, 6 };
891
+
892
+ double prob = ks_2samp(base, bs, high, hs, 2);
893
+ return double_expect(prob, "0.347222", "12x3");
894
}
895
+
896
+static int mc_unittest4(void) {
897
+ int bs = 12, hs = 3;
898
+ DIFFS_NUMBERS base[12] = { 1111, -2222, 33, 100, 100, 15555, -1, 19999, 888, 755, -1, -730 };
899
+ DIFFS_NUMBERS high[3] = { 365, -123, 0 };
900
+
901
+ double prob = ks_2samp(base, bs, high, hs, 2);
902
+ return double_expect(prob, "0.777778", "12x3");
903
+}
904
+
905
+int mc_unittest(void) {
906
+ int errors = 0;
907
+
908
+ errors += mc_unittest1();
909
+ errors += mc_unittest2();
910
+ errors += mc_unittest3();
911
+ errors += mc_unittest4();
912
+
913
+ return errors;
914
+}
915
+