% Copyright (C) 2012-2017,2018 John E. Davis
%
% This file is part of the S-Lang Library and may be distributed under the
% terms of the GNU General Public License.  See the file COPYING for
% more information.
%---------------------------------------------------------------------------
% See the slprof script for an example of using the profiler.
%
_boseos_info=0;
_bofeof_info=0;

%public variable __DEBUG_PROFILE_;

% For lines
private variable Profile_Info_Type = struct
{
   name,			       % (bos/eof)
   num_called,			       % (bos/bof) num times line called
   num_s_triggered,		       % (eof/eof) num child statements
   num_f_triggered,		       % (eof/eof) num functions triggered
   num_self_f,			       % (eos/) num functions directly called
   num_self_s,			       % num statements directly called
   self_time,			       % (eos/eof)
   cum_time,			       % (eos/eof)
};
Profile_Info_Type.num_called = 0;
Profile_Info_Type.num_s_triggered = 0;
Profile_Info_Type.num_f_triggered = 0;
Profile_Info_Type.num_self_f = 0;
Profile_Info_Type.num_self_s = 0;
Profile_Info_Type.self_time = 0.0;
Profile_Info_Type.cum_time = 0.0;

private variable L_Info_Table;
private variable F_Info_Table;

private define convert_profile_info_to_array (p)
{
   variable keys = assoc_get_keys (p);
   variable i, n = length (p);

   % Filter out the bad entries
   _for i (0, n-1, 1)
     {
	if (keys[i] == "")	       %  Dummy
	  assoc_delete_key (p, keys[i]);
     }
   n = length (p);
   keys = assoc_get_keys (p);

   variable s = @Profile_Info_Type;
   s.num_called = Int_Type[n];
   s.num_s_triggered = Int_Type[n];
   s.num_f_triggered = Int_Type[n];
   s.num_self_f = Int_Type[n];
   s.num_self_s = Int_Type[n];
   s.self_time = Double_Type[n];
   s.cum_time = Double_Type[n];
   s.name = keys;

   variable vals = assoc_get_values (p);
   _for i (0, n-1, 1)
     {
	variable v = vals[i];
	s.num_called[i] = v.num_called;
	s.num_s_triggered[i] = v.num_s_triggered;
	s.num_f_triggered[i] = v.num_f_triggered;
	s.num_self_f[i] = v.num_self_f;
	s.num_self_s[i] = v.num_self_s;
	s.self_time[i] = v.self_time;
	s.cum_time[i] = v.cum_time;
     }
   return s;
}

private define new_l_info (name)
{
   variable l = @Profile_Info_Type;
   L_Info_Table[name] = l;
   l.name = name;
   return l;
}

private define new_f_info (name)
{
   variable finfo = @Profile_Info_Type;
   F_Info_Table[name] = finfo;
   finfo.name = name;
   return finfo;
}

private variable Function_Stack_Type = struct
{
   f_info,
   s_counter,
   f_counter,
   self_s_counter,
   self_time,
   cum_time,
};
private variable Function_Stack;

private variable Line_Stack_Type = struct
{
   l_info,
   s_counter,
   f_counter,
   self_f_counter,
   self_time,
   cum_time,
};
private variable Line_Stack;

private variable L_Info;
private variable L_S_Counter;	       %  bos/eof
private variable L_F_Counter;	       %  bos/eof
private variable L_Self_Time;	       %  bof/eof
private variable L_Cum_Time;	       %  bof/eof

private variable F_Info;
private variable F_S_Counter;	       %  bof
private variable F_F_Counter;	       %  bof
private variable F_Self_S;	       %  bos
private variable F_Self_Time;	       %  bos/eos
private variable F_Cum_Time;	       %  bos/eos

private variable Num_Statement_Counts;
private variable Num_Fun_Calls;
private variable Tstart;

private variable Null_Struct = struct {dummy};

private variable BOS_Depth;
private variable BOS_Stack_Depth;
private define push_line_info ()
{
   variable s = @Line_Stack_Type;
   s.l_info = L_Info;
   s.s_counter = L_S_Counter;
   s.f_counter = L_F_Counter;
   s.self_time = L_Self_Time;
   s.cum_time = L_Cum_Time;
   list_append (Line_Stack, s);
   BOS_Stack_Depth++;
}

