# Compute and gather statistics about garbage collection in this process.
# This module depends on the CPython gc module and garbage collection behavior.

import gc
import logging
import pprint


verbose = False


# A mapping from type objects to a count of instances of those types in the
# garbage collectors all objects list on the previous call to
# _log_garbage_collector_stats().
_previous_obj_type_map = {}


# A set of object ids for everything in the all objects list on the
# previous call to _log_garbage_collector_stats().
_previous_obj_ids = set()


def _log_garbage_collector_stats(minimum_count=10):
    """
    Log statistics about how many of what type of Python object exist in this
    process.

    :param minimum_count: The minimum number of instances of a type for it
            to be considered worthy of logging.
    """
    global _previous_obj_type_map
    global _previous_obj_ids

    # We get all objects -before- creating any new objects within this function.
    # to avoid having our own local instances in the list.
    all_objects = gc.get_objects()
    obj = None
    new_objects = []
    try:
        obj_type_map = {}
        object_ids = set()
        for obj in all_objects:
            obj_type = type(obj)
            obj_type_map.setdefault(obj_type, 0)
            obj_type_map[obj_type] += 1
            object_ids.add(id(obj))
        whats_new_big_str = ''
        if verbose and _previous_obj_ids:
            new_object_ids = object_ids - _previous_obj_ids
            for obj in all_objects:
                if id(obj) in new_object_ids:
                    new_objects.append(obj)
            whats_new_big_str = pprint.pformat(new_objects, indent=1)
    finally:
        # Never keep references to stuff returned by gc.get_objects() around
        # or it'll just make the future cyclic gc runs more difficult.
        del all_objects
        del obj
        del new_objects

    delta = {}
    for obj_type, count in obj_type_map.iteritems():
        if obj_type not in _previous_obj_type_map:
            delta[obj_type] = count
        elif _previous_obj_type_map[obj_type] != count:
            delta[obj_type] = count - _previous_obj_type_map[obj_type]

    sorted_stats = reversed(sorted(
        (count, obj_type) for obj_type, count in obj_type_map.iteritems()))
    sorted_delta = reversed(sorted(
        (count, obj_type) for obj_type, count in delta.iteritems()))

    logging.debug('Garbage collector object type counts:')
    for count, obj_type in sorted_stats:
        if count >= minimum_count:
            logging.debug('  %d\t%s', count, obj_type)

    logging.info('Change in object counts since previous GC stats:')
    for change, obj_type in sorted_delta:
        if obj_type_map[obj_type] > minimum_count:
            logging.info('  %+d\t%s\tto %d', change, obj_type,
                         obj_type_map[obj_type])

    if verbose and whats_new_big_str:
        logging.debug('Pretty printed representation of the new objects:')
        logging.debug(whats_new_big_str)

    _previous_obj_type_map = obj_type_map
    if verbose:
        _previous_obj_ids = object_ids
