Skip to content

File resource_tracker.hpp

File List > base > resource_tracker.hpp

Go to the documentation of this file

/*
 * Copyright 2008 Search Solution Corporation
 * Copyright 2016 CUBRID Corporation
 *
 *  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.
 *
 */

//
// resource_tracker.hpp - interface to track and debug resource usage (allocations, page fixes)
//
// the purpose of this interface is to track resource usage and detect possible issues:
//
//    1. resource leaks (at the end of the scope, used resources are not freed/retired)
//    2. resource over-usage (a resource exceeds a maximum allowed of usage).
//    3. invalid free/retire.
//
// how to use:
//
//    1. specialize your tracker based on resource type:
//
//        using my_tracker_type = cubbase::resource_tracker<my_resource_type>;
//
//        note - my_resource_type must be comparable; resource_tracker uses internally a std::map specialized by
//               provided type and std::less of same type.
//        todo - template for compare function
//
//    2. create an instance of your tracker.
//
//        my_tracker_type my_tracker ("My Tracker", true, MAX_RESOURCES, "my res", MAX_REUSE);
//
//    3. start tracking:
//
//        my_tracker.push_track ();
//
//    4. track resource; resource over-usage may be detected:
//
//        my_tracker.increment (__FILE__, __LINE__, my_res_to_track, use_count /* default 1 */)
//
//    5. un-track resource; resource invalid free/retire may be detected:
//
//        my_tracker.decrement (my_res_to_track, use_count /* default 1 */);
//
//    6. stop tracking; resource leaks may be detected:
//
//        my_tracker.pop_track ();
//

#ifndef _RESOURCE_TRACKER_HPP_
#define _RESOURCE_TRACKER_HPP_

#include "fileline_location.hpp"

#include <map>
#include <forward_list>
#include <sstream>

#include <cassert>

namespace cubbase
{
  //
  // resource_tracker_item - helper structure used by resource tracker to store information one resource.
  //
  struct resource_tracker_item
  {
    public:

      // ctor - file, line, initial reuse count
      resource_tracker_item (const char *fn_arg, int l_arg, unsigned reuse);

      fileline_location m_first_location;   // first increment location
      unsigned m_reuse_count;               // current reuse count
  };
  // printing a resource_tracker_item to output stream
  std::ostream &operator<< (std::ostream &os, const resource_tracker_item &item);

  //
  // resource tracker - see more details on file description
  //
  // Res template is used to allow any resource specialization. It is restricted to comparable types.
  //
  template <typename Res>
  class resource_tracker
  {
    public:

      // internal types
      using res_type = Res;
      using map_type = std::map<res_type, resource_tracker_item>;

      // ctor/dtor
      resource_tracker (const char *name, bool enabled, std::size_t max_resources, const char *res_name,
            unsigned max_reuse = 1);
      ~resource_tracker (void);

      // using resources
      // increment "uses" for given resource.
      // if it is first use, file & line are saved.
      // detects if maximum uses is exceeded.
      void increment (const char *filename, const int line, const res_type &res, unsigned use_count = 1);
      // decrement "uses" for given resource.
      // if use count becomes 0, resource is removed from tracker
      // detects invalid free/retire (existing use count is less than given count)
      void decrement (const res_type &res, unsigned use_count = 1);

      // tracking; is stackable (allows nested tracking).
      // push tracking; a new level of tracking is started
      void push_track (void);
      // pop tracking; finish last level of tacking. resource leaks are checked for last level
      void pop_track (void);
      // clear all tracks.
      void clear_all (void);

    private:

      map_type &get_current_map (void);     // get map for current level of tracking

      void abort (void);                    // abort tracking when maximum number of resources is exceeded
      void dump (void) const;               // dump all tracked resources on all levels
      void dump_map (const map_type &map, std::ostream &out) const;   // dump one tracker level
      unsigned get_total_use_count (void) const;        // compute and return total use count

      const char *m_name;                   // tracker name; used for dumping
      const char *m_res_alias;              // resource alias; used for dumping