private define pop_line_info ()
{
   variable s = list_pop (Line_Stack, -1);
   L_Info = s.l_info;
   L_Self_Time = s.self_time;
   L_Cum_Time = s.cum_time;

   L_Info.num_self_f++;
   L_Info.num_s_triggered += (Num_Statement_Counts - s.s_counter);
   L_Info.num_f_triggered += (Num_Fun_Calls - s.f_counter);
   L_F_Counter = Num_Fun_Calls;
   L_S_Counter = Num_Statement_Counts;
   BOS_Stack_Depth--;
}

% This variable tracks sequential calls to the BOS and EOS handlers
% This can happen when profiled code calls non-profiled, which calls
% profiled.
private variable Last_Was_BOS_EOS = 0;
private variable Dummy_L_Info;

private define bos_handler (file, line)
{
   variable dt = toc() - Tstart;

   F_Cum_Time += dt;
   F_Self_Time += dt;
   F_Self_S++;
   if (BOS_Depth >= BOS_Stack_Depth)
     {
#ifexists __DEBUG_PROFILE_
	%vmessage ("# bos pushing (%d,%d)", BOS_Depth,BOS_Stack_Depth);
#endif
	L_Cum_Time += dt;
	L_Self_Time += dt;
	push_line_info ();
     }
   BOS_Depth = BOS_Stack_Depth;

   Num_Statement_Counts++;

   file = sprintf ("%s:%d", file, line);
   variable l = L_Info_Table[file];
   if (l == Null_Struct)
     l = new_l_info (file);

   l.num_called++;

   L_Info = l;
   L_S_Counter = Num_Statement_Counts;
   L_F_Counter = Num_Fun_Calls;
   L_Self_Time = 0.0;
   L_Cum_Time = 0.0;

#ifexists __DEBUG_PROFILE_
   vmessage ("BOS: (%d, %d) %S", BOS_Depth, BOS_Stack_Depth, file);
#endif
   Tstart = toc ();
}

private define eos_handler ()
{
   variable dt = toc()-Tstart;

   if (BOS_Depth < BOS_Stack_Depth)
     {
#ifexists __DEBUG_PROFILE_
	%vmessage ("# eos popping (%d,%d)", BOS_Depth,BOS_Stack_Depth);
#endif
	pop_line_info ();
     }
   BOS_Depth--;

   L_Self_Time += dt;
   L_Cum_Time += dt;
   F_Cum_Time += L_Cum_Time;
   F_Self_Time += L_Self_Time;

   L_Info.self_time += L_Self_Time;
   L_Info.cum_time += L_Cum_Time;

#ifexists __DEBUG_PROFILE_
   vmessage ("EOS: (%d,%d) %S", BOS_Depth, BOS_Stack_Depth, L_Info.name);
#endif

   % This is necessary if a BOF/EOF sequence follows to avoid another
   % push/pop of this object since
   if (Last_Was_BOS_EOS == -1)
     L_Info = Dummy_L_Info;

   Last_Was_BOS_EOS = -1;
   Tstart = toc();
}

private define bof_handler (fun, file)
{
   variable dt = toc () - Tstart;
   Num_Fun_Calls++;

#ifexists __DEBUG_PROFILE_
   %vmessage ("# bof pushing (%d,%d)", BOS_Depth,BOS_Stack_Depth);
#endif
   L_Self_Time += dt;
   L_Cum_Time += dt;
   push_line_info ();
   %BOS_Stack_Depth++;

   variable f = @Function_Stack_Type;
   f.f_info = F_Info;
   f.s_counter = F_S_Counter;
   f.f_counter = F_F_Counter;
   f.self_time = F_Self_Time;
   f.self_s_counter = F_Self_S;
   f.cum_time = F_Cum_Time;
   list_append (Function_Stack, f);

   file = sprintf ("%S:%S", fun, file);
   variable f_info = F_Info_Table[file];
   if (f_info == Null_Struct)
     f_info = new_f_info (file);
   f_info.num_called++;

   F_Info = f_info;
   F_S_Counter = Num_Statement_Counts;
   F_F_Counter = Num_Fun_Calls;
   F_Cum_Time = 0.0;
   F_Self_Time = 0.0;
   F_Self_S = 0;

#ifexists __DEBUG_PROFILE_
   vmessage ("BOF: (%d,%d) %S", BOS_Depth, BOS_Stack_Depth, file);
#endif
   Tstart = toc ();
}

