Packages

Code instrumentation and metrics collection package.

Current section

Files

Jump to
basho_exometer_core src exometer_slot_slide.erl
Raw

src/exometer_slot_slide.erl

%% -------------------------------------------------------------------
%%
%% Copyright (c) 2014 Basho Technologies, Inc. All Rights Reserved.
%%
%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at http://mozilla.org/MPL/2.0/.
%%
%% -------------------------------------------------------------------
%% @doc
%%
%% == Introduction ==
%% This module defines a sliding time-window histogram with execution
%% cost control.
%%
%% The problem with traditional histograms is that every sample is
%% stored and processed, no matter what the desired resolution is.
%%
%% If a histogram has a sliding window of 100 seconds, and we have a
%% sample rate of 100Hz will give us 10000 elements to process every time
%% we want to calculate that average, which is expensive.
%% The same goes for min/max finds, percentile calculations, etc.
%%
%% The solution is to introduce cost-control, where we can balance
%% execution time against sample resolution.
%%
%% The obvious implementation is to lower the sample rate by throwing
%% away samples and just store evey N samples. However, this will
%% mean potentially missed min/max values, and other extreme data
%% that is seen by the edge code but is thrown away.
%%
%% A slotted slide histogram will define a number of time slots, each
%% spanning a fixed number of milliseconds. The slider then stores
%% slots that cover a given timespan into the past (a rolling
%% historgram). All slots older than the timespan are discarded.
%%
%% The "current" slot is defined as the time period between the time
%% stamp of the last stored slot and the time when it is time to store
%% the next slot. If the slot period (size) is 100ms, and the last
%% slot was stored in the histogram at msec 1200, the current slot
%% period ends at msec 1300.
%%
%% All samples received during the current slot are processed by a
%% low-cost fun that updates the current slot state. When the current
%% slot ends, another fun is used to transform the current slot state
%% to a value that is stored in the histogram.
%%
%% If a simple average is to be calculated for all samples received,
%% the sample-processing fun will add to the sum of the received
%% samples, and increment sample counter. When the current slot
%% expires, the result of SampleSum / SampleCount is stored in the
%% slot.
%%
%% If min/max are to be stored by the slotted histograms, the current
%% slot state would have a { Min, Max } tuple that is upadted with the
%% smallest and largest values received during the period. At slot
%% expiration the min/max tuple is simply stored, by the
%% transformation fun, in the histogram slots.
%%
%% By adjusting the slot period and the total histogram duration, the
%% cost of analysing the entire histogram can be balanced against
%% the resolution of that analysis.
%%
%%
%% == SLOT HISTOGRAM MANAGEMENT ==
%%
%% The slide state maintains a list of { TimeStamp, SlotElem }
%% slot tuples. TimeStamp is the time period (in monotonic ms),
%% rounded down to the resolution of the slot period, and SlotElem is
%% the tuple generated by the current slot transformation MFA. The
%% list is sorted on descending time stamps (newest slot first).
%%
%% Normally each element in the list has a timestamp that is
%% SlotPeriod milliseconds newer than the next slot in the
%% list. However, if no samples are received during the current slot,
%% no slot for that time stamp will be stored, leaving a hole in the
%% list. Normally, the the slot list would look like this (with a 100
%% msec slot period and a simple average value):
%%
%% <pre lang="erlang">
%% [ { 1400, 23.2 }, { 1300, 23.1 }, { 1200, 22.8 }, { 1100, 23.0 } ]
%% </pre>
%%
%% If no samples were received during the period between 1200 and 1300
%% (ms), no slot would be stored at that time stamp, yielding the
%% following list:
%%
%% <pre lang="erlang">
%% [ { 1400, 23.2 }, { 1300, 23.1 }, { 1100, 23.0 } ]
%% </pre>
%%
%% This means that the total length of the slot list may vary, even
%% if it always covers the same time span into the past.
%%
%% == SLOT LISTS ==
%%
%% The slotted slider stores its slots in two lists, list1, and list2.
%% list1 contains the newest slots. Once the oldest element in list1
%% is older than the time span covered by the histogram, the entire
%% content of list1 is shifted into list2, and list1 is set to [].
%% The old content of list2, if any, is discarded during the shift.
%%
%% When the content of the histogram is to be retrieved (through
%% fold{l,r}(), or to_list()), the entire content of list1 is prepended to
%% the part of list2 that is within than the time span covered by the
%% histogram.
%%
%% If the time span of the histogram is 5 seconds, with a 1 second
%% slot period, list1 can look like this :
%%
%% <pre lang="erlang">
%% list1 = [ {5000, 1.2}, {4000, 2.1}, {3000, 2.0}, {2000, 2.3}, {1000, 2.8} ]
%% </pre>
%%
%% When the next slot is stored in the list, add_slot() will detect
%% that the list is full since the oldest element ({1000, 20.8}) will
%% fall outside the time span covered by the histogram. List1 will
%% shifted to List2, and List1 will be set to the single new slot that
%% is to be stored:
%%
%% <pre lang="erlang">
%% list1 = [ {6000, 1.8} ]
%% list2 = [ {5000, 1.2}, {4000, 2.1}, {3000, 2.0}, {2000, 2.3}, {1000, 2.8} ]
%% </pre>
%%
%% To_list() and fold{l,r}() will return list1, and the first four elements
%% of list2 in order to get a complete histogram covering the entire
%% time span:
%%
%% <pre lang="erlang">
%% [ {6000, 1.8}, {5000, 1.2}, {4000, 2.1}, {3000, 2.0}, {2000, 2.3} ]
%% </pre>
%%
%%
%% == SAMPLE PROCESSING AND TRANSFORMATION FUN ==
%%
%% Two funs are provided to the new() function of the slotted slide
%% histogram. The processing function is called by add_element() and
%% will take the same sample value provided to that function together
%% with the current timestamp and slot state as arguments. The
%% function will return the new current slot state.
%%
%% <pre lang="erlang">
%% M:F(TimeStamp, Value, State) -> NewState
%% </pre>
%%
%% The first call to the sample processing fun when the current slot
%% is newly reset (just after a slot has been added to the histogram),
%% state will be set to 'undefined'
%%
%% <pre lang="erlang">
%% M:F(TimeStamp, Value, undefined) -> NewState
%% </pre>
%%
%% The transformation fun is called when the current slot has expired
%% and is to be stored in the histogram. It will receive the current
%% timestamp and slot state as arguments and returns the element to
%% be stored (together with a slot timestamp) in the slot histogram.
%%
%% <pre lang="erlang">
%% M:F(TimeStamp, State) -> Element
%% </pre>
%%
%% Element will present in the lists returned by to_list() and fold{l,r}().
%% If the transformation MFA cannot do its job, for example because
%% no samples have been processed by the sample processing fun,
%% the transformation fun should return 'undefined'
%%
%% See new/2 and its avg_sample() and avg_transform() functions for an
%% example of a simple average value implementation.
%%
%% @end
-module(exometer_slot_slide).
-export([new/2, new/4, new/5,
add_element/2,
add_element/3,
add_element/4,
reset/1,
to_list/1,
foldl/3,
foldl/4,
foldr/3,
foldr/4]).
-compile(inline).
-type(timestamp() :: integer()).
-type(value() :: any()).
-type(cur_state() :: any()).
-type(sample_fun() :: fun((timestamp(), value(), cur_state()) -> cur_state())).
-type transform_fun() :: fun((timestamp(), cur_state()) -> cur_state()).
%% Fixed size event buffer
%% A slot is indexed by taking the current timestamp (in ms) divided by the slot_period
%%
-record(slide, {timespan = 0 :: integer(), % How far back in time do we go, in slot period increments.
sample_fun :: sample_fun(),
transform_fun :: transform_fun(),
slot_period :: integer(), % Period, in ms, of each slot
cur_slot = 0 :: integer(), % Current slot as in
cur_state = undefined :: any(), % Total for the current slot
list1_start_slot = 0 ::integer(), % Slot of the first list1 element
list1 = [] :: list(),
list2 = [] :: list()}).
-spec new(integer(), integer()) -> #slide{}.
new(HistogramTimeSpan, SlotPeriod) ->
new(HistogramTimeSpan, SlotPeriod, fun avg_sample/3, fun avg_transform/2).
-spec new(integer(), integer(), sample_fun(), transform_fun()) -> #slide{}.
new(HistogramTimeSpan, SlotPeriod, SampleF, TransformF) ->
new(HistogramTimeSpan, SlotPeriod, SampleF, TransformF, []).
-spec new(integer(), integer(), sample_fun(), transform_fun(), list()) -> #slide{}.
new(HistogramTimeSpan, SlotPeriod, SampleF, TransformF, _Options)
when is_function(SampleF, 3), is_function(TransformF, 2) ->
#slide{timespan = trunc(HistogramTimeSpan / SlotPeriod),
sample_fun = SampleF,
transform_fun = TransformF,
slot_period = SlotPeriod,
cur_slot = trunc(timestamp() / SlotPeriod),
cur_state = undefined,
list1_start_slot = 0,
list1 = [],
list2 = []}.
-spec add_element(any(), #slide{}) -> #slide{}.
add_element(Val, Slide) ->
add_element(timestamp(), Val, Slide, false).
add_element(TS, Val, Slide) ->
add_element(TS, Val, Slide, false).
add_element(TS, Val, #slide{cur_slot = CurrentSlot,
sample_fun = SampleF} = Slide, Wrap) ->
TSSlot = get_slot(TS, Slide),
%% We have two options here:
%% 1. We have moved into a new slot since last call to add_element().
%% In this case, we invoke the transform MFA for the now completed slot
%% and add the returned element to the slider using the add_slot() function.
%% Once that is done, we start with fresh 'undefined' current slot state
%% that the next call to the sample MFA will receive.
%%
%% 2. We are in the same slot as during the last call to add_element().
%% In this case, we will simply call the sample MFA to update the
%% current slot state.
%%
%%
{Flag, Slide1} =
if TSSlot =/= CurrentSlot ->
add_slot(TS, Slide);
true ->
{false, Slide}
end,
%%
%% Invoke the sample MFA to get a new state to work with
%%
ret(Wrap, Flag,
Slide1#slide {cur_state = SampleF(TS, Val, Slide1#slide.cur_state)}).
ret(false, _, Slide) ->
Slide;
ret(true, Flag, Slide) ->
{Flag, Slide}.
-spec to_list(#slide{}) -> list().
%%
to_list(#slide{timespan = TimeSpan}) when TimeSpan == 0 ->
[];
%% Convert the whole histograms, in both buffers, to a list.
to_list(#slide{timespan = TimeSpan } = Slide) ->
Oldest = get_slot(Slide) - TimeSpan,
take_since(Oldest, Slide).
-spec reset(#slide{}) -> #slide{}.
reset(#slide{} = Slide) ->
Slide#slide { cur_state = undefined,
cur_slot = 0,
list1 = [],
list2 = [],
list1_start_slot = 0}.
foldl(TS, Fun, Acc, #slide{timespan = TimeSpan} = Slide) ->
Oldest = get_slot(TS, Slide) - TimeSpan,
lists:foldl(Fun, Acc, take_since(Oldest, Slide)).
foldl(Fun, Acc, Slide) ->
foldl(timestamp(), Fun, Acc, Slide).
foldr(TS, Fun, Acc, #slide{timespan = TimeSpan} = Slide) ->
Oldest = get_slot(TS, Slide) - TimeSpan,
lists:foldr(Fun, Acc, take_since(Oldest, Slide)).
foldr(Fun, Acc, Slide) ->
foldr(timestamp(), Fun, Acc, Slide).
%% Collect all elements in the histogram with a timestamp that falls
%% within the timespan ranging from Oldest up to the current timme.
%%
take_since(Oldest, #slide{%% cur_slot = CurrentSlot,
cur_state = CurrentState,
slot_period = SlotPeriod } =Slide) ->
%% Check if we need to add a slot for the current time period
%% before we start to grab data.
%% We add the current slot if it has a sample (cur_state =/= undefined)
%% and the slot period has expired.
TS = timestamp(),
%% TSSlot = get_slot(TS, Slide),
#slide { list1 = List1,
list2 = List2} =
%% if TSSlot =/= CurrentSlot, CurrentState =/= undefined ->
if CurrentState =/= undefined ->
{_, Sl} = add_slot(TS, Slide),
Sl;
true ->
Slide
end,
take_since(List2, Oldest, SlotPeriod, take_since(List1, Oldest, SlotPeriod, [])).
take_since([{TS,Element} |T], Oldest, SlotPeriod, Acc) when TS >= Oldest ->
take_since(T, Oldest, SlotPeriod, [{TS * SlotPeriod, Element} |Acc]);
take_since(_, _, _, Acc) ->
%% Don't reverse; already the wanted order.
Acc.
%%
get_slot(Slide) ->
get_slot(timestamp(), Slide).
get_slot(TS, #slide{slot_period = Period}) ->
trunc(TS / Period).
%%
%% Calculate the average data sampled during the current slot period
%% and push that average to the new slot.
%%
add_slot(TS, #slide{timespan = TimeSpan,
slot_period = SlotPeriod,
cur_slot = CurrentSlot,
cur_state = CurrentState,
transform_fun = TransformF,
list1 = List1,
list1_start_slot = StartSlot} = Slide) ->
%% Transform current slot state to an element to be deposited
%% in the histogram list
TSSlot = trunc(TS / SlotPeriod),
case TransformF(TS, CurrentState) of
undefined -> %% Transformation function could not produce an element
%% Reset the time slot to the current slot. Reset state./
{false, Slide#slide{ cur_slot = TSSlot,
cur_state = undefined}};
%% The transform function produced an element to store
Element ->
%%
%% Check if it is time to do a buffer swap.
%% If the oldset element of list1,
%%
if StartSlot < TSSlot - TimeSpan ->
%% Shift list1 into list2.
%% Add the new slot as the initial element of list1
{true, Slide#slide{ list1 = [{ CurrentSlot, Element }],
list2 = List1,
list1_start_slot = CurrentSlot,
cur_slot = TSSlot,
cur_state = undefined}};
true ->
%% No shift necessary. Tack on the new slot to list1.
{false,
Slide#slide{list1 = [{CurrentSlot, Element} | List1],
cur_slot = TSSlot,
cur_state = undefined}}
end
end.
%% Simple sample processor that maintains the average value
%% of all samples received during the current slot.
avg_sample(_TS, Value, undefined) ->
{ 1, Value };
avg_sample(_TS, Value, {Count, Total}) ->
{ Count + 1, Total + Value }.
%% If avg_sample() has not been called for the current time slot,
%% then the provided state will still be 'undefined'
avg_transform(_TS, undefined) ->
undefined;
%% Calculate the average of all received samples during the current slot
%% and return it as the element to be stored in the histogram.
avg_transform(_TS, { Count, Total }) ->
Total / Count. %% Return the average value.
timestamp() ->
%% Invented epoc is {1258,0,0}, or 2009-11-12, 4:26:40
%% Millisecond resolution
{MS,S,US} = os:timestamp(),
(MS-1258)*1000000000 + S*1000 + US div 1000.