      bool m_enabled;                       // if disabled, all tracker functions do nothing.
      // it is used to avoid branching when calling tracker functions.
      bool m_is_aborted;                    // set to true when tracking had to be aborted;
      std::size_t m_max_resources;          // maximum number of resources allowed to track; if exceeded, the tracker
      // is aborted
      std::size_t m_resource_count;         // current resource count
      unsigned m_max_reuse;                 // maximum reuse for a resource. by default is one.

      // a list of maps of tracked resources.
      // each map represents one level in tracker
      // list holds all levels
      std::forward_list<map_type> m_tracked_stack;
  };

  // other functions & stuff

  // functions used mainly to disable debug crashing and convert into an error; for unit testing
  // todo - convert from global to instance

  // pop existing error
  // returns true if error exists and false otherwise. error is reset.
  bool restrack_pop_error (void);
  // if set_error is true, error is set. if set_error is false, error remains as is.
  void restrack_set_error (bool set_error);
  // set assert mode
  // if suppress is true, assert is suppressed and error is set instead; if false, assert is allowed.
  void restrack_set_suppress_assert (bool suppress);
  // get current assert mode
  bool restrack_is_assert_suppressed (void);
  // check condition to be true; based on assert mode, assert is hit or error is set
  inline void restrack_assert (bool cond);

  void restrack_log (const std::string &str);

  // template/inline implementation

  template <typename Res>
  resource_tracker<Res>::resource_tracker (const char *name, bool enabled, std::size_t max_resources,
      const char *res_name, unsigned max_reuse /* = 1 */)
    : m_name (name)
    , m_res_alias (res_name)
    , m_enabled (enabled)
    , m_is_aborted (false)
    , m_max_resources (max_resources)
    , m_resource_count (0)
    , m_max_reuse (max_reuse)
    , m_tracked_stack ()
  {
    //
  }

  template <typename Res>
  resource_tracker<Res>::~resource_tracker (void)
  {
    // check no leaks
    restrack_assert (m_tracked_stack.empty ());
    restrack_assert (m_resource_count == 0);

    clear_all ();
  }

  template <typename Res>
  void
  resource_tracker<Res>::increment (const char *filename, const int line, const res_type &res,
                    unsigned use_count /* = 1 */)
  {
    if (!m_enabled || m_is_aborted)
      {
    return;
      }

    if (m_tracked_stack.empty ())
      {
    // not active
    return;
      }

    // given use_count cannot exceed max reuse
    restrack_assert (use_count <= m_max_reuse);

    // get current level map
    map_type &map = get_current_map ();

    // add resource to map or increment the use count of existing entry
    //
    // std::map::try_emplace returns a pair <it_insert_or_found, did_insert>
    // it_insert_or_found = did_insert ? iterator to inserted : iterator to existing
    // it_insert_or_found is an iterator to pair <res_type, resource_tracker_item>
    //
    // as of c++17 we could just do:
    // auto inserted = map.try_emplace (res, filename, line, use_count);
    //
    // however, for now we have to use next piece of code I got from
    // https://en.cppreference.com/w/cpp/container/map/emplace
    auto inserted = map.emplace (std::piecewise_construct, std::forward_as_tuple (res),
                 std::forward_as_tuple (filename, line, use_count));
    if (inserted.second)
      {
    // did insert
    if (++m_resource_count > m_max_resources)
      {
        // max size reached. abort the tracker
        abort ();
        return;
      }
      }
    else
      {
    // already exists
    // increment use_count
    inserted.first->second.m_reuse_count += use_count;

    restrack_assert (inserted.first->second.m_reuse_count <= m_max_reuse);
      }
  }