private define eof_handler ()
{
   variable dt = toc() - Tstart;

#ifexists __DEBUG_PROFILE_
   %vmessage ("# eof popping (%d,%d)", BOS_Depth,BOS_Stack_Depth);
#endif
   F_Cum_Time += dt;
   F_Self_Time += dt;
   F_Info.cum_time += F_Cum_Time;
   F_Info.self_time += F_Self_Time;
   F_Info.num_s_triggered += (Num_Statement_Counts - F_S_Counter);
   F_Info.num_f_triggered += (Num_Fun_Calls - F_F_Counter);
   F_Info.num_self_s += F_Self_S;

   pop_line_info ();
   % Add on time spent in the function to this line's value
   L_Cum_Time += F_Cum_Time;

#ifexists __DEBUG_PROFILE_
   vmessage ("EOF: (%d,%d) %S", BOS_Depth, BOS_Stack_Depth, F_Info.name);
#endif

   variable f = list_pop (Function_Stack, -1);
   F_Info = f.f_info;
   F_Info.num_self_f++;
   F_S_Counter = f.s_counter;
   F_F_Counter = f.f_counter;
   F_Self_Time = f.self_time;
   F_Self_S = f.self_s_counter;
   F_Cum_Time = f.cum_time;

   %F_Cum_Time += dt;
   %F_Self_Time += dt;

   Tstart = toc();
}

% In function-only mode, lines are not profiled.  The self-time of the
% function is its cumulative time minus the cumulative times of the
% functions that it directly calls.  The F_Self_Time variable will
% track those.
private define f_bof_handler (fun, file)
{
   variable dt = toc () - Tstart;
   Num_Fun_Calls++;

   variable f = @Function_Stack_Type;
   f.f_info = F_Info;
   f.f_counter = F_F_Counter;
   f.self_time = F_Self_Time;
   f.cum_time = F_Cum_Time + dt;
   list_append (Function_Stack, f);

   file = sprintf ("%S:%S", fun, file);
   variable f_info = F_Info_Table[file];
   if (f_info == Null_Struct)
     f_info = new_f_info (file);
   f_info.num_called++;

   F_Info = f_info;
   F_F_Counter = Num_Fun_Calls;
   F_Cum_Time = 0.0;
   F_Self_Time = 0.0;

   Tstart = toc ();
}

private define f_eof_handler ()
{
   variable dt = toc() - Tstart;

   F_Cum_Time += dt;
   F_Info.cum_time += F_Cum_Time;
   F_Info.self_time += (F_Cum_Time - F_Self_Time);
   F_Info.num_f_triggered += (Num_Fun_Calls - F_F_Counter);

   variable f = list_pop (Function_Stack, -1);
   F_Info = f.f_info;
   F_Info.num_self_f++;
   F_F_Counter = f.f_counter;
   F_Self_Time = f.self_time + F_Cum_Time;
   F_Cum_Time += f.cum_time;

   Tstart = toc();
}

% Usage: profile_on (do_line_profile)
define profile_on ()
{
   _boseos_info = 0;
   _bofeof_info = 1;
   if (_NARGS)
     {
	variable arg = ();
	if (arg)
	  _boseos_info = 3;
     }
}

define profile_off ()
{
   _boseos_info = 0;
   _bofeof_info = 0;
}

private define setup_profiler_variables ()
{
   L_Info_Table = Assoc_Type[Struct_Type, Null_Struct];
   F_Info_Table = Assoc_Type[Struct_Type, Null_Struct];
   Line_Stack = {};
   Function_Stack = {};

   Dummy_L_Info = new_l_info ("");
   L_Info = Dummy_L_Info;
   L_S_Counter = 0;
   L_F_Counter = 0;
   L_Self_Time = 0;
   L_Cum_Time = 0;

   F_Info = new_f_info ("");
   F_S_Counter = 0;
   F_F_Counter = 0;
   F_Self_Time = 0;
   F_Self_S = 0;
   F_Cum_Time = 0;

   Tstart = 0;
   Num_Statement_Counts = 0;
   Num_Fun_Calls = 0;
   BOS_Stack_Depth=0;
   BOS_Depth=0;
}

