// Copyright (c) 2013-2020, SIB - Swiss Institute of Bioinformatics and
//                          Biozentrum - University of Basel
// 
// 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.


/// \brief Defines functionality for runtime profiling.
/// All profiling can be completely turned off by setting 
/// PM3_RUNTIME_PROFILING_LEVEL=0

// timing for 1000000 timings
// -> def = time loop where we just do Start/Stop (compiled with -O2)
//    nested = time added to timer doing nested Start/Stop call
//    error = measured time between Start/Stop w/o doing anything
//    lowlevel = level > PM3_RUNTIME_PROFILING_LEVEL
//    disabled = PM3_RUNTIME_PROFILING_LEVEL==0
// - from C++:    Start/Stop: 0.12s , StartScoped: 0.21s
//   -> nested:   Start/Stop: 0.066s, StartScoped: 0.12s
//   -> error:    Start/Stop: 0.047s, StartScoped: 0.077s
//   -> disabled or lowlevel: 0s (with -O2 or more)
// - from Python: Start/Stop: 0.8s , StartScoped: 0.74s
//   -> nested:   Start/Stop: 0.5s , StartScoped: 0.5s
//   -> error:    Start/Stop: 0.32s, StartScoped: 0.18s
//   -> lowlevel: Start/Stop: 0.65s, StartScoped: 0.38s
//   -> disabled: same as lowlevel
// - overheads are mostly from actually measuring the time

#ifndef PROMOD3_RUNTIME_PROFILING_HH
#define PROMOD3_RUNTIME_PROFILING_HH

#include <iostream>
#include <promod3/config.hh>
#include <ost/base.hh>
#include <boost/shared_ptr.hpp>

// define dummies if PM3 profiling disabled
#if PM3_RUNTIME_PROFILING_LEVEL==0 || defined(WIN32)

namespace promod3 { namespace core {

class ScopedTimer { };
typedef boost::shared_ptr<ScopedTimer> ScopedTimerPtr;

// NOTE: only this interface guaranteed to be available
class StaticRuntimeProfiler {
public:
  static void Start(const char* id, int level = 1) { }
  static ScopedTimerPtr StartScoped(const char* id, int level = 1) {
    return ScopedTimerPtr();
  }
  static void Stop(int level = 1) { }
  static void PrintSummary(int max_to_show = -1) { }
  static void Clear() { }
};

}} // ns

#else // PM3_RUNTIME_PROFILING_LEVEL!=0

#include <cstdio>
#include <map>
#include <vector>
#include <utility>
#include <algorithm>
#include <sys/time.h>   // gettimeofday

namespace promod3 { namespace core {

/// \brief A simple stop watch. Internally using gettimeofday().
///
/// Notes on timers (as tested on CentOS 6.7 with gcc 4.7.2):
/// - clock_gettime: ns accuracy BUT needs -lrt (on CentOS 6.7)
/// - gettimeofday:  us accuracy BUT it could get affected by NTP
/// - std::chrono:   all clocks the same as gettimeofday?
/// - clock():       0.01 accuracy
///
/// Usage:
/// Stopwatch s;    // set it up
/// s.GetElapsed(); // get elapsed time in s (here: 0)
/// s.Start();      // start
/// // this will be timed (X1 s)
/// s.Stop();       // stop timing
/// s.GetElapsed(); // get elapsed time in s (here: X1)
/// s.Start();      // resume timing
/// // this will be timed (X2 s)
/// s.Stop();       // stop timing
/// s.GetElapsed(); // get elapsed time in s (here: X1+X2)
/// s.Reset();      // reset elapsed time to 0
class Stopwatch {
public:
  Stopwatch(): elapsed_time_(0) { }
  void Start() {
    gettimeofday(&start_time_, NULL);
  }
  void Stop() {
    struct timeval end_time;
    gettimeofday(&end_time, NULL);
    double diff = double(end_time.tv_sec - start_time_.tv_sec)
                + double(1e-6) * (end_time.tv_usec - start_time_.tv_usec);
    elapsed_time_ += static_cast<Real>(diff);
  }
  void Reset() { elapsed_time_ = 0; }
  Real GetElapsed() const { return elapsed_time_; }
private:
  Real elapsed_time_;
  struct timeval start_time_;
};

/// \brief Simple container for performance measurement of multiple runs
///        for the same ID.
class RuntimeProfilerEntry {
public:
  RuntimeProfilerEntry(): total_elapsed_(0.0), num_runs_(0) { }
  void AddRun(Real elapsed_time) {
    total_elapsed_ += elapsed_time;
    ++num_runs_;
  }
  Real GetTotalElapsed() const { return total_elapsed_; }
  uint GetNumRuns() const { return num_runs_; }
private:
  Real total_elapsed_;
  uint num_runs_;
};

// fw decl. needed for RuntimeProfiler
class ScopedTimer;
typedef boost::shared_ptr<ScopedTimer> ScopedTimerPtr;

/// \brief Keep track of multiple profiled entried (identified by a String).
///
/// Usage:
/// RuntimeProfiler rp;
/// rp.Start("some_stuff");
/// // do some_stuff
/// rp.Start("some_sub_stuff");
/// // do some_sub_stuff (not counted to time spent in some_stuff)
/// rp.Stop();
/// // do more of some_stuff
/// rp.Stop();
/// // show stats of runtimes
/// rp.PrintSummary();
class RuntimeProfiler {
  typedef std::vector< std::pair<Real, String> > SortedList;
public:
  typedef std::map<String, RuntimeProfilerEntry> EntriesMap;
  
