Home | History | Annotate | Line # | Download | only in isc
      1 /*	$NetBSD: histo_test.c,v 1.3 2026/01/29 18:37:57 christos Exp $	*/
      2 
      3 /*
      4  * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
      5  *
      6  * SPDX-License-Identifier: MPL-2.0
      7  *
      8  * This Source Code Form is subject to the terms of the Mozilla Public
      9  * License, v. 2.0. If a copy of the MPL was not distributed with this
     10  * file, you can obtain one at https://mozilla.org/MPL/2.0/.
     11  *
     12  * See the COPYRIGHT file distributed with this work for additional
     13  * information regarding copyright ownership.
     14  */
     15 
     16 /* ! \file */
     17 
     18 #include <inttypes.h>
     19 #include <math.h>
     20 #include <sched.h> /* IWYU pragma: keep */
     21 #include <setjmp.h>
     22 #include <stdarg.h>
     23 #include <stddef.h>
     24 #include <stdlib.h>
     25 #include <string.h>
     26 
     27 #define UNIT_TESTING
     28 #include <cmocka.h>
     29 
     30 #include <isc/histo.h>
     31 #include <isc/result.h>
     32 #include <isc/time.h>
     33 
     34 #include <tests/isc.h>
     35 
     36 #define TIME_LIMIT (123 * NS_PER_MS)
     37 
     38 #define SUBRANGE 69
     39 
     40 #if VERBOSE
     41 
     42 #define TRACE(fmt, ...)                                                        \
     43 	fprintf(stderr, "%s:%u:%s(): " fmt "\n", __FILE__, __LINE__, __func__, \
     44 		__VA_ARGS__)
     45 
     46 #define TRACETIME(fmt, ...) \
     47 	TRACE("%u bits %.1f ms " fmt, bits, millis_since(start), ##__VA_ARGS__)
     48 
     49 static double
     50 millis_since(isc_nanosecs_t start) {
     51 	isc_nanosecs_t end = isc_time_monotonic();
     52 	return (double)(end - start) / NS_PER_MS;
     53 }
     54 
     55 #else
     56 #define TRACE(...)
     57 #define TRACETIME(...) UNUSED(start)
     58 #endif
     59 
     60 /*
     61  * Note: in many of these tests when adding data to a histogram,
     62  * we need to iterate using `key++` instead of `isc_histo_next()`
     63  * because the latter skips chunks that we want to fill but have
     64  * not yet done so.
     65  */
     66 
     67 ISC_RUN_TEST_IMPL(basics) {
     68 	isc_result_t result;
     69 	for (uint bits = ISC_HISTO_MINBITS; bits <= ISC_HISTO_MAXBITS; bits++) {
     70 		isc_nanosecs_t start = isc_time_monotonic();
     71 
     72 		isc_histo_t *hg = NULL;
     73 		isc_histo_create(mctx, bits, &hg);
     74 
     75 		isc_histo_inc(hg, 0);
     76 
     77 		uint64_t min, max, count;
     78 
     79 		uint64_t prev_max = 0;
     80 		uint key = 0;
     81 		result = isc_histo_get(hg, key, &min, &max, &count);
     82 		while (result == ISC_R_SUCCESS) {
     83 			/* previous iteration already bumped this bucket */
     84 			assert_int_equal(count, 1);
     85 
     86 			/* min maps to this bucket */
     87 			isc_histo_inc(hg, min);
     88 			result = isc_histo_get(hg, key, &min, &max, &count);
     89 			assert_int_equal(result, ISC_R_SUCCESS);
     90 			assert_int_equal(count, 2);
     91 
     92 			/* max maps to this bucket */
     93 			isc_histo_add(hg, max, 2);
     94 			result = isc_histo_get(hg, key, &min, &max, &count);
     95 			assert_int_equal(result, ISC_R_SUCCESS);
     96 			assert_int_equal(count, 4);
     97 
     98 			/* put range covers this bucket */
     99 			isc_histo_put(hg, min, max, 4);
    100 			result = isc_histo_get(hg, key, &min, &max, &count);
    101 			assert_int_equal(result, ISC_R_SUCCESS);
    102 			assert_int_equal(count, 8);
    103 
    104 			if (max < UINT64_MAX) {
    105 				/* max + 1 maps to next bucket */
    106 				isc_histo_inc(hg, max + 1);
    107 				result = isc_histo_get(hg, key, &min, &max,
    108 						       &count);
    109 				assert_int_equal(result, ISC_R_SUCCESS);
    110 				/* this bucket was not bumped */
    111 				assert_int_equal(count, 8);
    112 			}
    113 
    114 			if (key == 0) {
    115 				assert_int_equal(min, 0);
    116 				assert_int_equal(max, 0);
    117 			} else {
    118 				/* no gap between buckets */
    119 				assert_int_equal(min, prev_max + 1);
    120 			}
    121 
    122 			prev_max = max;
    123 			key++;
    124 			result = isc_histo_get(hg, key, &min, &max, &count);
    125 
    126 			/* these tests can be slow */
    127 			if (isc_time_monotonic() > start + TIME_LIMIT) {
    128 				break;
    129 			}
    130 		}
    131 
    132 		/* if we did not stop early */
    133 		if (result != ISC_R_SUCCESS) {
    134 			/* last bucket goes up to last possible value */
    135 			assert_int_equal(max, UINT64_MAX);
    136 
    137 			double pop;
    138 			isc_histo_moments(hg, &pop, NULL, NULL);
    139 			assert_int_equal((uint64_t)pop, key * 8);
    140 		}
    141 
    142 		isc_histo_destroy(&hg);
    143 
    144 		TRACETIME("%u keys", key);
    145 	}
    146 }
    147 
    148 ISC_RUN_TEST_IMPL(quantiles) {
    149 	for (uint bits = ISC_HISTO_MINBITS; bits <= ISC_HISTO_MAXBITS; bits++) {
    150 		isc_result_t result;
    151 		uint64_t min, max, count;
    152 		double pop, mean, sd;
    153 		uint key;
    154 
    155 		isc_nanosecs_t start = isc_time_monotonic();
    156 
    157 		isc_histo_t *hg = NULL;
    158 		isc_histo_create(mctx, bits, &hg);
    159 
    160 		/* ensure empty histogram does not divide by zero */
    161 		isc_histo_moments(hg, &pop, &mean, &sd);
    162 		assert_true(pop == 0.0);
    163 		assert_true(mean == 0.0);
    164 		assert_true(sd == 0.0);
    165 
    166 		for (key = 0; isc_histo_get(hg, key, &min, &max, &count) ==
    167 			      ISC_R_SUCCESS;
    168 		     key++)
    169 		{
    170 			/* inc twice so we can check bucket's midpoint */
    171 			assert_int_equal(count, 0);
    172 			isc_histo_inc(hg, min);
    173 			isc_histo_inc(hg, max);
    174 		}
    175 
    176 		const uint buckets = key;
    177 
    178 		/* no incs were lost */
    179 		isc_histo_moments(hg, &pop, NULL, NULL);
    180 		assert_float_equal(pop, buckets * 2, 0.5);
    181 
    182 		/* two ranks per bucket */
    183 		const uint quantum = ISC_HISTO_MAXQUANTILES / 2 - 1;
    184 		uint64_t value[ISC_HISTO_MAXQUANTILES];
    185 		double frac[ISC_HISTO_MAXQUANTILES];
    186 		uint base = 0;
    187 
    188 		for (key = 0; key < buckets; key++) {
    189 			/* fill in the values one quantum at a time */
    190 			if (key == 0 || key % quantum == buckets % quantum) {
    191 				base = key;
    192 				for (uint k = 0; k < quantum; k++) {
    193 					double rank = (base + k) * 2;
    194 					uint i = (quantum - k) * 2;
    195 					frac[i - 1] = (rank + 1.0) / pop;
    196 					frac[i - 0] = rank / pop;
    197 				}
    198 				frac[0] = (base + quantum) * 2 / pop;
    199 				result = isc_histo_quantiles(
    200 					hg, quantum * 2 + 1, frac, value);
    201 				assert_int_equal(result, ISC_R_SUCCESS);
    202 			}
    203 
    204 			result = isc_histo_get(hg, key, &min, &max, &count);
    205 			assert_int_equal(result, ISC_R_SUCCESS);
    206 			assert_int_equal(count, 2);
    207 
    208 			uint64_t lomin = min == 0 ? min : min - 1;
    209 			uint64_t himin = min;
    210 			uint64_t lomid = floor(min / 2.0 + max / 2.0);
    211 			uint64_t himid = ceil(min / 2.0 + max / 2.0);
    212 			uint64_t lomax = max;
    213 			uint64_t himax = max == UINT64_MAX ? max : max + 1;
    214 
    215 			uint i = (quantum + base - key) * 2;
    216 
    217 			/* check fenceposts */
    218 			assert_uint_in_range(value[i - 0], lomin, himin);
    219 			assert_uint_in_range(value[i - 1], lomid, himid);
    220 			assert_uint_in_range(value[i - 2], lomax, himax);
    221 
    222 			/* these tests can be slow */
    223 			if (isc_time_monotonic() > start + TIME_LIMIT) {
    224 				break;
    225 			}
    226 		}
    227 
    228 		isc_histo_destroy(&hg);
    229 
    230 		TRACETIME("");
    231 	}
    232 }
    233 
    234 /*
    235  * ensure relative error is as expected
    236  */
    237 ISC_RUN_TEST_IMPL(sigfigs) {
    238 	assert_int_equal(ISC_HISTO_MINBITS,
    239 			 isc_histo_digits_to_bits(ISC_HISTO_MINDIGITS));
    240 	assert_int_equal(ISC_HISTO_MINDIGITS,
    241 			 isc_histo_bits_to_digits(ISC_HISTO_MINBITS));
    242 	assert_int_equal(ISC_HISTO_MAXBITS,
    243 			 isc_histo_digits_to_bits(ISC_HISTO_MAXDIGITS));
    244 	assert_int_equal(ISC_HISTO_MAXDIGITS,
    245 			 isc_histo_bits_to_digits(ISC_HISTO_MAXBITS));
    246 
    247 	uint log10 = 1;
    248 	double exp10 = 1.0; /* sigdigs == 1 gives relative error of 1 */
    249 
    250 	for (uint bits = ISC_HISTO_MINBITS; bits <= ISC_HISTO_MAXBITS; bits++) {
    251 		isc_histo_t *hg = NULL;
    252 		isc_histo_create(mctx, bits, &hg);
    253 
    254 		uint digits = isc_histo_bits_to_digits(bits);
    255 		assert_true(bits >= isc_histo_digits_to_bits(digits));
    256 
    257 		if (log10 < digits) {
    258 			log10 += 1;
    259 			exp10 *= 10.0;
    260 			assert_int_equal(log10, digits);
    261 		}
    262 
    263 		TRACE("%u binary %f decimal", 1 << bits, exp10);
    264 
    265 		/* binary precision is better than decimal precision */
    266 		double nominal = 1.0 / (double)(1 << bits);
    267 		assert_true(nominal < 1.0 / exp10);
    268 
    269 		/* start with key = 1 to avoid division by zero */
    270 		uint64_t imin, imax;
    271 		for (uint key = 1; isc_histo_get(hg, key, &imin, &imax, NULL) ==
    272 				   ISC_R_SUCCESS;
    273 		     key++)
    274 		{
    275 			double min = (double)imin;
    276 			double max = (double)imax;
    277 			double error = (max - min) / (max + min);
    278 			assert_true(error < nominal);
    279 		}
    280 
    281 		isc_histo_destroy(&hg);
    282 	}
    283 }
    284 
    285 ISC_RUN_TEST_IMPL(subrange) {
    286 	for (uint bits = ISC_HISTO_MINBITS; bits <= ISC_HISTO_MAXBITS; bits++) {
    287 		isc_result_t result;
    288 		uint64_t min, max, count;
    289 
    290 		isc_nanosecs_t start = isc_time_monotonic();
    291 
    292 		isc_histo_t *hg = NULL;
    293 		isc_histo_create(mctx, bits, &hg);
    294 
    295 		uint64_t value[SUBRANGE + 1];
    296 		double frac[SUBRANGE + 1];
    297 		for (uint i = 0; i <= SUBRANGE; i++) {
    298 			frac[i] = (double)(SUBRANGE - i) / (double)(SUBRANGE);
    299 		}
    300 
    301 		result = isc_histo_quantiles(hg, ARRAY_SIZE(frac), frac, value);
    302 		assert_int_equal(result, ISC_R_UNSET);
    303 
    304 		for (uint key = 0, top = SUBRANGE - 1;; key++, top++) {
    305 			if (isc_histo_get(hg, key, &min, NULL, NULL) !=
    306 			    ISC_R_SUCCESS)
    307 			{
    308 				break;
    309 			}
    310 			if (isc_histo_get(hg, top, NULL, &max, NULL) !=
    311 			    ISC_R_SUCCESS)
    312 			{
    313 				break;
    314 			}
    315 			/*
    316 			 * If we try adding more than one sample per bucket
    317 			 * here, the test fails when buckets have different
    318 			 * sizes because [min,max] spans multiple chunks.
    319 			 */
    320 			isc_histo_put(hg, min, max, SUBRANGE);
    321 
    322 			result = isc_histo_quantiles(hg, ARRAY_SIZE(frac), frac,
    323 						     value);
    324 			assert_int_equal(result, ISC_R_SUCCESS);
    325 
    326 			for (uint bucket = 0; bucket < SUBRANGE; bucket++) {
    327 				result = isc_histo_get(hg, key + bucket, &min,
    328 						       &max, &count);
    329 				assert_int_equal(result, ISC_R_SUCCESS);
    330 				/* did isc_histo_put() spread evenly? */
    331 				assert_int_equal(count, 1);
    332 				/* do the quantile values match? */
    333 				assert_int_equal(value[SUBRANGE - bucket], min);
    334 			}
    335 			assert_int_equal(value[0], max);
    336 
    337 			isc_histo_destroy(&hg);
    338 			isc_histo_create(mctx, bits, &hg);
    339 
    340 			/* these tests can be slow */
    341 			if (isc_time_monotonic() > start + TIME_LIMIT) {
    342 				break;
    343 			}
    344 		}
    345 		isc_histo_destroy(&hg);
    346 
    347 		TRACETIME("");
    348 	}
    349 }
    350 
    351 ISC_TEST_LIST_START
    352 
    353 ISC_TEST_ENTRY(basics)
    354 ISC_TEST_ENTRY(quantiles)
    355 ISC_TEST_ENTRY(sigfigs)
    356 ISC_TEST_ENTRY(subrange)
    357 
    358 ISC_TEST_LIST_END
    359 
    360 ISC_TEST_MAIN
    361