define profile_begin ()
{
   variable arg = 0;
   if (_NARGS)
     arg = ();
   if (arg)
     {
	()=_set_bos_handler (&bos_handler);
	()=_set_eos_handler (&eos_handler);
	()=_set_bof_handler (&bof_handler);
	()=_set_eof_handler (&eof_handler);
     }
   else
     {
	()=_set_bos_handler (NULL);
	()=_set_eos_handler (NULL);
	()=_set_bof_handler (&f_bof_handler);
	()=_set_eof_handler (&f_eof_handler);
     }
   setup_profiler_variables ();
   profile_on (arg);
   tic ();
}

define profile_end ()
{
   ()=_set_bos_handler (NULL);
   ()=_set_eos_handler (NULL);
   ()=_set_bof_handler (NULL);
   ()=_set_eof_handler (NULL);
   profile_off ();
}

% Calibration Notes:
%
%  If the execution of a statememt causes M other statements to
%  execute and N function calls, then the amount of profiler overhead
%  is given by
%
%     N*dF + M*dS
%
%  where dF is the amount of overhead per function call for running
%  the bof/eof-handlers, and dS is the overhead per statement in
%  running the bos/eos-handlers.
%
%  The timers used to measure the amount of time spent in a function
%  or statement are stopped when a handler is called, and started
%  again when the handler returns.  This introduces systematic error
%  in the measurements.  Call dt_S the systematic error introduced by
%  bos/eos handlers and dt_F systematic error from the bof/eof handlers.
%  Then the observed time as given by the interval times is related to
%  the "true" time t via
%
%     t_obs = t + N*dt_F + M*dt_S + dt_S
%
%  The self-time of the statement t_self is determined through an
%  interval timer that stops when a function is called, and starts
%  again after the function returns.  If the statement makes N_self
%  direct function calls, then t_self is related to its observed
%  self-time t_self_obs by
%
%     t_self_obs = t_self + N_self*dt_F + dt_S
%
%  The self-time of a function is the sum of the self-times of the
%  individual statements that were executed directly by the function.
%  Suppose the function executes M_self statements that in turn
%  directly call a total of N_self functions.  Then, for functions it
%  follows that:
%
%     t_self_obs_f = t_self_f + N_self*dt_F + M_self*dt_S + dt_F
%
%  where the last term accounts for the overhead of the bof/eof
%  handler for the function itself.
%
%  ----------------------------------------------------------------
%
%  The values dF, dS, dt_S, and dt_F can be determined as follows:
%
%  With no handlers in place, t can be accurately measured and can be
%  considered t be a known quantity.  Suppose the statement has no
%  function calls and let that statement be executed a very large
%  number of times B with handlers in place.  Then the elapsed time
%  t_elapsed as given by the wallclock will be given by
%
%     t_elapsed = B*(t+dS)   ===> dS = (t_elapsed - B*t)/B
%
%  The total measured or observed time t_obs, as given by the
%  profiler's starting and stopping of the clock upon each execution
%  will be given by
%
%     t_obs = B*(t+dt_S)      ===> dt_S = (t_obs - B*t)/B
%
%  If the statement is just a function call to a function that does
%  nothing, then
%
%     t_elapsed = B*(t + dS + dF)
%
%  Using the prior determination of dS, we obtain
%
%     dF = t_elapsed/B - t - dS
%
%  For just a single function call with no arguments, the self-time is
%  defined to be 0.  So:
%
%     t_self_obs = 0 + 1*dt_F + dt_S  ==> dt_F = t_self_obs-dt_S
%
private variable Overhead_Per_Statement = 0.0;   %  dS
private variable Error_Per_Statement = 0.0;   %  dt_S
private variable Overhead_Per_Function = 0.0;   %  dF
private variable Error_Per_Function = 0.0;   %  dt_F

profile_off ();

