function t = gputimeit(f, num_outputs)
%GPUTIMEIT Measures time required to run function on GPU
%   T = GPUTIMEIT(F) measures the typical time (in seconds) required to run
%   the function specified by the function handle F that takes no input
%   argument.
%
%   T = GPUTIMEIT(F,N) calls F with N output arguments.  By default,
%   GPUTIMEIT calls the function F with one output, or no output if the F
%   does not return any output.
%
%   GPUTIMEIT is preferable to TIMEIT for functions that use the GPU,
%   because it makes sure that all operations on the GPU have finished
%   before recording the time and compensates for the overhead time added
%   by this check. For operations that do not use the GPU, use TIMEIT as it
%   will achieve greater precision.
%
%   Limitations
%   -----------
%   The function F should not call tic or toc.
%   It is currently not possible to use tic and toc to measure the
%   execution time of GPUTIMEIT itself.
%
%   Examples
%   --------
%   How much time does it take to compute sum(A.' .* B, 1), where A is
%   12000-by-400 and B is 400-by-12000?
%
%       A = gpuArray.rand(12000, 400);
%       B = gpuArray.rand(400, 12000);
%       f = @() sum(A.' .* B, 1);
%       gputimeit(f)
%
%   How much time does it take to call svd with three output arguments?
%
%       X = gpuArray.rand(1000);
%       f = @() svd(X);
%       gputimeit(f, 3)
%
%   See also: TIMEIT, TIC, TOC, GPUARRAY, parallel.gpu.GPUDevice/wait.
    
%   Copyright 2008-2017 The MathWorks, Inc.

%   For detailed discussion on MATLAB Performance Measurement:
%   https://www.mathworks.com/matlabcentral/fileexchange/18510-matlab-performance-measurement

narginchk(1,2);

% Check f is a function handle that either has 0 inputs or a variable number of
% inputs (which returns -1 for nargin).
if ~isa(f,'function_handle') || ~ismember(nargin(f),[0 -1])
    error(message('parallel:gpu:gputimeit:InvalidFunctionHandle'));
end

if nargin < 2
    num_outputs = min(numOutputs(f), 1);
else
    try
        % Check if num_outputs is a non-negative integer
        validateattributes(num_outputs, {'numeric'}, {'integer','nonnegative'});
    catch
        % Error if num_outputs is a negative number or non-integer
        error(message('parallel:gpu:gputimeit:InvalidNumberOfOutputs'));
    end
end

t = parallel.internal.gpu.timeit(f,  num_outputs);

end

function n = numOutputs(f)
%   Return the number of output arguments to be used when calling the function
%   handle f.  
%   * If nargout(f) > 0, return 1.
%   * If nargout(f) == 0, return 0.
%   * If nargout(f) < 0, use try/catch to determine whether to call f with one
%     or zero output arguments.
%     Note: It is not documented (as of R2008b) that nargout can return -1.
%     However, it appears to do so for functions that use varargout and for
%     anonymous function handles.  

n = nargout(f);
if n < 0
   try
      a = f(); %#ok<NASGU>
      % If the line above doesn't throw an error, then it's OK to call f() with
      % one output argument.
      n = 1;
      
   catch exception
       % Check errorIDS for too many outputs and for scripts
       if any(strcmp(exception.identifier,{'MATLAB:TooManyOutputs','MATLAB:maxlhs','MATLAB:scriptNotAFunction'}))          
           % If we get here, assume it's because f() has zero output arguments.
           n = 0;
       else
           rethrow(exception);
       end
   end
end

end

