1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2001-2016. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
20%%% File    : bench_generate.hrl
21%%% Author  : Hakan Mattsson <hakan@cslab.ericsson.se>
22%%% Purpose : Start request generators and collect statistics
23%%% Created : 21 Jun 2001 by Hakan Mattsson <hakan@cslab.ericsson.se>
24%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
25
26-module(bench_generate).
27-author('hakan@cslab.ericsson.se').
28
29-include("bench.hrl").
30
31%% Public
32-export([start/1]).
33
34%% Internal
35-export([
36	 monitor_init/2,
37	 generator_init/2,
38	 worker_init/1
39	]).
40
41%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
42%% The traffic generator
43%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
44
45%% -------------------------------------------------------------------
46%% Start request generators
47%% -------------------------------------------------------------------
48
49start(C) when is_record(C, config) ->
50    MonPid = spawn_link(?MODULE, monitor_init, [C, self()]),
51    receive
52	{'EXIT', MonPid, Reason} ->
53	    exit(Reason);
54	{monitor_done, MonPid, Res} ->
55	    Res
56    end.
57
58monitor_init(C, Parent) when is_record(C, config) ->
59    process_flag(trap_exit, true),
60    %% net_kernel:monitor_nodes(true), %% BUGBUG: Needed in order to re-start generators
61    Nodes     = C#config.generator_nodes,
62    PerNode   = C#config.n_generators_per_node,
63    Timer     = C#config.generator_warmup,
64    ?d("~n", []),
65    ?d("Start ~p request generators each at ~p nodes...~n",
66       [PerNode, length(Nodes)]),
67    ?d("~n", []),
68    warmup_sticky(C),
69    ?d("    ~p seconds warmup...~n", [Timer div 1000]),
70    Alive = spawn_generators(C, Nodes, PerNode),
71    erlang:send_after(Timer, self(), warmup_done),
72    monitor_loop(C, Parent, Alive, []).
73
74spawn_generators(C, Nodes, PerNode) ->
75    [spawn_link(Node, ?MODULE, generator_init, [self(), C]) ||
76	Node <- Nodes,
77	_    <- lists:seq(1, PerNode)].
78
79warmup_sticky(C) ->
80    %% Select one node per fragment as master node
81    Tabs = [subscriber, session, server, suffix],
82    Fun = fun(S) ->
83		  {[Node | _], _, Wlock} = nearest_node(S, transaction, C),
84		  Stick = fun() -> [mnesia:read({T, S}, S, Wlock) || T <- Tabs] end,
85		  Args = [transaction, Stick, [], mnesia_frag],
86		  rpc:call(Node, mnesia, activity, Args)
87	  end,
88    Suffixes = lists:seq(0, C#config.n_fragments - 1), % Assume even distrib.
89    lists:foreach(Fun, Suffixes).
90
91%% Main loop for benchmark monitor
92monitor_loop(C, Parent, Alive, Deceased) ->
93    receive
94        warmup_done ->
95            multicall(Alive, reset_statistics),
96	    Timer = C#config.generator_duration,
97	    ?d("    ~p seconds actual benchmarking...~n", [Timer div 1000]),
98	    erlang:send_after(Timer, self(), measurement_done),
99	    monitor_loop(C, Parent, Alive, Deceased);
100        measurement_done ->
101            Stats = multicall(Alive, get_statistics),
102	    Timer = C#config.generator_cooldown,
103	    ?d("    ~p seconds cooldown...~n", [Timer div 1000]),
104	    erlang:send_after(Timer, self(), {cooldown_done, Stats}),
105	    monitor_loop(C, Parent, Alive, Deceased);
106        {cooldown_done, Stats} ->
107            multicall(Alive, stop),
108            display_statistics(Stats, C),
109            Parent ! {monitor_done, self(), ok},
110	    unlink(Parent),
111	    exit(monitor_done);
112	{nodedown, _Node} ->
113	    monitor_loop(C, Parent, Alive, Deceased);
114	{nodeup, Node} ->
115	    NeedsBirth = [N || N <- Deceased, N == Node],
116	    Born = spawn_generators(C, NeedsBirth, 1),
117	    monitor_loop(C, Parent, Born ++ Alive, Deceased -- NeedsBirth);
118        {'EXIT', Pid, Reason} when Pid == Parent ->
119	    exit(Reason);
120        {'EXIT', Pid, Reason} ->
121            case lists:member(Pid, Alive) of
122                true ->
123		    ?d("Generator on node ~p died: ~p~n", [node(Pid), Reason]),
124                    monitor_loop(C, Parent, Alive -- [Pid], [node(Pid) | Deceased]);
125                false ->
126                    monitor_loop(C, Parent, Alive, Deceased)
127            end
128    end.
129
130%% Send message to a set of processes and wait for their replies
131multicall(Pids, Message) ->
132    Send =
133        fun(Pid) ->
134                Ref = erlang:monitor(process, Pid),
135                Pid ! {self(), Ref, Message},
136                {Pid, Ref}
137        end,
138    PidRefs = lists:map(Send, Pids),
139    Collect =
140        fun({Pid, Ref}) ->
141                receive
142                    {'DOWN', Ref, process, Pid, Reason} ->
143                        {Pid, {'EXIT', Reason}};
144                    {Pid, Ref, Reply} ->
145                        erlang:demonitor(Ref),
146                        {Pid, Reply}
147                end
148        end,
149    lists:map(Collect, PidRefs).
150
151%% Initialize a traffic generator
152generator_init(Monitor, C) ->
153    process_flag(trap_exit, true),
154    Tables = mnesia:system_info(tables),
155    ok = mnesia:wait_for_tables(Tables, infinity),
156    rand:seed(exsplus),
157    Counters = reset_counters(C, C#config.statistics_detail),
158    SessionTab = ets:new(bench_sessions, [public, {keypos, 1}]),
159    generator_loop(Monitor, C, SessionTab, Counters).
160
161%% Main loop for traffic generator
162generator_loop(Monitor, C, SessionTab, Counters) ->
163    receive
164        {ReplyTo, Ref, get_statistics} ->
165	    Stats = get_counters(C, Counters),
166	    ReplyTo ! {self(), Ref, Stats},
167	    generator_loop(Monitor, C, SessionTab, Counters);
168        {ReplyTo, Ref, reset_statistics} ->
169	    Stats = get_counters(C, Counters),
170	    Counters2 = reset_counters(C, Counters),
171	    ReplyTo ! {self(), Ref, Stats},
172	    generator_loop(Monitor, C, SessionTab, Counters2);
173        {_ReplyTo, _Ref, stop} ->
174	    exit(shutdown);
175        {'EXIT', Pid, Reason} when Pid == Monitor ->
176	    exit(Reason);
177	{'EXIT', Pid, Reason} ->
178	    Node = node(Pid),
179	    ?d("Worker on node ~p(~p) died: ~p~n", [Node, node(), Reason]),
180	    Key = {worker,Node},
181	    case get(Key) of
182		undefined -> ignore;
183		Pid       -> erase(Key);
184		_         -> ignore
185	    end,
186	    generator_loop(Monitor, C, SessionTab, Counters)
187    after 0 ->
188	    {Name, {Nodes, Activity, Wlock}, Fun, CommitSessions} =
189		gen_trans(C, SessionTab),
190	    Before = erlang:monotonic_time(),
191	    Res  = call_worker(Nodes, Activity, Fun, Wlock, mnesia_frag),
192	    After = erlang:monotonic_time(),
193	    Elapsed = elapsed(Before, After),
194	    post_eval(Monitor, C, Elapsed, Res, Name, CommitSessions, SessionTab, Counters)
195    end.
196
197%% Perform a transaction on a node near the data
198call_worker([Node | _], Activity, Fun, Wlock, Mod) when Node == node() ->
199    {Node, catch mnesia:activity(Activity, Fun, [Wlock], Mod)};
200call_worker([Node | _] = Nodes, Activity, Fun, Wlock, Mod) ->
201    Key = {worker,Node},
202    case get(Key) of
203	Pid when is_pid(Pid) ->
204	    Args = [Activity, Fun, [Wlock], Mod],
205	    Pid ! {activity, self(), Args},
206	    receive
207		{'EXIT', Pid, Reason} ->
208		    ?d("Worker on node ~p(~p) died: ~p~n", [Node, node(), Reason]),
209		    erase(Key),
210		    retry_worker(Nodes, Activity, Fun, Wlock, Mod, {'EXIT', Reason});
211		{activity_result, Pid, Result} ->
212		    case Result of
213			{'EXIT', {aborted, {not_local, _}}} ->
214			    retry_worker(Nodes, Activity, Fun, Wlock, Mod, Result);
215			_ ->
216			    {Node, Result}
217		    end
218	    end;
219	undefined ->
220	    GenPid = self(),
221	    Pid = spawn_link(Node, ?MODULE, worker_init, [GenPid]),
222	    put(Key, Pid),
223	    call_worker(Nodes, Activity, Fun, Wlock, Mod)
224    end.
225
226retry_worker([], _Activity, _Fun, _Wlock, _Mod, Reason) ->
227    {node(), Reason};
228retry_worker([BadNode | SpareNodes], Activity, Fun, Wlock, Mod, Reason) ->
229    Nodes = SpareNodes -- [BadNode],
230    case Nodes of
231	[] ->
232	    {BadNode, Reason};
233	[_] ->
234	    call_worker(Nodes, Activity, Fun, write, Mod);
235	_ ->
236	    call_worker(Nodes, Activity, Fun, Wlock, Mod)
237    end.
238
239worker_init(Parent) ->
240    Tables = mnesia:system_info(tables),
241    ok = mnesia:wait_for_tables(Tables, infinity),
242    worker_loop(Parent).
243
244%% Main loop for remote workers
245worker_loop(Parent) ->
246    receive
247	{activity, Parent, [Activity, Fun, Extra, Mod]} ->
248	    Result = (catch mnesia:activity(Activity, Fun, Extra, Mod)),
249	    Parent ! {activity_result, self(), Result},
250	    worker_loop(Parent)
251    end.
252
253
254elapsed(Before, After) ->
255    erlang:convert_time_unit(After-Before, native, micro_seconds).
256
257%% Lookup counters
258get_counters(_C, {table, Tab}) ->
259    ets:match_object(Tab, '_');
260get_counters(_C, {NM, NC, NA, NB}) ->
261    Trans = any,
262    Node  = somewhere,
263    [{{Trans, n_micros, Node}, NM},
264     {{Trans, n_commits, Node}, NC},
265     {{Trans, n_aborts, Node}, NA},
266     {{Trans, n_branches_executed, Node}, NB}].
267
268% Clear all counters
269reset_counters(_C, normal) ->
270    {0, 0, 0, 0};
271reset_counters(C, {_, _, _, _}) ->
272    reset_counters(C, normal);
273reset_counters(C, debug) ->
274    CounterTab = ets:new(bench_pending, [public, {keypos, 1}]),
275    reset_counters(C, {table, CounterTab});
276reset_counters(C, debug2) ->
277    CounterTab = ets:new(bench_pending, [public, {keypos, 1}]),
278    reset_counters(C, {table, CounterTab});
279reset_counters(C, {table, Tab} = Counters) ->
280    Names = [n_micros, n_commits, n_aborts, n_branches_executed],
281    Nodes = C#config.generator_nodes ++ C#config.table_nodes,
282    TransTypes = [t1, t2, t3, t4, t5, ping],
283    [ets:insert(Tab, {{Trans, Name, Node}, 0}) || Name <- Names,
284						  Node <- Nodes,
285						  Trans <- TransTypes],
286    Counters.
287
288%% Determine the outcome of a transaction and increment the counters
289post_eval(Monitor, C, Elapsed, {Node, Res}, Name, CommitSessions, SessionTab, {table, Tab} = Counters) ->
290    case Res of
291	{do_commit, BranchExecuted, _} ->
292	    incr(Tab, {Name, n_micros, Node}, Elapsed),
293	    incr(Tab, {Name, n_commits, Node}, 1),
294	    case BranchExecuted of
295		true  ->
296		    incr(Tab, {Name, n_branches_executed, Node}, 1),
297		    commit_session(CommitSessions),
298		    generator_loop(Monitor, C, SessionTab, Counters);
299		false ->
300		    generator_loop(Monitor, C, SessionTab, Counters)
301	    end;
302	{'EXIT', {aborted, {do_rollback, BranchExecuted, _}}} ->
303	    incr(Tab, {Name, n_micros, Node}, Elapsed),
304	    incr(Tab, {Name, n_aborts, Node}, 1),
305	    case BranchExecuted of
306		true  ->
307		    incr(Tab, {Name, n_branches_executed, Node}, 1),
308		    generator_loop(Monitor, C, SessionTab, Counters);
309		false ->
310		    generator_loop(Monitor, C, SessionTab, Counters)
311	    end;
312	_ ->
313	    ?d("Failed(~p): ~p~n", [Node, Res]),
314	    incr(Tab, {Name, n_micros, Node}, Elapsed),
315	    incr(Tab, {Name, n_aborts, Node}, 1),
316	    generator_loop(Monitor, C, SessionTab, Counters)
317    end;
318post_eval(Monitor, C, Elapsed, {_Node, Res}, _Name, CommitSessions, SessionTab, {NM, NC, NA, NB}) ->
319    case Res of
320	{do_commit, BranchExecuted, _} ->
321	    case BranchExecuted of
322		true  ->
323		    commit_session(CommitSessions),
324		    generator_loop(Monitor, C, SessionTab, {NM + Elapsed, NC + 1, NA, NB + 1});
325		false ->
326		    generator_loop(Monitor, C, SessionTab, {NM + Elapsed, NC + 1, NA, NB})
327	    end;
328	{'EXIT', {aborted, {do_rollback, BranchExecuted, _}}} ->
329	    case BranchExecuted of
330		true  ->
331		    generator_loop(Monitor, C, SessionTab, {NM + Elapsed, NC, NA + 1, NB + 1});
332		false ->
333		    generator_loop(Monitor, C, SessionTab, {NM + Elapsed, NC, NA + 1, NB})
334	    end;
335	_ ->
336	    ?d("Failed: ~p~n", [Res]),
337	    generator_loop(Monitor, C, SessionTab, {NM + Elapsed, NC, NA + 1, NB})
338    end.
339
340incr(Tab, Counter, Incr) ->
341    ets:update_counter(Tab, Counter, Incr).
342
343commit_session(no_fun) ->
344    ignore;
345commit_session(Fun) when is_function(Fun, 0) ->
346    Fun().
347
348%% Randlomly choose a transaction type according to benchmar spec
349gen_trans(C, SessionTab) when C#config.generator_profile == random ->
350    case rand:uniform(100) of
351        Rand when Rand >   0, Rand =<  25 -> gen_t1(C, SessionTab);
352        Rand when Rand >  25, Rand =<  50 -> gen_t2(C, SessionTab);
353        Rand when Rand >  50, Rand =<  70 -> gen_t3(C, SessionTab);
354        Rand when Rand >  70, Rand =<  85 -> gen_t4(C, SessionTab);
355        Rand when Rand >  85, Rand =< 100 -> gen_t5(C, SessionTab)
356    end;
357gen_trans(C, SessionTab) ->
358    case C#config.generator_profile of
359        t1   -> gen_t1(C, SessionTab);
360        t2   -> gen_t2(C, SessionTab);
361        t3   -> gen_t3(C, SessionTab);
362        t4   -> gen_t4(C, SessionTab);
363        t5   -> gen_t5(C, SessionTab);
364	ping -> gen_ping(C, SessionTab)
365    end.
366
367gen_t1(C, _SessionTab) ->
368    SubscrId    = rand:uniform(C#config.n_subscribers) - 1,
369    SubscrKey   = bench_trans:number_to_key(SubscrId, C),
370    Location    = 4711,
371    ChangedBy   = <<4711:(8*25)>>,
372    ChangedTime = <<4711:(8*25)>>,
373    {t1,
374     nearest_node(SubscrId, transaction, C),
375     fun(Wlock) -> bench_trans:update_current_location(Wlock, SubscrKey, Location, ChangedBy, ChangedTime) end,
376     no_fun
377    }.
378
379gen_t2(C, _SessionTab) ->
380    SubscrId  = rand:uniform(C#config.n_subscribers) - 1,
381    SubscrKey = bench_trans:number_to_key(SubscrId, C),
382    {t2,
383     nearest_node(SubscrId, sync_dirty, C),
384     %%nearest_node(SubscrId, transaction, C),
385     fun(Wlock) -> bench_trans:read_current_location(Wlock, SubscrKey) end,
386     no_fun
387    }.
388
389gen_t3(C, SessionTab) ->
390    case ets:first(SessionTab) of
391	'$end_of_table' ->
392	    %% This generator does not have any session,
393	    %% try reading someone elses session details
394	    SubscrId  = rand:uniform(C#config.n_subscribers) - 1,
395	    SubscrKey = bench_trans:number_to_key(SubscrId, C),
396	    ServerId  = rand:uniform(C#config.n_servers) - 1,
397	    ServerBit = 1 bsl ServerId,
398	    {t3,
399	     nearest_node(SubscrId, transaction, C),
400	     fun(Wlock) -> bench_trans:read_session_details(Wlock, SubscrKey, ServerBit, ServerId) end,
401	     no_fun
402	    };
403	{SubscrId, SubscrKey, ServerId}  ->
404	    %% This generator do have a session,
405	    %% read its session details
406	    ServerBit = 1 bsl ServerId,
407	    {t3,
408	     nearest_node(SubscrId, transaction, C),
409	     fun(Wlock) -> bench_trans:read_session_details(Wlock, SubscrKey, ServerBit, ServerId) end,
410	     no_fun
411	    }
412    end.
413
414gen_t4(C, SessionTab) ->
415    %% This generator may already have sessions,
416    %% create a new session and hope that no other
417    %% generator already has occupied it
418    SubscrId   = rand:uniform(C#config.n_subscribers) - 1,
419    SubscrKey  = bench_trans:number_to_key(SubscrId, C),
420    ServerId   = rand:uniform(C#config.n_servers) - 1,
421    ServerBit  = 1 bsl ServerId,
422    Details    = <<4711:(8*2000)>>,
423    DoRollback = (rand:uniform(100) =< 2),
424    Insert     = fun() -> ets:insert(SessionTab, {{SubscrId, SubscrKey, ServerId}, self()}) end,
425    {t4,
426     nearest_node(SubscrId, transaction, C),
427     fun(Wlock) -> bench_trans:create_session_to_server(Wlock, SubscrKey, ServerBit, ServerId, Details, DoRollback) end,
428     Insert
429    }.
430
431gen_t5(C, SessionTab) ->
432    case ets:first(SessionTab) of
433	'$end_of_table' ->
434	    %% This generator does not have any session,
435	    %% try to delete someone elses session details
436	    SubscrId   = rand:uniform(C#config.n_subscribers) - 1,
437	    SubscrKey  = bench_trans:number_to_key(SubscrId, C),
438	    ServerId   = rand:uniform(C#config.n_servers) - 1,
439	    ServerBit  = 1 bsl ServerId,
440	    DoRollback = (rand:uniform(100) =< 2),
441	    {t5,
442	     nearest_node(SubscrId, transaction, C),
443	     fun(Wlock) -> bench_trans:delete_session_from_server(Wlock, SubscrKey, ServerBit, ServerId, DoRollback) end,
444	     no_fun
445	    };
446	{SubscrId, SubscrKey, ServerId}  ->
447	    %% This generator do have at least one session,
448	    %% delete it.
449	    ServerBit  = 1 bsl ServerId,
450	    DoRollback = (rand:uniform(100) =< 2),
451	    Delete     = fun() -> ets:delete(SessionTab, {SubscrId, SubscrKey, ServerId}) end,
452	    {t5,
453	     nearest_node(SubscrId, transaction, C),
454	     fun(Wlock) -> bench_trans:delete_session_from_server(Wlock, SubscrKey, ServerBit, ServerId, DoRollback) end,
455	     Delete
456	    }
457    end.
458
459gen_ping(C, _SessionTab) ->
460    SubscrId   = rand:uniform(C#config.n_subscribers) - 1,
461    {ping,
462     nearest_node(SubscrId, transaction, C),
463     fun(_Wlock) -> {do_commit, true, []} end,
464     no_fun
465    }.
466
467%% Select a node as near as the subscriber data as possible
468nearest_node(SubscrId, Activity, C) ->
469    Suffix = bench_trans:number_to_suffix(SubscrId),
470    case mnesia_frag:table_info(t, s, {suffix, Suffix}, where_to_write) of
471	[] ->
472	    {[node()], Activity, write};
473	[Node] ->
474	    {[Node], Activity, write};
475	Nodes ->
476	    Wlock = C#config.write_lock_type,
477	    if
478		C#config.always_try_nearest_node; Wlock =:= write ->
479		    case lists:member(node(), Nodes) of
480			true ->
481			    {[node() | Nodes], Activity, Wlock};
482			false ->
483			    Node = pick_node(Suffix, C, Nodes),
484			    {[Node | Nodes], Activity, Wlock}
485		    end;
486		Wlock == sticky_write ->
487		    Node = pick_node(Suffix, C, Nodes),
488		    {[Node | Nodes], Activity, Wlock}
489	    end
490    end.
491
492pick_node(Suffix, C, Nodes) ->
493    Ordered = lists:sort(Nodes),
494    NumberOfActive = length(Ordered),
495    PoolSize = length(C#config.table_nodes),
496    Suffix2 =
497	case PoolSize rem NumberOfActive of
498	    0 -> Suffix div (PoolSize div NumberOfActive);
499	    _ -> Suffix
500	end,
501    N = (Suffix2 rem NumberOfActive) + 1,
502    lists:nth(N, Ordered).
503
504display_statistics(Stats, C) ->
505    GoodStats = [{node(GenPid), GenStats} || {GenPid, GenStats} <- Stats,
506					     is_list(GenStats)],
507    FlatStats = [{Type, Name, EvalNode, GenNode, Count} ||
508                    {GenNode, GenStats} <- GoodStats,
509                    {{Type, Name, EvalNode}, Count} <- GenStats],
510    TotalStats = calc_stats_per_tag(lists:keysort(2, FlatStats), 2, []),
511    {value, {n_aborts, 0, NA, 0, 0}} =
512     lists:keysearch(n_aborts, 1, TotalStats ++ [{n_aborts, 0, 0, 0, 0}]),
513    {value, {n_commits, NC, 0, 0, 0}} =
514	lists:keysearch(n_commits, 1, TotalStats ++ [{n_commits, 0, 0, 0, 0}]),
515    {value, {n_branches_executed, 0, 0, _NB, 0}} =
516	lists:keysearch(n_branches_executed, 1, TotalStats ++ [{n_branches_executed, 0, 0, 0, 0}]),
517    {value, {n_micros, 0, 0, 0, AccMicros}} =
518	lists:keysearch(n_micros, 1, TotalStats ++ [{n_micros, 0, 0, 0, 0}]),
519    NT = NA + NC,
520    NG = length(GoodStats),
521    NTN = length(C#config.table_nodes),
522    WallMicros = C#config.generator_duration * 1000 * NG,
523    Overhead = (catch (WallMicros - AccMicros) / WallMicros),
524    ?d("~n", []),
525    ?d("Benchmark result...~n", []),
526    ?d("~n", []),
527    ?d("    ~p transactions per second (TPS).~n", [catch ((NT * 1000000 * NG) div AccMicros)]),
528    ?d("    ~p TPS per table node.~n", [catch ((NT * 1000000 * NG) div (AccMicros * NTN))]),
529    ?d("    ~p micro seconds in average per transaction, including latency.~n",
530       [catch (AccMicros div NT)]),
531    ?d("    ~p transactions. ~f% generator overhead.~n", [NT, Overhead * 100]),
532
533    TypeStats = calc_stats_per_tag(lists:keysort(1, FlatStats), 1, []),
534    EvalNodeStats = calc_stats_per_tag(lists:keysort(3, FlatStats), 3, []),
535    GenNodeStats = calc_stats_per_tag(lists:keysort(4, FlatStats), 4, []),
536    if
537	C#config.statistics_detail == normal ->
538	    ignore;
539	true ->
540	    ?d("~n", []),
541	    ?d("Statistics per transaction type...~n", []),
542	    ?d("~n", []),
543	    display_type_stats("    ", TypeStats, NT, AccMicros),
544
545	    ?d("~n", []),
546	    ?d("Transaction statistics per table node...~n", []),
547	    ?d("~n", []),
548	    display_calc_stats("    ", EvalNodeStats, NT, AccMicros),
549
550	    ?d("~n", []),
551	    ?d("Transaction statistics per generator node...~n", []),
552	    ?d("~n", []),
553	    display_calc_stats("    ", GenNodeStats, NT, AccMicros)
554    end,
555    if
556	C#config.statistics_detail /= debug2 ->
557	    ignore;
558	true ->
559	    io:format("~n", []),
560	    io:format("------ Test Results ------~n", []),
561	    io:format("Length        : ~p sec~n", [C#config.generator_duration div 1000]),
562	    Host = lists:nth(2, string:tokens(atom_to_list(node()), [$@])),
563	    io:format("Processor     : ~s~n", [Host]),
564	    io:format("Number of Proc: ~p~n", [NG]),
565	    io:format("~n", []),
566	    display_trans_stats("    ", TypeStats, NT, AccMicros, NG),
567	    io:format("~n", []),
568	    io:format("  Overall Statistics~n", []),
569	    io:format("     Transactions: ~p~n", [NT]),
570	    io:format("     Inner       : ~p TPS~n", [catch ((NT * 1000000 * NG) div AccMicros)]),
571	    io:format("     Outer       : ~p TPS~n", [catch ((NT * 1000000 * NG) div WallMicros)]),
572	    io:format("~n", [])
573    end.
574
575
576display_calc_stats(Prefix, [{_Tag, 0, 0, 0, 0} | Rest], NT, Micros) ->
577    display_calc_stats(Prefix, Rest, NT, Micros);
578display_calc_stats(Prefix, [{Tag, NC, NA, _NB, NM} | Rest], NT, Micros) ->
579    ?d("~s~s n=~s%\ttime=~s%~n",
580       [Prefix, left(Tag), percent(NC + NA, NT), percent(NM, Micros)]),
581    display_calc_stats(Prefix, Rest, NT, Micros);
582display_calc_stats(_, [], _, _) ->
583    ok.
584
585display_type_stats(Prefix, [{_Tag, 0, 0, 0, 0} | Rest], NT, Micros) ->
586    display_type_stats(Prefix, Rest, NT, Micros);
587display_type_stats(Prefix, [{Tag, NC, NA, NB, NM} | Rest], NT, Micros) ->
588    ?d("~s~s n=~s%\ttime=~s%\tavg micros=~p~n",
589       [
590	Prefix,
591	left(Tag),
592	percent(NC + NA, NT),
593	percent(NM, Micros),
594	catch (NM div (NC + NA))
595       ]),
596    case NA /= 0 of
597	true  -> ?d("~s    ~s% aborted~n", [Prefix, percent(NA, NC + NA)]);
598	false -> ignore
599    end,
600    case NB /= 0 of
601	true  -> ?d("~s    ~s% branches executed~n", [Prefix, percent(NB, NC + NA)]);
602	false -> ignore
603    end,
604    display_type_stats(Prefix, Rest, NT, Micros);
605display_type_stats(_, [], _, _) ->
606    ok.
607
608left(Term) ->
609    string:left(lists:flatten(io_lib:format("~p", [Term])), 27, $.).
610
611percent(_Part, 0)     -> "infinity";
612percent(Part, Total) -> io_lib:format("~8.4f", [(Part * 100) / Total]).
613
614calc_stats_per_tag([], _Pos, Acc) ->
615    lists:sort(Acc);
616calc_stats_per_tag([Tuple | _] = FlatStats, Pos, Acc) when size(Tuple) == 5 ->
617    Tag = element(Pos, Tuple),
618    do_calc_stats_per_tag(FlatStats, Pos, {Tag, 0, 0, 0, 0}, Acc).
619
620do_calc_stats_per_tag([Tuple | Rest], Pos, {Tag, NC, NA, NB, NM}, Acc)
621  when element(Pos, Tuple) == Tag ->
622    Val = element(5, Tuple),
623    case element(2, Tuple) of
624        n_commits ->
625            do_calc_stats_per_tag(Rest, Pos, {Tag, NC + Val, NA, NB, NM}, Acc);
626        n_aborts ->
627            do_calc_stats_per_tag(Rest, Pos, {Tag, NC, NA + Val, NB, NM}, Acc);
628        n_branches_executed ->
629            do_calc_stats_per_tag(Rest, Pos, {Tag, NC, NA, NB + Val, NM}, Acc);
630        n_micros ->
631            do_calc_stats_per_tag(Rest, Pos, {Tag, NC, NA, NB, NM + Val}, Acc)
632    end;
633do_calc_stats_per_tag(GenStats, Pos, CalcStats, Acc) ->
634    calc_stats_per_tag(GenStats, Pos, [CalcStats | Acc]).
635
636display_trans_stats(Prefix, [{_Tag, 0, 0, 0, 0} | Rest], NT, Micros, NG) ->
637    display_trans_stats(Prefix, Rest, NT, Micros, NG);
638display_trans_stats(Prefix, [{Tag, NC, NA, NB, NM} | Rest], NT, Micros, NG) ->
639    Common =
640	fun(Name) ->
641           Sec = NM / (1000000 * NG),
642	   io:format("  ~s: ~p (~p%) Time: ~p sec TPS = ~p~n",
643		     [Name,
644		      NC + NA,
645		      round(((NC + NA) * 100) / NT),
646		      round(Sec),
647		      round((NC + NA) / Sec)])
648        end,
649    Branch =
650	fun() ->
651            io:format("      Branches Executed: ~p (~p%)~n",
652		      [NB, round((NB * 100) / (NC + NA))])
653	end,
654    Rollback =
655	fun() ->
656            io:format("      Rollback Executed: ~p (~p%)~n",
657		      [NA, round((NA * 100) / (NC + NA))])
658	end,
659    case Tag of
660	t1 ->
661	    Common("T1");
662	t2 ->
663	    Common("T2");
664	t3 ->
665	    Common("T3"),
666	    Branch();
667	t4 ->
668	    Common("T4"),
669	    Branch(),
670	    Rollback();
671	t5 ->
672	    Common("T5"),
673	    Branch(),
674	    Rollback();
675	_ ->
676	    Common(io_lib:format("~p", [Tag]))
677    end,
678    display_trans_stats(Prefix, Rest, NT, Micros, NG);
679display_trans_stats(_, [], _, _, _) ->
680    ok.
681
682