private define cal_nop_0 ();
private define cal_f_0 (n)
{
   loop (n)
     {
	cal_nop_0(); cal_nop_0(); cal_nop_0(); cal_nop_0(); cal_nop_0();
	cal_nop_0(); cal_nop_0(); cal_nop_0(); cal_nop_0(); cal_nop_0();
	cal_nop_0(); cal_nop_0(); cal_nop_0(); cal_nop_0(); cal_nop_0();
	cal_nop_0(); cal_nop_0(); cal_nop_0(); cal_nop_0(); cal_nop_0();
     }
}

private define cal_s_0 (n)
{
   loop (n)
     {
	() = 1;	() = 1;	() = 1;	() = 1; () = 1;
	() = 1;	() = 1;	() = 1;	() = 1; () = 1;
	() = 1;	() = 1;	() = 1;	() = 1; () = 1;
	() = 1;	() = 1;	() = 1;	() = 1; () = 1;
     }
}

profile_on (1);

private define cal_nop_1 ();
private define cal_f_1 (n)
{
   loop (n)
     {
	cal_nop_1(); cal_nop_1(); cal_nop_1(); cal_nop_1(); cal_nop_1();
	cal_nop_1(); cal_nop_1(); cal_nop_1(); cal_nop_1(); cal_nop_1();
	cal_nop_1(); cal_nop_1(); cal_nop_1(); cal_nop_1(); cal_nop_1();
	cal_nop_1(); cal_nop_1(); cal_nop_1(); cal_nop_1(); cal_nop_1();
     }
}
private define cal_s_1 (n)
{
   loop (n)
     {
	() = 1;	() = 1;	() = 1;	() = 1; () = 1;
	() = 1;	() = 1;	() = 1;	() = 1; () = 1;
	() = 1;	() = 1;	() = 1;	() = 1; () = 1;
	() = 1;	() = 1;	() = 1;	() = 1; () = 1;
     }
}
profile_off ();

#ifnexists sum
private define sum (x)
{
   variable s = 0.0;
   foreach (x) s += ();
   return s;
}
#endif

define profile_calibrate ()
{
#ifexists __DEBUG_PROFILE_
   return;
#endif
   if (_NARGS == 0) 1000;
   variable n1 = ();
   variable n0 = 100*n1;

   tic();
   cal_s_0 (n0);
   variable t0 = toc;
   variable t_expected = ((t0*n1)/n0); %  when done n1 times

   profile_begin (1);
   tic ();
   cal_s_1 (n1);
   variable t_elapsed = toc ();
   profile_end ();

   variable s = convert_profile_info_to_array (L_Info_Table);
   variable num = sum (s.num_called);
   variable t_obs = sum(s.cum_time);
   Error_Per_Statement = (t_obs - t_expected)/num;
   Overhead_Per_Statement = (t_elapsed-t_expected)/num;

   % Now calibrate the function call overhead
   tic();
   cal_f_0(n0);
   t0 = toc;
   t_expected = ((t0*n1)/n0); %  when done n1 times

   profile_begin (1);
   tic ();
   cal_f_1 (n1);
   t_elapsed = toc ();
   profile_end ();

   %variable f = convert_func_info_to_array ();
   s = convert_profile_info_to_array (L_Info_Table);
   num = sum (s.num_called);
   Overhead_Per_Function = (t_elapsed - t_expected)/num - Overhead_Per_Statement;
   t_obs = sum(s.self_time);
   Error_Per_Function = t_obs/num - Error_Per_Statement;
   if (Error_Per_Function < 0)
     Error_Per_Function = 0.0;
}

