jaydoane commented on code in PR #4672:
URL: https://github.com/apache/couchdb/pull/4672#discussion_r1263116905


##########
src/couch_stats/src/couch_stats_histogram.erl:
##########
@@ -0,0 +1,449 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+% This module implements windowed base-2 histograms using Erlang counters [1].
+%
+% Base-2 histograms use power of 2 exponentially increasing bin widths. This
+% allows capturing a range of values from microseconds to hours with a
+% relatively small number of bins. The same principle is used when encoding
+% floating point numbers [2]. In fact, our histograms rely on the ease of
+% constructing and mainpululating binary representations of 64 bit floats in
+% Erlang to do all of its heavy lifting.
+%
+% As a refresher, the standard (IEEE 754) 64 bit floating point representations
+% looks something like:
+%
+%  sign  exponent    mantissa
+%  [s64] [e63...e53] [m52...m1]
+%  <-1-> <---11----> <---52--->
+%
+%
+% The simplest scheme migth be to use the exponent to select the histogram bin
+% and throw away the mantissa bits. However, in that case bin sizes end up
+% growing a bit too fast and we lose resolution quickly. To increase the
+% resolution, use a few most significant bit from the mantissa. For example,
+% use 3 more bits mantissa bits for a total of 14 bits: [e63...e53] + [m52,
+% m51, m50]:
+%
+%  sign  exponent    mantissa
+%  [s64] [e63...e53] [m52, m51, m50, m49...m1]
+%  <-1-> <-----------14------------> <--49--->
+%        ^^^^^^^ bin index ^^^^^^^^^
+%
+% With Erlang's wonderful binary matching capabilities this becomes a
+% one-liner:
+%
+%    <<0:1, BinIndex:14, _/bitstring>> = <<Val/float>>
+%
+% The internal implementation is a tuple of counter:new(?BIN_COUNT)
+% elements. The tuple size is determined by the time window parameter when the
+% histogram is created. After a histogram object is created, its Erlang term
+% structure is static so it's suitable to be stored in a persistent term [3].
+% The structure looks something like:
+%
+%  {
+%     1          = [1, 2, ..., ?BIN_COUNT]
+%     2          = [1, 2, ..., ?BIN_COUNT]
+%     ...
+%     TimeWindow = [1, 2, ..., ?BIN_COUNT]
+%  }
+%
+% The representation can also be regarded as a table with rows as abstract time
+% units: 1 = 1st second, 2 = 2nd second, etc, and the columns as histogram
+% bins. The update/3 function takes a value and a current time as parameters.
+% The time is used to select which row to update, and the value picks which
+% bin to increment.
+%
+% In practice, the time window would be used as a circular buffer. The time
+% parameter to the update/3 function might be the system monotonic clock time
+% and then the histogram time index is compuated as `Time rem TimeWindow`. So,
+% as the monotonic time is advancing forward, the histogram time index will
+% loop around. This comes with a minor annoynance of having to allocate a
+% larger time window to accomodate some process which cleans stale (expired)
+% histogram entries, possibly with some extra buffers to ensure the currently
+% updated interval and the interval ready to be cleaned would not overlap.
+%
+% Reading a histogram can be done via the read/3 function. The function takes a
+% start time and an interval. All the histogram entries in the interval will be
+% summed together, bin by bin, and returned as a new counters object. This
+% functionality can be used to gather and merge multiple histogram together.
+%
+% To get a stats summary across a time window use the stats/3 function. Just
+% like the read/3 function, it takes a start time and a time interval over
+% which to summarize the data.
+%
+% In addition to the new/1, update/3, read/3, stats/3 there are simple
+% functions which default to using WindowSize = 1. The intent is they would be
+% used when a simple histogram is needed without the time window functionality.
+%
+% [1] https://www.erlang.org/doc/man/counters.html
+% [2] https://en.wikipedia.org/wiki/IEEE_754
+% [3] https://www.erlang.org/doc/man/persistent_term.html
+
+-module(couch_stats_histogram).
+
+-export([
+    new/0,
+    new/1,
+
+    update/2,
+    update/3,
+
+    stats/1,
+    stats/3,
+
+    read/1,
+    read/3,
+
+    clear/1,
+    clear/3,
+
+    bin_min/1,
+    bin_max/1,
+    bin_middle/1,
+
+    calc_bin_offset/0,
+    calc_bin_count/0,
+    get_bin_boundaries/0
+]).
+
+-on_load(check_constants/0).
+
+-define(FLOAT_SIZE, 64).
+
+% 11 standard float64 exponent bits + 3 more extra msb mantissa bits
+%
+-define(INDEX_BITS, 14).
+
+% Some practical min and max values to be able to have a fixed number of
+% histogram bins. When used for timing typical units are milliseconds, so use 
0.01
+% msec as the minimum, and 4M msec (over 1 hour) for maximum.
+%
+-define(MIN_VAL, 0.01).
+-define(MAX_VAL, 4000000.0).
+
+% These are computed from previously defined constants. Recompute them with
+% cal_bin_offset() and calc_bin_count(), respectively, if any of the constants
+% above change.
+%
+-define(BIN_OFFSET, -8129).
+-define(BIN_COUNT, 230).
+
+% Public API
+
+new() ->
+    new(1).
+
+new(TimeWindow) when is_integer(TimeWindow), TimeWindow >= 1 ->
+    list_to_tuple([counter() || _ <- lists:seq(1, TimeWindow)]).
+
+update(Ctx, Val) ->
+    update(Ctx, 1, Val).
+
+update(Ctx, Time, Val) when is_integer(Val) ->
+    update(Ctx, Time, float(Val));
+update(Ctx, Time, Val) when is_integer(Time), is_float(Val) ->
+    Val1 = min(max(Val, ?MIN_VAL), ?MAX_VAL),
+    Counter = hist_at(Ctx, Time),
+    counters:add(Counter, bin_index(Val1), 1).
+
+read(Ctx) ->
+    read(Ctx, 1, 1).
+
+read(Ctx, Time, Ticks) when is_integer(Time), is_integer(Ticks), Ticks >= 1 ->
+    Ticks1 = min(tuple_size(Ctx), Ticks),
+    read_fold(Ctx, Time, Ticks1, counter()).
+
+stats(Ctx) ->
+    stats(Ctx, 1, 1).
+
+stats(Ctx, Time, Ticks) when is_integer(Time), is_integer(Ticks), Ticks >= 1 ->
+    Counter = read(Ctx, Time, Ticks),
+    couch_stats_math:summary(Counter, ?BIN_COUNT).
+
+clear(Ctx) ->
+    clear(Ctx, 1, 1).
+
+clear(Ctx, Time, Ticks) when is_integer(Time), is_integer(Ticks), Ticks >= 1 ->
+    Ticks1 = min(tuple_size(Ctx), Ticks),
+    clear_fold(Ctx, Time, Ticks1).
+
+% Utility functions
+
+% Use this to recompute ?BIN_OFFSET if ?INDEX_BITS or MIN_VAL changes.
+%
+calc_bin_offset() ->
+    <<0:1, I:?INDEX_BITS, _/bitstring>> = <<?MIN_VAL/float>>,
+    % "1" because counter indices start at 1
+    1 - I.
+
+% Use this to recompute ?BIN_COUNT if ?INDEX_BITS, MIN_VAL, or MAX_VAL changes.
+%
+calc_bin_count() ->
+    bin_index(?MAX_VAL).
+
+get_bin_boundaries() ->
+    [{bin_min(I), bin_max(I)} || I <- lists:seq(1, ?BIN_COUNT)].
+
+% Private functions
+
+% Called from -on_load() directive. Verify that our constants are sane
+% if some are not updated module loading will throw an error.
+%
+check_constants() ->
+    case calc_bin_count() of
+        ?BIN_COUNT -> ok;
+        OtherBinCount -> error({bin_count_stale, ?BIN_COUNT, OtherBinCount})
+    end,
+    case calc_bin_offset() of
+        ?BIN_OFFSET -> ok;
+        OtherBinOffset -> error({bin_offset_stale, ?BIN_OFFSET, 
OtherBinOffset})
+    end.
+
+bin_index(Val) ->
+    % Select the exponent bits plus a few most significant bits from
+    % mantissa. ?BIN_OFFSET shifts the index into the range starting with 1
+    % so we can index counter bins (those start with 1, just like tuples).
+    <<0:1, BinIndex:?INDEX_BITS, _/bitstring>> = <<Val/float>>,
+    BinIndex + ?BIN_OFFSET.
+
+bin_min(Index) when is_integer(Index), Index >= 1, Index =< ?BIN_COUNT ->
+    BiasedIndex = Index - ?BIN_OFFSET,
+    % 1 is the sign bit
+    BinBitSize = ?FLOAT_SIZE - 1 - ?INDEX_BITS,
+    % Minimum value is the one with all the rest of mantissa bits set to 0
+    <<Min/float>> = <<0:1, BiasedIndex:?INDEX_BITS, 0:BinBitSize>>,
+    Min.
+
+bin_max(Index) when is_integer(Index), Index >= 1, Index =< ?BIN_COUNT ->
+    BiasedIndex = Index - ?BIN_OFFSET,
+    % 1 is the sign bit
+    BinBitSize = ?FLOAT_SIZE - 1 - ?INDEX_BITS,
+    % For Max the intuition is we first construct a next highest power of two
+    % value, by shifting left BinBitSize, then subtract 1. That sets all the
+    % bits to 1. (for ex.:  1 bsl 4 = 1000, 1000 - 1 = 111)
+    <<Max/float>> = <<0:1, BiasedIndex:?INDEX_BITS, ((1 bsl BinBitSize) - 
1):BinBitSize>>,
+    Max.
+
+bin_middle(Index) when is_integer(Index), Index >= 1, Index =< ?BIN_COUNT ->
+    BiasedIndex = Index - ?BIN_OFFSET,
+    % 1 is the sign bit
+    BinBitSize = ?FLOAT_SIZE - 1 - ?INDEX_BITS,
+    % Shift left 1 bit less than we do in bin_max, which is effectively
+    % is Max / 2.
+    <<Mid/float>> = <<0:1, BiasedIndex:?INDEX_BITS, (1 bsl (BinBitSize - 
1)):BinBitSize>>,
+    Mid.
+
+read_fold(_, _, 0, Acc) ->
+    Acc;
+read_fold(Counters, Time, Ticks, Acc) ->
+    Acc1 = merge(Acc, hist_at(Counters, Time), ?BIN_COUNT),
+    read_fold(Counters, Time + 1, Ticks - 1, Acc1).
+
+clear_fold(_, _, 0) ->
+    ok;
+clear_fold(Counters, Time, Ticks) ->
+    reset(hist_at(Counters, Time), ?BIN_COUNT),
+    clear_fold(Counters, Time + 1, Ticks - 1).
+
+merge(A, _, 0) ->
+    A;
+merge(A, B, I) when is_integer(I), I > 0 ->
+    counters:add(A, I, counters:get(B, I)),
+    merge(A, B, I - 1).
+
+reset(_, 0) ->
+    ok;
+reset(Counters, I) when is_integer(I), I > 0 ->
+    counters:put(Counters, I, 0),
+    reset(Counters, I - 1).
+
+counter() ->
+    counters:new(?BIN_COUNT, [write_concurrency]).
+
+hist_at(Counters, Time) when is_tuple(Counters), is_integer(Time) ->
+    % Erlang monotonic time can be negative, so add a TimeWindow to it, to make
+    % it positive again. Add +1 because counter indices start with 1 but X rem
+    % Y returns values betweeen 0 and Y-1.
+    TimeWindow = tuple_size(Counters),
+    case Time rem TimeWindow of
+        Idx when Idx < 0 -> element(Idx + TimeWindow + 1, Counters);
+        Idx -> element(Idx + 1, Counters)
+    end.
+
+-ifdef(TEST).
+
+-include_lib("couch/include/couch_eunit.hrl").
+
+basics_test() ->
+    H = new(),
+    ?assert(is_tuple(H)),
+    ?assertEqual(1, tuple_size(H)),
+    ?assertEqual(2, tuple_size(new(2))),
+    ?assertEqual([], bins(H)),
+    ?assertMatch([{_, _} | _], stats(H)),
+    ?assertEqual(0, proplists:get_value(n, stats(H))),
+    ?assertEqual(0, proplists:get_value(min, stats(H))),
+    ?assertEqual(0, proplists:get_value(max, stats(H))).
+
+update_test() ->
+    H = new(),
+    ?assertMatch(#{size := ?BIN_COUNT}, counters:info(read(H))),
+    ?assertEqual([], bins(H)),
+    update(H, 10.42),
+    ?assertEqual([{81, 1}], bins(H)),
+    ?assertEqual(81, bin_index(10.42)),
+    % Update again to see how histogram gets bumped to 2
+    update(H, 10.42),
+    ?assertEqual([{81, 2}], bins(H)),
+    ?assertEqual(2, proplists:get_value(n, stats(H))),
+    ?assert(proplists:get_value(min, stats(H)) >= 10.0),
+    ?assert(proplists:get_value(max, stats(H)) =< 11.0),
+    clear(H),
+    ?assertEqual([], bins(H)).
+
+update_with_small_value_test() ->
+    H = new(),
+    % 0 is below the minimum value
+    update(H, 0),
+    ?assertEqual([{1, 1}], bins(H)),
+    ?assertMatch([{_, _} | _], stats(H)),
+    ?assertEqual(1, proplists:get_value(n, stats(H))),
+    ?assert(bin_min(1) =< proplists:get_value(min, stats(H))),
+    ?assert(proplists:get_value(max, stats(H)) =< bin_max(1)),
+    clear(H),
+    ?assertEqual([], bins(H)).
+
+update_negative_time_index_test() ->
+    H = new(3),
+    update(H, -12, 0.42),
+    update(H, -11, 4.2),
+    update(H, -10, 4.2001),
+
+    ?assertEqual(44, bin_index(0.42)),
+    ?assertEqual(71, bin_index(4.2)),
+    ?assertEqual(71, bin_index(4.2001)),
+
+    ?assertEqual([{44, 1}], bins(H, -12, 1)),
+    ?assertEqual([{71, 1}], bins(H, -11, 1)),
+    ?assertEqual([{71, 1}], bins(H, -10, 1)),
+
+    % Combine 1st and 2nd
+    ?assertEqual([{44, 1}, {71, 1}], bins(H, -12, 2)),
+    % Combine 2nd and 3rd
+    ?assertEqual([{71, 2}], bins(H, -11, 2)),
+    % Combine all three
+    ?assertEqual([{44, 1}, {71, 2}], bins(H, -12, 3)),
+    % Wrap around
+    ?assertEqual([{44, 1}, {71, 2}], bins(H, -10, 3)),
+
+    % Clear last two
+    clear(H, -11, 2),
+    ?assertEqual([{44, 1}], bins(H, -12, 3)),
+
+    % Clear all
+    clear(H, -12, 3),
+    ?assertEqual([], bins(H, -12, 3)).
+
+update_positive_time_index_test() ->
+    H = new(3),
+    update(H, 1, 0.42),
+    update(H, 2, 4.2),
+    update(H, 3, 4.2001),
+
+    ?assertEqual([{44, 1}], bins(H, 1, 1)),
+    ?assertEqual([{71, 1}], bins(H, 2, 1)),
+    ?assertEqual([{71, 1}], bins(H, 3, 1)),
+
+    % Combine 1st and 2nd
+    ?assertEqual([{44, 1}, {71, 1}], bins(H, 1, 2)),
+    % Combine 2nd and 3rd
+    ?assertEqual([{71, 2}], bins(H, 2, 2)),
+    % Combine all three
+    ?assertEqual([{44, 1}, {71, 2}], bins(H, 1, 3)),
+    % Wrap around
+    ?assertEqual([{44, 1}, {71, 2}], bins(H, 3, 3)),
+
+    % Clear last two
+    clear(H, 2, 2),
+    ?assertEqual([{44, 1}], bins(H, 1, 3)),
+
+    % Clear all
+    clear(H, 1, 3),
+    ?assertEqual([], bins(H, 1, 3)).
+
+update_negative_and_positive_time_index_test() ->
+    H = new(3),
+    update(H, -1, 0.42),
+    update(H, 0, 4.2),
+    update(H, 1, 4.2001),
+
+    ?assertEqual([{44, 1}], bins(H, -1, 1)),
+    ?assertEqual([{71, 1}], bins(H, 0, 1)),
+    ?assertEqual([{71, 1}], bins(H, 1, 1)),
+
+    % Combine 1st and 2nd
+    ?assertEqual([{44, 1}, {71, 1}], bins(H, -1, 2)),
+    % Combine 2nd and 3rd
+    ?assertEqual([{71, 2}], bins(H, 0, 2)),
+    % Combine all three
+    ?assertEqual([{44, 1}, {71, 2}], bins(H, -1, 3)),
+    % Wrap around
+    ?assertEqual([{44, 1}, {71, 2}], bins(H, 1, 3)),
+
+    % Clear all
+    clear(H, -1, 3),
+    ?assertEqual([], bins(H, -1, 3)).
+
+update_with_large_value_test() ->
+    H = new(),
+    % Update with value > max
+    [update(H, 1.0e300) || _ <- lists:seq(1, 1000)],
+    ?assertEqual([{?BIN_COUNT, 1000}], bins(H)),
+    clear(H),
+    ?assertEqual([], bins(H)).
+
+calculated_constants_test() ->
+    ?assertEqual(?BIN_OFFSET, calc_bin_offset()),
+    ?assertEqual(?BIN_COUNT, calc_bin_count()).
+
+get_bin_boundaries_test() ->
+    Boundaries = get_bin_boundaries(),
+    ?assertEqual(?BIN_COUNT, length(Boundaries)),
+    %io:format(standard_error, "~n--- bin boundaries ---~n~n", []),
+    lists:foreach(
+        fun({Min, Max}) ->
+            %io:format(standard_error, "[~p  ~p]~n", [Min, Max]),

Review Comment:
   Did you want to keep these comments?



##########
src/couch_stats/src/couch_stats_histogram.erl:
##########
@@ -0,0 +1,449 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+% This module implements windowed base-2 histograms using Erlang counters [1].
+%
+% Base-2 histograms use power of 2 exponentially increasing bin widths. This
+% allows capturing a range of values from microseconds to hours with a
+% relatively small number of bins. The same principle is used when encoding
+% floating point numbers [2]. In fact, our histograms rely on the ease of
+% constructing and mainpululating binary representations of 64 bit floats in
+% Erlang to do all of its heavy lifting.
+%
+% As a refresher, the standard (IEEE 754) 64 bit floating point representations
+% looks something like:
+%
+%  sign  exponent    mantissa
+%  [s64] [e63...e53] [m52...m1]
+%  <-1-> <---11----> <---52--->
+%
+%
+% The simplest scheme migth be to use the exponent to select the histogram bin
+% and throw away the mantissa bits. However, in that case bin sizes end up
+% growing a bit too fast and we lose resolution quickly. To increase the
+% resolution, use a few most significant bit from the mantissa. For example,
+% use 3 more bits mantissa bits for a total of 14 bits: [e63...e53] + [m52,
+% m51, m50]:
+%
+%  sign  exponent    mantissa
+%  [s64] [e63...e53] [m52, m51, m50, m49...m1]
+%  <-1-> <-----------14------------> <--49--->
+%        ^^^^^^^ bin index ^^^^^^^^^
+%
+% With Erlang's wonderful binary matching capabilities this becomes a
+% one-liner:
+%
+%    <<0:1, BinIndex:14, _/bitstring>> = <<Val/float>>
+%
+% The internal implementation is a tuple of counter:new(?BIN_COUNT)
+% elements. The tuple size is determined by the time window parameter when the
+% histogram is created. After a histogram object is created, its Erlang term
+% structure is static so it's suitable to be stored in a persistent term [3].
+% The structure looks something like:
+%
+%  {
+%     1          = [1, 2, ..., ?BIN_COUNT]
+%     2          = [1, 2, ..., ?BIN_COUNT]
+%     ...
+%     TimeWindow = [1, 2, ..., ?BIN_COUNT]
+%  }
+%
+% The representation can also be regarded as a table with rows as abstract time
+% units: 1 = 1st second, 2 = 2nd second, etc, and the columns as histogram
+% bins. The update/3 function takes a value and a current time as parameters.
+% The time is used to select which row to update, and the value picks which
+% bin to increment.
+%
+% In practice, the time window would be used as a circular buffer. The time
+% parameter to the update/3 function might be the system monotonic clock time
+% and then the histogram time index is compuated as `Time rem TimeWindow`. So,
+% as the monotonic time is advancing forward, the histogram time index will
+% loop around. This comes with a minor annoynance of having to allocate a
+% larger time window to accomodate some process which cleans stale (expired)
+% histogram entries, possibly with some extra buffers to ensure the currently
+% updated interval and the interval ready to be cleaned would not overlap.
+%
+% Reading a histogram can be done via the read/3 function. The function takes a
+% start time and an interval. All the histogram entries in the interval will be
+% summed together, bin by bin, and returned as a new counters object. This
+% functionality can be used to gather and merge multiple histogram together.
+%
+% To get a stats summary across a time window use the stats/3 function. Just
+% like the read/3 function, it takes a start time and a time interval over
+% which to summarize the data.
+%
+% In addition to the new/1, update/3, read/3, stats/3 there are simple
+% functions which default to using WindowSize = 1. The intent is they would be
+% used when a simple histogram is needed without the time window functionality.
+%
+% [1] https://www.erlang.org/doc/man/counters.html
+% [2] https://en.wikipedia.org/wiki/IEEE_754
+% [3] https://www.erlang.org/doc/man/persistent_term.html
+
+-module(couch_stats_histogram).
+
+-export([
+    new/0,
+    new/1,
+
+    update/2,
+    update/3,
+
+    stats/1,
+    stats/3,
+
+    read/1,
+    read/3,
+
+    clear/1,
+    clear/3,
+
+    bin_min/1,
+    bin_max/1,
+    bin_middle/1,
+
+    calc_bin_offset/0,
+    calc_bin_count/0,
+    get_bin_boundaries/0
+]).
+
+-on_load(check_constants/0).

Review Comment:
   This is a nice feature! Confirmed that it works as expected:
   ```
   [error] 2023-07-14T18:59:49.869391Z [email protected] emulator -------- Error 
in process <0.228.0> on node '[email protected]' with exit value:
   
{{bin_count_stale,231,230},[{couch_stats_histogram,check_constants,0,[{file,"src/couch_stats_histogram.erl"},{line,205}]},{code_server,'-handle_on_load/5-fun-0-',1,[{file,"code_server.erl"},{line,1317}]}]}
   ```



##########
src/couch_stats/src/couch_stats_server.erl:
##########
@@ -0,0 +1,246 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+% couch_stats_server is in charge of:
+%   - Initial metric loading from application stats descriptions.
+%   - Recycling(resetting to 0) stale histogram counters.
+%   - Checking and reloading if stats descriptions change.
+%   - Checking and reloading if histogram window size config value changes.

Review Comment:
   Is this referred to as "histogram interval" elsewhere?



##########
src/couch_stats/src/couch_stats_server.erl:
##########
@@ -0,0 +1,246 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+% couch_stats_server is in charge of:
+%   - Initial metric loading from application stats descriptions.
+%   - Recycling(resetting to 0) stale histogram counters.
+%   - Checking and reloading if stats descriptions change.
+%   - Checking and reloading if histogram window size config value changes.
+%
+
+-module(couch_stats_server).
+
+-behaviour(gen_server).
+
+-export([
+    reload/0
+]).
+
+-export([
+    start_link/0,
+    init/1,
+    handle_call/3,
+    handle_cast/2,
+    handle_info/2
+]).
+
+-define(RELOAD_INTERVAL_SEC, 600).
+
+-record(st, {
+    hist_interval,
+    histograms,
+    hist_tref,
+    reload_tref
+}).
+
+reload() ->
+    gen_server:call(?MODULE, reload).
+
+start_link() ->
+    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
+
+init([]) ->
+    St = #st{
+        hist_interval = config:get("stats", "interval"),
+        hist_tref = erlang:send_after(hist_msec(), self(), clean),
+        reload_tref = erlang:send_after(reload_msec(), self(), reload)
+    },
+    {_, Stats} = try_reload(St),
+    {ok, St#st{histograms = couch_stats_util:histograms(Stats)}}.
+
+handle_call(reload, _From, #st{} = St) ->
+    {reply, ok, do_reload(St)};
+handle_call(Msg, _From, #st{} = St) ->
+    {stop, {unknown_call, Msg}, unknown_call, St}.
+
+handle_cast(Msg, #st{} = St) ->
+    {stop, {unknown_cast, Msg}, St}.
+
+handle_info(reload, #st{} = St) ->
+    {noreply, do_reload(St)};
+handle_info(clean, #st{} = St) ->
+    {noreply, do_clean(St)};
+handle_info(Msg, #st{} = St) ->
+    {stop, {unknown_info, Msg}, St}.
+
+do_clean(#st{} = St) ->
+    timer:cancel(St#st.hist_tref),
+    HistTRef = erlang:send_after(hist_msec(), self(), clean),
+    NowSec = erlang:monotonic_time(second),
+    BufferSec = couch_stats_util:histogram_safety_buffer_size_sec(),
+    IntervalSec = couch_stats_util:histogram_interval_sec(),
+    % The histogram timeline looks something like:
+    %
+    %  |<--buffer-->|<--stale-->|<--buffer-->|<--current-->|
+    %                ^                                    ^
+    %                StartSec                             NowSec
+    %
+    % To get to the start of "stale" part to clean it, subtract one interval,
+    % then a buffer, then another interval from NowSec.
+    %
+    StartSec = NowSec - IntervalSec - BufferSec - (IntervalSec - 1),
+    % Last -1 is because the interval ends are inclusive
+    maps:foreach(
+        fun(_, {_, Ctx, _}) ->
+            couch_stats_histogram:clear(Ctx, StartSec, IntervalSec)
+        end,
+        St#st.histograms
+    ),
+    St#st{hist_tref = HistTRef}.
+
+do_reload(#st{} = St) ->
+    timer:cancel(St#st.reload_tref),
+    RTRef = erlang:send_after(reload_msec(), self(), reload),
+    case try_reload(St) of
+        {true, NewStats} ->
+            timer:cancel(St#st.hist_tref),
+            Histograms = couch_stats_util:histograms(NewStats),
+            HTRef = erlang:send_after(hist_msec(), self(), clean),
+            St#st{
+                histograms = Histograms,
+                hist_tref = HTRef,
+                reload_tref = RTRef,
+                hist_interval = config:get("stats", "interval")
+            };
+        {false, _} ->
+            St#st{reload_tref = RTRef}
+    end.
+
+try_reload(#st{} = St) ->
+    NewDefs = couch_stats_util:load_metrics_for_applications(),
+    Stats = couch_stats_util:stats(),
+    MetricsChanged = couch_stats_util:metrics_changed(Stats, NewDefs),
+    IntervalChanged = interval_changed(St),
+    case MetricsChanged orelse IntervalChanged of
+        true ->
+            couch_stats_util:reset_histogram_interval_sec(),
+            NewStats = couch_stats_util:create_metrics(NewDefs),
+            couch_stats_util:replace_stats(NewStats),
+            {true, NewStats};
+        false ->
+            {false, Stats}
+    end.
+
+interval_changed(#st{hist_interval = OldInterval}) ->
+    case config:get("stats", "interval") of
+        Interval when OldInterval =:= Interval ->
+            false;
+        _ ->
+            true
+    end.
+
+reload_msec() ->
+    1000 * ?RELOAD_INTERVAL_SEC.
+
+hist_msec() ->
+    (couch_stats_util:histogram_interval_sec() * 1000) div 2.

Review Comment:
   Why `div 2`?



##########
src/couch_stats/src/couch_stats_util.erl:
##########
@@ -0,0 +1,190 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_stats_util).
+
+-export([
+    % Load metrics from apps
+    create_metrics/1,
+    load_metrics_for_applications/0,
+    metrics_changed/2,
+
+    % Get various metric types
+    get_counter/2,
+    get_gauge/2,
+    get_histogram/2,
+
+    % Get histogram interval config settings
+    histogram_interval_sec/0,
+    histogram_safety_buffer_size_sec/0,
+    reset_histogram_interval_sec/0,
+
+    % Manage the main stats (metrics) persistent term map
+    replace_stats/1,
+    stats/0,
+    histograms/1,
+
+    % Fetch stats values
+    fetch/3,
+    sample/4
+]).
+
+-define(DEFAULT_INTERVAL_SEC, 10).
+
+% Histogram types
+-define(HIST, histogram).
+-define(CNTR, counter).
+-define(GAUGE, gauge).
+
+% Safety buffer before and after current window to prevent
+% overwrites from the cleaner process
+-define(HIST_WRAP_BUFFER_SIZE_SEC, 5).
+
+% Persistent term keys
+-define(STATS_KEY, {?MODULE, stats}).
+-define(HIST_TIME_INTERVAL_KEY, {?MODULE, hist_time_interval}).
+
+load_metrics_for_applications() ->
+    Apps = [element(1, A) || A <- application:loaded_applications()],
+    lists:foldl(fun load_metrics_for_application_fold/2, #{}, Apps).
+
+load_metrics_for_application_fold(AppName, #{} = Acc) ->
+    case code:priv_dir(AppName) of
+        {error, _Error} ->
+            Acc;
+        Dir ->
+            case file:consult(Dir ++ "/stats_descriptions.cfg") of
+                {ok, Descriptions} ->
+                    DescMap = maps:map(
+                        fun(_, TypeDesc) ->
+                            Type = proplists:get_value(type, TypeDesc, 
counter),
+                            Desc = proplists:get_value(desc, TypeDesc, <<>>),
+                            {Type, Desc}
+                        end,
+                        maps:from_list(Descriptions)
+                    ),
+                    maps:merge(Acc, DescMap);
+                {error, _Error} ->
+                    Acc
+            end
+    end.
+
+metrics_changed(#{} = Map1, #{} = Map2) when map_size(Map1) =/= map_size(Map2) 
->
+    % If their sizes are differently they are obvioulsy not the same
+    true;
+metrics_changed(#{} = Map1, #{} = Map2) when map_size(Map1) =:= map_size(Map2) 
->
+    % If their intersection size is not the same as their individual size
+    % they are also not the same
+    map_size(maps:intersect(Map1, Map2)) =/= map_size(Map1).
+
+get_counter(Name, #{} = Stats) ->
+    get_metric(Name, ?CNTR, Stats).
+
+get_gauge(Name, #{} = Stats) ->
+    get_metric(Name, ?GAUGE, Stats).
+
+get_histogram(Name, #{} = Stats) ->
+    get_metric(Name, ?HIST, Stats).
+
+get_metric(Name, Type, Stats) when is_atom(Type), is_map(Stats) ->
+    case maps:get(Name, Stats, unknown_metric) of
+        {FoundType, Metric, _Desc} when FoundType =:= Type ->
+            {ok, Metric};
+        {OtherType, _, _} ->
+            error_logger:error_msg("invalid metric: ~p ~p =/= ~p", [Name, 
Type, OtherType]),

Review Comment:
   Why use `error_logger` instead of `couch_log`?



##########
src/couch_stats/src/couch_stats_histogram.erl:
##########
@@ -0,0 +1,449 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+% This module implements windowed base-2 histograms using Erlang counters [1].
+%
+% Base-2 histograms use power of 2 exponentially increasing bin widths. This
+% allows capturing a range of values from microseconds to hours with a
+% relatively small number of bins. The same principle is used when encoding
+% floating point numbers [2]. In fact, our histograms rely on the ease of
+% constructing and mainpululating binary representations of 64 bit floats in
+% Erlang to do all of its heavy lifting.
+%
+% As a refresher, the standard (IEEE 754) 64 bit floating point representations
+% looks something like:
+%
+%  sign  exponent    mantissa
+%  [s64] [e63...e53] [m52...m1]
+%  <-1-> <---11----> <---52--->
+%
+%
+% The simplest scheme migth be to use the exponent to select the histogram bin
+% and throw away the mantissa bits. However, in that case bin sizes end up
+% growing a bit too fast and we lose resolution quickly. To increase the
+% resolution, use a few most significant bit from the mantissa. For example,
+% use 3 more bits mantissa bits for a total of 14 bits: [e63...e53] + [m52,
+% m51, m50]:
+%
+%  sign  exponent    mantissa
+%  [s64] [e63...e53] [m52, m51, m50, m49...m1]
+%  <-1-> <-----------14------------> <--49--->
+%        ^^^^^^^ bin index ^^^^^^^^^
+%
+% With Erlang's wonderful binary matching capabilities this becomes a
+% one-liner:
+%
+%    <<0:1, BinIndex:14, _/bitstring>> = <<Val/float>>
+%
+% The internal implementation is a tuple of counter:new(?BIN_COUNT)
+% elements. The tuple size is determined by the time window parameter when the
+% histogram is created. After a histogram object is created, its Erlang term
+% structure is static so it's suitable to be stored in a persistent term [3].
+% The structure looks something like:
+%
+%  {
+%     1          = [1, 2, ..., ?BIN_COUNT]
+%     2          = [1, 2, ..., ?BIN_COUNT]
+%     ...
+%     TimeWindow = [1, 2, ..., ?BIN_COUNT]
+%  }
+%
+% The representation can also be regarded as a table with rows as abstract time
+% units: 1 = 1st second, 2 = 2nd second, etc, and the columns as histogram
+% bins. The update/3 function takes a value and a current time as parameters.
+% The time is used to select which row to update, and the value picks which
+% bin to increment.
+%
+% In practice, the time window would be used as a circular buffer. The time
+% parameter to the update/3 function might be the system monotonic clock time
+% and then the histogram time index is compuated as `Time rem TimeWindow`. So,

Review Comment:
   s/compuated/computed/



##########
src/couch_stats/src/couch_stats_histogram.erl:
##########
@@ -0,0 +1,449 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+% This module implements windowed base-2 histograms using Erlang counters [1].
+%
+% Base-2 histograms use power of 2 exponentially increasing bin widths. This
+% allows capturing a range of values from microseconds to hours with a
+% relatively small number of bins. The same principle is used when encoding
+% floating point numbers [2]. In fact, our histograms rely on the ease of
+% constructing and mainpululating binary representations of 64 bit floats in
+% Erlang to do all of its heavy lifting.
+%
+% As a refresher, the standard (IEEE 754) 64 bit floating point representations
+% looks something like:
+%
+%  sign  exponent    mantissa
+%  [s64] [e63...e53] [m52...m1]
+%  <-1-> <---11----> <---52--->
+%
+%
+% The simplest scheme migth be to use the exponent to select the histogram bin
+% and throw away the mantissa bits. However, in that case bin sizes end up
+% growing a bit too fast and we lose resolution quickly. To increase the
+% resolution, use a few most significant bit from the mantissa. For example,
+% use 3 more bits mantissa bits for a total of 14 bits: [e63...e53] + [m52,
+% m51, m50]:
+%
+%  sign  exponent    mantissa
+%  [s64] [e63...e53] [m52, m51, m50, m49...m1]
+%  <-1-> <-----------14------------> <--49--->
+%        ^^^^^^^ bin index ^^^^^^^^^
+%
+% With Erlang's wonderful binary matching capabilities this becomes a
+% one-liner:
+%
+%    <<0:1, BinIndex:14, _/bitstring>> = <<Val/float>>
+%
+% The internal implementation is a tuple of counter:new(?BIN_COUNT)
+% elements. The tuple size is determined by the time window parameter when the
+% histogram is created. After a histogram object is created, its Erlang term
+% structure is static so it's suitable to be stored in a persistent term [3].
+% The structure looks something like:
+%
+%  {
+%     1          = [1, 2, ..., ?BIN_COUNT]
+%     2          = [1, 2, ..., ?BIN_COUNT]
+%     ...
+%     TimeWindow = [1, 2, ..., ?BIN_COUNT]
+%  }
+%
+% The representation can also be regarded as a table with rows as abstract time
+% units: 1 = 1st second, 2 = 2nd second, etc, and the columns as histogram
+% bins. The update/3 function takes a value and a current time as parameters.
+% The time is used to select which row to update, and the value picks which
+% bin to increment.
+%
+% In practice, the time window would be used as a circular buffer. The time
+% parameter to the update/3 function might be the system monotonic clock time
+% and then the histogram time index is compuated as `Time rem TimeWindow`. So,
+% as the monotonic time is advancing forward, the histogram time index will
+% loop around. This comes with a minor annoynance of having to allocate a
+% larger time window to accomodate some process which cleans stale (expired)
+% histogram entries, possibly with some extra buffers to ensure the currently
+% updated interval and the interval ready to be cleaned would not overlap.
+%
+% Reading a histogram can be done via the read/3 function. The function takes a
+% start time and an interval. All the histogram entries in the interval will be
+% summed together, bin by bin, and returned as a new counters object. This
+% functionality can be used to gather and merge multiple histogram together.
+%
+% To get a stats summary across a time window use the stats/3 function. Just
+% like the read/3 function, it takes a start time and a time interval over
+% which to summarize the data.
+%
+% In addition to the new/1, update/3, read/3, stats/3 there are simple
+% functions which default to using WindowSize = 1. The intent is they would be
+% used when a simple histogram is needed without the time window functionality.
+%
+% [1] https://www.erlang.org/doc/man/counters.html
+% [2] https://en.wikipedia.org/wiki/IEEE_754
+% [3] https://www.erlang.org/doc/man/persistent_term.html
+
+-module(couch_stats_histogram).
+
+-export([
+    new/0,
+    new/1,
+
+    update/2,
+    update/3,
+
+    stats/1,
+    stats/3,
+
+    read/1,
+    read/3,
+
+    clear/1,
+    clear/3,
+
+    bin_min/1,
+    bin_max/1,
+    bin_middle/1,
+
+    calc_bin_offset/0,
+    calc_bin_count/0,
+    get_bin_boundaries/0
+]).
+
+-on_load(check_constants/0).

Review Comment:
   It is a little awkward to change these values though, since 
`calc_bin_offset` and `calc_bin_count` can only be run once 
`couch_stats_histogram` is loaded, and it can't be loaded if any values are 
changed. However, the error messages are good enough to use for making the 
updates.
   
   I could see an argument for the minimum value be one microsecond, so I 
decided to see how things change if we drop `MIN_VAL` to 0.001. `BIN_COUNT` 
increases to 256, which is maybe too much memory overhead for such a small 
increase in low value resolution.



##########
src/couch_stats/src/couch_stats_histogram.erl:
##########
@@ -0,0 +1,449 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+% This module implements windowed base-2 histograms using Erlang counters [1].
+%
+% Base-2 histograms use power of 2 exponentially increasing bin widths. This
+% allows capturing a range of values from microseconds to hours with a
+% relatively small number of bins. The same principle is used when encoding
+% floating point numbers [2]. In fact, our histograms rely on the ease of
+% constructing and mainpululating binary representations of 64 bit floats in
+% Erlang to do all of its heavy lifting.
+%
+% As a refresher, the standard (IEEE 754) 64 bit floating point representations
+% looks something like:
+%
+%  sign  exponent    mantissa
+%  [s64] [e63...e53] [m52...m1]
+%  <-1-> <---11----> <---52--->
+%
+%
+% The simplest scheme migth be to use the exponent to select the histogram bin
+% and throw away the mantissa bits. However, in that case bin sizes end up
+% growing a bit too fast and we lose resolution quickly. To increase the
+% resolution, use a few most significant bit from the mantissa. For example,
+% use 3 more bits mantissa bits for a total of 14 bits: [e63...e53] + [m52,
+% m51, m50]:
+%
+%  sign  exponent    mantissa
+%  [s64] [e63...e53] [m52, m51, m50, m49...m1]
+%  <-1-> <-----------14------------> <--49--->
+%        ^^^^^^^ bin index ^^^^^^^^^
+%
+% With Erlang's wonderful binary matching capabilities this becomes a
+% one-liner:
+%
+%    <<0:1, BinIndex:14, _/bitstring>> = <<Val/float>>
+%
+% The internal implementation is a tuple of counter:new(?BIN_COUNT)
+% elements. The tuple size is determined by the time window parameter when the
+% histogram is created. After a histogram object is created, its Erlang term
+% structure is static so it's suitable to be stored in a persistent term [3].
+% The structure looks something like:
+%
+%  {
+%     1          = [1, 2, ..., ?BIN_COUNT]
+%     2          = [1, 2, ..., ?BIN_COUNT]
+%     ...
+%     TimeWindow = [1, 2, ..., ?BIN_COUNT]
+%  }
+%
+% The representation can also be regarded as a table with rows as abstract time
+% units: 1 = 1st second, 2 = 2nd second, etc, and the columns as histogram
+% bins. The update/3 function takes a value and a current time as parameters.
+% The time is used to select which row to update, and the value picks which
+% bin to increment.
+%
+% In practice, the time window would be used as a circular buffer. The time
+% parameter to the update/3 function might be the system monotonic clock time
+% and then the histogram time index is compuated as `Time rem TimeWindow`. So,
+% as the monotonic time is advancing forward, the histogram time index will
+% loop around. This comes with a minor annoynance of having to allocate a
+% larger time window to accomodate some process which cleans stale (expired)
+% histogram entries, possibly with some extra buffers to ensure the currently
+% updated interval and the interval ready to be cleaned would not overlap.
+%
+% Reading a histogram can be done via the read/3 function. The function takes a
+% start time and an interval. All the histogram entries in the interval will be
+% summed together, bin by bin, and returned as a new counters object. This
+% functionality can be used to gather and merge multiple histogram together.
+%
+% To get a stats summary across a time window use the stats/3 function. Just
+% like the read/3 function, it takes a start time and a time interval over
+% which to summarize the data.
+%
+% In addition to the new/1, update/3, read/3, stats/3 there are simple
+% functions which default to using WindowSize = 1. The intent is they would be
+% used when a simple histogram is needed without the time window functionality.
+%
+% [1] https://www.erlang.org/doc/man/counters.html
+% [2] https://en.wikipedia.org/wiki/IEEE_754
+% [3] https://www.erlang.org/doc/man/persistent_term.html
+
+-module(couch_stats_histogram).
+
+-export([
+    new/0,
+    new/1,
+
+    update/2,
+    update/3,
+
+    stats/1,
+    stats/3,
+
+    read/1,
+    read/3,
+
+    clear/1,
+    clear/3,
+
+    bin_min/1,
+    bin_max/1,
+    bin_middle/1,
+
+    calc_bin_offset/0,
+    calc_bin_count/0,
+    get_bin_boundaries/0
+]).
+
+-on_load(check_constants/0).
+
+-define(FLOAT_SIZE, 64).
+
+% 11 standard float64 exponent bits + 3 more extra msb mantissa bits
+%
+-define(INDEX_BITS, 14).
+
+% Some practical min and max values to be able to have a fixed number of
+% histogram bins. When used for timing typical units are milliseconds, so use 
0.01
+% msec as the minimum, and 4M msec (over 1 hour) for maximum.
+%
+-define(MIN_VAL, 0.01).
+-define(MAX_VAL, 4000000.0).
+
+% These are computed from previously defined constants. Recompute them with
+% cal_bin_offset() and calc_bin_count(), respectively, if any of the constants
+% above change.
+%
+-define(BIN_OFFSET, -8129).
+-define(BIN_COUNT, 230).
+
+% Public API
+
+new() ->
+    new(1).
+
+new(TimeWindow) when is_integer(TimeWindow), TimeWindow >= 1 ->
+    list_to_tuple([counter() || _ <- lists:seq(1, TimeWindow)]).
+
+update(Ctx, Val) ->
+    update(Ctx, 1, Val).
+
+update(Ctx, Time, Val) when is_integer(Val) ->
+    update(Ctx, Time, float(Val));
+update(Ctx, Time, Val) when is_integer(Time), is_float(Val) ->
+    Val1 = min(max(Val, ?MIN_VAL), ?MAX_VAL),
+    Counter = hist_at(Ctx, Time),
+    counters:add(Counter, bin_index(Val1), 1).
+
+read(Ctx) ->
+    read(Ctx, 1, 1).
+
+read(Ctx, Time, Ticks) when is_integer(Time), is_integer(Ticks), Ticks >= 1 ->
+    Ticks1 = min(tuple_size(Ctx), Ticks),
+    read_fold(Ctx, Time, Ticks1, counter()).
+
+stats(Ctx) ->
+    stats(Ctx, 1, 1).
+
+stats(Ctx, Time, Ticks) when is_integer(Time), is_integer(Ticks), Ticks >= 1 ->
+    Counter = read(Ctx, Time, Ticks),
+    couch_stats_math:summary(Counter, ?BIN_COUNT).
+
+clear(Ctx) ->
+    clear(Ctx, 1, 1).
+
+clear(Ctx, Time, Ticks) when is_integer(Time), is_integer(Ticks), Ticks >= 1 ->
+    Ticks1 = min(tuple_size(Ctx), Ticks),
+    clear_fold(Ctx, Time, Ticks1).
+
+% Utility functions
+
+% Use this to recompute ?BIN_OFFSET if ?INDEX_BITS or MIN_VAL changes.
+%
+calc_bin_offset() ->
+    <<0:1, I:?INDEX_BITS, _/bitstring>> = <<?MIN_VAL/float>>,
+    % "1" because counter indices start at 1
+    1 - I.
+
+% Use this to recompute ?BIN_COUNT if ?INDEX_BITS, MIN_VAL, or MAX_VAL changes.
+%
+calc_bin_count() ->
+    bin_index(?MAX_VAL).
+
+get_bin_boundaries() ->
+    [{bin_min(I), bin_max(I)} || I <- lists:seq(1, ?BIN_COUNT)].
+
+% Private functions
+
+% Called from -on_load() directive. Verify that our constants are sane
+% if some are not updated module loading will throw an error.
+%
+check_constants() ->
+    case calc_bin_count() of
+        ?BIN_COUNT -> ok;
+        OtherBinCount -> error({bin_count_stale, ?BIN_COUNT, OtherBinCount})
+    end,
+    case calc_bin_offset() of
+        ?BIN_OFFSET -> ok;
+        OtherBinOffset -> error({bin_offset_stale, ?BIN_OFFSET, 
OtherBinOffset})
+    end.
+
+bin_index(Val) ->
+    % Select the exponent bits plus a few most significant bits from
+    % mantissa. ?BIN_OFFSET shifts the index into the range starting with 1
+    % so we can index counter bins (those start with 1, just like tuples).
+    <<0:1, BinIndex:?INDEX_BITS, _/bitstring>> = <<Val/float>>,
+    BinIndex + ?BIN_OFFSET.
+
+bin_min(Index) when is_integer(Index), Index >= 1, Index =< ?BIN_COUNT ->
+    BiasedIndex = Index - ?BIN_OFFSET,
+    % 1 is the sign bit
+    BinBitSize = ?FLOAT_SIZE - 1 - ?INDEX_BITS,
+    % Minimum value is the one with all the rest of mantissa bits set to 0
+    <<Min/float>> = <<0:1, BiasedIndex:?INDEX_BITS, 0:BinBitSize>>,
+    Min.
+
+bin_max(Index) when is_integer(Index), Index >= 1, Index =< ?BIN_COUNT ->
+    BiasedIndex = Index - ?BIN_OFFSET,
+    % 1 is the sign bit
+    BinBitSize = ?FLOAT_SIZE - 1 - ?INDEX_BITS,
+    % For Max the intuition is we first construct a next highest power of two
+    % value, by shifting left BinBitSize, then subtract 1. That sets all the
+    % bits to 1. (for ex.:  1 bsl 4 = 1000, 1000 - 1 = 111)
+    <<Max/float>> = <<0:1, BiasedIndex:?INDEX_BITS, ((1 bsl BinBitSize) - 
1):BinBitSize>>,
+    Max.
+
+bin_middle(Index) when is_integer(Index), Index >= 1, Index =< ?BIN_COUNT ->
+    BiasedIndex = Index - ?BIN_OFFSET,
+    % 1 is the sign bit
+    BinBitSize = ?FLOAT_SIZE - 1 - ?INDEX_BITS,
+    % Shift left 1 bit less than we do in bin_max, which is effectively
+    % is Max / 2.

Review Comment:
   extra "is" in "is effectively is Max"?



##########
src/couch_stats/src/couch_stats_histogram.erl:
##########
@@ -0,0 +1,449 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+% This module implements windowed base-2 histograms using Erlang counters [1].
+%
+% Base-2 histograms use power of 2 exponentially increasing bin widths. This
+% allows capturing a range of values from microseconds to hours with a
+% relatively small number of bins. The same principle is used when encoding
+% floating point numbers [2]. In fact, our histograms rely on the ease of
+% constructing and mainpululating binary representations of 64 bit floats in
+% Erlang to do all of its heavy lifting.
+%
+% As a refresher, the standard (IEEE 754) 64 bit floating point representations
+% looks something like:
+%
+%  sign  exponent    mantissa
+%  [s64] [e63...e53] [m52...m1]
+%  <-1-> <---11----> <---52--->
+%
+%
+% The simplest scheme migth be to use the exponent to select the histogram bin
+% and throw away the mantissa bits. However, in that case bin sizes end up
+% growing a bit too fast and we lose resolution quickly. To increase the
+% resolution, use a few most significant bit from the mantissa. For example,
+% use 3 more bits mantissa bits for a total of 14 bits: [e63...e53] + [m52,
+% m51, m50]:
+%
+%  sign  exponent    mantissa
+%  [s64] [e63...e53] [m52, m51, m50, m49...m1]
+%  <-1-> <-----------14------------> <--49--->
+%        ^^^^^^^ bin index ^^^^^^^^^
+%
+% With Erlang's wonderful binary matching capabilities this becomes a
+% one-liner:
+%
+%    <<0:1, BinIndex:14, _/bitstring>> = <<Val/float>>
+%
+% The internal implementation is a tuple of counter:new(?BIN_COUNT)
+% elements. The tuple size is determined by the time window parameter when the
+% histogram is created. After a histogram object is created, its Erlang term
+% structure is static so it's suitable to be stored in a persistent term [3].
+% The structure looks something like:
+%
+%  {
+%     1          = [1, 2, ..., ?BIN_COUNT]
+%     2          = [1, 2, ..., ?BIN_COUNT]
+%     ...
+%     TimeWindow = [1, 2, ..., ?BIN_COUNT]
+%  }
+%
+% The representation can also be regarded as a table with rows as abstract time
+% units: 1 = 1st second, 2 = 2nd second, etc, and the columns as histogram
+% bins. The update/3 function takes a value and a current time as parameters.
+% The time is used to select which row to update, and the value picks which
+% bin to increment.
+%
+% In practice, the time window would be used as a circular buffer. The time
+% parameter to the update/3 function might be the system monotonic clock time
+% and then the histogram time index is compuated as `Time rem TimeWindow`. So,
+% as the monotonic time is advancing forward, the histogram time index will
+% loop around. This comes with a minor annoynance of having to allocate a
+% larger time window to accomodate some process which cleans stale (expired)
+% histogram entries, possibly with some extra buffers to ensure the currently
+% updated interval and the interval ready to be cleaned would not overlap.
+%
+% Reading a histogram can be done via the read/3 function. The function takes a
+% start time and an interval. All the histogram entries in the interval will be
+% summed together, bin by bin, and returned as a new counters object. This
+% functionality can be used to gather and merge multiple histogram together.
+%
+% To get a stats summary across a time window use the stats/3 function. Just
+% like the read/3 function, it takes a start time and a time interval over
+% which to summarize the data.
+%
+% In addition to the new/1, update/3, read/3, stats/3 there are simple
+% functions which default to using WindowSize = 1. The intent is they would be
+% used when a simple histogram is needed without the time window functionality.
+%
+% [1] https://www.erlang.org/doc/man/counters.html
+% [2] https://en.wikipedia.org/wiki/IEEE_754
+% [3] https://www.erlang.org/doc/man/persistent_term.html
+
+-module(couch_stats_histogram).
+
+-export([
+    new/0,
+    new/1,
+
+    update/2,
+    update/3,
+
+    stats/1,
+    stats/3,
+
+    read/1,
+    read/3,
+
+    clear/1,
+    clear/3,
+
+    bin_min/1,
+    bin_max/1,
+    bin_middle/1,
+
+    calc_bin_offset/0,
+    calc_bin_count/0,
+    get_bin_boundaries/0
+]).
+
+-on_load(check_constants/0).
+
+-define(FLOAT_SIZE, 64).
+
+% 11 standard float64 exponent bits + 3 more extra msb mantissa bits
+%
+-define(INDEX_BITS, 14).
+
+% Some practical min and max values to be able to have a fixed number of
+% histogram bins. When used for timing typical units are milliseconds, so use 
0.01
+% msec as the minimum, and 4M msec (over 1 hour) for maximum.
+%
+-define(MIN_VAL, 0.01).
+-define(MAX_VAL, 4000000.0).
+
+% These are computed from previously defined constants. Recompute them with
+% cal_bin_offset() and calc_bin_count(), respectively, if any of the constants
+% above change.
+%
+-define(BIN_OFFSET, -8129).
+-define(BIN_COUNT, 230).
+
+% Public API
+
+new() ->
+    new(1).
+
+new(TimeWindow) when is_integer(TimeWindow), TimeWindow >= 1 ->
+    list_to_tuple([counter() || _ <- lists:seq(1, TimeWindow)]).
+
+update(Ctx, Val) ->
+    update(Ctx, 1, Val).
+
+update(Ctx, Time, Val) when is_integer(Val) ->
+    update(Ctx, Time, float(Val));
+update(Ctx, Time, Val) when is_integer(Time), is_float(Val) ->
+    Val1 = min(max(Val, ?MIN_VAL), ?MAX_VAL),
+    Counter = hist_at(Ctx, Time),
+    counters:add(Counter, bin_index(Val1), 1).
+
+read(Ctx) ->
+    read(Ctx, 1, 1).
+
+read(Ctx, Time, Ticks) when is_integer(Time), is_integer(Ticks), Ticks >= 1 ->
+    Ticks1 = min(tuple_size(Ctx), Ticks),
+    read_fold(Ctx, Time, Ticks1, counter()).
+
+stats(Ctx) ->
+    stats(Ctx, 1, 1).
+
+stats(Ctx, Time, Ticks) when is_integer(Time), is_integer(Ticks), Ticks >= 1 ->
+    Counter = read(Ctx, Time, Ticks),
+    couch_stats_math:summary(Counter, ?BIN_COUNT).
+
+clear(Ctx) ->
+    clear(Ctx, 1, 1).
+
+clear(Ctx, Time, Ticks) when is_integer(Time), is_integer(Ticks), Ticks >= 1 ->
+    Ticks1 = min(tuple_size(Ctx), Ticks),
+    clear_fold(Ctx, Time, Ticks1).
+
+% Utility functions
+
+% Use this to recompute ?BIN_OFFSET if ?INDEX_BITS or MIN_VAL changes.
+%
+calc_bin_offset() ->
+    <<0:1, I:?INDEX_BITS, _/bitstring>> = <<?MIN_VAL/float>>,
+    % "1" because counter indices start at 1
+    1 - I.
+
+% Use this to recompute ?BIN_COUNT if ?INDEX_BITS, MIN_VAL, or MAX_VAL changes.
+%
+calc_bin_count() ->
+    bin_index(?MAX_VAL).
+
+get_bin_boundaries() ->
+    [{bin_min(I), bin_max(I)} || I <- lists:seq(1, ?BIN_COUNT)].
+
+% Private functions
+
+% Called from -on_load() directive. Verify that our constants are sane
+% if some are not updated module loading will throw an error.
+%
+check_constants() ->
+    case calc_bin_count() of
+        ?BIN_COUNT -> ok;
+        OtherBinCount -> error({bin_count_stale, ?BIN_COUNT, OtherBinCount})
+    end,
+    case calc_bin_offset() of
+        ?BIN_OFFSET -> ok;
+        OtherBinOffset -> error({bin_offset_stale, ?BIN_OFFSET, 
OtherBinOffset})
+    end.
+
+bin_index(Val) ->
+    % Select the exponent bits plus a few most significant bits from
+    % mantissa. ?BIN_OFFSET shifts the index into the range starting with 1
+    % so we can index counter bins (those start with 1, just like tuples).
+    <<0:1, BinIndex:?INDEX_BITS, _/bitstring>> = <<Val/float>>,

Review Comment:
   I think `<<0:1` implies that `MIN_VAL` cannot be negative (which makes sense 
for measures of duration).
   
   For kicks, I tried:
   ```
   -define(MIN_VAL, -0.01).
   ```
   and saw this error:
   ```
   [error] 2023-07-14T22:09:19.738493Z [email protected] emulator -------- Error 
in process <0.20153.0> on node '[email protected]' with exit value:
   
{{badmatch,<<191,132,122,225,71,174,20,123>>},[{couch_stats_histogram,calc_bin_offset,0,[{file,"src/couch_stats_histogram.erl"},{line,185}]},{couch_stats_histogram,check_constants,0,[{file,"src/couch_stats_histogram.erl"},{line,207}]},{code_server,'-handle_on_load/5-fun-0-',1,[{file,"code_server.erl"},{line,1317}]}]}
   ```
   Chould that be more explicitly asserted somewhere?
   



##########
src/couch_stats/src/couch_stats_server.erl:
##########
@@ -0,0 +1,246 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+% couch_stats_server is in charge of:
+%   - Initial metric loading from application stats descriptions.
+%   - Recycling(resetting to 0) stale histogram counters.
+%   - Checking and reloading if stats descriptions change.
+%   - Checking and reloading if histogram window size config value changes.
+%
+
+-module(couch_stats_server).
+
+-behaviour(gen_server).
+
+-export([
+    reload/0
+]).
+
+-export([
+    start_link/0,
+    init/1,
+    handle_call/3,
+    handle_cast/2,
+    handle_info/2
+]).
+
+-define(RELOAD_INTERVAL_SEC, 600).
+
+-record(st, {
+    hist_interval,
+    histograms,
+    hist_tref,
+    reload_tref
+}).
+
+reload() ->
+    gen_server:call(?MODULE, reload).
+
+start_link() ->
+    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
+
+init([]) ->
+    St = #st{
+        hist_interval = config:get("stats", "interval"),
+        hist_tref = erlang:send_after(hist_msec(), self(), clean),

Review Comment:
   Would `clean_tref` be a more consistent name? What about `clean_msec` 
instead of `hist_msec`?



##########
src/couch_stats/src/couch_stats_math.erl:
##########
@@ -0,0 +1,403 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, eithe r express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_stats_math).
+
+-export([
+    summary/2
+]).
+
+% Stats are computed over two passes. First one, with acc1, gathers some
+% basics, then the second pass, with acc2, computes additional, more complex,
+% statistics.
+%
+-record(acc1, {
+    % Non-zero bins = [{BinIndex, Counts}, ...]
+    bins = [],
+    n = 0,
+    sum = 0,
+    sum_log = 0,
+    sum_inv = 0
+}).
+
+-record(acc2, {
+    diff_2_sum = 0,
+    diff_3_sum = 0,
+    diff_4_sum = 0,
+    % pXX = {Rank, PercentileValue}
+    p50 = {0, 0},
+    p75 = {0, 0},
+    p90 = {0, 0},
+    p95 = {0, 0},
+    p99 = {0, 0},
+    p999 = {0, 0}
+}).
+
+summary(Counter, BinCount) when is_tuple(Counter), is_integer(BinCount) ->
+    calc_stats(pass1(Counter, BinCount)).
+
+calc_stats(#acc1{n = 0}) ->
+    % Can't do much with 0 items. Instead of inserting division checks for
+    % 0 everywhere, just return a empty stats object
+    n0_stats();
+calc_stats(#acc1{n = N} = Stats) when is_integer(N), N > 0 ->
+    #acc1{bins = Bins, sum = Sum, sum_log = SumLog, sum_inv = SumInv} = Stats,
+    Mean = Sum / N,
+    Acc2 = pass2(Bins, N, Mean),
+    #acc2{
+        diff_2_sum = Diff2Sum,
+        diff_3_sum = Diff3Sum,
+        diff_4_sum = Diff4Sum,
+        p50 = {_, P50},
+        p75 = {_, P75},
+        p90 = {_, P90},
+        p95 = {_, P95},
+        p99 = {_, P99},
+        p999 = {_, P999}
+    } = Acc2,
+    Variance = Diff2Sum / N,
+    StdDev = math:sqrt(Variance),
+    [
+        {n, N},
+        {min, couch_stats_histogram:bin_min(element(1, hd(Bins)))},
+        {max, couch_stats_histogram:bin_max(element(1, lists:last(Bins)))},
+        {arithmetic_mean, Mean},
+        {geometric_mean, math:exp(SumLog / N)},
+        {harmonic_mean, N / SumInv},
+        {median, P50},
+        {variance, Variance},
+        {standard_deviation, StdDev},
+        {skewness, skewness(N, Diff3Sum, StdDev)},
+        {kurtosis, kurtosis(N, Diff4Sum, StdDev)},
+        {percentile, [
+            {50, P50},
+            {75, P75},
+            {90, P90},
+            {95, P95},
+            {99, P99},
+            {999, P999}
+        ]},
+        % Emit for compatibility reasons
+        {histogram, [{0, 0}]}
+    ].
+
+% Fist pass removes 0 bins and calculates some basics like sums, counts, sum
+% logs. Some of these can only be used after the second pass over the data.
+%
+pass1(Counter, BinCount) ->
+    lists:foldl(
+        fun(Index, #acc1{} = Acc) ->
+            case counters:get(Counter, Index) of
+                Count when is_integer(Count), Count =< 0 ->
+                    Acc;
+                Count when is_integer(Count), Count > 0 ->
+                    Val = couch_stats_histogram:bin_middle(Index),
+                    Acc#acc1{
+                        bins = [{Index, Count} | Acc#acc1.bins],
+                        n = Acc#acc1.n + Count,
+                        sum = Acc#acc1.sum + Count * Val,
+                        sum_log = Acc#acc1.sum_log + Count * math:log(Val),
+                        sum_inv = Acc#acc1.sum_inv + Count / Val
+                    }
+            end
+        end,
+        #acc1{},
+        lists:seq(BinCount, 1, -1)
+    ).
+
+% Second statistics pass. This calculates the diff squared, cubed and 4th power
+% sums. These are used later to get the 2nd, 3rd and 4th central momements to
+% calcuate variance, skewness and kurtosis. In this pass we also calculate
+% percentiles.
+%
+pass2(Bins, N, Mean) ->
+    % Initialize each percentile's rank value as N * Q. During traversal each
+    % corresponding rank will be decremeneted by the bin's count value until we
+    % get to the bin where percentile rank < bin count value. After we
+    % calculate a percentile (as a non-0 value), we ignore that percentile
+    % entry and stop updating its rank.
+    %
+    Acc0 = #acc2{
+        p50 = {N * 0.50, 0},
+        p75 = {N * 0.75, 0},
+        p90 = {N * 0.90, 0},
+        p95 = {N * 0.95, 0},
+        p99 = {N * 0.99, 0},
+        p999 = {N * 0.999, 0}
+    },
+    lists:foldl(
+        fun({Index, Count}, #acc2{} = Acc) ->
+            Acc1 = percentiles(Acc, Index, Count),
+            Diff = couch_stats_histogram:bin_middle(Index) - Mean,
+            Diff2 = Diff * Diff,
+            Diff3 = Diff2 * Diff,
+            Diff4 = Diff2 * Diff2,
+            Acc1#acc2{
+                diff_2_sum = Acc1#acc2.diff_2_sum + Count * Diff2,
+                diff_3_sum = Acc1#acc2.diff_3_sum + Count * Diff3,
+                diff_4_sum = Acc1#acc2.diff_4_sum + Count * Diff4
+            }
+        end,
+        Acc0,
+        Bins
+    ).
+
+%% erlfmt-ignore
+percentiles(#acc2{} = Acc1, Index, Count) ->
+    Acc2 = Acc1#acc2{p50 = percentile(Acc1#acc2.p50, Index, Count)},
+    Acc3 = Acc2#acc2{p75 = percentile(Acc2#acc2.p75, Index, Count)},
+    Acc4 = Acc3#acc2{p90 = percentile(Acc3#acc2.p90, Index, Count)},
+    Acc5 = Acc4#acc2{p95 = percentile(Acc4#acc2.p95, Index, Count)},
+    Acc6 = Acc5#acc2{p99 = percentile(Acc5#acc2.p99, Index, Count)},
+    Acc7 = Acc6#acc2{p999 = percentile(Acc6#acc2.p999, Index, Count)},
+    Acc7.
+
+percentile({Rank, 0}, Index, Count) when Rank < Count ->
+    Min = couch_stats_histogram:bin_min(Index),
+    Max = couch_stats_histogram:bin_max(Index),
+    Width = Max - Min,
+    % Count should not be 0, we already filtered out Count == 0 bins
+    Frac = Rank / Count,
+    % Do a bit of extra work to get a nicer interpolated percentile value. Frac
+    % is the fractional part of the bin width based on the rank left-over. For
+    % example, if the Count = 1000:
+    %
+    %   If Rank = 1, then Frac = 0.001 : We're closer to bin min
+    %   If Rank = 500, then Frac = 0.5 : We're closer the middle
+    %   If Rank = 950, then Frac = 0.95 : We're closer to bin max
+    %
+    Percentile = Min + Width * Frac,
+    {Rank, Percentile};
+percentile({Rank, 0}, _Index, Count) ->
+    % Haven't reached our bin yet, reduce this percentile's rank by Count
+    % amount and keep going.
+    {Rank - Count, 0};
+percentile({Rank, Percentile}, _, _) when is_number(Percentile), Percentile > 
0 ->
+    % Nothing left to do, we already calculated this percentile value.
+    {Rank, Percentile}.
+
+skewness(N, Diff3Sum, StdDev) ->
+    % https://en.wikipedia.org/wiki/Skewness
+    %   Skewness = M3 / StdDev^3
+    %   M3 = mean(Diff3Sum) = Diff3Sum/N. (3rd central moment)
+    case math:pow(StdDev, 3) of
+        StdDev3 when StdDev3 < 1.0e-12 ->
+            % If StdDev is too low avoid dividing by 0, assume skewness = 0
+            0;
+        StdDev3 ->
+            M3 = Diff3Sum / N,
+            M3 / StdDev3
+    end.
+
+kurtosis(N, Diff4Sum, StdDev) ->
+    % http://en.wikipedia.org/wiki/Kurtosis
+    %     Kurtosis = M4 / StdDev^4 - 3
+    %     M4 = mean(Diff4Sum) = Diff4Sum/4. (4th central momement)
+    %
+    % Normal distribution kurtosis is 3 so we subtract 3 to get excess kurtosis
+    % to show how it's different from a normal distribution.
+    case math:pow(StdDev, 4) of
+        StdDev4 when StdDev4 < 1.0e-12 ->
+            0;
+        StdDev4 ->
+            M4 = Diff4Sum / N,
+            M4 / StdDev4 - 3
+    end.
+
+n0_stats() ->
+    [
+        {n, 0},
+        {min, 0},
+        {max, 0},
+        {arithmetic_mean, 0},
+        {geometric_mean, 0},
+        {harmonic_mean, 0},
+        {median, 0},
+        {variance, 0},
+        {standard_deviation, 0},
+        {skewness, 0},
+        {kurtosis, 0},
+        {percentile, [
+            {50, 0},
+            {75, 0},
+            {90, 0},
+            {95, 0},
+            {99, 0},
+            {999, 0}
+        ]},
+        {histogram, [{0, 0}]}
+    ].
+
+-ifdef(TEST).
+
+-include_lib("couch/include/couch_eunit.hrl").
+
+basic_test() ->
+    H = couch_stats_histogram:new(),
+    Vals = [0.05, 0.9, 0.7, 0.7, 10.1, 11, 100.5, 0.10, 13.5],
+    [couch_stats_histogram:update(H, V) || V <- Vals],
+    Stats = couch_stats_histogram:stats(H),
+    Percentiles = prop(percentile, Stats),
+    ?assertEqual(length(Vals), prop(n, Stats)),
+    ?assert(flim(0.05, prop(min, Stats))),
+    ?assert(flim(104, prop(max, Stats))),
+    ?assert(flim(15.3, prop(arithmetic_mean, Stats))),
+    ?assert(flim(1.9, prop(geometric_mean, Stats))),
+    ?assert(flim(0.25, prop(harmonic_mean, Stats))),
+    ?assert(flim(0.9, prop(median, Stats))),
+    ?assert(flim(923, prop(variance, Stats))),
+    ?assert(flim(30.4, prop(standard_deviation, Stats))),
+    % Values are skewed toward the left so we should have a positive skew
+    % https://en.wikipedia.org/wiki/Skewness
+    ?assert(flim(2.3, prop(skewness, Stats))),
+    % We have more extreme tail outliers compared to a normal distribution, so
+    % excess kurtosis should be > 0. In stats-speak the distribution would
+    % be "leptokurtic".
+    ?assert(flim(3.7, prop(kurtosis, Stats))),
+    ?assert(flim(0.9, prop(50, Percentiles))),
+    ?assert(flim(11.7, prop(75, Percentiles))),
+    ?assert(flim(97, prop(90, Percentiles))),
+    ?assert(flim(100, prop(95, Percentiles))),
+    ?assert(flim(103, prop(99, Percentiles))),
+    ?assert(flim(104, prop(999, Percentiles))).
+
+min_extreme_test() ->
+    % All the values in the smallest bin
+    H = couch_stats_histogram:new(),
+    N = 1000000,
+    [couch_stats_histogram:update(H, 0) || _ <- lists:seq(1, N)],
+    Stats = couch_stats_histogram:stats(H),
+    Percentiles = prop(percentile, Stats),
+    ?assertEqual(N, prop(n, Stats)),
+    ?assert(flim(0, prop(min, Stats))),
+    ?assert(flim(0, prop(max, Stats))),
+    ?assert(flim(0, prop(arithmetic_mean, Stats))),
+    ?assert(flim(0, prop(geometric_mean, Stats))),
+    ?assert(flim(0, prop(harmonic_mean, Stats))),
+    ?assert(flim(0, prop(median, Stats))),
+    ?assert(flim(0, prop(variance, Stats))),
+    ?assert(flim(0, prop(standard_deviation, Stats))),
+    ?assert(flim(0, prop(skewness, Stats))),
+    ?assert(flim(0, prop(kurtosis, Stats))),
+    ?assert(flim(0, prop(50, Percentiles))),
+    ?assert(flim(0, prop(75, Percentiles))),
+    ?assert(flim(0, prop(90, Percentiles))),
+    ?assert(flim(0, prop(95, Percentiles))),
+    ?assert(flim(0, prop(99, Percentiles))),
+    ?assert(flim(0, prop(999, Percentiles))).
+
+max_extreme_test() ->
+    % All the values are in the largest bin
+    H = couch_stats_histogram:new(),
+    N = 1000000,
+    % ?BIN_COUNT in couch_stats_histogram.erl
+    HighestBin = 230,
+    [couch_stats_histogram:update(H, 10000000) || _ <- lists:seq(1, N)],
+    Stats = couch_stats_histogram:stats(H),
+    Percentiles = prop(percentile, Stats),
+    ?assertEqual(N, prop(n, Stats)),
+    % Min would be the lower bound of the highest bin
+    BinMin = couch_stats_histogram:bin_min(HighestBin),
+    ?assert(flim(BinMin, prop(min, Stats))),
+    % Max would be the highest bound of the highest bin
+    BinMax = couch_stats_histogram:bin_max(HighestBin),
+    ?assert(flim(BinMax, prop(max, Stats))),
+    BinMid = couch_stats_histogram:bin_middle(HighestBin),
+    ?assert(flim(BinMid, prop(arithmetic_mean, Stats))),
+    ?assert(flim(BinMid, prop(geometric_mean, Stats))),
+    ?assert(flim(BinMid, prop(harmonic_mean, Stats))),
+    ?assert(flim(BinMid, prop(median, Stats))),
+    ?assert(flim(0, prop(variance, Stats))),
+    ?assert(flim(0, prop(standard_deviation, Stats))),
+    ?assert(flim(0, prop(skewness, Stats))),
+    ?assert(flim(0, prop(kurtosis, Stats))),
+    ?assert(flim(BinMid, prop(50, Percentiles))),
+    ?assert(flim(4128767, prop(75, Percentiles))),
+    ?assert(flim(4168089, prop(90, Percentiles))),
+    ?assert(flim(4181196, prop(95, Percentiles))),
+    ?assert(flim(4191682, prop(99, Percentiles))),
+    ?assert(flim(4194041, prop(999, Percentiles))).
+
+normal_dist_test() ->
+    H = couch_stats_histogram:new(),
+    rand:seed(default, {1, 2, 3}),
+    N = 1000000,
+    Mean = 50,
+    Var = 100,
+    [couch_stats_histogram:update(H, rand:normal(Mean, Var)) || _ <- 
lists:seq(1, N)],
+    Stats = couch_stats_histogram:stats(H),
+    Percentiles = prop(percentile, Stats),
+    ?assertEqual(N, prop(n, Stats)),
+    ?assert(flim(3.7, prop(min, Stats))),
+    ?assert(flim(104, prop(max, Stats))),
+    ?assert(flim(Mean, prop(arithmetic_mean, Stats))),
+    ?assert(flim(49, prop(geometric_mean, Stats))),
+    ?assert(flim(48, prop(harmonic_mean, Stats))),
+    % Median and mean of a normal distribution should be the same
+    ?assert(flim(Mean, prop(median, Stats))),
+    ?assert(flim(Var, prop(variance, Stats))),
+    ?assert(flim(math:sqrt(Var), prop(standard_deviation, Stats))),
+    % Skewness should be close to 0 as the distribution is symmetric
+    ?assert(flim(0.0, prop(skewness, Stats))),
+    % Excess kurtosis should be 0. In stats-speak normal distribution is
+    % "mesokurtic".
+    ?assert(flim(0.0, prop(kurtosis, Stats))),
+    % P50 = Median = Mean
+    ?assert(flim(Mean, prop(50, Percentiles))),
+    ?assert(flim(56, prop(75, Percentiles))),
+    ?assert(flim(63, prop(90, Percentiles))),
+    ?assert(flim(68, prop(95, Percentiles))),
+    ?assert(flim(74, prop(99, Percentiles))),
+    ?assert(flim(82, prop(999, Percentiles))).
+
+uniform_dist_test() ->
+    H = couch_stats_histogram:new(),
+    rand:seed(default, {1, 2, 3}),
+    N = 1000000,
+    % rand:uniform/1 returns values in [1,N], so subtract 1 to get values 
closer to 0
+    RandFun = fun() -> rand:uniform(10000001) / 10 - 1 end,
+    [couch_stats_histogram:update(H, RandFun()) || _ <- lists:seq(1, N)],
+    Stats = couch_stats_histogram:stats(H),
+    Percentiles = prop(percentile, Stats),
+    ?assertEqual(N, prop(n, Stats)),
+    ?assert(flim(0, prop(min, Stats))),
+    ?assert(flim(1040000, prop(max, Stats))),
+    ?assert(flim(500000, prop(arithmetic_mean, Stats))),
+    ?assert(flim(368000, prop(geometric_mean, Stats))),
+    ?assert(flim(8800, prop(harmonic_mean, Stats))),
+    ?assert(flim(500000, prop(median, Stats))),
+    % Variance and stddev should be large for a uniform distribution
+    ?assert(flim(83.0e9, prop(variance, Stats))),
+    ?assert(flim(290000, prop(standard_deviation, Stats))),
+    % Skewness should be close to 0 as the distribution is symmetric
+    ?assert(flim(0.0, prop(skewness, Stats))),
+    % Uniform distribution would be platykurtic. Excess kurtosis should be
+    % negative we'd have fewer extreme outliers (at the tails) than a normal
+    % distribution might have.
+    ?assert(flim(-1.2, prop(kurtosis, Stats))),
+    ?assert(flim(500000, prop(50, Percentiles))),
+    ?assert(flim(750000, prop(75, Percentiles))),
+    ?assert(flim(900000, prop(90, Percentiles))),
+    ?assert(flim(950000, prop(95, Percentiles))),
+    ?assert(flim(1010000, prop(99, Percentiles))),
+    ?assert(flim(1040000, prop(999, Percentiles))).
+
+prop(Prop, KVs) ->
+    proplists:get_value(Prop, KVs).
+
+flim(X, Y) ->
+    flim(X, Y, max(0.05, abs(X * 0.05))).
+
+flim(X, Y, Tol) ->
+    abs(X - Y) < Tol.

Review Comment:
   Could you add a little documentation for this function? It's hard to guess 
what it does from the name, although it's pretty clearly enabling assertions of 
approximate equality for the above tests.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to