  void Start(const String& id) {
    // pause whatever is already running
    if (!watches_.empty()) watches_.back().second.Stop();
    // get entry (create new if needed)
    RuntimeProfilerEntry* my_entry = &profile_entries_[id];
    // start watch
    watches_.push_back(std::make_pair(my_entry, Stopwatch()));
    watches_.back().second.Start();
  }

  ScopedTimerPtr StartScoped(const String& id);

  // note: no check done! Something must have been running!
  void Stop() {
    // stop whatever is running
    Stopwatch& s = watches_.back().second;
    s.Stop();
    // update entry
    watches_.back().first->AddRun(s.GetElapsed());
    // pop stack
    watches_.pop_back();
    // resume whatever was paused
    if (!watches_.empty()) watches_.back().second.Start();
  }

  void PrintSummary(int max_to_show = -1) {
    if (profile_entries_.empty()) return;
    // collect and sort data
    uint max_id_width = 10;
    SortedList sorted_entries;
    Real tot_time = SortMap_(profile_entries_, sorted_entries, max_id_width);
    // display
    String tmp = "ID";
    tmp.resize(max_id_width, ' ');
    std::cout << String(max_id_width + 28, '=') << std::endl;
    std::cout << "RUNTIME PROFILING SUMMARY (total: " << tot_time << "s)\n";
    std::cout << String(max_id_width + 28, '=') << std::endl;
    std::cout << tmp << ": num.runs, tot.(s), percent\n";
    std::cout << String(max_id_width + 28, '-') << std::endl;
    int shown = 0;
    for (SortedList::const_reverse_iterator it = sorted_entries.rbegin();
         it != sorted_entries.rend(); ++it) {
      if (max_to_show < 0 || shown < max_to_show) {
        std::cout << it->second << std::endl;
        ++shown;
      }
    }
    std::cout << String(max_id_width + 28, '-') << std::endl;
    // sum up summary?
    if (shown > 10) {
      // group'em
      EntriesMap grouped_entries;
      for (EntriesMap::const_iterator it = profile_entries_.begin();
         it != profile_entries_.end(); ++it) {
        // extract group
        String::size_type search = it->first.find("::");
        if (search != String::npos) {
          String group = it->first.substr(0, search);
          grouped_entries[group].AddRun(it->second.GetTotalElapsed());
        }
      }
      if (!grouped_entries.empty()) {
        // sort and display
        uint max_id_width = 10;
        SortMap_(grouped_entries, sorted_entries, max_id_width);
        String tmp = "GROUPNAME";
        tmp.resize(max_id_width, ' ');
        std::cout << tmp << ": num.fun., tot.(s), percent\n";
        std::cout << String(max_id_width + 28, '-') << std::endl;
        shown = 0;
        for (SortedList::const_reverse_iterator it = sorted_entries.rbegin();
             it != sorted_entries.rend(); ++it) {
          if (max_to_show < 0 || shown < max_to_show) {
            std::cout << it->second << std::endl;
            ++shown;
          }
        }
        std::cout << String(max_id_width + 28, '-') << std::endl;
      }
    }
  }

