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