%-*-Mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*- % ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et nomod: %%% %%%------------------------------------------------------------------------ %%% @doc %%% ==CloudI Task Size Calculation== %%% @end %%% %%% MIT License %%% %%% Copyright (c) 2009-2022 Michael Truog %%% %%% Permission is hereby granted, free of charge, to any person obtaining a %%% copy of this software and associated documentation files (the "Software"), %%% to deal in the Software without restriction, including without limitation %%% the rights to use, copy, modify, merge, publish, distribute, sublicense, %%% and/or sell copies of the Software, and to permit persons to whom the %%% Software is furnished to do so, subject to the following conditions: %%% %%% The above copyright notice and this permission notice shall be included in %%% all copies or substantial portions of the Software. %%% %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %%% DEALINGS IN THE SOFTWARE. %%% %%% @author Michael Truog %%% @copyright 2009-2022 Michael Truog %%% @version 2.0.5 {@date} {@time} %%%------------------------------------------------------------------------ -module(cloudi_task_size). -author('mjtruog at protonmail dot com'). %% external interface -export([new/7, get/2, reduce/3, put/4]). -include_lib("cloudi_core/include/cloudi_logger.hrl"). -record(node, { task_size :: number() }). -record(cloudi_task_size, { task_count :: pos_integer(), % count of concurrent tasks task_size_initial :: integer(), % count to control task size task_size_min :: integer(), task_size_max :: integer(), target_time :: float(), % in hours target_time_min :: float(), % in hours target_time_max :: float(), % in hours target_time_incr = 0 :: integer(), target_time_decr = 0 :: integer(), lookup = #{} :: #{node() := #node{}} }). -type state() :: #cloudi_task_size{}. -export_type([state/0]). -define(TARGET_TIME_ADJUST, 4). % number of consecutive incr/decr to % cause a target time adjustment -define(TARGET_TIME_ADJUST_FACTOR, 2.0). -define(TARGET_TIME_USAGE_FACTOR, 2.0). % defines task size tolerance %%%------------------------------------------------------------------------ %%% External interface functions %%%------------------------------------------------------------------------ %%------------------------------------------------------------------------- %% @doc %% ===Get a new task size lookup.=== %% @end %%------------------------------------------------------------------------- -spec new(TaskCount :: pos_integer(), TaskSizeInitial :: integer(), TaskSizeMin :: integer(), TaskSizeMax :: integer(), TargetTimeInitial :: float(), TargetTimeMin :: float(), TargetTimeMax :: float()) -> state(). new(TaskCount, TaskSizeInitial, TaskSizeMin, TaskSizeMax, TargetTimeInitial, TargetTimeMin, TargetTimeMax) when is_integer(TaskCount), TaskCount > 0, is_integer(TaskSizeInitial), is_integer(TaskSizeMin), is_integer(TaskSizeMax), TaskSizeInitial >= TaskSizeMin, TaskSizeInitial =< TaskSizeMax, is_float(TargetTimeInitial), TargetTimeInitial > 0.0, is_float(TargetTimeMin), is_float(TargetTimeMax), TargetTimeInitial >= TargetTimeMin, TargetTimeInitial =< TargetTimeMax -> #cloudi_task_size{task_count = TaskCount, task_size_initial = TaskSizeInitial, task_size_min = TaskSizeMin, task_size_max = TaskSizeMax, target_time = TargetTimeInitial, target_time_min = TargetTimeMin, target_time_max = TargetTimeMax}. %%------------------------------------------------------------------------- %% @doc %% ===Get the task size information.=== %% @end %%------------------------------------------------------------------------- -spec get(Pid :: pid(), State :: state()) -> {TaskSize :: integer(), Timeout :: cloudi_service:timeout_value_milliseconds()}. get(Pid, #cloudi_task_size{task_size_initial = TaskSizeInitial, target_time = TargetTime, lookup = Lookup}) when is_pid(Pid) -> Timeout = erlang:round(?TARGET_TIME_USAGE_FACTOR * TargetTime * 3600000.0), case maps:find(node(Pid), Lookup) of {ok, #node{task_size = TaskSize}} -> TaskSizeInteger = if is_float(TaskSize) -> floor(TaskSize); is_integer(TaskSize) -> TaskSize end, {TaskSizeInteger, Timeout}; error -> {TaskSizeInitial, Timeout} end. %%------------------------------------------------------------------------- %% @doc %% ===Reduce the task size after a timeout.=== %% @end %%------------------------------------------------------------------------- -spec reduce(Pid :: pid(), Multiplier :: float(), State :: state()) -> state(). reduce(Pid, Multiplier, #cloudi_task_size{task_size_min = TaskSizeMin, task_size_max = TaskSizeMax, lookup = Lookup} = State) when is_pid(Pid), is_float(Multiplier), Multiplier > 0.0, Multiplier =< 1.0 -> Node = node(Pid), case maps:find(Node, Lookup) of {ok, #node{task_size = TaskSize} = NodeState} -> NewTaskSize = task_size_clamp(TaskSize * Multiplier, TaskSizeMin, TaskSizeMax), NewNodeState = NodeState#node{task_size = NewTaskSize}, State#cloudi_task_size{lookup = maps:put(Node, NewNodeState, Lookup)}; error -> State end. %%------------------------------------------------------------------------- %% @doc %% ===Store task size information.=== %% ElapsedTime is in hours. %% @end %%------------------------------------------------------------------------- -spec put(Pid :: pid(), TaskSize :: integer(), ElapsedTime :: float(), State :: state()) -> state(). put(Pid, TaskSize, ElapsedTime, #cloudi_task_size{task_count = TaskCount, task_size_initial = TaskSizeInitial, task_size_min = TaskSizeMin, task_size_max = TaskSizeMax, target_time = TargetTime0, target_time_incr = TargetTimeIncr, target_time_decr = TargetTimeDecr, target_time_min = TargetTimeMin, target_time_max = TargetTimeMax, lookup = Lookup} = State) when is_pid(Pid), is_integer(TaskSize), is_float(ElapsedTime) -> Node = node(Pid), #node{task_size = OldTaskSize} = NodeState = case maps:find(Node, Lookup) of {ok, LookupValue} -> LookupValue; error -> #node{task_size = TaskSizeInitial} end, TaskSizeSmoothed = task_size_smoothed(TaskSize, OldTaskSize, TargetTime0, ElapsedTime, TaskCount), NewTaskSize = task_size_clamp(TaskSizeSmoothed, TaskSizeMin, TaskSizeMax), {NextTargetTimeIncr, TargetTime1} = if NewTaskSize < TaskSizeMin + 0.5 -> target_time_incr(TargetTimeIncr + 1, TargetTime0, TargetTimeMax); true -> {0, TargetTime0} end, {NextTargetTimeDecr, TargetTimeN} = if NewTaskSize > TaskSizeMax - 0.5 -> target_time_decr(TargetTimeDecr + 1, TargetTime1, TargetTimeMin); true -> {0, TargetTime1} end, {NewTargetTimeIncr, NewTargetTimeDecr} = if NextTargetTimeIncr > 0, NextTargetTimeDecr > 0 -> {0, 0}; true -> {NextTargetTimeIncr, NextTargetTimeDecr} end, NewLookup = maps:put(Node, NodeState#node{task_size = NewTaskSize}, Lookup), State#cloudi_task_size{target_time = TargetTimeN, target_time_incr = NewTargetTimeIncr, target_time_decr = NewTargetTimeDecr, lookup = NewLookup}. %%%------------------------------------------------------------------------ %%% Private functions %%%------------------------------------------------------------------------ task_size_clamp(TaskSize, TaskSizeMin, TaskSizeMax) when is_float(TaskSize) -> TaskSizeInteger = floor(TaskSize), if TaskSizeInteger < TaskSizeMin -> TaskSizeMin; TaskSizeInteger > TaskSizeMax -> TaskSizeMax; true -> TaskSize end. task_size_smoothed(CurrentTaskSize, OldTaskSize, TargetTimeTotal, ElapsedTime, TaskCount) when is_integer(CurrentTaskSize), is_number(OldTaskSize), is_float(TargetTimeTotal), is_float(ElapsedTime) -> TargetTime = TargetTimeTotal / ?TARGET_TIME_USAGE_FACTOR, NextTaskSize = CurrentTaskSize * (TargetTime / ElapsedTime), Difference = erlang:abs((TargetTime - ElapsedTime) / ElapsedTime), % determine the truncated moving average period based on the % percentage difference between the elapsed time and the target time % (empirically found solution that is relatively stable % and provides slow convergance, until a better solution is found) %product(L) when is_list(L) -> % lists:foldl(fun(X, Y) -> X * Y end, 1, L). %ceil(X) -> % T = erlang:trunc(X), % if % X > T -> % T + 1; % true -> % T % end. %floor(X) -> % T = erlang:trunc(X), % if % X < T -> % T - 1; % true -> % T % end. %SmoothingFactor = if % Difference =< 1.0 -> % product(lists:seq( % floor(math:log(Difference) / math:log(3.0)) * -2 + 1, % 1, -2)) * 2 * erlang:float(TaskCount); % true -> % product(lists:seq( % ceil(math:log(Difference) / math:log(50.0)) * 2 + 1, % 1, -2)) * 2 * erlang:float(TaskCount) %end, % smoothing method as separate sequences SmoothingFactor = TaskCount * (if Difference =< 0.0625 -> 1890.0; Difference =< 0.125 -> 210.0; Difference =< 0.25 -> 30.0; Difference =< 0.5 -> 6.0; Difference =< 1.0 -> 2.0; Difference =< 50.0 -> % = 1.0 * 50 6.0; % = 2.0 * 3 Difference =< 2500.0 -> % = 50.0 * 50 30.0; % = 6.0 * 5 Difference =< 125000.0 -> % = 2500.0 * 50 210.0; % = 30.0 * 7 Difference =< 6250000.0 -> % = 125000.0 * 50 1890.0; % = 210.0 * 9 true -> 20790.0 % = 1890.0 * 11 end), % perform truncated moving average Change = (NextTaskSize - OldTaskSize) / SmoothingFactor, OldTaskSize + Change. target_time_incr(?TARGET_TIME_ADJUST, TargetTime, TargetTimeMax) -> NewTargetTime = erlang:min(TargetTime * ?TARGET_TIME_ADJUST_FACTOR, TargetTimeMax), if NewTargetTime == TargetTimeMax -> ?LOG_ERROR("target time increase failed (~p, ~p)", [TargetTime, TargetTimeMax]); true -> ?LOG_WARN("target time increased to ~p hours", [NewTargetTime]) end, {0, NewTargetTime}; target_time_incr(TargetTimeIncr, TargetTime, _) -> {TargetTimeIncr, TargetTime}. target_time_decr(?TARGET_TIME_ADJUST, TargetTime, TargetTimeMin) -> NewTargetTime = erlang:max(TargetTime / ?TARGET_TIME_ADJUST_FACTOR, TargetTimeMin), if NewTargetTime == TargetTimeMin -> ?LOG_ERROR("target time decrease failed (~p, ~p)", [TargetTime, TargetTimeMin]); true -> ?LOG_WARN("target time decreased to ~p hours", [NewTargetTime]) end, {0, NewTargetTime}; target_time_decr(TargetTimeDecr, TargetTime, _) -> {TargetTimeDecr, TargetTime}.