1 | /* |
2 | * hist_track.c |
3 | * |
4 | * Copyright (C) 2012-2016 Aerospike, Inc. |
5 | * |
6 | * Portions may be licensed to Aerospike, Inc. under one or more contributor |
7 | * license agreements. |
8 | * |
9 | * This program is free software: you can redistribute it and/or modify it under |
10 | * the terms of the GNU Affero General Public License as published by the Free |
11 | * Software Foundation, either version 3 of the License, or (at your option) any |
12 | * later version. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, but WITHOUT |
15 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
16 | * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
17 | * details. |
18 | * |
19 | * You should have received a copy of the GNU Affero General Public License |
20 | * along with this program. If not, see http://www.gnu.org/licenses/ |
21 | */ |
22 | |
23 | /* |
24 | * A histogram with cached data. |
25 | */ |
26 | |
27 | |
28 | //========================================================== |
29 | // Includes |
30 | // |
31 | |
32 | #include "hist_track.h" |
33 | |
34 | #include <stdbool.h> |
35 | #include <stdint.h> |
36 | #include <stdio.h> |
37 | #include <stdlib.h> |
38 | #include <string.h> |
39 | #include <time.h> |
40 | |
41 | #include <aerospike/as_atomic.h> |
42 | #include <citrusleaf/alloc.h> |
43 | |
44 | #include "cf_mutex.h" |
45 | #include "dynbuf.h" |
46 | #include "fault.h" |
47 | #include "hist.h" |
48 | |
49 | |
50 | //========================================================== |
51 | // Private "Class Members" |
52 | // |
53 | |
54 | //------------------------------------------------ |
55 | // Constants |
56 | // |
57 | |
58 | // More than one day of 10 second slices uses too much memory. |
59 | const uint32_t MAX_NUM_ROWS = (24 * 60 * 60) / 10; |
60 | |
61 | // Caching this few is legal but silly. |
62 | const uint32_t MIN_NUM_ROWS = 2; |
63 | |
64 | // Don't track/report thresholds with a larger bucket index than this. |
65 | // This corresponds to the 32 second threshold - that should be big enough. |
66 | #define MAX_BUCKET 15 |
67 | |
68 | // Don't track/report more than this many thresholds. |
69 | // This could in principle be less than (MAX_BUCKET + 1), e.g. it could be |
70 | // 4, and we could track buckets 0, 5, 10, 15. |
71 | #define MAX_NUM_COLS (MAX_BUCKET + 1) |
72 | |
73 | #define DEFAULT_NUM_COLS 3 |
74 | const uint32_t default_buckets[DEFAULT_NUM_COLS] = { 0, 3, 6 }; |
75 | // For our standard latency histograms, 0: >1ms, 3: >8ms, 6: >64ms. |
76 | |
77 | // No output line can be longer than this. |
78 | #define MAX_FORMATTED_ROW_SIZE 512 |
79 | #define MAX_FORMATTED_SETTINGS_SIZE 512 |
80 | |
81 | //------------------------------------------------ |
82 | // Data |
83 | // |
84 | |
85 | typedef struct row_s { |
86 | uint32_t timestamp; |
87 | uint64_t total; |
88 | uint64_t overs[]; |
89 | } row; |
90 | |
91 | struct cf_hist_track_s { |
92 | // Base Histogram (must be first) |
93 | histogram hist; |
94 | |
95 | // Tracking-related |
96 | row* rows; |
97 | size_t row_size; |
98 | uint32_t num_rows; |
99 | uint32_t write_row_n; |
100 | uint32_t oldest_row_n; |
101 | cf_mutex rows_lock; |
102 | uint32_t slice_sec; |
103 | uint32_t buckets[MAX_NUM_COLS]; |
104 | uint32_t num_cols; |
105 | }; |
106 | |
107 | //------------------------------------------------ |
108 | // Function Declarations |
109 | // |
110 | |
111 | static inline row* get_row(cf_hist_track* this, uint32_t row_n); |
112 | static uint32_t get_start_row_n(cf_hist_track* this, uint32_t back_sec); |
113 | static void output_header(cf_hist_track* this, uint32_t start_ts, |
114 | uint32_t num_cols, cf_hist_track_info_format info_fmt, |
115 | cf_dyn_buf* db_p); |
116 | static void output_slice(cf_hist_track* this, row* prev_row_p, row* row_p, |
117 | uint32_t diff_sec, uint32_t num_cols, |
118 | cf_hist_track_info_format info_fmt, cf_dyn_buf* db_p); |
119 | static int threshold_to_bucket(int threshold); |
120 | static int thresholds_to_buckets(const char* thresholds, uint32_t buckets[]); |
121 | |
122 | |
123 | //========================================================== |
124 | // Public API |
125 | // |
126 | |
127 | //------------------------------------------------ |
128 | // Create a cf_hist_track object. |
129 | // |
130 | cf_hist_track* |
131 | cf_hist_track_create(const char* name, histogram_scale scale) |
132 | { |
133 | cf_assert(name, AS_INFO, "null histogram name" ); |
134 | cf_assert(strlen(name) < HISTOGRAM_NAME_SIZE, AS_INFO, |
135 | "bad histogram name %s" , name); |
136 | cf_assert(scale >= 0 && scale < HIST_SCALE_MAX_PLUS_1, AS_INFO, |
137 | "bad histogram scale %d" , scale); |
138 | |
139 | cf_hist_track* this = (cf_hist_track*)cf_malloc(sizeof(cf_hist_track)); |
140 | |
141 | cf_mutex_init(&this->rows_lock); |
142 | |
143 | // Base histogram setup, same as in histogram_create(): |
144 | strcpy(this->hist.name, name); |
145 | memset((void*)this->hist.counts, 0, sizeof(this->hist.counts)); |
146 | |
147 | // If cf_hist_track_insert_data_point() is called for a size or count |
148 | // histogram, the divide by 0 will crash - consider that a high-performance |
149 | // assert. |
150 | |
151 | switch (scale) { |
152 | case HIST_MILLISECONDS: |
153 | this->hist.scale_tag = HIST_TAG_MILLISECONDS; |
154 | this->hist.time_div = 1000 * 1000; |
155 | break; |
156 | case HIST_MICROSECONDS: |
157 | this->hist.scale_tag = HIST_TAG_MICROSECONDS; |
158 | this->hist.time_div = 1000; |
159 | break; |
160 | case HIST_SIZE: |
161 | this->hist.scale_tag = HIST_TAG_SIZE; |
162 | this->hist.time_div = 0; |
163 | break; |
164 | case HIST_COUNT: |
165 | this->hist.scale_tag = HIST_TAG_COUNT; |
166 | this->hist.time_div = 0; |
167 | break; |
168 | default: |
169 | cf_crash(AS_INFO, "%s: unrecognized histogram scale %d" , name, scale); |
170 | break; |
171 | } |
172 | |
173 | // Start with tracking off. |
174 | this->rows = NULL; |
175 | |
176 | return this; |
177 | } |
178 | |
179 | //------------------------------------------------ |
180 | // Destroy a cf_hist_track object. |
181 | // |
182 | void |
183 | cf_hist_track_destroy(cf_hist_track* this) |
184 | { |
185 | cf_hist_track_stop(this); |
186 | cf_mutex_destroy(&this->rows_lock); |
187 | cf_free(this); |
188 | } |
189 | |
190 | //------------------------------------------------ |
191 | // Start tracking. May call this again without |
192 | // first calling cf_hist_track_disable() to use |
193 | // different caching parameters, but previous |
194 | // cache is lost. |
195 | // |
196 | // TODO - resolve errors ??? |
197 | bool |
198 | cf_hist_track_start(cf_hist_track* this, uint32_t back_sec, uint32_t slice_sec, |
199 | const char* thresholds) |
200 | { |
201 | if (slice_sec == 0) { |
202 | return false; |
203 | } |
204 | |
205 | uint32_t num_rows = back_sec / slice_sec; |
206 | |
207 | // Check basic sanity of row-related parameters. |
208 | if (num_rows > MAX_NUM_ROWS || num_rows < MIN_NUM_ROWS) { |
209 | return false; |
210 | } |
211 | |
212 | // If thresholds aren't specified, use defaults. |
213 | uint32_t* buckets = (uint32_t*)default_buckets; |
214 | int num_cols = DEFAULT_NUM_COLS; |
215 | |
216 | // Parse non-default thresholds and check resulting buckets. |
217 | uint32_t parsed_buckets[MAX_NUM_COLS]; |
218 | |
219 | if (thresholds) { |
220 | buckets = parsed_buckets; |
221 | num_cols = thresholds_to_buckets(thresholds, buckets); |
222 | |
223 | if (num_cols < 0) { |
224 | return false; |
225 | } |
226 | } |
227 | |
228 | cf_mutex_lock(&this->rows_lock); |
229 | |
230 | if (this->rows) { |
231 | cf_free(this->rows); |
232 | } |
233 | |
234 | this->row_size = sizeof(row) + (num_cols * sizeof(uint64_t)); |
235 | this->rows = (row*)cf_malloc(num_rows * this->row_size); |
236 | this->num_rows = num_rows; |
237 | this->write_row_n = 0; |
238 | this->oldest_row_n = 0; |
239 | this->slice_sec = slice_sec; |
240 | |
241 | for (int i = 0; i < num_cols; i++) { |
242 | this->buckets[i] = buckets[i]; |
243 | } |
244 | |
245 | this->num_cols = (uint32_t)num_cols; |
246 | |
247 | cf_mutex_unlock(&this->rows_lock); |
248 | |
249 | return true; |
250 | } |
251 | |
252 | //------------------------------------------------ |
253 | // Stop tracking, freeing cache. |
254 | // |
255 | void |
256 | cf_hist_track_stop(cf_hist_track* this) |
257 | { |
258 | cf_mutex_lock(&this->rows_lock); |
259 | |
260 | if (this->rows) { |
261 | cf_free(this->rows); |
262 | this->rows = NULL; |
263 | } |
264 | |
265 | cf_mutex_unlock(&this->rows_lock); |
266 | } |
267 | |
268 | //------------------------------------------------ |
269 | // Clear histogram buckets, and if tracking, stop. |
270 | // Must call cf_hist_track_enable() to start |
271 | // tracking again. |
272 | // |
273 | void |
274 | cf_hist_track_clear(cf_hist_track* this) |
275 | { |
276 | cf_hist_track_stop(this); |
277 | histogram_clear((histogram*)this); |
278 | } |
279 | |
280 | //------------------------------------------------ |
281 | // Print all non-zero histogram buckets, and if |
282 | // tracking, cache timestamp, total data points, |
283 | // and threshold data. |
284 | // |
285 | void |
286 | cf_hist_track_dump(cf_hist_track* this) |
287 | { |
288 | // Always print the histogram. |
289 | histogram_dump((histogram*)this); |
290 | |
291 | // If tracking is enabled, save a row in the cache. |
292 | cf_mutex_lock(&this->rows_lock); |
293 | |
294 | if (! this->rows) { |
295 | cf_mutex_unlock(&this->rows_lock); |
296 | return; |
297 | } |
298 | |
299 | uint32_t now_ts = (uint32_t)time(NULL); |
300 | |
301 | // But don't save row if slice_sec hasn't elapsed since last saved row. |
302 | if (this->write_row_n != 0 && |
303 | now_ts - get_row(this, this->write_row_n - 1)->timestamp < |
304 | this->slice_sec) { |
305 | cf_mutex_unlock(&this->rows_lock); |
306 | return; |
307 | } |
308 | |
309 | row* row_p = get_row(this, this->write_row_n); |
310 | |
311 | // "Freeze" the histogram for consistency of total. |
312 | uint64_t counts[N_BUCKETS]; |
313 | uint64_t total_count = 0; |
314 | |
315 | for (int j = 0; j < N_BUCKETS; j++) { |
316 | counts[j] = as_load_uint64(&this->hist.counts[j]); |
317 | total_count += counts[j]; |
318 | } |
319 | |
320 | uint64_t subtotal = 0; |
321 | |
322 | // b's "over" is total minus sum of values in all buckets 0 thru b. |
323 | for (int i = 0, b = 0; i < this->num_cols; b++) { |
324 | subtotal += counts[b]; |
325 | |
326 | if (this->buckets[i] == b) { |
327 | row_p->overs[i++] = total_count - subtotal; |
328 | } |
329 | } |
330 | |
331 | row_p->total = total_count; |
332 | row_p->timestamp = now_ts; |
333 | |
334 | // Increment the current and oldest row indexes. |
335 | this->write_row_n++; |
336 | |
337 | if (this->write_row_n > this->num_rows) { |
338 | this->oldest_row_n++; |
339 | } |
340 | |
341 | cf_mutex_unlock(&this->rows_lock); |
342 | } |
343 | |
344 | //------------------------------------------------ |
345 | // Pass-through to base histogram. |
346 | // |
347 | uint64_t |
348 | cf_hist_track_insert_data_point(cf_hist_track* this, uint64_t start_ns) |
349 | { |
350 | return histogram_insert_data_point((histogram*)this, start_ns); |
351 | } |
352 | |
353 | //------------------------------------------------ |
354 | // Pass-through to base histogram. |
355 | // |
356 | void |
357 | cf_hist_track_insert_raw(cf_hist_track* this, uint64_t value) |
358 | { |
359 | histogram_insert_raw((histogram*)this, value); |
360 | } |
361 | |
362 | //------------------------------------------------ |
363 | // Get time-sliced info from cache. |
364 | // |
365 | void |
366 | cf_hist_track_get_info(cf_hist_track* this, uint32_t back_sec, |
367 | uint32_t duration_sec, uint32_t slice_sec, bool throughput_only, |
368 | cf_hist_track_info_format info_fmt, cf_dyn_buf* db_p) |
369 | { |
370 | cf_mutex_lock(&this->rows_lock); |
371 | |
372 | if (! this->rows) { |
373 | cf_dyn_buf_append_string(db_p, "error-not-tracking;" ); |
374 | cf_mutex_unlock(&this->rows_lock); |
375 | return; |
376 | } |
377 | |
378 | uint32_t start_row_n = get_start_row_n(this, back_sec); |
379 | |
380 | if (start_row_n == -1) { |
381 | cf_dyn_buf_append_string(db_p, "error-no-data-yet-or-back-too-small;" ); |
382 | cf_mutex_unlock(&this->rows_lock); |
383 | return; |
384 | } |
385 | |
386 | uint32_t num_cols = throughput_only ? 0 : this->num_cols; |
387 | row* prev_row_p = get_row(this, start_row_n); |
388 | |
389 | output_header(this, prev_row_p->timestamp, num_cols, info_fmt, db_p); |
390 | |
391 | if (slice_sec == 0) { |
392 | row* row_p = get_row(this, this->write_row_n - 1); |
393 | uint32_t diff_sec = row_p->timestamp - prev_row_p->timestamp; |
394 | |
395 | output_slice(this, prev_row_p, row_p, diff_sec, num_cols, info_fmt, |
396 | db_p); |
397 | |
398 | cf_mutex_unlock(&this->rows_lock); |
399 | return; |
400 | } |
401 | |
402 | uint32_t start_ts = prev_row_p->timestamp; |
403 | bool no_slices = true; |
404 | |
405 | for (uint32_t row_n = start_row_n + 1; row_n < this->write_row_n; row_n++) { |
406 | row* row_p = get_row(this, row_n); |
407 | |
408 | uint32_t diff_sec = row_p->timestamp - prev_row_p->timestamp; |
409 | |
410 | if (diff_sec < slice_sec) { |
411 | continue; |
412 | } |
413 | |
414 | output_slice(this, prev_row_p, row_p, diff_sec, num_cols, info_fmt, |
415 | db_p); |
416 | no_slices = false; |
417 | |
418 | // Doing this at the end guarantees we get at least one slice. |
419 | if (duration_sec != 0 && row_p->timestamp - start_ts > duration_sec) { |
420 | break; |
421 | } |
422 | |
423 | prev_row_p = row_p; |
424 | } |
425 | |
426 | if (no_slices) { |
427 | cf_dyn_buf_append_string(db_p, |
428 | "error-slice-too-big-or-back-too-small;" ); |
429 | } |
430 | |
431 | cf_mutex_unlock(&this->rows_lock); |
432 | } |
433 | |
434 | //------------------------------------------------ |
435 | // Get current settings which were passed into |
436 | // cf_hist_track_start(), in format suitable for |
437 | // info_command_config_get(). |
438 | // |
439 | void |
440 | cf_hist_track_get_settings(cf_hist_track* this, cf_dyn_buf* db_p) |
441 | { |
442 | cf_mutex_lock(&this->rows_lock); |
443 | |
444 | if (! this->rows) { |
445 | cf_mutex_unlock(&this->rows_lock); |
446 | return; |
447 | } |
448 | |
449 | const char* name = ((histogram*)this)->name; |
450 | char output[MAX_FORMATTED_SETTINGS_SIZE]; |
451 | char* write_p = output; |
452 | char* end_p = output + MAX_FORMATTED_SETTINGS_SIZE - 2; |
453 | |
454 | write_p += snprintf(output, MAX_FORMATTED_SETTINGS_SIZE - 2, |
455 | "%s-hist-track-back=%u;" |
456 | "%s-hist-track-slice=%u;" |
457 | "%s-hist-track-thresholds=" , |
458 | name, this->num_rows * this->slice_sec, |
459 | name, this->slice_sec, |
460 | name); |
461 | |
462 | for (int i = 0; i < this->num_cols; i++) { |
463 | write_p += snprintf(write_p, end_p - write_p, "%u," , |
464 | (uint32_t)1 << this->buckets[i]); |
465 | } |
466 | |
467 | if (this->num_cols > 0) { |
468 | write_p--; |
469 | } |
470 | |
471 | *write_p++ = ';'; |
472 | *write_p = 0; |
473 | |
474 | cf_dyn_buf_append_string(db_p, output); |
475 | |
476 | cf_mutex_unlock(&this->rows_lock); |
477 | } |
478 | |
479 | |
480 | //========================================================== |
481 | // Private Functions |
482 | // |
483 | |
484 | //------------------------------------------------ |
485 | // Get row pointer for specified row count. Note |
486 | // that row_size is determined dynamically, so we |
487 | // can't just do rows[i]. |
488 | // |
489 | static inline row* |
490 | get_row(cf_hist_track* this, uint32_t row_n) |
491 | { |
492 | return (row*)((uint8_t*)this->rows + |
493 | ((row_n % this->num_rows) * this->row_size)); |
494 | } |
495 | |
496 | //------------------------------------------------ |
497 | // Find row at or after timestamp specified by |
498 | // back_sec. |
499 | // |
500 | static uint32_t |
501 | get_start_row_n(cf_hist_track* this, uint32_t back_sec) |
502 | { |
503 | // Must be at least two rows to get a slice. |
504 | if (this->write_row_n < 2) { |
505 | return -1; |
506 | } |
507 | |
508 | uint32_t now_ts = (uint32_t)time(NULL); |
509 | |
510 | // In case we call this with default back_sec (0) or back_sec more than UTC |
511 | // epoch to now - start from the beginning. |
512 | if (back_sec == 0 || back_sec >= now_ts) { |
513 | return this->oldest_row_n; |
514 | } |
515 | |
516 | uint32_t start_ts = now_ts - back_sec; |
517 | |
518 | // Find the most recent slice interval. |
519 | uint32_t last_row_n = this->write_row_n - 1; |
520 | uint32_t slice_sec = get_row(this, last_row_n)->timestamp - |
521 | get_row(this, last_row_n - 1)->timestamp; |
522 | |
523 | // Use recent slice interval to guess how many rows back to look. |
524 | uint32_t back_row_n = back_sec / slice_sec; |
525 | uint32_t guess_row_n = last_row_n > back_row_n ? |
526 | last_row_n - back_row_n : 0; |
527 | |
528 | if (guess_row_n < this->oldest_row_n) { |
529 | guess_row_n = this->oldest_row_n; |
530 | } |
531 | |
532 | // Begin at guessed row, and iterate to find exact row to start at. |
533 | uint32_t guess_ts = get_row(this, guess_row_n)->timestamp; |
534 | uint32_t start_row_n; |
535 | |
536 | if (guess_ts < start_ts) { |
537 | for (start_row_n = guess_row_n + 1; start_row_n < last_row_n; |
538 | start_row_n++) { |
539 | if (get_row(this, start_row_n)->timestamp >= start_ts) { |
540 | break; |
541 | } |
542 | } |
543 | } |
544 | else if (guess_ts > start_ts) { |
545 | for (start_row_n = guess_row_n; start_row_n > this->oldest_row_n; |
546 | start_row_n--) { |
547 | if (get_row(this, start_row_n - 1)->timestamp < start_ts) { |
548 | break; |
549 | } |
550 | } |
551 | } |
552 | else { |
553 | start_row_n = guess_row_n; |
554 | } |
555 | |
556 | // Make sure when default query is run (e.g. latency:), we return at least |
557 | // valid last data instead of returning an error. This case happens when the |
558 | // query is timed such that it's right when histogram is being dumped. |
559 | if (start_row_n == last_row_n) { |
560 | start_row_n = last_row_n - 1; |
561 | } |
562 | |
563 | // Can't get a slice if there isn't at least one row after the start row. |
564 | return start_row_n < last_row_n ? start_row_n : -1; |
565 | } |
566 | |
567 | //------------------------------------------------ |
568 | // Make info "header" and append it to db_p. |
569 | // |
570 | static void |
571 | (cf_hist_track* this, uint32_t start_ts, uint32_t num_cols, |
572 | cf_hist_track_info_format info_fmt, cf_dyn_buf* db_p) |
573 | { |
574 | cf_dyn_buf_append_string(db_p, ((histogram*)this)->name); |
575 | |
576 | const char* time_fmt; |
577 | const char* rate_fmt; |
578 | const char* pcts_fmt; |
579 | char line_sep; |
580 | |
581 | switch (info_fmt) { |
582 | case CF_HIST_TRACK_FMT_PACKED: |
583 | default: |
584 | time_fmt = ":%T-GMT" ; |
585 | rate_fmt = ",ops/sec" ; |
586 | pcts_fmt = ",>%ums" ; |
587 | line_sep = ';'; |
588 | break; |
589 | case CF_HIST_TRACK_FMT_TABLE: |
590 | time_fmt = ":\n%T GMT % > (ms)" ; |
591 | rate_fmt = "\n to ops/sec" ; |
592 | pcts_fmt = " %6u" ; |
593 | line_sep = '\n'; |
594 | break; |
595 | } |
596 | |
597 | char output[MAX_FORMATTED_ROW_SIZE]; |
598 | char* write_p = output; |
599 | char* end_p = output + MAX_FORMATTED_ROW_SIZE - 2; |
600 | time_t start_ts_time_t = (time_t)start_ts; |
601 | struct tm start_tm; |
602 | |
603 | gmtime_r(&start_ts_time_t, &start_tm); |
604 | write_p += strftime(output, MAX_FORMATTED_ROW_SIZE - 2, time_fmt, &start_tm); |
605 | write_p += snprintf(write_p, end_p - write_p, "%s" , rate_fmt); |
606 | |
607 | for (int i = 0; i < num_cols; i++) { |
608 | write_p += snprintf(write_p, end_p - write_p, pcts_fmt, |
609 | (uint32_t)(1 << this->buckets[i])); |
610 | } |
611 | |
612 | *write_p++ = line_sep; |
613 | *write_p = 0; |
614 | |
615 | cf_dyn_buf_append_string(db_p, output); |
616 | } |
617 | |
618 | //------------------------------------------------ |
619 | // Calculate output info for slice defined by two |
620 | // rows, and append to db_p. |
621 | // |
622 | static void |
623 | output_slice(cf_hist_track* this, row* prev_row_p, row* row_p, |
624 | uint32_t diff_sec, uint32_t num_cols, |
625 | cf_hist_track_info_format info_fmt, cf_dyn_buf* db_p) |
626 | { |
627 | const char* time_fmt; |
628 | const char* rate_fmt; |
629 | const char* pcts_fmt; |
630 | char line_sep; |
631 | |
632 | switch (info_fmt) { |
633 | case CF_HIST_TRACK_FMT_PACKED: |
634 | default: |
635 | time_fmt = "%T" ; |
636 | rate_fmt = ",%.1f" ; |
637 | pcts_fmt = ",%.2f" ; |
638 | line_sep = ';'; |
639 | break; |
640 | case CF_HIST_TRACK_FMT_TABLE: |
641 | time_fmt = "%T" ; |
642 | rate_fmt = " %9.1f" ; |
643 | pcts_fmt = " %6.2f" ; |
644 | line_sep = '\n'; |
645 | break; |
646 | } |
647 | |
648 | char output[MAX_FORMATTED_ROW_SIZE]; |
649 | char* write_p = output; |
650 | char* end_p = output + MAX_FORMATTED_ROW_SIZE - 2; |
651 | time_t row_ts_time_t = (time_t)row_p->timestamp; |
652 | struct tm row_tm; |
653 | |
654 | gmtime_r(&row_ts_time_t, &row_tm); |
655 | write_p += strftime(output, MAX_FORMATTED_ROW_SIZE - 2, time_fmt, &row_tm); |
656 | |
657 | uint64_t diff_total = row_p->total - prev_row_p->total; |
658 | double ops_per_sec = (double)(diff_total) / diff_sec; |
659 | |
660 | write_p += snprintf(write_p, end_p - write_p, rate_fmt, ops_per_sec); |
661 | |
662 | for (int i = 0; i < num_cols; i++) { |
663 | // We "freeze" the histogram to calculate "overs", so it shouldn't be |
664 | // possible for an "over" to be less than the one in the previous row. |
665 | uint64_t diff_overs = row_p->overs[i] - prev_row_p->overs[i]; |
666 | double pcts_over_i = diff_total != 0 ? |
667 | (double)(diff_overs * 100) / diff_total : 0; |
668 | |
669 | write_p += snprintf(write_p, end_p - write_p, pcts_fmt, pcts_over_i); |
670 | } |
671 | |
672 | *write_p++ = line_sep; |
673 | *write_p = 0; |
674 | |
675 | cf_dyn_buf_append_string(db_p, output); |
676 | } |
677 | |
678 | //------------------------------------------------ |
679 | // Convert threshold milliseconds to bucket index. |
680 | // |
681 | static int |
682 | threshold_to_bucket(int threshold) |
683 | { |
684 | if (threshold < 1) { |
685 | return -1; |
686 | } |
687 | |
688 | int n = threshold; |
689 | int b = 0; |
690 | |
691 | while (n > 1) { |
692 | n >>= 1; |
693 | b++; |
694 | } |
695 | |
696 | // Check that threshold is an exact power of 2. |
697 | return (1 << b) == threshold ? b : -1; |
698 | } |
699 | |
700 | //------------------------------------------------ |
701 | // Convert thresholds string to buckets array. |
702 | // |
703 | static int |
704 | thresholds_to_buckets(const char* thresholds, uint32_t buckets[]) |
705 | { |
706 | // Copy since strtok_r() is destructive. |
707 | char toks[strlen(thresholds) + 1]; |
708 | |
709 | strcpy(toks, thresholds); |
710 | |
711 | char* save_ptr = NULL; |
712 | char* tok = strtok_r(toks, "," , &save_ptr); |
713 | |
714 | int i = 0; |
715 | |
716 | while (tok) { |
717 | if (i == MAX_NUM_COLS) { |
718 | return -1; |
719 | } |
720 | |
721 | int b = threshold_to_bucket(atoi(tok)); |
722 | |
723 | // Make sure it's a rising sequence of valid bucket indexes. |
724 | if (b < 0 || b > MAX_BUCKET || (i > 0 && b <= buckets[i - 1])) { |
725 | return -1; |
726 | } |
727 | |
728 | buckets[i++] = (uint32_t)b; |
729 | |
730 | tok = strtok_r(NULL, "," , &save_ptr); |
731 | } |
732 | |
733 | return i; |
734 | } |
735 | |