%%
%% %CopyrightBegin%
%%
%% SPDX-License-Identifier: Apache-2.0
%%
%% Copyright Ericsson AB 1997-2025. 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.
%%
%% %CopyrightEnd%
%%

%%
%% Getting started:
%%
%% 1 Start one or more distributed Erlang nodes
%% 2a Connect the nodes, e.g. with net_adm:ping/1
%% 3a Run mnesia_meter:go()
%% 3b Run mnesia_meter:go(ReplicaType)
%% 3c Run mnesia_meter:go(ReplicaType, Nodes)

-module(mnesia_meter).
-author('hakan@erix.ericsson.se').
-export([
         go/0, 
         go/1, 
         go/2,
         repeat_meter/2
        ]).

-record(person, {name,        %% atomic, unique key
                 data,        %% compound structure
                 married_to,  %% name of partner or undefined
                 children}).  %% list of children

-record(meter, {desc, init, meter, micros}).

-record(result, {desc, list}).

-define(TIMES, 1000).

go() ->
    go(ram_copies).

go(ReplicaType) ->
    go(ReplicaType, [node() | nodes()]).

go(ReplicaType, Nodes) ->
    {ok, FunOverhead} = tc(fun(_) -> {atomic, ok} end, ?TIMES),
    Size = size(term_to_binary(#person{})),
    io:format("A fun apply costs ~p micro seconds. Record size is ~p bytes.~n",
              [FunOverhead, Size]),
    Res = go(ReplicaType, Nodes, [], FunOverhead, []),
    NewRes = rearrange(Res, []),
    DescHeader = lists:flatten(io_lib:format("~w on ~w", [ReplicaType, Nodes])),
    ItemHeader = lists:seq(1, length(Nodes)),
    Header = #result{desc = DescHeader, list = ItemHeader},
    SepList = ['--------' || _ <- Nodes],
    Separator = #result{desc = "", list = SepList},
    display([Separator, Header, Separator | NewRes] ++ [Separator]).

go(_ReplicaType, [], _Config, _FunOverhead, Acc) ->
    Acc;
go(ReplicaType, [H | T], OldNodes, FunOverhead, Acc) ->
    Nodes = [H | OldNodes],
    Config = [{ReplicaType, Nodes}],
    Res = run(Nodes, Config, FunOverhead),
    go(ReplicaType, T, Nodes, FunOverhead, [{ReplicaType, Nodes, Res} | Acc]).

rearrange([{_ReplicaType, _Nodes, Meters} | Tail], Acc) ->
    Acc2 = [add_meter(M, Acc) || M <- Meters],
    rearrange(Tail, Acc2);
rearrange([], Acc) ->
    Acc.

add_meter(M, Acc) ->
    case lists:keysearch(M#meter.desc, #result.desc, Acc) of
        {value, R} ->
            R#result{list = [M#meter.micros | R#result.list]};
        false ->
            #result{desc = M#meter.desc, list = [M#meter.micros]}
    end.

display(Res) ->
    MaxDesc = lists:max([length(R#result.desc) || R <- Res]),
    Format = lists:concat(["! ~-", MaxDesc, "s"]),
    display(Res, Format, MaxDesc).

display([R | Res], Format, MaxDesc) ->
    case R#result.desc of
	"" ->
	    io:format(Format, [lists:duplicate(MaxDesc, "-")]);
	Desc ->
	    io:format(Format, [Desc])
    end,
    display_items(R#result.list, R#result.desc),
    io:format(" !~n", []),
    display(Res, Format, MaxDesc);
display([], _Format, _MaxDesc) ->
    ok.
    
display_items([_Item | Items], "") ->
    io:format(" ! ~s", [lists:duplicate(10, $-)]),
    display_items(Items, "");
display_items([Micros | Items], Desc) ->
    io:format(" ! ~10w", [Micros]),
    display_items(Items, Desc);
display_items([], _Desc) ->
    ok.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

meters() ->
    [#meter{desc = "transaction update two records with read and write", 
            init = fun write_records/2,
            meter = fun update_records/1}, 
     #meter{desc = "transaction update two records with wread and write", 
            init = fun write_records/2,
            meter = fun w_update_records/1}, 
     #meter{desc = "transaction update two records with read and s_write", 
            init = fun s_write_records/2,
            meter = fun s_update_records/1}, 
     #meter{desc = "sync_dirty  update two records with read and write", 
            init = fun sync_dirty_write_records/2,
            meter = fun sync_dirty_update_records/1}, 
     #meter{desc = "async_dirty update two records with read and write", 
            init = fun async_dirty_write_records/2,
            meter = fun async_dirty_update_records/1}, 
     #meter{desc = "plain fun   update two records with dirty_read and dirty_write", 
            init = fun dirty_write_records/2,
            meter = fun dirty_update_records/1}, 
     #meter{desc = "ets         update two records with read and write (local only)", 
            init = fun ets_opt_write_records/2,
            meter = fun ets_update_records/1}, 
     #meter{desc = "plain fun   update two records with ets:lookup and ets:insert (local only)", 
            init = fun bif_opt_write_records/2,
            meter = fun bif_update_records/1}, 
     #meter{desc = "plain fun   update two records with dets:lookup and dets:insert (local only)", 
            init = fun dets_opt_write_records/2,
            meter = fun dets_update_records/1}, 
     
     #meter{desc = "transaction write two records with write", 
            init = fun write_records/2,
            meter = fun(X) -> write_records(X, 0-X) end}, 
     #meter{desc = "transaction write two records with s_write", 
            init = fun s_write_records/2,
            meter = fun(X) -> s_write_records(X, 0-X) end}, 
     #meter{desc = "sync_dirty  write two records with write", 
            init = fun sync_dirty_write_records/2,
            meter = fun(X) -> sync_dirty_write_records(X, 0-X) end}, 
     #meter{desc = "async_dirty write two records with write", 
            init = fun async_dirty_write_records/2,
            meter = fun(X) -> async_dirty_write_records(X, 0-X) end}, 
     #meter{desc = "plain fun   write two records with dirty_write", 
            init = fun dirty_write_records/2,
            meter = fun(X) -> dirty_write_records(X, 0-X) end}, 
     #meter{desc = "ets         write two records with write (local only)", 
            init = fun ets_opt_write_records/2,
            meter = fun(X) -> ets_write_records(X, 0-X) end}, 
     #meter{desc = "plain fun   write two records with ets:insert (local only)", 
            init = fun bif_opt_write_records/2,
            meter = fun(X) -> bif_write_records(X, 0-X) end}, 
     #meter{desc = "plain fun   write two records with dets:insert (local only)", 
            init = fun dets_opt_write_records/2,
            meter = fun(X) -> dets_write_records(X, 0-X) end}, 
     
     #meter{desc = "transaction read two records with read", 
            init = fun write_records/2,
            meter = fun(X) -> read_records(X, 0-X) end}, 
     #meter{desc = "sync_dirty  read two records with read", 
            init = fun sync_dirty_write_records/2,
            meter = fun(X) -> sync_dirty_read_records(X, 0-X) end}, 
     #meter{desc = "async_dirty read two records with read", 
            init = fun async_dirty_write_records/2,
            meter = fun(X) -> async_dirty_read_records(X, 0-X) end}, 
     #meter{desc = "plain fun   read two records with dirty_read", 
            init = fun dirty_write_records/2,
            meter = fun(X) -> dirty_read_records(X, 0-X) end}, 
     #meter{desc = "ets         read two records with read", 
            init = fun ets_opt_write_records/2,
            meter = fun(X) -> ets_read_records(X, 0-X) end},
     #meter{desc = "plain fun   read two records with ets:lookup", 
            init = fun bif_opt_write_records/2,
            meter = fun(X) -> bif_read_records(X, 0-X) end},
     #meter{desc = "plain fun   read two records with dets:lookup", 
            init = fun dets_opt_write_records/2,
            meter = fun(X) -> dets_read_records(X, 0-X) end}
    ].

