Merge pull request #26 from basho/rdb-slides-squash
Add a sliding window sample types for histograms
diff --git a/include/folsom.hrl b/include/folsom.hrl
index 8b32fee..d0cc8ec 100644
--- a/include/folsom.hrl
+++ b/include/folsom.hrl
@@ -9,10 +9,26 @@
-define(DEFAULT_LIMIT, 5).
-define(DEFAULT_SIZE, 1028). % mimic codahale's metrics
+-define(DEFAULT_SLIDING_WINDOW, 60). % sixty second sliding window
-define(DEFAULT_ALPHA, 0.015). % mimic codahale's metrics
-define(DEFAULT_INTERVAL, 5000).
-define(DEFAULT_SAMPLE_TYPE, uniform).
+-record(slide, {
+ window = ?DEFAULT_SLIDING_WINDOW,
+ reservoir = folsom_metrics_histogram_ets:new(folsom_slide,
+ [duplicate_bag, {write_concurrency, true}, public]),
+ server
+ }).
+
+-record(slide_uniform, {
+ window = ?DEFAULT_SLIDING_WINDOW,
+ size = ?DEFAULT_SIZE,
+ reservoir = folsom_metrics_histogram_ets:new(folsom_slide_uniform,[ordered_set, {write_concurrency, true}, public]),
+ seed = now(),
+ server
+ }).
+
-record(uniform, {
size = ?DEFAULT_SIZE,
n = 1,
diff --git a/rebar.config b/rebar.config
index 1728cb5..479ba7b 100644
--- a/rebar.config
+++ b/rebar.config
@@ -1,7 +1,8 @@
{sub_dirs, ["deps"]}.
{deps, [
- {'bear', ".*", {git, "git://github.com/boundary/bear.git", "master"}}
+ {'bear', ".*", {git, "git://github.com/boundary/bear.git", "master"}},
+ {meck, ".*", {git, "git://github.com/eproxus/meck", "master"}}
]}.
{erl_opts, [debug_info]}.
diff --git a/src/folsom_ets.erl b/src/folsom_ets.erl
index a17e195..44fdd9b 100644
--- a/src/folsom_ets.erl
+++ b/src/folsom_ets.erl
@@ -266,6 +266,18 @@
delete_histogram(Name, #histogram{type = exdec}) ->
true = ets:delete(?HISTOGRAM_TABLE, Name),
true = ets:delete(?FOLSOM_TABLE, Name),
+ ok;
+delete_histogram(Name, #histogram{type = slide, sample = #slide{reservoir = Reservoir, server=Pid}}) ->
+ folsom_sample_slide_server:stop(Pid),
+ true = ets:delete(?HISTOGRAM_TABLE, Name),
+ true = ets:delete(?FOLSOM_TABLE, Name),
+ true = ets:delete(Reservoir),
+ ok;
+delete_histogram(Name, #histogram{type = slide_uniform, sample = #slide_uniform{reservoir = Reservoir, server=Pid}}) ->
+ folsom_sample_slide_server:stop(Pid),
+ true = ets:delete(?HISTOGRAM_TABLE, Name),
+ true = ets:delete(?FOLSOM_TABLE, Name),
+ true = ets:delete(Reservoir),
ok.
delete_history(Name, #history{tid = Tid}) ->
diff --git a/src/folsom_metrics_histogram.erl b/src/folsom_metrics_histogram.erl
index 8cd9117..401e29f 100644
--- a/src/folsom_metrics_histogram.erl
+++ b/src/folsom_metrics_histogram.erl
@@ -38,6 +38,10 @@
new(Name) ->
new(Name, uniform).
+new(Name, slide) ->
+ new(Name, slide, ?DEFAULT_SLIDING_WINDOW);
+new(Name, slide_uniform) ->
+ new(Name, slide_uniform, {?DEFAULT_SLIDING_WINDOW, ?DEFAULT_SIZE});
new(Name, SampleType) ->
new(Name, SampleType, ?DEFAULT_SIZE).
diff --git a/src/folsom_sample.erl b/src/folsom_sample.erl
index fe5a05f..f08d233 100644
--- a/src/folsom_sample.erl
+++ b/src/folsom_sample.erl
@@ -36,12 +36,20 @@
%% API
+new(slide) ->
+ new(slide, ?DEFAULT_SLIDING_WINDOW);
+new(slide_uniform) ->
+ new(slide_uniform, {?DEFAULT_SLIDING_WINDOW, ?DEFAULT_SIZE});
new(Type) ->
new(Type, ?DEFAULT_SIZE, ?DEFAULT_ALPHA).
new(Type, Size) ->
new(Type, Size, ?DEFAULT_ALPHA).
+new(slide, Size, _) ->
+ folsom_sample_slide:new(Size);
+new(slide_uniform, Sizes, _) ->
+ folsom_sample_slide_uniform:new(Sizes);
new(uniform, Size, _) ->
folsom_sample_uniform:new(Size);
new(none, Size, _) ->
@@ -54,11 +62,20 @@
update(none, Sample, Value) ->
folsom_sample_none:update(Sample, Value);
update(exdec, Sample, Value) ->
- folsom_sample_exdec:update(Sample, Value).
+ folsom_sample_exdec:update(Sample, Value);
+update(slide, Sample, Value) ->
+ folsom_sample_slide:update(Sample, Value);
+update(slide_uniform, Sample, Value) ->
+ folsom_sample_slide_uniform:update(Sample, Value).
+
get_values(uniform, Sample) ->
folsom_sample_uniform:get_values(Sample);
get_values(none, Sample) ->
folsom_sample_none:get_values(Sample);
get_values(exdec, Sample) ->
- folsom_sample_exdec:get_values(Sample).
+ folsom_sample_exdec:get_values(Sample);
+get_values(slide, Sample) ->
+ folsom_sample_slide:get_values(Sample);
+get_values(slide_uniform, Sample) ->
+ folsom_sample_slide_uniform:get_values(Sample).
diff --git a/src/folsom_sample_slide.erl b/src/folsom_sample_slide.erl
new file mode 100644
index 0000000..4b13a36
--- /dev/null
+++ b/src/folsom_sample_slide.erl
@@ -0,0 +1,55 @@
+%%%
+%%% Copyright 2012, Basho Technologies, Inc. All Rights Reserved.
+%%%
+%%% 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.
+%%%
+%%%-------------------------------------------------------------------
+%%% File: folsom_sample_slide.erl
+%%% @author Russell Brown <russelldb@basho.com>
+%%% @doc
+%%% Sliding window sample. Last Window seconds readings are recorded.
+%%% @end
+%%%-----------------------------------------------------------------
+
+-module(folsom_sample_slide).
+
+-export([
+ new/1,
+ update/2,
+ get_values/1,
+ moment/0,
+ trim/2
+ ]).
+
+-include("folsom.hrl").
+
+new(Size) ->
+ Sample = #slide{window = Size},
+ Pid = folsom_sample_slide_sup:start_slide_server(?MODULE, Sample#slide.reservoir, Sample#slide.window),
+ Sample#slide{server=Pid}.
+
+update(#slide{reservoir = Reservoir} = Sample, Value) ->
+ Moment = moment(),
+ ets:insert(Reservoir, {Moment, Value}),
+ Sample.
+
+get_values(#slide{window = Window, reservoir = Reservoir}) ->
+ Oldest = moment() - Window,
+ ets:select(Reservoir, [{{'$1','$2'},[{'>=', '$1', Oldest}],['$2']}]).
+
+moment() ->
+ folsom_utils:now_epoch().
+
+trim(Reservoir, Window) ->
+ Oldest = moment() - Window,
+ ets:select_delete(Reservoir, [{{'$1','_'},[{'<', '$1', Oldest}],['true']}]).
diff --git a/src/folsom_sample_slide_server.erl b/src/folsom_sample_slide_server.erl
new file mode 100644
index 0000000..5986918
--- /dev/null
+++ b/src/folsom_sample_slide_server.erl
@@ -0,0 +1,73 @@
+%% -------------------------------------------------------------------
+%%
+%% Copyright (c) 2011 Basho Technologies, Inc.
+%%
+%% This file is provided to you 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.
+%%
+%% -------------------------------------------------------------------
+%%%-------------------------------------------------------------------
+%%% File: folsom_sample_slide_server.erl
+%%% @author Russell Brown <russelldb@basho.com>
+%%% @doc
+%%% Serialization point for folsom_sample_slide. Handles
+%%% pruning of older smaples. One started per histogram.
+%%% See folsom.hrl, folsom_sample_slide, folsom_sample_slide_sup
+%%% @end
+%%%-----------------------------------------------------------------
+-module(folsom_sample_slide_server).
+
+-behaviour(gen_server).
+
+%% API
+-export([start_link/3, stop/1]).
+
+-record(state, {sample_mod, reservoir, window}).
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+start_link(SampleMod, Reservoir, Window) ->
+ gen_server:start_link(?MODULE, [SampleMod, Reservoir, Window], []).
+
+stop(Pid) ->
+ gen_server:cast(Pid, stop).
+
+init([SampleMod, Reservoir, Window]) ->
+ {ok, #state{sample_mod = SampleMod, reservoir = Reservoir, window = Window}, timeout(Window)}.
+
+handle_call(_Request, _From, State) ->
+ Reply = ok,
+ {reply, Reply, State}.
+
+handle_cast(stop, State) ->
+ {stop, normal, State};
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info(timeout, State=#state{sample_mod = SampleMod, reservoir = Reservoir, window = Window}) ->
+ SampleMod:trim(Reservoir, Window),
+ {noreply, State, timeout(Window)};
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+timeout(Window) ->
+ timer:seconds(Window) div 2.
diff --git a/src/folsom_sample_slide_sup.erl b/src/folsom_sample_slide_sup.erl
new file mode 100644
index 0000000..c8929d1
--- /dev/null
+++ b/src/folsom_sample_slide_sup.erl
@@ -0,0 +1,51 @@
+%% -------------------------------------------------------------------
+%%
+%% Copyright (c) 2011 Basho Technologies, Inc.
+%%
+%% This file is provided to you 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.
+%%
+%% -------------------------------------------------------------------
+%%%-------------------------------------------------------------------
+%%% File: folsom_sample_slide_sup.erl
+%%% @author Russell Brown <russelldb@basho.com>
+%%% @doc
+%%% Starts simple_one_for_one children per slide sample
+%%% @end
+%%%-----------------------------------------------------------------
+-module(folsom_sample_slide_sup).
+-behaviour(supervisor).
+
+%% beahvior functions
+-export([start_link/0,
+ init/1
+ ]).
+
+%% public functions
+-export([start_slide_server/3]).
+
+start_link () ->
+ supervisor:start_link({local,?MODULE},?MODULE,[]).
+
+start_slide_server(SampleMod, Reservoir, Window) ->
+ {ok, Pid} = supervisor:start_child(?MODULE, [SampleMod, Reservoir, Window]),
+ Pid.
+
+%% @private
+init ([]) ->
+ {ok,{{simple_one_for_one,10,10},
+ [
+ {undefined, {folsom_sample_slide_server, start_link, []},
+ transient, brutal_kill, worker, [folsom_sample_slide_server]}
+ ]}}.
diff --git a/src/folsom_sample_slide_uniform.erl b/src/folsom_sample_slide_uniform.erl
new file mode 100644
index 0000000..7e7b5eb
--- /dev/null
+++ b/src/folsom_sample_slide_uniform.erl
@@ -0,0 +1,72 @@
+%%%
+%%% Copyright 2012, Basho Technologies, Inc. All Rights Reserved.
+%%%
+%%% 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.
+%%%
+%%%-------------------------------------------------------------------
+%%% File: folsom_sample_slide.erl
+%%% @author Russell Brown <russelldb@basho.com>
+%%% @doc
+%%% Sliding window sample. Last Window seconds readings are recorded.
+%%% @end
+%%%-----------------------------------------------------------------
+
+-module(folsom_sample_slide_uniform).
+
+-export([
+ new/1,
+ update/2,
+ get_values/1,
+ moment/0,
+ trim/2
+ ]).
+
+-include("folsom.hrl").
+
+new({Window, SampleSize}) ->
+ Sample = #slide_uniform{window = Window, size = SampleSize},
+ Pid = folsom_sample_slide_sup:start_slide_server(?MODULE, Sample#slide_uniform.reservoir, Sample#slide_uniform.window),
+ Sample#slide_uniform{server=Pid}.
+
+update(#slide_uniform{reservoir = Reservoir, size = Size, seed = Seed} = Sample0, Value) ->
+ Moment = moment(),
+ ets:insert_new(Reservoir, {Moment, 0}),
+ MCnt = ets:update_counter(Reservoir, Moment, 1),
+ Sample = case MCnt > Size of
+ true ->
+ {Rnd, NewSeed} = random:uniform_s(Size, Seed),
+ maybe_update(Reservoir, {{Moment, Rnd}, Value}, Size),
+ Sample0#slide_uniform{seed = NewSeed};
+ false ->
+ ets:insert(Reservoir, {{Moment, MCnt}, Value}),
+ Sample0
+ end,
+ Sample.
+
+maybe_update(Reservoir, {{_Moment, Rnd}, _Value}=Obj, Size) when Rnd =< Size ->
+ ets:insert(Reservoir, Obj);
+maybe_update(_Reservoir, _Obj, _Size) ->
+ ok.
+
+get_values(#slide_uniform{window = Window, reservoir = Reservoir}) ->
+ Oldest = moment() - Window,
+ ets:select(Reservoir, [{{{'$1', '_'},'$2'},[{'>=', '$1', Oldest}],['$2']}]).
+
+moment() ->
+ folsom_utils:now_epoch().
+
+trim(Reservoir, Window) ->
+ Oldest = moment() - Window,
+ ets:select_delete(Reservoir, [{{{'$1', '_'},'_'},[{'<', '$1', Oldest}],['true']}]),
+ %% and trim the counters
+ ets:select_delete(Reservoir, [{{'$1','_'},[{is_integer, '$1'}, {'<', '$1', Oldest}],['true']}]).
diff --git a/src/folsom_sup.erl b/src/folsom_sup.erl
index 5d0da83..3d30704 100644
--- a/src/folsom_sup.erl
+++ b/src/folsom_sup.erl
@@ -89,7 +89,10 @@
{folsom_metrics_histogram_ets, start_link, []},
Restart, Shutdown, Type, [folsom_metrics_histogram_ets]},
- {ok, {SupFlags, [TimerServer, HistETSServer]}}.
+ SlideSup = {folsom_sample_slide_sup, {folsom_sample_slide_sup, start_link, []},
+ permanent, 5000, supervisor, [folsom_sample_slide_sup]},
+
+ {ok, {SupFlags, [SlideSup, TimerServer, HistETSServer]}}.
%%%===================================================================
%%% Internal functions
diff --git a/src/folsom_utils.erl b/src/folsom_utils.erl
index 0faa9a8..d6192cd 100644
--- a/src/folsom_utils.erl
+++ b/src/folsom_utils.erl
@@ -50,4 +50,3 @@
get_ets_size(Tab) ->
ets:info(Tab, size).
-
diff --git a/test/folsom_sample_slide_test.erl b/test/folsom_sample_slide_test.erl
new file mode 100644
index 0000000..b1a0fd5
--- /dev/null
+++ b/test/folsom_sample_slide_test.erl
@@ -0,0 +1,105 @@
+%%%
+%%% Copyright 2012 - Basho Technologies, Inc. All Rights Reserved.
+%%%
+%%% 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.
+%%%
+
+%%%-------------------------------------------------------------------
+%%% File: folsom_sample_slide.erl
+%%% @author Russell Brown <russelldb@basho.com>
+%%% @doc eunit test for folsom_sample_slide.erl
+%%% @end
+%%%------------------------------------------------------------------
+
+-module(folsom_sample_slide_test).
+
+-include_lib("eunit/include/eunit.hrl").
+-include("folsom.hrl").
+
+-define(HISTO, test_slide).
+-define(WINDOW, 30).
+-define(RUNTIME, 90).
+-define(READINGS, 10).
+
+slide_test_() ->
+ {setup,
+ fun () -> folsom:start(),
+ meck:new(folsom_utils)
+ end,
+ fun (_) -> meck:unload(folsom_utils),
+ folsom:stop() end,
+ [{"Create sliding window",
+ fun create/0},
+ {"test sliding window",
+ {timeout, 30, fun exercise/0}}
+ ]}.
+
+create() ->
+ ok = folsom_metrics:new_histogram(?HISTO, slide, ?WINDOW),
+ #histogram{sample=Slide} = folsom_metrics_histogram:get_value(?HISTO),
+ ?assert(is_pid(Slide#slide.server)),
+ ?assertEqual(?WINDOW, Slide#slide.window),
+ ?assertEqual(0, ets:info(Slide#slide.reservoir, size)).
+
+exercise() ->
+ %% don't want a trim to happen
+ %% unless we call trim
+ %% so kill the trim server process
+ #histogram{sample=Slide} = folsom_metrics_histogram:get_value(?HISTO),
+ ok = folsom_sample_slide_server:stop(Slide#slide.server),
+ Moments = lists:seq(1, ?RUNTIME),
+ %% pump in 90 seconds worth of readings
+ Moment = lists:foldl(fun(_X, Tick) ->
+ Tock = tick(Tick),
+ [folsom_sample_slide:update(Slide, N) ||
+ N <- lists:duplicate(?READINGS, Tock)],
+ Tock end,
+ 0,
+ Moments),
+ %% are all readings in the table?
+ check_table(Slide, Moments),
+ %% get values only returns last ?WINDOW seconds
+ ExpectedValues = lists:sort(lists:flatten([lists:duplicate(?READINGS, N) ||
+ N <- lists:seq(?RUNTIME - ?WINDOW, ?RUNTIME)])),
+ Values = lists:sort(folsom_sample_slide:get_values(Slide)),
+ ?assertEqual(ExpectedValues, Values),
+ %% trim the table
+ Trimmed = folsom_sample_slide:trim(Slide#slide.reservoir, ?WINDOW),
+ ?assertEqual((?RUNTIME - ?WINDOW - 1) * ?READINGS, Trimmed),
+ check_table(Slide, lists:seq(?RUNTIME - ?WINDOW, ?RUNTIME)),
+ %% increment the clock past the window
+ tick(Moment, ?WINDOW * 2),
+ %% get values should be empty
+ ?assertEqual([], folsom_sample_slide:get_values(Slide)),
+ %% trim, and table should be empty
+ Trimmed2 = folsom_sample_slide:trim(Slide#slide.reservoir, ?WINDOW),
+ ?assertEqual((?RUNTIME * ?READINGS) - ((?RUNTIME - ?WINDOW - 1) * ?READINGS), Trimmed2),
+ check_table(Slide, []),
+ ok.
+
+tick(Moment0, IncrBy) ->
+ Moment = Moment0 + IncrBy,
+ meck:expect(folsom_utils, now_epoch, fun() ->
+ Moment end),
+ Moment.
+
+tick(Moment) ->
+ tick(Moment, 1).
+
+check_table(Slide, Moments) ->
+ Tab = lists:sort(ets:tab2list(Slide#slide.reservoir)),
+ {Ks, Vs} = lists:unzip(Tab),
+ ExpectedVs = lists:sort(lists:flatten([lists:duplicate(10, N) || N <- Moments])),
+ Keys = lists:usort(Ks),
+ ?assertEqual(Moments, Keys),
+ ?assertEqual(ExpectedVs, lists:sort(Vs)).
diff --git a/test/slide_statem_eqc.erl b/test/slide_statem_eqc.erl
new file mode 100644
index 0000000..a3fddd9
--- /dev/null
+++ b/test/slide_statem_eqc.erl
@@ -0,0 +1,164 @@
+%%%
+%%% Copyright 2012 - Basho Technologies, Inc. All Rights Reserved.
+%%%
+%%% 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.
+%%%
+
+%%%-------------------------------------------------------------------
+%%% File: slide_statem_eqc.erl
+%%% @author Russell Brown <russelldb@basho.com>
+%%% @doc quickcheck test for the folsom_sample_slide.erl
+%%% @end
+%%%------------------------------------------------------------------
+
+-module(slide_statem_eqc).
+
+-compile(export_all).
+
+-ifdef(TEST).
+-ifdef(EQC).
+
+-include("folsom.hrl").
+
+-include_lib("eqc/include/eqc.hrl").
+-include_lib("eqc/include/eqc_statem.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+
+-define(NUMTESTS, 200).
+-define(QC_OUT(P),
+ eqc:on_output(fun(Str, Args) ->
+ io:format(user, Str, Args) end, P)).
+
+-define(WINDOW, 60).
+
+-record(state, {moment=1000,
+ sample,
+ name,
+ values=[]}).
+
+initial_state() ->
+ meck:expect(folsom_utils, now_epoch, fun() -> 1000 end),
+ #state{}.
+
+command(S) ->
+ oneof(
+ [{call, ?MODULE, new_histo, []} || S#state.sample == undefined] ++
+ [{call, ?MODULE, tick, [S#state.moment]} || S#state.sample /= undefined] ++
+ [{call, ?MODULE, update, [S#state.sample, int()]} || S#state.sample /= undefined] ++
+ [{call, ?MODULE, trim, [S#state.sample, ?WINDOW]} || S#state.sample /= undefined] ++
+ [{call, ?MODULE, get_values, [S#state.sample]} || S#state.sample /= undefined]
+ ).
+
+%% Next state transformation, S is the current state
+next_state(S, V, {call, ?MODULE, new_histo, []}) ->
+ S#state{name={call, erlang, element, [1, V]}, sample={call, erlang, element, [2, V]}};
+next_state(S, V, {call, ?MODULE, tick, [_Moment]}) ->
+ S#state{moment=V};
+next_state(#state{moment=Moment, values=Values0}=S, V, {call, ?MODULE, update, [_, _Val]}) ->
+ S#state{values=Values0 ++ [{Moment, V}]};
+next_state(#state{values=Values, moment=Moment}=S, _V, {call, ?MODULE, trim, _}) ->
+ %% trim the model
+ S#state{values = trim(Values, Moment, ?WINDOW)};
+next_state(S,_V,{call, ?MODULE, _, _}) ->
+ S.
+
+%% Precondition, checked before command is added to the command sequence
+precondition(S, {call, _, new_histo, _}) ->
+ S#state.sample == undefined;
+precondition(S, _) when S#state.sample == undefined ->
+ false;
+precondition(_S, {call, _, _, _}) ->
+ true.
+
+%% Postcondition, checked after command has been evaluated
+%% OBS: S is the state before next_state(S,_,<command>)
+postcondition(#state{values=Values0, moment=Moment}, {call, ?MODULE, get_values, _}, Res) ->
+ Values = [V || {K, V} <- Values0, K >= Moment - ?WINDOW],
+ case lists:sort(Values) == lists:sort(Res) of
+ true ->
+ true;
+ _ ->
+ {"get values", {"model", lists:sort(Values)},
+ {"smaple", lists:sort(Res)}}
+ end;
+postcondition(#state{values=Values, sample=Sample, moment=Moment}, {call, ?MODULE, trim, _}, _TrimCnt) ->
+ %% check that values and the actual table contents are the same after a trim
+ Table = ets:tab2list(Sample#slide.reservoir),
+ Model = lists:sort(trim(Values, Moment, ?WINDOW)),
+ case Model == lists:sort(Table) of
+ true ->
+ true;
+ _ ->
+ {"after trim", {"model", Model}, {"sample", lists:sort(Table)}}
+ end;
+postcondition(_S, {call, ?MODULE, _, _}, _Res) ->
+ true.
+
+prop_window_test_() ->
+ {setup, fun() -> ok end, fun(_X) -> (catch meck:unload(folsom_utils)), folsom:stop() end,
+ fun(_X) ->
+ ?_assert(eqc:quickcheck(eqc:numtests(?NUMTESTS, ?QC_OUT(prop_window())))) end}.
+
+prop_window() ->
+ folsom:start(),
+ (catch meck:new(folsom_utils)),
+ ?FORALL(Cmds, commands(?MODULE),
+ aggregate(command_names(Cmds),
+ begin
+ {H, S, Res} = run_commands(?MODULE, Cmds),
+ {Actual, Expected} = case S#state.sample of
+ undefined ->
+ {S#state.values, []};
+ Sample ->
+ A = folsom_metrics:get_metric_value(S#state.name),
+ E = [V || {K, V} <- S#state.values, K >= S#state.moment - ?WINDOW],
+ folsom_metrics:delete_metric(S#state.name),
+ {A, E}
+ end,
+ ?WHENFAIL(
+ io:format("History: ~p~nState: ~p~nActual: ~p~nExpected: ~p~nRes: ~p~n", [H, S, Actual, Expected, Res]),
+ conjunction([{total, equals(lists:sort(Actual), lists:sort(Expected))},
+ {eq, equals(Res, ok)}]))
+ end)).
+
+%% Commands
+new_histo() ->
+ Ref = make_ref(),
+ folsom_metrics:new_histogram(Ref, slide, ?WINDOW),
+ #histogram{sample=Slide} = folsom_metrics_histogram:get_value(Ref),
+ ok = folsom_sample_slide_server:stop(Slide#slide.server),
+ {Ref, Slide}.
+
+tick(Moment) ->
+ IncrBy = trunc(random:uniform(10)),
+ meck:expect(folsom_utils, now_epoch, fun() -> Moment + IncrBy end),
+ Moment+IncrBy.
+
+update(Sample, Val) ->
+ Sample = folsom_sample_slide:update(Sample, Val),
+ Val.
+
+trim(Sample, Window) ->
+ folsom_sample_slide:trim(Sample#slide.reservoir, Window).
+
+get_values(Sample) ->
+ folsom_sample_slide:get_values(Sample).
+
+%% private
+trim(L, Moment, Window) ->
+ [{K, V} || {K, V} <- L, K >= Moment - Window].
+
+-endif.
+-endif.
diff --git a/test/slide_uniform_eqc.erl b/test/slide_uniform_eqc.erl
new file mode 100644
index 0000000..99deb90
--- /dev/null
+++ b/test/slide_uniform_eqc.erl
@@ -0,0 +1,175 @@
+%%%
+%%% Copyright 2012 - Basho Technologies, Inc. All Rights Reserved.
+%%%
+%%% 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.
+%%%
+
+%%%-------------------------------------------------------------------
+%%% File: slide_uniform_eqc.erl
+%%% @author Russell Brown <russelldb@basho.com>
+%%% @doc quickcheck test for the folsom_sample_slide.erl
+%%% @end
+%%%------------------------------------------------------------------
+
+-module(slide_uniform_eqc).
+
+-compile(export_all).
+
+-ifdef(TEST).
+-ifdef(EQC).
+-include("folsom.hrl").
+
+-include_lib("eqc/include/eqc.hrl").
+-include_lib("eqc/include/eqc_statem.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+-define(NUMTESTS, 200).
+-define(QC_OUT(P),
+ eqc:on_output(fun(Str, Args) ->
+ io:format(user, Str, Args) end, P)).
+
+-define(WINDOW, 60).
+-define(SIZE, 5).
+
+-record(state, {moment=1000,
+ sample,
+ name,
+ values=[]}).
+
+initial_state() ->
+ meck:expect(folsom_utils, now_epoch, fun() -> 1000 end),
+ #state{}.
+
+command(S) ->
+ oneof(
+ [{call, ?MODULE, new_histo, []} || S#state.sample == undefined] ++
+ [{call, ?MODULE, tick, [S#state.moment]} || S#state.sample /= undefined] ++
+ [{call, ?MODULE, update, [S#state.sample, int()]} || S#state.sample /= undefined] ++
+ [{call, ?MODULE, trim, [S#state.sample, ?WINDOW]} || S#state.sample /= undefined] ++
+ [{call, ?MODULE, get_values, [S#state.sample]} || S#state.sample /= undefined]
+ ).
+
+%% Next state transformation, S is the current state
+next_state(S, V, {call, ?MODULE, new_histo, []}) ->
+ S#state{name={call, erlang, element, [1, V]}, sample={call, erlang, element, [2, V]}};
+next_state(S, V, {call, ?MODULE, tick, [_Moment]}) ->
+ S#state{moment=V};
+next_state(#state{moment=Moment, values=Values0, sample=Sample}=S, NewSample, {call, ?MODULE, update, [_, Val]}) ->
+ S#state{values={call, slide_uniform_eqc, new_state_values, [Sample, Moment, Values0, Val]},
+ sample=NewSample};
+next_state(#state{values=Values, moment=Moment}=S, _V, {call, ?MODULE, trim, _}) ->
+ %% trim the model
+ S#state{values={call, ?MODULE, trim, [Values, Moment, ?WINDOW]}};
+next_state(S,_V,{call, ?MODULE, _, _}) ->
+ S.
+
+%% Precondition, checked before command is added to the command sequence
+precondition(S, {call, _, new_histo, _}) ->
+ S#state.sample == undefined;
+precondition(S, _) when S#state.sample == undefined ->
+ false;
+precondition(_S, {call, _, _, _}) ->
+ true.
+
+%% Postcondition, checked after command has been evaluated
+%% OBS: S is the state before next_state(S,_,<command>)
+postcondition(#state{values=Values0, moment=Moment}, {call, ?MODULE, get_values, _}, Res) ->
+ Values = [V || {{M, _C}, V} <- Values0, M >= Moment - ?WINDOW],
+ case lists:sort(Values) == lists:sort(Res) of
+ true ->
+ true;
+ _ ->
+ {"get values", {"model", lists:sort(Values)}, {"sample", lists:sort(Res)}}
+ end;
+postcondition(#state{values=Values, sample=Sample, moment=Moment}, {call, ?MODULE, trim, _}, _TrimCnt) ->
+ %% check that values and the actual table contents are the same after a trim
+ Table = [ Elem || {K, _V}=Elem <- ets:tab2list(Sample#slide_uniform.reservoir)
+ , is_tuple(K)], %% filter out counter,
+ case lists:sort(trim(Values, Moment, ?WINDOW)) == lists:sort(Table) of
+ true ->
+ true;
+ _ ->
+ {"after trim", {"model", lists:sort(trim(Values, Moment, ?WINDOW))}, {"sample", lists:sort(Table)}}
+ end;
+postcondition(_S, {call, ?MODULE, _, _}, _Res) ->
+ true.
+
+prop_window_test_() ->
+ {setup, fun() -> ok end, fun(_X) -> (catch meck:unload(folsom_utils)), folsom:stop() end,
+ fun(_X) ->
+ ?_assert(eqc:quickcheck(eqc:numtests(?NUMTESTS, ?QC_OUT(prop_window())))) end}.
+
+prop_window() ->
+ folsom:start(),
+ (catch meck:new(folsom_utils)),
+ ?FORALL(Cmds, commands(?MODULE),
+ aggregate(command_names(Cmds),
+ begin
+ {H, S, Res} = run_commands(?MODULE, Cmds),
+ {Actual, Expected, Tab} = case S#state.sample of
+ undefined ->
+ {S#state.values, [], []};
+ Sample ->
+ A = folsom_metrics:get_metric_value(S#state.name),
+ E = [V || {{M, _C}, V} <- S#state.values, M >= S#state.moment - ?WINDOW],
+ T = ets:tab2list(Sample#slide_uniform.reservoir),
+ folsom_metrics:delete_metric(S#state.name),
+ {A, E, T}
+ end,
+ ?WHENFAIL(
+ io:format("History: ~p~nState: ~p~nActual: ~p~nExpected: ~p~nRes: ~p~nTab: ~p~n",
+ [H, S, Actual, Expected, Res, Tab]),
+ conjunction([{total, equals(lists:sort(Actual), lists:sort(Expected))},
+ {eq, equals(Res, ok)}]))
+ end)).
+
+%% Commands
+new_histo() ->
+ Ref = make_ref(),
+ folsom_metrics:new_histogram(Ref, slide_uniform, {?WINDOW, ?SIZE}),
+ #histogram{sample=Slide} = folsom_metrics_histogram:get_value(Ref),
+ ok = folsom_sample_slide_server:stop(Slide#slide_uniform.server),
+ {Ref, Slide}.
+
+tick(Moment) ->
+ IncrBy = trunc(random:uniform(10)),
+ meck:expect(folsom_utils, now_epoch, fun() -> Moment + IncrBy end),
+ Moment+IncrBy.
+
+update(Sample, Val) ->
+ folsom_sample_slide_uniform:update(Sample, Val).
+
+trim(Sample, Window) ->
+ folsom_sample_slide_uniform:trim(Sample#slide_uniform.reservoir, Window).
+
+get_values(Sample) ->
+ folsom_sample_slide_uniform:get_values(Sample).
+
+%% private
+trim(L, Moment, Window) ->
+ [{K, V} || {{M, _C}=K, V} <- L, M >= Moment - Window].
+
+new_state_values(Sample, Moment, Values, Val) ->
+ Cnt = length([true || {{M, _C}, _V} <- Values, M == Moment]),
+ case Cnt >= ?SIZE of
+ true ->
+ %% replace
+ {Rnd, _} = random:uniform_s(?SIZE, Sample#slide_uniform.seed),
+ lists:keyreplace({Moment, Rnd}, 1, Values, {{Moment, Rnd}, Val});
+ false ->
+ %% insert
+ Values ++ [{{Moment, Cnt+1}, Val}]
+ end.
+
+-endif.
+-endif.