  template <typename Res>
  void
  resource_tracker<Res>::decrement (const res_type &res, unsigned use_count /* = 1 */)
  {
    if (!m_enabled || m_is_aborted)
      {
    return;
      }

    if (m_tracked_stack.empty ())
      {
    // not active
    return;
      }

    map_type &map = get_current_map ();

    // note - currently resources from different levels cannot be mixed. one level cannot free resources from another
    //        level... if we want to do that, I don't think the stacking has any use anymore. we can just keep only
    //        one level and that's that.

    // std::map::find returns a pair to <res_type, resource_tracker_item>
    auto tracked = map.find (res);
    if (tracked == map.end ())
      {
    // not found; invalid free
    restrack_assert (false);
      }
    else
      {
    // decrement
    if (use_count > tracked->second.m_reuse_count)
      {
        // more than expected; invalid free
        restrack_assert (false);
        map.erase (tracked);
        m_resource_count--;
      }
    else
      {
        tracked->second.m_reuse_count -= use_count;
        if (tracked->second.m_reuse_count == 0)
          {
        // remove from map
        map.erase (tracked);
        m_resource_count--;
          }
      }
      }
  }

  template <typename Res>
  unsigned
  resource_tracker<Res>::get_total_use_count (void) const
  {
    // iterate through all resources and collect use count
    unsigned total = 0;
    for (auto stack_it : m_tracked_stack)
      {
    for (auto map_it : stack_it)
      {
        total += map_it.second.m_reuse_count;
      }
      }
    return total;
  }

  template <typename Res>
  typename resource_tracker<Res>::map_type &
  resource_tracker<Res>::get_current_map (void)
  {
    return m_tracked_stack.front ();
  }

  template <typename Res>
  void
  resource_tracker<Res>::clear_all (void)
  {
    // clear stack
    while (!m_tracked_stack.empty ())
      {
    pop_track ();
      }
    restrack_assert (m_resource_count == 0);
  }

  template <typename Res>
  void
  resource_tracker<Res>::abort (void)
  {
    m_is_aborted = true;
  }

  template <typename Res>
  void
  resource_tracker<Res>::dump (void) const
  {
    std::stringstream out;

    out << std::endl;
    out << "   +--- " << m_name << std::endl;
    out << "         +--- amount = " << get_total_use_count () << " (threshold = " << m_max_resources << ")" << std::endl;

    std::size_t level = 0;
    for (auto stack_it : m_tracked_stack)
      {
    out << "         +--- stack_level = " << level << std::endl;
    dump_map (stack_it, out);
    level++;
      }

    restrack_log (out.str ());
  }

  template <typename Res>
  void
  resource_tracker<Res>::dump_map (const map_type &map, std::ostream &out) const
  {
    for (auto map_it : map)
      {
    out << "            +--- tracked " << m_res_alias << "=" << map_it.first;
    out << " " << map_it.second;
    out << std::endl;
      }
    out << "            +--- tracked " << m_res_alias << " count = " << map.size () << std::endl;
  }

  template <typename Res>
  void
  resource_tracker<Res>::push_track (void)
  {
    if (!m_enabled)
      {
    return;
      }

    if (m_tracked_stack.empty ())
      {
    // fresh start. set abort as false
    m_resource_count = 0;
    m_is_aborted = false;
      }

    m_tracked_stack.emplace_front ();
  }

  template <typename Res>
  void
  resource_tracker<Res>::pop_track (void)
  {
    if (!m_enabled)
      {
    return;
      }

    if (m_tracked_stack.empty ())
      {
    restrack_assert (false);
    return;
      }

    // get current level map
    map_type &map = m_tracked_stack.front ();
    if (!map.empty ())
      {
    if (!m_is_aborted)
      {
        // resources are leaked
        dump ();
        restrack_assert (false);
      }
    else
      {
        // tracker was aborted; we don't know if there are really leaks
      }
    // remove resources
    m_resource_count -= map.size ();
      }
    // remove level from stack
    m_tracked_stack.pop_front ();

    // if no levels, resource count should be zero
    restrack_assert (!m_tracked_stack.empty () || m_resource_count == 0);
  }

  // other
  void
  restrack_assert (bool cond)
  {
#if !defined (NDEBUG)
    if (restrack_is_assert_suppressed ())
      {
    restrack_set_error (!cond);
      }
    else
      {
    assert (cond);
      }
#endif // NDEBUG
  }

} // namespace cubbase

#endif // _RESOURCE_TRACKER_HPP_