Current section

Files

Jump to
xprof_core src xprof_core_hist.erl
Raw

src/xprof_core_hist.erl

%%%
%%% High Dynamic Range (HDR) Histogram for Erlang
%%%
%%% This implementation is based on the Elixir version found at
%%% https://github.com/2nd/histogrex/ with adjustments based on
%%% https://github.com/HdrHistogram/hdr_histogram_erl.
%%%
%%%
%%% The MIT License (MIT)
%%%
%%% Copyright (c) 2017 Second Spectrum
%%%
%%% Permission is hereby granted, free of charge, to any person obtaining a copy
%%% of this software and associated documentation files (the "Software"), to deal
%%% in the Software without restriction, including without limitation the rights
%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
%%% copies of the Software, and to permit persons to whom the Software is
%%% furnished to do so, subject to the following conditions:
%%%
%%% The above copyright notice and this permission notice shall be included in all
%%% copies or substantial portions of the Software.
%%%
%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
%%% SOFTWARE.
-module(xprof_core_hist).
-export([new/2,
new/3,
new_concurrent/4,
record/2,
record_many/3,
reset/1,
delete/1,
total_count/1,
max/1,
min/1,
mean/1,
percentile/2,
stats/1
]).
%% compatibility API with hdr_histogram_erl for testing
-export([open/2,
open/3,
close/1,
get_total_count/1,
same/3
]).
%% API with compile time configurable backend
-export([hdr_new/2,
hdr_record/2,
hdr_reset/1,
hdr_stats/1
]).
-define(TABLE, ?MODULE).
-define(TOTAL_COUNT_INDEX, 2).
-record(hist,
{table,
%% field names from elixir
name,
bucket_count,
counts_length,
unit_magnitude,
sub_bucket_mask,
sub_bucket_count,
sub_bucket_half_count,
sub_bucket_half_count_magnitude
%% additional field names from C
, min %% lowest_trackable_value,
, max %% highest_trackable_value,
, precision %% significant_figures
}).
%%
%% API with compile time configurable backend
%%
-ifdef(XPROF_ERL_HIST).
hdr_new(Max, Prec) ->
new(Max, Prec).
hdr_record(HistRef, Value) ->
record(HistRef, Value).
hdr_reset(HistRef) ->
reset(HistRef).
hdr_stats(HistRef) ->
stats(HistRef).
-else.
hdr_new(Max, Prec) ->
hdr_histogram:open(Max, Prec).
hdr_record(HistRef, Value) ->
hdr_histogram:record(HistRef, Value).
hdr_reset(HistRef) ->
hdr_histogram:reset(HistRef).
hdr_stats(HistRef) ->
[{count, hdr_histogram:get_total_count(HistRef)},
{min, hdr_histogram:min(HistRef)},
{mean, hdr_histogram:mean(HistRef)},
{max, hdr_histogram:max(HistRef)},
{p50, hdr_histogram:percentile(HistRef, 50.0)},
{p75, hdr_histogram:percentile(HistRef, 75.0)},
{p90, hdr_histogram:percentile(HistRef, 90.0)},
{p99, hdr_histogram:percentile(HistRef, 99.0)}
].
-endif.
%%
%% Aliases from hdr_histogram NIF API
%%
open(Max, Prec) ->
new(1, Max, Prec).
open(Name, Max, Prec) ->
new_concurrent(Name, 1, Max, Prec).
close(H) ->
delete(H).
get_total_count(H) ->
total_count(H).
same(H, A, B) ->
ets:first(H#hist.table), %% badarg if H was deleted (table does not exist)
lowest_equivalent_value(H, A) =:= lowest_equivalent_value(H, B).
%%
%% Primary API
%%
new(Max, Precision) ->
new(1, Max, Precision).
new(Min, Max, Precision) ->
Tid = storage_new(),
do_new(Tid, Min, Max, Precision).
new_concurrent(Name, Min, Max, Precision) ->
Tid = storage_new_concurrent(Name),
do_new(Tid, Min, Max, Precision).
do_new(Table, Min, Max, Precision)
when Min > 0 andalso Max > Min
andalso 1 =< Precision andalso Precision =< 5 ->
LargestValueWithSingleUnitResolution = 2 * math:pow(10, Precision),
SubBucketCountMagnitude = int_ceil(math_log2(LargestValueWithSingleUnitResolution)),
SubBucketHalfCountMagnitude =
case SubBucketCountMagnitude < 1 of
true -> 1;
false -> SubBucketCountMagnitude - 1
end,
UnitMagnitude =
case int_floor(math_log2(Min)) of
N when N < 0 -> 0;
N -> N
end,
SubBucketCount = round(math:pow(2, SubBucketHalfCountMagnitude + 1)),
SubBucketHalfCount = round(SubBucketCount / 2),
SubBucketMask = (SubBucketCount - 1) bsl UnitMagnitude,
BucketCount = calculate_bucket_count(SubBucketCount bsl UnitMagnitude, Max, 1),
CountsLength = round((BucketCount + 1) * (SubBucketCount / 2)),
H = #hist{
table = Table,
name = hist_key,
bucket_count = BucketCount,
counts_length = CountsLength,
unit_magnitude = UnitMagnitude,
sub_bucket_mask = SubBucketMask,
sub_bucket_count = SubBucketCount,
sub_bucket_half_count = SubBucketHalfCount,
sub_bucket_half_count_magnitude = SubBucketHalfCountMagnitude,
min = Min,
max = Max,
precision = Precision
},
reset(H),
{ok, H}.
record(H, Value) when is_integer(Value) ->
do_record(H, Value, 1).
record_many(H, Value, N) when is_integer(Value), is_integer(N), N > 0 ->
do_record(H, Value, N).
do_record(H, Value, N) ->
Index = get_value_index(H, Value),
case H#hist.max < Value orelse
Index < 0 orelse H#hist.counts_length =< Index of
true ->
{error, value_out_of_range};
false ->
storage_record(H, Index, N)
end.
reset(H) ->
storage_reset(H).
delete(H) ->
storage_delete(H).
%% @doc Get the total number of recorded values. This is O(1)
-spec total_count(#hist{}) -> non_neg_integer().
total_count(H) ->
Counts = storage_get_counts(H),
element(?TOTAL_COUNT_INDEX, Counts).
max(H) ->
hd(do_get_multi_value(iterator(H), [max])).
min(H) ->
hd(do_get_multi_value(iterator(H), [min])).
mean(H) ->
do_mean(iterator(H)).
-spec percentile(#hist{}, float()) -> float().
percentile(H, Q) when Q > 0 andalso Q =< 100 ->
hd(do_get_multi_value(iterator(H), [{percentile, Q}])).
stats(H) ->
It = iterator(H),
[Min, P50, P75, P90, P99, Max] =
do_get_multi_value(
It,
[min,
{percentile, 50.0},
{percentile, 75.0},
{percentile, 90.0},
{percentile, 99.0},
max]),
[{count, do_total_count(It)},
{min, Min},
{mean, do_mean(It)},
{max, Max},
{p50, P50},
{p75, P75},
{p90, P90},
{p99, P99}
].
%%
%% Storage
%%
storage_new() ->
ets:new(?MODULE, [set, private]).
storage_new_concurrent(Name) ->
ets:new(Name, [set, public, {write_concurrency, true}]).
storage_record(H, Index, N) ->
ets:update_counter(H#hist.table, H#hist.name,
[{?TOTAL_COUNT_INDEX, N},
{Index + ?TOTAL_COUNT_INDEX + 1, N}]),
ok.
storage_get_counts(H) ->
case ets:lookup(H#hist.table, H#hist.name) of
[] ->
throw(data_missing_from_ets);
[Counts] ->
Counts
end.
storage_reset(H) ->
ets:insert(H#hist.table, create_row(H#hist.name, H#hist.counts_length)),
ok.
storage_delete(H) ->
ets:delete(H#hist.table),
ok.
create_row(Name, Count) ->
%% counters come after name and total_count that are stored at the start
erlang:make_tuple(?TOTAL_COUNT_INDEX + Count, 0, [{1, Name}]).
%%
%% Calculations
%%
round_to_significant_figures(0, _) ->
0;
round_to_significant_figures(V, Prec) ->
Factor = math:pow(10.0, Prec - int_ceil(math:log10(abs(V)))),
round(V * Factor) / Factor.
calculate_bucket_count(SmallestUntrackableValue, Max, BucketCount) ->
case SmallestUntrackableValue < Max of
false -> BucketCount;
true -> calculate_bucket_count((SmallestUntrackableValue bsl 1),
Max, BucketCount + 1)
end.
get_value_index(H, Value) ->
{Bucket, Sub} = get_bucket_indexes(H, Value),
get_count_index(H, Bucket, Sub).
get_bucket_indexes(H, Value) ->
Ceiling = bit_length((Value bor H#hist.sub_bucket_mask), 0),
BucketIndex = Ceiling - H#hist.unit_magnitude - (H#hist.sub_bucket_half_count_magnitude + 1),
SubBucketIndex = Value bsr (BucketIndex + H#hist.unit_magnitude),
{BucketIndex, SubBucketIndex}.
get_bucket_indexes_from_index(H, Index) when Index < H#hist.sub_bucket_half_count ->
{0, Index};
get_bucket_indexes_from_index(H, Index) ->
%%Magn = H#hist.sub_bucket_half_count_magnitude,
BucketIndex = (Index bsr H#hist.sub_bucket_half_count_magnitude) - 1,
SubBucketIndex = (Index + H#hist.sub_bucket_half_count)
- ((BucketIndex + 1) bsl H#hist.sub_bucket_half_count_magnitude),
{BucketIndex, SubBucketIndex}.
bit_length(Value, N) when Value >= 32768 ->
bit_length((Value bsr 16), N + 16);
bit_length(Value, N) ->
{Value2, N2} = case Value >= 128 of
true -> {(Value bsr 8), N + 8};
false -> {Value, N}
end,
{Value3, N3} = case Value2 >= 8 of
true -> {(Value2 bsr 4), N2 + 4};
false -> {Value2, N2}
end,
{Value4, N4} = case Value3 >= 2 of
true -> {(Value3 bsr 2), N3 + 2};
false -> {Value3, N3}
end,
case Value4 =:= 1 of
true -> N4 + 1;
false -> N4
end.
get_count_index(H, BucketIndex, SubBucketIndex) ->
BucketBaseIndex =
(BucketIndex + 1) bsl H#hist.sub_bucket_half_count_magnitude,
OffsetInBucket = SubBucketIndex - H#hist.sub_bucket_half_count,
BucketBaseIndex + OffsetInBucket.
value_from_index(H, BucketIndex, SubBucketIndex) ->
SubBucketIndex bsl (BucketIndex + H#hist.unit_magnitude).
highest_equivalent_value(H, BucketIndex, SubBucketIndex) ->
next_non_equivalent_value(H, BucketIndex, SubBucketIndex) - 1.
lowest_equivalent_value(H, Value) ->
{BucketIndex, SubBucketIndex} = get_bucket_indexes(H, Value),
lowest_equivalent_value(H, BucketIndex, SubBucketIndex).
lowest_equivalent_value(H, BucketIndex, SubBucketIndex) ->
value_from_index(H, BucketIndex, SubBucketIndex).
next_non_equivalent_value(H, BucketIndex, SubBucketIndex) ->
lowest_equivalent_value(H, BucketIndex, SubBucketIndex)
+ size_of_equivalent_value_range(H, BucketIndex, SubBucketIndex).
median_equivalent_value(H, BucketIndex, SubBucketIndex) ->
lowest_equivalent_value(H, BucketIndex, SubBucketIndex)
+ (size_of_equivalent_value_range(H, BucketIndex, SubBucketIndex) bsr 1).
size_of_equivalent_value_range(H, BucketIndex, SubBucketIndex) ->
AdjustedBucketIndex =
case SubBucketIndex >= H#hist.sub_bucket_count of
true -> BucketIndex + 1;
false -> BucketIndex
end,
1 bsl (H#hist.unit_magnitude + AdjustedBucketIndex).
%%
%% Iteration
%%
-record(it,
{h :: #hist{},
total_count,
counts
}).
iterator(H) ->
Counts = storage_get_counts(H),
#it{h = H,
counts = Counts,
total_count = element(?TOTAL_COUNT_INDEX, Counts)}.
do_total_count(It) ->
It#it.total_count.
do_mean(It) ->
case It#it.total_count of
0 -> 0;
TotalCount ->
TotalSum = do_mean_loop(It, 0, 0, 0),
Mean = TotalSum / TotalCount,
%% the NIF does this rounding on the value returned from c code
round_to_significant_figures(Mean, It#it.h#hist.precision)
end.
do_mean_loop(It, Index, CountToIndex, Total0) ->
case CountToIndex >= It#it.total_count of
true -> Total0;
false ->
CountAtIndex = count_at_index(It, Index),
Total =
case CountAtIndex of
0 -> Total0;
N ->
{BucketIndex, SubBucketIndex} =
get_bucket_indexes_from_index(It#it.h, Index),
Total0 + N * median_equivalent_value(
It#it.h,
BucketIndex,
SubBucketIndex)
end,
do_mean_loop(It, Index + 1, CountToIndex + CountAtIndex, Total)
end.
do_get_multi_value(#it{total_count = 0}, QList) ->
[0 || _ <- QList];
do_get_multi_value(It, QList) ->
PreparedQList =
[case Item of
max ->
{max, It#it.total_count};
min ->
{min, 1};
{percentile, Q} ->
CountAtPercentile = round(Q / 100 * It#it.total_count),
{percentile, CountAtPercentile}
end
|| Item <- QList],
CountAtIndex = count_at_index(It, 0),
do_multi_loop(It, 0, CountAtIndex, PreparedQList, []).
do_multi_loop(It, Index, CountToIndex, [{Tag, CountAtPercentile}|Multi], Res) ->
do_multi_loop(It, Index, CountToIndex, Tag, CountAtPercentile, Multi, Res);
do_multi_loop(_, _, _, [], Res) ->
lists:reverse(Res).
do_multi_loop(It, Index, CountToIndex, Tag, CountAtPercentile, Multi, Res) ->
case CountToIndex >= CountAtPercentile of
true ->
do_multi_loop(It, Index, CountToIndex, Multi,
[get_value_from_index(It#it.h, Tag, Index)|Res]);
false ->
NextIndex = Index + 1,
CountAtNextIndex = count_at_index(It, NextIndex),
CountToNextIndex = CountToIndex + CountAtNextIndex,
do_multi_loop(It, NextIndex, CountToNextIndex, Tag, CountAtPercentile, Multi, Res)
end.
get_value_from_index(H, max, MaxIndex) ->
{MaxBucketIndex, MaxSubBucketIndex} =
get_bucket_indexes_from_index(H, MaxIndex),
%% The NIF uses an old version of the c code which calls lowest.
%% In newer version of HdrHistogram_c hdr_max was refactorred and
%% besides other changes it uses highest.
%%highest_equivalent_value(It#it.h, MaxValue).
lowest_equivalent_value(H, MaxBucketIndex, MaxSubBucketIndex);
get_value_from_index(H, min, MinIndex) ->
{MinBucketIndex, MinSubBucketIndex} =
get_bucket_indexes_from_index(H, MinIndex),
lowest_equivalent_value(H, MinBucketIndex, MinSubBucketIndex);
get_value_from_index(H, percentile, Index) ->
{BucketIndex, SubBucketIndex} =
get_bucket_indexes_from_index(H, Index),
V = highest_equivalent_value(H, BucketIndex, SubBucketIndex),
%% the NIF does this rounding on the value returned from c code
round_to_significant_figures(V, H#hist.precision).
count_at_index(It, Index) ->
%% 1 is the name
%% 2 is the total_count
%% the real count buckets start at 3
%% Index is zero based
element(Index + ?TOTAL_COUNT_INDEX + 1, It#it.counts).
%% ceil/1 and floor/1 were introduced in OTP 20
-ifdef(ceil_floor).
int_ceil(F) ->
erlang:ceil(F).
int_floor(F) ->
erlang:floor(F).
-else.
int_ceil(F) ->
R = round(F),
if R < F -> R + 1;
true -> R
end.
int_floor(F) ->
R = round(F),
if R > F -> R - 1;
true -> R
end.
-endif.
-ifdef(before_OTP_18).
math_log2(V) ->
math:log(V)/math:log(2).
-else.
%% math:log2 was introduced in OTP 18
math_log2(V) ->
math:log2(V).
-endif.