private define profile_report_lines (fp, s)
{
   s.cum_time -= (Error_Per_Statement * (s.num_called + s.num_s_triggered)
		  + s.num_f_triggered * Error_Per_Function);
   s.self_time -= (Error_Per_Statement * s.num_called
		   + s.num_self_f * Error_Per_Function);

   variable rates = s.cum_time/s.num_called;
   variable i = array_sort (s.self_time);
   %variable i = array_sort (s.cum_time);
   array_reverse (i);
   rates = rates[i];
   variable num_called = s.num_called[i];
   variable self_time = s.self_time[i];
   variable cum_time = s.cum_time[i];
   variable num_self_f = s.num_self_f[i];
   variable num_s_triggered = s.num_s_triggered[i];
   variable num_f_triggered = s.num_f_triggered[i];
   variable name = s.name[i];

#iffalse
   variable total_counts = sum(num_called);
   variable total_calls = sum(num_self_f);
   () = fprintf (fp, "#Number of profiled statements executed: %g\n", total_counts);
   () = fprintf (fp, "#Number of profiled function calls: %g\n", total_calls);
   () = fprintf (fp, "#Profiler Overhead per statement: %g ms\n", Overhead_Per_Statement*1e3);
   () = fprintf (fp, "#Profiler Overhead per function : %g ms\n", Overhead_Per_Function*1e3);
   () = fprintf (fp, "#Profiler Error per statement   : %g ms\n", Error_Per_Statement*1e3);
   () = fprintf (fp, "#Profiler Error per function    : %g ms\n", Error_Per_Function*1e3);
   () = fprintf (fp, "#Total Profiler Overhead: %g secs\n",
		 total_counts * Overhead_Per_Statement + total_calls*Overhead_Per_Function);
   () = fprintf (fp, "\n\n");
#endif

   () = fprintf (fp, "#ncalls      ms/call totalselfms     totalsecs  Fcalls   Scalls File:line\n");
   %                  1234567 1234567890AB 1234567890AB 1234567890AB 1234567  1234567
   _for i (0, length(rates)-1, 1)
     {
	() = fprintf (fp, "%7d %12.5f %12.5f %12.5f %7d %7d %s\n",
		      num_called[i], rates[i]*1e3, self_time[i]*1e3,
		      cum_time[i], num_f_triggered[i], num_s_triggered[i], name[i]);
     }
}

private define profile_report_funcs (fp, s)
{
#ifnfalse
   s.cum_time -= (Error_Per_Statement * s.num_s_triggered
		  + s.num_f_triggered * Error_Per_Function);
   s.self_time -= (Error_Per_Statement * s.num_self_s
		   + s.num_self_f * Error_Per_Function);
#endif
   variable rates = s.cum_time/s.num_called;
   variable i = array_sort (s.self_time);
   %variable i = array_sort (s.cum_time);
   array_reverse (i);
   rates = rates[i];
   variable num_called = s.num_called[i];
   variable self_time = s.self_time[i];
   variable cum_time = s.cum_time[i];
   variable num_self_f = s.num_self_f[i];
   variable num_self_s = s.num_self_s[i];
   variable num_s_triggered = s.num_s_triggered[i];
   variable num_f_triggered = s.num_f_triggered[i];
   variable name = s.name[i];

   () = fprintf (fp, "#function                 ncalls      ms/call  totalselfms    totalsecs Function File\n");
   %                  123456789012345678901234 1234567 1234567890AB 1234567890AB 1234567890AB
   _for i (0, length(rates)-1, 1)
     {
	variable func_file = name[i];
	variable j = is_substr (func_file, ":");
	variable func = substr (func_file, 1, j-1);
	variable file = substr (func_file, j+1, -1);
	() = fprintf (fp, "%-24s %7d %12.4f %12.4f %12.4f %7d %7d %s\n",
		      func,
		      num_called[i], rates[i]*1e3, self_time[i]*1e3,
		      cum_time[i], num_self_f[i], num_self_s[i],
		      file
		     );
     }
}

private define output_title (fp, title)
{
   variable s = "----------------------------------------------------------------";
   variable spaces = "";
   loop ((strlen(s) - strlen(title))/2)
     spaces = strcat (spaces, " ");
   () = fprintf (fp, "\n#%s\n#%s%s\n#%s\n\n", s, spaces, title, s);;
}

define profile_report (file)
{
   if (0 == __is_initialized (&L_Info_Table))
     return;

   variable fp = file;
   if (typeof (file) == String_Type)
     fp = fopen (fp, "w");

   variable f = convert_profile_info_to_array (F_Info_Table);
   if (length (f.cum_time))
     {
	output_title (fp, "Function Call Profile Report");
	profile_report_funcs (fp, f);
     }

   variable s = convert_profile_info_to_array (L_Info_Table);
   if (length (s.cum_time))
     {
	output_title (fp, "Line by Line Profile Report");
	profile_report_lines (fp, s);
     }

   ifnot (_eqs (fp,file))
     () = fclose (fp);
}

provide ("profile");