update_fun(Name) ->
    fun() ->
            case mnesia:read({person, Name}) of
                [] -> 
                    mnesia:abort(no_such_person);
                [Pers] ->
                    [Partner] = mnesia:read({person, Pers#person.married_to}), 
                    mnesia:write(Pers#person{married_to = undefined}), 
                    mnesia:write(Partner#person{married_to = undefined})
            end
    end.
    
update_records(Name) ->
  mnesia:transaction(update_fun(Name)).

sync_dirty_update_records(Name) ->
  {atomic, mnesia:sync_dirty(update_fun(Name))}.

async_dirty_update_records(Name) ->
  {atomic, mnesia:async_dirty(update_fun(Name))}.

ets_update_records(Name) ->
  {atomic, mnesia:ets(update_fun(Name))}.

w_update_records(Name) ->
    F = fun() ->
                case mnesia:wread({person, Name}) of
                    [] -> 
                        mnesia:abort(no_such_person);
                    [Pers] ->
                        [Partner] = mnesia:wread({person, Pers#person.married_to}), 
                        mnesia:write(Pers#person{married_to = undefined}), 
                        mnesia:write(Partner#person{married_to = undefined})
                end
        end, 
    mnesia:transaction(F).

s_update_records(Name) ->
    F = fun() ->
                case mnesia:read({person, Name}) of
                    [] -> 
                        mnesia:abort(no_such_person);
                    [Pers] ->
                        [Partner] = mnesia:read({person, Pers#person.married_to}), 
                        mnesia:s_write(Pers#person{married_to = undefined}), 
                        mnesia:s_write(Partner#person{married_to = undefined})
                end
        end, 
    mnesia:transaction(F).

dirty_update_records(Name) ->
    case mnesia:dirty_read({person, Name}) of
        [] -> 
            mnesia:abort(no_such_person);
        [Pers] ->
            [Partner] = mnesia:dirty_read({person, Pers#person.married_to}), 
            mnesia:dirty_write(Pers#person{married_to = undefined}), 
            mnesia:dirty_write(Partner#person{married_to = undefined})
    end,
    {atomic, ok}.

bif_update_records(Name) ->
    case ets:lookup(person, Name) of
        [] -> 
            mnesia:abort(no_such_person);
        [Pers] ->
            [Partner] = ets:lookup(person, Pers#person.married_to), 
            ets:insert(person, Pers#person{married_to = undefined}), 
            ets:insert(person, Partner#person{married_to = undefined})
    end,
    {atomic, ok}.

dets_update_records(Name) ->
    case dets:lookup(person, Name) of
        [] -> 
            mnesia:abort(no_such_person);
        [Pers] ->
            [Partner] = dets:lookup(person, Pers#person.married_to), 
            dets:insert(person, Pers#person{married_to = undefined}), 
            dets:insert(person, Partner#person{married_to = undefined})
    end,
    {atomic, ok}.

write_records_fun(Pers, Partner) ->
    fun() ->
            P = #person{children = [ulla, bella]}, 
            mnesia:write(P#person{name = Pers, married_to = Partner}), 
            mnesia:write(P#person{name = Partner, married_to = Pers})
    end.

write_records(Pers, Partner) ->
    mnesia:transaction(write_records_fun(Pers, Partner)).

sync_dirty_write_records(Pers, Partner) ->
    {atomic, mnesia:sync_dirty(write_records_fun(Pers, Partner))}.

async_dirty_write_records(Pers, Partner) ->
    {atomic, mnesia:async_dirty(write_records_fun(Pers, Partner))}.

ets_write_records(Pers, Partner) ->
    {atomic, mnesia:ets(write_records_fun(Pers, Partner))}.

s_write_records(Pers, Partner) ->
    F = fun() ->
                P = #person{children = [ulla, bella]}, 
                mnesia:s_write(P#person{name = Pers, married_to = Partner}), 
                mnesia:s_write(P#person{name = Partner, married_to = Pers})
        end,
    mnesia:transaction(F).

dirty_write_records(Pers, Partner) ->
    P = #person{children = [ulla, bella]}, 
    mnesia:dirty_write(P#person{name = Pers, married_to = Partner}), 
    mnesia:dirty_write(P#person{name = Partner, married_to = Pers}),
    {atomic, ok}.

ets_opt_write_records(Pers, Partner) ->
    case mnesia:table_info(person, where_to_commit) of
        [{N, ram_copies}] when N == node() ->
            ets_write_records(Pers, Partner);
        _ ->
            throw(skipped)
    end.

bif_opt_write_records(Pers, Partner) ->
    case mnesia:table_info(person, where_to_commit) of
        [{N, ram_copies}] when N == node() ->
            bif_write_records(Pers, Partner);
        _ ->
            throw(skipped)
    end.

bif_write_records(Pers, Partner) ->
    P = #person{children = [ulla, bella]}, 
    ets:insert(person, P#person{name = Pers, married_to = Partner}), 
    ets:insert(person, P#person{name = Partner, married_to = Pers}),
    {atomic, ok}.

dets_opt_write_records(Pers, Partner) ->
    case mnesia:table_info(person, where_to_commit) of
        [{N, disc_only_copies}] when N == node() ->
            dets_write_records(Pers, Partner);
        _ ->
            throw(skipped)
    end.

dets_write_records(Pers, Partner) ->
    P = #person{children = [ulla, bella]}, 
    dets:insert(person, P#person{name = Pers, married_to = Partner}), 
    dets:insert(person, P#person{name = Partner, married_to = Pers}),
    {atomic, ok}.

read_records_fun(Pers, Partner) ->
    fun() ->
            case {mnesia:read({person, Pers}), 
                  mnesia:read({person, Partner})} of
                {[_], [_]} ->
                    ok;
                _ ->
                    mnesia:abort(no_such_person)
            end
    end.

read_records(Pers, Partner) ->
    mnesia:transaction(read_records_fun(Pers, Partner)).

sync_dirty_read_records(Pers, Partner) ->
    {atomic, mnesia:sync_dirty(read_records_fun(Pers, Partner))}.

async_dirty_read_records(Pers, Partner) ->
    {atomic, mnesia:async_dirty(read_records_fun(Pers, Partner))}.

ets_read_records(Pers, Partner) ->
    {atomic, mnesia:ets(read_records_fun(Pers, Partner))}.

dirty_read_records(Pers, Partner) ->
    case {mnesia:dirty_read({person, Pers}), 
          mnesia:dirty_read({person, Partner})} of
        {[_], [_]} ->
            {atomic, ok};
        _ ->
            mnesia:abort(no_such_person)
    end.

bif_read_records(Pers, Partner) ->
    case {ets:lookup(person, Pers), 
          ets:lookup(person, Partner)} of
        {[_], [_]} ->
            {atomic, ok};
        _ ->
            mnesia:abort(no_such_person)
    end.

dets_read_records(Pers, Partner) ->
    case {dets:lookup(person, Pers), 
          dets:lookup(person, Partner)} of
        {[_], [_]} ->
            {atomic, ok};
        _ ->
            mnesia:abort(no_such_person)
    end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

run(Nodes, Config, FunOverhead) ->
    Meters = meters(),
    io:format("Run ~w meters with table config: ~w~n", [length(Meters), Config]),
    rpc:multicall(Nodes, mnesia, lkill, []),
    start(Nodes, Config),
    Res = [run_meter(Data, Nodes, FunOverhead) || Data <- Meters],
    stop(Nodes),
    Res.

run_meter(M, Nodes, FunOverhead) when is_record(M, meter) ->
    io:format(".", []),
    case catch init_records(M#meter.init, ?TIMES) of
        {atomic, ok} ->
            rpc:multicall(Nodes, mnesia, dump_log, []),
            case tc(M#meter.meter, ?TIMES) of
                {ok, Micros} ->
                    M#meter{micros = lists:max([0, Micros - FunOverhead])};
                {error, Reason} ->
                    M#meter{micros = Reason}
            end;
        Res ->
            M#meter{micros = Res}
    end.

start(Nodes, Config) ->
    mnesia:delete_schema(Nodes), 
    ok = mnesia:create_schema(Nodes),
    Args = [[{dump_log_write_threshold, ?TIMES div 2},
             {dump_log_time_threshold, timer:hours(10)}]],
    lists:foreach(fun(Node) -> rpc:call(Node, mnesia, start, Args) end, Nodes), 
    Attrs = record_info(fields, person), 
    TabDef = [{attributes, Attrs} | Config], 
    {atomic, _} = mnesia:create_table(person, TabDef).
    
stop(Nodes) ->
    rpc:multicall(Nodes, mnesia, stop, []).

%% Generate some dummy persons    
init_records(_Fun, 0) ->
    {atomic, ok};
init_records(Fun, Times) ->
    {atomic, ok} = Fun(Times, 0 - Times),
    init_records(Fun, Times - 1).

tc(Fun, Times) ->
    case catch timer:tc(?MODULE, repeat_meter, [Fun, Times]) of
        {Micros, ok} ->
            {ok, Micros div Times};
        {_Micros, {error, Reason}} ->
            {error, Reason};
        {'EXIT', Reason} ->
            {error, Reason}
    end.

%% The meter must return {atomic, ok}
repeat_meter(Meter, Times) ->
    repeat_meter(Meter, {atomic, ok}, Times).

repeat_meter(_, {atomic, ok}, 0) ->
    ok;
repeat_meter(Meter, {atomic, _Result}, Times) when Times > 0 ->
    repeat_meter(Meter, Meter(Times), Times - 1);
repeat_meter(_Meter, Reason, _Times) ->
    {error, Reason}.

