Merge pull request #45 from basho/adt-speedups
Performance improvements to spiral, counter, histogram & spiral_uniform
diff --git a/include/folsom.hrl b/include/folsom.hrl
index 255ec40..a7aa543 100644
--- a/include/folsom.hrl
+++ b/include/folsom.hrl
@@ -17,7 +17,7 @@
-record(spiral, {
tid = folsom_metrics_histogram_ets:new(folsom_spiral,
- [ordered_set,
+ [set,
{write_concurrency, true},
public]),
server
@@ -33,7 +33,7 @@
-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]),
+ reservoir = folsom_metrics_histogram_ets:new(folsom_slide_uniform,[set, {write_concurrency, true}, public]),
seed = now(),
server
}).
diff --git a/src/folsom_metrics_counter.erl b/src/folsom_metrics_counter.erl
index 9698150..1861ffe 100644
--- a/src/folsom_metrics_counter.erl
+++ b/src/folsom_metrics_counter.erl
@@ -32,27 +32,34 @@
get_value/1,
clear/1]).
+-define(WIDTH, 16). %% Keep this a power of two
+
-include("folsom.hrl").
new(Name) ->
- Counter = {Name, 0},
- ets:insert(?COUNTER_TABLE, Counter).
+ Counters = [{{Name,N}, 0} || N <- lists:seq(0,?WIDTH-1)],
+ ets:insert(?COUNTER_TABLE, Counters).
inc(Name) ->
- ets:update_counter(?COUNTER_TABLE, Name, 1).
+ ets:update_counter(?COUNTER_TABLE, key(Name), 1).
inc(Name, Value) ->
- ets:update_counter(?COUNTER_TABLE, Name, Value).
+ ets:update_counter(?COUNTER_TABLE, key(Name), Value).
dec(Name) ->
- ets:update_counter(?COUNTER_TABLE, Name, -1).
+ ets:update_counter(?COUNTER_TABLE, key(Name), -1).
dec(Name, Value) ->
- ets:update_counter(?COUNTER_TABLE, Name, -Value).
+ ets:update_counter(?COUNTER_TABLE, key(Name), -Value).
get_value(Name) ->
- [{_, Values}] = ets:lookup(?COUNTER_TABLE, Name),
- Values.
+ Count = lists:sum(ets:select(?COUNTER_TABLE, [{{{Name,'_'},'$1'},[],['$1']}])),
+ Count.
clear(Name) ->
new(Name).
+
+key(Name) ->
+ X = erlang:system_info(scheduler_id),
+ Rnd = X band (?WIDTH-1),
+ {Name, Rnd}.
diff --git a/src/folsom_metrics_histogram.erl b/src/folsom_metrics_histogram.erl
index 401e29f..b24db65 100644
--- a/src/folsom_metrics_histogram.erl
+++ b/src/folsom_metrics_histogram.erl
@@ -57,8 +57,14 @@
update(Name, Value) ->
Hist = get_value(Name),
- NewSample = folsom_sample:update(Hist#histogram.type, Hist#histogram.sample, Value),
- ets:insert(?HISTOGRAM_TABLE, {Name, Hist#histogram{sample = NewSample}}).
+ Sample = Hist#histogram.sample,
+ case folsom_sample:update(Hist#histogram.type, Hist#histogram.sample, Value) of
+ Sample ->
+ %% sample didn't change, don't need to write it back
+ true;
+ NewSample ->
+ ets:insert(?HISTOGRAM_TABLE, {Name, Hist#histogram{sample = NewSample}})
+ end.
% gets the histogram record from ets
get_value(Name) ->
diff --git a/src/folsom_metrics_spiral.erl b/src/folsom_metrics_spiral.erl
index 547fe8b..d206dc4 100644
--- a/src/folsom_metrics_spiral.erl
+++ b/src/folsom_metrics_spiral.erl
@@ -34,6 +34,7 @@
%% size of the window in seconds
-define(WINDOW, 60).
+-define(WIDTH, 16). %% Keep this a power of two
-include("folsom.hrl").
@@ -42,15 +43,17 @@
Pid = folsom_sample_slide_sup:start_slide_server(?MODULE,
Spiral#spiral.tid,
?WINDOW),
- ets:insert_new(Spiral#spiral.tid, {count, 0}),
+ ets:insert_new(Spiral#spiral.tid,
+ [{{count, N}, 0} || N <- lists:seq(0,?WIDTH-1)]),
ets:insert(?SPIRAL_TABLE, {Name, Spiral#spiral{server=Pid}}).
update(Name, Value) ->
#spiral{tid=Tid} = get_value(Name),
Moment = folsom_utils:now_epoch(),
- ets:insert_new(Tid, {Moment, 0}),
- ets:update_counter(Tid, Moment, Value),
- ets:update_counter(Tid, count, Value).
+ X = erlang:system_info(scheduler_id),
+ Rnd = X band (?WIDTH-1),
+ folsom_utils:update_counter(Tid, {Moment, Rnd}, Value),
+ ets:update_counter(Tid, {count, Rnd}, Value).
get_value(Name) ->
[{Name, Spiral}] = ets:lookup(?SPIRAL_TABLE, Name),
@@ -58,13 +61,13 @@
trim(Tid, _Window) ->
Oldest = oldest(),
- ets:select_delete(Tid, [{{'$1','_'}, [{is_integer, '$1'}, {'<', '$1', Oldest}], ['true']}]).
+ ets:select_delete(Tid, [{{{'$1','_'},'_'}, [{is_integer, '$1'}, {'<', '$1', Oldest}], ['true']}]).
get_values(Name) ->
Oldest = oldest(),
#spiral{tid=Tid} = get_value(Name),
- [{count, Count}] = ets:lookup(Tid, count),
- One =lists:sum(ets:select(Tid, [{{'$1','$2'},[{is_integer, '$1'}, {'>=', '$1', Oldest}],['$2']}])),
+ Count = lists:sum(ets:select(Tid, [{{{count,'_'},'$1'},[],['$1']}])),
+ One = lists:sum(ets:select(Tid, [{{{'$1','_'},'$2'},[{is_integer, '$1'}, {'>=', '$1', Oldest}],['$2']}])),
[{count, Count}, {one, One}].
diff --git a/src/folsom_sample_slide_uniform.erl b/src/folsom_sample_slide_uniform.erl
index 7e7b5eb..1d0204c 100644
--- a/src/folsom_sample_slide_uniform.erl
+++ b/src/folsom_sample_slide_uniform.erl
@@ -38,19 +38,19 @@
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),
+update(#slide_uniform{reservoir = Reservoir, size = Size} = Sample0, Value) ->
+ Now = folsom_utils:timestamp(),
+ Moment = folsom_utils:now_epoch(Now),
+ MCnt = folsom_utils:update_counter(Reservoir, Moment, 1),
Sample = case MCnt > Size of
true ->
- {Rnd, NewSeed} = random:uniform_s(Size, Seed),
+ {Rnd, _NewSeed} = random:uniform_s(MCnt, Now),
maybe_update(Reservoir, {{Moment, Rnd}, Value}, Size),
- Sample0#slide_uniform{seed = NewSeed};
- false ->
- ets:insert(Reservoir, {{Moment, MCnt}, Value}),
- Sample0
- end,
+ Sample0;
+ false ->
+ ets:insert(Reservoir, {{Moment, MCnt}, Value}),
+ Sample0
+ end,
Sample.
maybe_update(Reservoir, {{_Moment, Rnd}, _Value}=Obj, Size) when Rnd =< Size ->
diff --git a/src/folsom_utils.erl b/src/folsom_utils.erl
index 8b63ba3..bbea804 100644
--- a/src/folsom_utils.erl
+++ b/src/folsom_utils.erl
@@ -28,8 +28,11 @@
to_atom/1,
convert_tags/1,
now_epoch/0,
+ now_epoch/1,
now_epoch_micro/0,
- get_ets_size/1
+ timestamp/0,
+ get_ets_size/1,
+ update_counter/3
]).
to_atom(Binary) when is_binary(Binary) ->
@@ -41,12 +44,39 @@
[to_atom(Tag) || Tag <- Tags].
now_epoch() ->
- {Mega, Sec, _} = os:timestamp(),
+ now_epoch(os:timestamp()).
+
+now_epoch({Mega, Sec, _}) ->
(Mega * 1000000 + Sec).
now_epoch_micro() ->
{Mega, Sec, Micro} = os:timestamp(),
(Mega * 1000000 + Sec) * 1000000 + Micro.
+%% useful because you can't meck os:timestamp for some reason
+timestamp() ->
+ os:timestamp().
+
get_ets_size(Tab) ->
ets:info(Tab, size).
+
+%% @doc
+%% Same as {@link ets:update_counter/3} but inserts `{Key, Value}' if object
+%% is missing in the table.
+update_counter(Tid, Key, Value) when is_integer(Value) ->
+ %% try to update the counter, will badarg if it doesn't exist
+ try ets:update_counter(Tid, Key, Value) of
+ Res ->
+ Res
+ catch
+ error:badarg ->
+ %% row didn't exist, create it
+ %% use insert_new to avoid races
+ case ets:insert_new(Tid, {Key, Value}) of
+ true ->
+ Value;
+ false ->
+ %% someone beat us to it
+ ets:update_counter(Tid, Key, Value)
+ end
+ end.
diff --git a/test/folsom_tests.erl b/test/folsom_tests.erl
index a003053..04f2d94 100644
--- a/test/folsom_tests.erl
+++ b/test/folsom_tests.erl
@@ -73,3 +73,27 @@
application:stop(folsom),
application:unload(folsom),
ok.
+
+update_counter_test() ->
+ Tid = ets:new(sometable, [public, set]),
+ Workers = [spawn_monitor(fun() -> timer:sleep(100-N), folsom_utils:update_counter(Tid, hello, N) end) || N <- lists:seq(1, 100)],
+ wait_for_results(Workers),
+ ?assertEqual([{hello, 5050}], ets:lookup(Tid, hello)).
+
+wait_for_results([]) ->
+ ok;
+wait_for_results(Workers) ->
+ receive
+ {'DOWN', _, _, Pid, Reason} ->
+ case lists:keyfind(Pid, 1, Workers) of
+ false ->
+ wait_for_results(Workers);
+ _ ->
+ case Reason of
+ normal ->
+ wait_for_results(lists:keydelete(Pid, 1, Workers));
+ _ ->
+ erlang:error(Reason)
+ end
+ end
+ end.
diff --git a/test/slide_uniform_eqc.erl b/test/slide_uniform_eqc.erl
index 99deb90..12f6cb0 100644
--- a/test/slide_uniform_eqc.erl
+++ b/test/slide_uniform_eqc.erl
@@ -44,9 +44,11 @@
-record(state, {moment=1000,
sample,
name,
+ count=orddict:new(),
values=[]}).
initial_state() ->
+ meck:expect(folsom_utils, now_epoch, fun(_Now) -> 1000 end),
meck:expect(folsom_utils, now_epoch, fun() -> 1000 end),
#state{}.
@@ -64,8 +66,9 @@
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]},
+next_state(#state{moment=Moment, values=Values0, sample=Sample, count=Count}=S, NewSample, {call, ?MODULE, update, [_, Val]}) ->
+ S#state{values={call, slide_uniform_eqc, new_state_values, [Sample, Moment, Values0, Val, Count]},
+ count={call, orddict, update_counter, [Moment, 1, Count]},
sample=NewSample};
next_state(#state{values=Values, moment=Moment}=S, _V, {call, ?MODULE, trim, _}) ->
%% trim the model
@@ -107,11 +110,14 @@
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}.
+ {timeout, 30,
+ ?_assert(eqc:quickcheck(eqc:numtests(?NUMTESTS, ?QC_OUT(prop_window()))))} end}.
prop_window() ->
folsom:start(),
(catch meck:new(folsom_utils)),
+ (catch meck:expect(folsom_utils, update_counter, fun(Tid, Key, Value) -> meck:passthrough([Tid, Key, Value]) end)),
+ (catch meck:expect(folsom_utils, timestamp, fun() -> Res = os:timestamp(), put(timestamp, Res), Res end)),
?FORALL(Cmds, commands(?MODULE),
aggregate(command_names(Cmds),
begin
@@ -144,6 +150,7 @@
tick(Moment) ->
IncrBy = trunc(random:uniform(10)),
meck:expect(folsom_utils, now_epoch, fun() -> Moment + IncrBy end),
+ meck:expect(folsom_utils, now_epoch, fun(_Now) -> Moment + IncrBy end),
Moment+IncrBy.
update(Sample, Val) ->
@@ -159,16 +166,28 @@
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
+new_state_values(_Sample, Moment, Values, Val, Count) ->
+ %Cnt = length([true || {{M, _C}, _V} <- Values, M == Moment]),
+ Cnt =
+ case orddict:find(Moment, Count) of
+ error ->
+ 1;
+ {ok, V} ->
+ V+1
+ end,
+ case Cnt > ?SIZE of
true ->
%% replace
- {Rnd, _} = random:uniform_s(?SIZE, Sample#slide_uniform.seed),
- lists:keyreplace({Moment, Rnd}, 1, Values, {{Moment, Rnd}, Val});
+ {Rnd, _} = random:uniform_s(Cnt, get(timestamp)),
+ case Rnd =< ?SIZE of
+ true ->
+ lists:keyreplace({Moment, Rnd}, 1, Values, {{Moment, Rnd}, Val});
+ false ->
+ Values
+ end;
false ->
%% insert
- Values ++ [{{Moment, Cnt+1}, Val}]
+ Values ++ [{{Moment, Cnt}, Val}]
end.
-endif.