  // clear all entries (again: no checks! Assumes all watches stopped)
  void Clear() {
    profile_entries_.clear();
    watches_.clear();
  }

  EntriesMap GetEntries() {
    return profile_entries_;
  }
  // note: no error checks done!
  RuntimeProfilerEntry& GetEntry(const String& id) {
    return profile_entries_[id];
  }
private:
  EntriesMap profile_entries_;
  std::vector< std::pair<RuntimeProfilerEntry*, Stopwatch> > watches_;
  // helper to sort EntriesMap: sets sorted_entries and max_id_width
  Real SortMap_(const EntriesMap& entries, SortedList& sorted_entries,
                uint& max_id_width) {
    sorted_entries.clear();
    // get total measured runtime
    Real total_runtime = 0;
    for (EntriesMap::const_iterator it = entries.begin();
         it != entries.end(); ++it) {
      max_id_width = std::max(max_id_width, uint(it->first.length()));
      total_runtime += it->second.GetTotalElapsed();
    }
    max_id_width = std::min(uint(64), max_id_width);
    // collect and sort data (we just use a map for that)
    for (EntriesMap::const_iterator it = entries.begin();
         it != entries.end(); ++it) {
      // format string
      char buf[128]; // need max_id_width + 28
      String tmp = it->first;
      tmp.resize(max_id_width, ' ');
      Real my_time = it->second.GetTotalElapsed();
      Real my_frac = 100*my_time/total_runtime;
      sprintf(buf, "%s:%9d,%8.2g,%7.2f%%", tmp.c_str(),
              it->second.GetNumRuns(), my_time, my_frac);
      sorted_entries.push_back(std::make_pair(my_frac, String(buf)));
    }
    // sort'em
    std::sort(sorted_entries.begin(), sorted_entries.end());
    return total_runtime;
  }
};

/// \brief Helper for StartScoped
class ScopedTimer {
public:
  ScopedTimer(RuntimeProfiler* rp, const String& id) {
    rp_ = rp;
    rp->Start(id);
  }
  ~ScopedTimer() {
    rp_->Stop();
  }
private:
  RuntimeProfiler* rp_;
};

// circular use RuntimeProfiler<->ScopedTimer, this must be here!
inline ScopedTimerPtr RuntimeProfiler::StartScoped(const String& id) {
  return ScopedTimerPtr(new ScopedTimer(this, id));
}

/// \brief Wrapper for RuntimeProfiler with static access.
/// This makes sure we can remove all profiling stuff at compile-time.
class StaticRuntimeProfiler {
public:
  static void Start(const char* id, int level = 1) {
    if (level <= PM3_RUNTIME_PROFILING_LEVEL) { GetInstance_().Start(id); }
  }
  static ScopedTimerPtr StartScoped(const char* id, int level = 1) {
    if (level <= PM3_RUNTIME_PROFILING_LEVEL) { 
      return GetInstance_().StartScoped(id);
    } else {
      return ScopedTimerPtr();
    }
  }
  static void Stop(int level=1) {
    if (level <= PM3_RUNTIME_PROFILING_LEVEL) { GetInstance_().Stop(); }
  }
  static void PrintSummary(int max_to_show = -1) {
    GetInstance_().PrintSummary(max_to_show);
  }
  static void Clear() {
    GetInstance_().Clear();
  }
private:
  // singleton access to one profiler instance.
  static RuntimeProfiler& GetInstance_() {
    // note: this is guaranteed to be created on first use and destroyed at end
    static RuntimeProfiler prof;
    return prof;
  }
};

}}//ns

#endif // PM3_RUNTIME_PROFILING_LEVEL

#endif // PROMOD3_RUNTIME_PROFILING_HH
