1 /*
2 * Copyright 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 //#define LOG_NDEBUG 0
18 #define LOG_TAG "audio_utils_statistics_tests"
19 #include <audio_utils/Statistics.h>
20
21 #include <random>
22 #include <stdio.h>
23 #include <gtest/gtest.h>
24
25 // create uniform distribution
26 template <typename T, typename V>
initUniform(V & data,T rangeMin,T rangeMax)27 static void initUniform(V& data, T rangeMin, T rangeMax) {
28 const size_t count = data.capacity();
29 std::minstd_rand gen(count);
30 std::uniform_real_distribution<T> dis(rangeMin, rangeMax);
31
32 // for_each works for scalars
33 for (auto& datum : data) {
34 android::audio_utils::for_each(datum, [&](T &value) { return value = dis(gen);});
35 }
36 }
37
38 // create gaussian distribution
39 template <typename T, typename V>
initNormal(V & data,T mean,T stddev)40 static void initNormal(V& data, T mean, T stddev) {
41 const size_t count = data.capacity();
42 std::minstd_rand gen(count);
43
44 // values near the mean are the most likely
45 // standard deviation affects the dispersion of generated values from the mean
46 std::normal_distribution<> dis{mean, stddev};
47
48 // for_each works for scalars
49 for (auto& datum : data) {
50 android::audio_utils::for_each(datum, [&](T &value) { return value = dis(gen);});
51 }
52 }
53
54 // Used to create compile-time reference constants for variance testing.
55 template <typename T>
56 class ConstexprStatistics {
57 public:
58 template <size_t N>
ConstexprStatistics(const T (& a)[N])59 explicit constexpr ConstexprStatistics(const T (&a)[N])
60 : mN{N}
61 , mMax{android::audio_utils::max(a)}
62 , mMin{android::audio_utils::min(a)}
63 , mMean{android::audio_utils::sum(a) / mN}
64 , mM2{android::audio_utils::sumSqDiff(a, mMean)}
65 , mPopVariance{mM2 / mN}
66 , mPopStdDev{android::audio_utils::sqrt_constexpr(mPopVariance)}
67 , mVariance{mM2 / (mN - 1)}
68 , mStdDev{android::audio_utils::sqrt_constexpr(mVariance)}
69 { }
70
getN() const71 constexpr int64_t getN() const { return mN; }
getMin() const72 constexpr T getMin() const { return mMin; }
getMax() const73 constexpr T getMax() const { return mMax; }
getWeight() const74 constexpr double getWeight() const { return (double)mN; }
getMean() const75 constexpr double getMean() const { return mMean; }
getVariance() const76 constexpr double getVariance() const { return mVariance; }
getStdDev() const77 constexpr double getStdDev() const { return mStdDev; }
getPopVariance() const78 constexpr double getPopVariance() const { return mPopVariance; }
getPopStdDev() const79 constexpr double getPopStdDev() const { return mPopStdDev; }
80
81 private:
82 const size_t mN;
83 const T mMax;
84 const T mMin;
85 const double mMean;
86 const double mM2;
87 const double mPopVariance;
88 const double mPopStdDev;
89 const double mVariance;
90 const double mStdDev;
91 };
92
93 class StatisticsTest : public testing::TestWithParam<const char *>
94 {
95 };
96
97 // find power of 2 that is small enough that it doesn't add to 1. due to finite mantissa.
98 template <typename T>
smallp2()99 constexpr T smallp2() {
100 T smallOne{};
101 for (smallOne = T{1.}; smallOne + T{1.} > T{1.}; smallOne *= T(0.5));
102 return smallOne;
103 }
104
105 // Our near expectation is 16x the bit that doesn't fit the mantissa.
106 // this works so long as we add values close in exponent with each other
107 // realizing that errors accumulate as the sqrt of N (random walk, lln, etc).
108 #define TEST_EXPECT_NEAR(e, v) \
109 EXPECT_NEAR((e), (v), abs((e) * std::numeric_limits<decltype(e)>::epsilon() * 8))
110
111 #define PRINT_AND_EXPECT_EQ(expected, expr) { \
112 auto value = (expr); \
113 printf("(%s): %s\n", #expr, std::to_string(value).c_str()); \
114 if ((expected) == (expected)) { EXPECT_EQ((expected), (value)); } \
115 EXPECT_EQ((expected) != (expected), (value) != (value)); /* nan check */\
116 }
117
118 #define PRINT_AND_EXPECT_NEAR(expected, expr) { \
119 auto ref = (expected); \
120 auto value = (expr); \
121 printf("(%s): %s\n", #expr, std::to_string(value).c_str()); \
122 TEST_EXPECT_NEAR(ref, value); \
123 }
124
125 template <typename T, typename S>
verify(const T & stat,const S & refstat)126 static void verify(const T &stat, const S &refstat) {
127 EXPECT_EQ(refstat.getN(), stat.getN());
128 EXPECT_EQ(refstat.getMin(), stat.getMin());
129 EXPECT_EQ(refstat.getMax(), stat.getMax());
130 TEST_EXPECT_NEAR(refstat.getWeight(), stat.getWeight());
131 TEST_EXPECT_NEAR(refstat.getMean(), stat.getMean());
132 TEST_EXPECT_NEAR(refstat.getVariance(), stat.getVariance());
133 TEST_EXPECT_NEAR(refstat.getStdDev(), stat.getStdDev());
134 TEST_EXPECT_NEAR(refstat.getPopVariance(), stat.getPopVariance());
135 TEST_EXPECT_NEAR(refstat.getPopStdDev(), stat.getPopStdDev());
136 }
137
138 // Test against fixed reference
139
TEST(StatisticsTest,high_precision_sums)140 TEST(StatisticsTest, high_precision_sums)
141 {
142 static const double simple[] = { 1., 2., 3. };
143
144 double rssum = android::audio_utils::sum<double, double>(simple);
145 PRINT_AND_EXPECT_EQ(6., rssum);
146 double kssum =
147 android::audio_utils::sum<double, android::audio_utils::KahanSum<double>>(simple);
148 PRINT_AND_EXPECT_EQ(6., kssum);
149 double nmsum =
150 android::audio_utils::sum<double, android::audio_utils::NeumaierSum<double>>(simple);
151 PRINT_AND_EXPECT_EQ(6., nmsum);
152
153 double rs{};
154 android::audio_utils::KahanSum<double> ks{};
155 android::audio_utils::NeumaierSum<double> ns{};
156
157 // add 1.
158 rs += 1.;
159 ks += 1.;
160 ns += 1.;
161
162 static constexpr double smallOne = std::numeric_limits<double>::epsilon() * 0.5;
163 // add lots of small values
164 static const int loop = 1000;
165 for (int i = 0; i < loop; ++i) {
166 rs += smallOne;
167 ks += smallOne;
168 ns += smallOne;
169 }
170
171 // remove 1.
172 rs += -1.;
173 ks += -1.;
174 ns += -1.;
175
176 const double totalAdded = smallOne * loop;
177 printf("totalAdded: %lg\n", totalAdded);
178 PRINT_AND_EXPECT_EQ(0., rs); // normal count fails
179 PRINT_AND_EXPECT_EQ(totalAdded, ks); // kahan succeeds
180 PRINT_AND_EXPECT_EQ(totalAdded, ns); // neumaier succeeds
181
182 // test case where kahan fails and neumaier method succeeds.
183 static const double tricky[] = { 1e100, 1., -1e100 };
184
185 rssum = android::audio_utils::sum<double, double>(tricky);
186 PRINT_AND_EXPECT_EQ(0., rssum);
187 kssum = android::audio_utils::sum<double, android::audio_utils::KahanSum<double>>(tricky);
188 PRINT_AND_EXPECT_EQ(0., kssum);
189 nmsum = android::audio_utils::sum<double, android::audio_utils::NeumaierSum<double>>(tricky);
190 PRINT_AND_EXPECT_EQ(1., nmsum);
191 }
192
TEST(StatisticsTest,minmax_bounds)193 TEST(StatisticsTest, minmax_bounds)
194 {
195 // range based min and max use iterator forms of min and max.
196
197 static constexpr double one[] = { 1. };
198
199 PRINT_AND_EXPECT_EQ(std::numeric_limits<double>::infinity(),
200 android::audio_utils::min(&one[0], &one[0]));
201
202 PRINT_AND_EXPECT_EQ(-std::numeric_limits<double>::infinity(),
203 android::audio_utils::max(&one[0], &one[0]));
204
205 static constexpr int un[] = { 1 };
206
207 PRINT_AND_EXPECT_EQ(std::numeric_limits<int>::max(),
208 android::audio_utils::min(&un[0], &un[0]));
209
210 PRINT_AND_EXPECT_EQ(std::numeric_limits<int>::min(),
211 android::audio_utils::max(&un[0], &un[0]));
212
213 double nanarray[] = { nan(""), nan(""), nan("") };
214
215 PRINT_AND_EXPECT_EQ(std::numeric_limits<double>::infinity(),
216 android::audio_utils::min(nanarray));
217
218 PRINT_AND_EXPECT_EQ(-std::numeric_limits<double>::infinity(),
219 android::audio_utils::max(nanarray));
220
221 android::audio_utils::Statistics<double> s(nanarray);
222
223 PRINT_AND_EXPECT_EQ(std::numeric_limits<double>::infinity(),
224 s.getMin());
225
226 PRINT_AND_EXPECT_EQ(-std::numeric_limits<double>::infinity(),
227 s.getMax());
228 }
229
230 /*
231 TEST(StatisticsTest, sqrt_convergence)
232 {
233 union {
234 int i;
235 float f;
236 } u;
237
238 for (int i = 0; i < INT_MAX; ++i) {
239 u.i = i;
240 const float f = u.f;
241 if (!android::audio_utils::isnan(f)) {
242 const float sf = android::audio_utils::sqrt(f);
243 if ((i & (1 << 16) - 1) == 0) {
244 printf("i: %d f:%f sf:%f\n", i, f, sf);
245 }
246 }
247 }
248 }
249 */
250
TEST(StatisticsTest,minmax_simple_array)251 TEST(StatisticsTest, minmax_simple_array)
252 {
253 static constexpr double ary[] = { -1.5, 1.5, -2.5, 2.5 };
254
255 PRINT_AND_EXPECT_EQ(-2.5, android::audio_utils::min(ary));
256
257 PRINT_AND_EXPECT_EQ(2.5, android::audio_utils::max(ary));
258
259 static constexpr int ray[] = { -1, 1, -2, 2 };
260
261 PRINT_AND_EXPECT_EQ(-2, android::audio_utils::min(ray));
262
263 PRINT_AND_EXPECT_EQ(2, android::audio_utils::max(ray));
264 }
265
TEST(StatisticsTest,sqrt)266 TEST(StatisticsTest, sqrt)
267 {
268 // check doubles
269 PRINT_AND_EXPECT_EQ(std::numeric_limits<double>::infinity(),
270 android::audio_utils::sqrt(std::numeric_limits<double>::infinity()));
271
272 PRINT_AND_EXPECT_EQ(std::nan(""),
273 android::audio_utils::sqrt(-std::numeric_limits<double>::infinity()));
274
275 PRINT_AND_EXPECT_NEAR(sqrt(std::numeric_limits<double>::epsilon()),
276 android::audio_utils::sqrt(std::numeric_limits<double>::epsilon()));
277
278 PRINT_AND_EXPECT_EQ(3.,
279 android::audio_utils::sqrt(9.));
280
281 PRINT_AND_EXPECT_EQ(0.,
282 android::audio_utils::sqrt(0.));
283
284 PRINT_AND_EXPECT_EQ(std::nan(""),
285 android::audio_utils::sqrt(-1.));
286
287 PRINT_AND_EXPECT_EQ(std::nan(""),
288 android::audio_utils::sqrt(std::nan("")));
289
290 // check floats
291 PRINT_AND_EXPECT_EQ(std::numeric_limits<float>::infinity(),
292 android::audio_utils::sqrt(std::numeric_limits<float>::infinity()));
293
294 PRINT_AND_EXPECT_EQ(std::nanf(""),
295 android::audio_utils::sqrt(-std::numeric_limits<float>::infinity()));
296
297 PRINT_AND_EXPECT_NEAR(sqrtf(std::numeric_limits<float>::epsilon()),
298 android::audio_utils::sqrt(std::numeric_limits<float>::epsilon()));
299
300 PRINT_AND_EXPECT_EQ(2.f,
301 android::audio_utils::sqrt(4.f));
302
303 PRINT_AND_EXPECT_EQ(0.f,
304 android::audio_utils::sqrt(0.f));
305
306 PRINT_AND_EXPECT_EQ(std::nanf(""),
307 android::audio_utils::sqrt(-1.f));
308
309 PRINT_AND_EXPECT_EQ(std::nanf(""),
310 android::audio_utils::sqrt(std::nanf("")));
311 }
312
TEST(StatisticsTest,stat_reference)313 TEST(StatisticsTest, stat_reference)
314 {
315 // fixed reference compile time constants.
316 static constexpr double data[] = {0.1, -0.1, 0.2, -0.3};
317 static constexpr ConstexprStatistics<double> rstat(data); // use alpha = 1.
318 static constexpr android::audio_utils::Statistics<double> stat{data};
319
320 verify(stat, rstat);
321 }
322
TEST(StatisticsTest,stat_variable_alpha)323 TEST(StatisticsTest, stat_variable_alpha)
324 {
325 constexpr size_t TEST_SIZE = 1 << 20;
326 std::vector<double> data(TEST_SIZE);
327 std::vector<double> alpha(TEST_SIZE);
328
329 initUniform(data, -1., 1.);
330 initUniform(alpha, .95, .99);
331
332 android::audio_utils::ReferenceStatistics<double> rstat;
333 android::audio_utils::Statistics<double> stat;
334
335 static_assert(std::is_trivially_copyable<decltype(stat)>::value,
336 "basic statistics must be trivially copyable");
337
338 for (size_t i = 0; i < TEST_SIZE; ++i) {
339 rstat.setAlpha(alpha[i]);
340 rstat.add(data[i]);
341
342 stat.setAlpha(alpha[i]);
343 stat.add(data[i]);
344 }
345
346 printf("statistics: %s\n", stat.toString().c_str());
347 printf("ref statistics: %s\n", rstat.toString().c_str());
348 verify(stat, rstat);
349 }
350
TEST(StatisticsTest,stat_vector)351 TEST(StatisticsTest, stat_vector)
352 {
353 // for operator overloading...
354 using namespace android::audio_utils;
355
356 using data_t = std::tuple<double, double>;
357 using covariance_t = std::tuple<double, double, double, double>;
358 using covariance_ut_t = std::tuple<double, double, double>;
359
360 constexpr size_t TEST_SIZE = 1 << 20;
361 std::vector<data_t> data(TEST_SIZE);
362 // std::vector<double> alpha(TEST_SIZE);
363
364 initUniform(data, -1., 1.);
365
366 std::cout << "sample data[0]: " << data[0] << "\n";
367
368 Statistics<data_t, data_t, data_t, double, double, innerProduct_scalar<data_t>> stat;
369 Statistics<data_t, data_t, data_t, double,
370 covariance_t, outerProduct_tuple<data_t>> stat_outer;
371 Statistics<data_t, data_t, data_t, double,
372 covariance_ut_t, outerProduct_UT_tuple<data_t>> stat_outer_ut;
373
374 using pair_t = std::pair<double, double>;
375 std::vector<pair_t> pairs(TEST_SIZE);
376 initUniform(pairs, -1., 1.);
377 Statistics<pair_t, pair_t, pair_t, double, double, innerProduct_scalar<pair_t>> stat_pair;
378
379 using array_t = std::array<double, 2>;
380 using array_covariance_ut_t = std::array<double, 3>;
381 std::vector<array_t> arrays(TEST_SIZE);
382 initUniform(arrays, -1., 1.);
383 Statistics<array_t, array_t, array_t, double,
384 double, innerProduct_scalar<array_t>> stat_array;
385 Statistics<array_t, array_t, array_t, double,
386 array_covariance_ut_t, outerProduct_UT_array<array_t>> stat_array_ut;
387
388 for (size_t i = 0; i < TEST_SIZE; ++i) {
389 stat.add(data[i]);
390 stat_outer.add(data[i]);
391 stat_outer_ut.add(data[i]);
392 stat_pair.add(pairs[i]);
393 stat_array.add(arrays[i]);
394 stat_array_ut.add(arrays[i]);
395 }
396
397 #if 0
398 // these aren't trivially copyable
399 static_assert(std::is_trivially_copyable<decltype(stat)>::value,
400 "tuple based inner product not trivially copyable");
401 static_assert(std::is_trivially_copyable<decltype(stat_outer)>::value,
402 "tuple based outer product not trivially copyable");
403 static_assert(std::is_trivially_copyable<decltype(stat_outer_ut)>::value,
404 "tuple based outer product not trivially copyable");
405 #endif
406 static_assert(std::is_trivially_copyable<decltype(stat_array)>::value,
407 "array based inner product not trivially copyable");
408 static_assert(std::is_trivially_copyable<decltype(stat_array_ut)>::value,
409 "array based inner product not trivially copyable");
410
411 // inner product variance should be same as outer product diagonal sum
412 const double variance = stat.getPopVariance();
413 EXPECT_NEAR(variance,
414 std::get<0>(stat_outer.getPopVariance()) +
415 std::get<3>(stat_outer.getPopVariance()),
416 variance * std::numeric_limits<double>::epsilon() * 128);
417
418 // outer product covariance should be identical
419 PRINT_AND_EXPECT_NEAR(std::get<1>(stat_outer.getPopVariance()),
420 std::get<2>(stat_outer.getPopVariance()));
421
422 // upper triangular computation should be identical to outer product
423 PRINT_AND_EXPECT_NEAR(std::get<0>(stat_outer.getPopVariance()),
424 std::get<0>(stat_outer_ut.getPopVariance()));
425 PRINT_AND_EXPECT_NEAR(std::get<1>(stat_outer.getPopVariance()),
426 std::get<1>(stat_outer_ut.getPopVariance()));
427 PRINT_AND_EXPECT_NEAR(std::get<3>(stat_outer.getPopVariance()),
428 std::get<2>(stat_outer_ut.getPopVariance()));
429
430 PRINT_AND_EXPECT_EQ(variance, stat_pair.getPopVariance());
431
432 EXPECT_TRUE(equivalent(stat_array_ut.getPopVariance(), stat_outer_ut.getPopVariance()));
433
434 printf("statistics_inner: %s\n", stat.toString().c_str());
435 printf("statistics_outer: %s\n", stat_outer.toString().c_str());
436 printf("statistics_outer_ut: %s\n", stat_outer_ut.toString().c_str());
437 }
438
TEST(StatisticsTest,stat_linearfit)439 TEST(StatisticsTest, stat_linearfit)
440 {
441 using namespace android::audio_utils; // for operator overload
442 LinearLeastSquaresFit<double> fit;
443
444 static_assert(std::is_trivially_copyable<decltype(fit)>::value,
445 "LinearLeastSquaresFit must be trivially copyable");
446
447 using array_t = std::array<double, 2>;
448 array_t data{0.0, 1.5};
449
450 for (size_t i = 0; i < 10; ++i) {
451 fit.add(data);
452 data = data + array_t{0.1, 0.2};
453 }
454
455 // check the y line equation
456 {
457 double a, b, r2;
458 fit.computeYLine(a, b, r2);
459 printf("y line - a:%lf b:%lf r2:%lf\n", a, b, r2);
460 PRINT_AND_EXPECT_NEAR(1.5, a); // y intercept
461 PRINT_AND_EXPECT_NEAR(2.0, b); // y slope
462 PRINT_AND_EXPECT_NEAR(1.0, r2); // correlation coefficient.
463
464 // check same as static variant
465 double ac, bc, r2c;
466 computeYLineFromStatistics(ac, bc, r2c,
467 std::get<0>(fit.getMean()), /* mean_x */
468 std::get<1>(fit.getMean()), /* mean_y */
469 std::get<0>(fit.getPopVariance()), /* var_x */
470 std::get<1>(fit.getPopVariance()), /* cov_xy */
471 std::get<2>(fit.getPopVariance())); /* var_y */
472
473 EXPECT_EQ(a, ac);
474 EXPECT_EQ(b, bc);
475 EXPECT_EQ(r2, r2c);
476
477 TEST_EXPECT_NEAR(1.9, fit.getYFromX(0.2));
478 TEST_EXPECT_NEAR(0.2, fit.getXFromY(1.9));
479 TEST_EXPECT_NEAR(1.0, fit.getR2());
480 }
481
482 // check the x line equation
483 {
484 double a, b, r2;
485 fit.computeXLine(a, b, r2);
486 printf("x line - a:%lf b:%lf r2:%lf\n", a, b, r2);
487 PRINT_AND_EXPECT_NEAR(-0.75, a); // x intercept
488 PRINT_AND_EXPECT_NEAR(0.5, b); // x slope
489 PRINT_AND_EXPECT_NEAR(1.0, r2); // correlation coefficient.
490 }
491 }
492
TEST(StatisticsTest,stat_linearfit_noise)493 TEST(StatisticsTest, stat_linearfit_noise)
494 {
495 using namespace android::audio_utils; // for operator overload
496 using array_t = std::array<double, 2>;
497 LinearLeastSquaresFit<double> fit;
498
499 // We use 1000 steps for a linear line going from (0, 0) to (1, 1) as true data for
500 // our linear fit.
501 constexpr size_t ELEMENTS = 1000;
502 array_t incr{1. / ELEMENTS, 1. / ELEMENTS};
503
504 // To simulate additive noise, we use a Gaussian with stddev of 1, and then scale
505 // achieve the desired stddev. We precompute our noise here (1000 of them).
506 std::vector<array_t> noise(ELEMENTS);
507 initNormal(noise, 0. /* mean */, 1. /* stddev */);
508
509 for (int i = 0; i < 30; ++i) {
510 // We run through 30 trials, with noise stddev ranging from 0 to 1.
511 // The steps increment linearly from 0.001 to 0.01, linearly from 0.01 to 0.1, and
512 // linearly again from 0.1 to 1.0.
513 // 0.001, 0.002, ... 0.009, 0.01, 0.02, ....0.09, 0.1, 0.2, .... 1.0
514 const double stddev = (i <= 10) ? i / 1000. : (i <= 20) ? (i - 9) / 100. : (i - 19) / 10.;
515 fit.reset();
516
517 for (size_t j = 0; j < ELEMENTS; ++j) {
518 array_t data = j * incr + noise[j] * stddev;
519 fit.add(data);
520 }
521
522 double a, b, r2;
523 fit.computeYLine(a, b, r2);
524 printf("stddev: %lf y line - N:%lld a:%lf b:%lf r2:%lf\n",
525 stddev, (long long) fit.getN(), a, b, r2);
526 }
527 }
528
529
TEST_P(StatisticsTest,stat_simple_char)530 TEST_P(StatisticsTest, stat_simple_char)
531 {
532 const char *param = GetParam();
533
534 android::audio_utils::Statistics<char> stat(0.9);
535 android::audio_utils::ReferenceStatistics<char> rstat(0.9);
536
537 // feed the string character by character to the statistics collectors.
538 for (size_t i = 0; param[i] != '\0'; ++i) {
539 stat.add(param[i]);
540 rstat.add(param[i]);
541 }
542
543 printf("statistics for %s: %s\n", param, stat.toString().c_str());
544 printf("ref statistics for %s: %s\n", param, rstat.toString().c_str());
545 // verify that the statistics are the same
546 verify(stat, rstat);
547 }
548
549 // find the variance of pet names as signed characters.
550 const char *pets[] = {"cat", "dog", "elephant", "mountain lion"};
551 INSTANTIATE_TEST_CASE_P(PetNameStatistics, StatisticsTest,
552 ::testing::ValuesIn(pets));
553
TEST(StatisticsTest,simple_stats)554 TEST(StatisticsTest, simple_stats)
555 {
556 simple_stats_t ss{};
557
558 for (const double value : { -1., 1., 3.}) {
559 simple_stats_log(&ss, value);
560 }
561
562 PRINT_AND_EXPECT_EQ(3., ss.last);
563 PRINT_AND_EXPECT_EQ(1., ss.mean);
564 PRINT_AND_EXPECT_EQ(-1., ss.min);
565 PRINT_AND_EXPECT_EQ(3., ss.max);
566 PRINT_AND_EXPECT_EQ(3, ss.n);
567
568 char buffer[256];
569 simple_stats_to_string(&ss, buffer, sizeof(buffer));
570 printf("simple_stats: %s", buffer);
571 }
572