/* see License.txt for copyright and terms of use */

#include <assert.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
#undef REGION_PROFILE
#include "libregion/regions.h"
#include "libregion/profile.h"

typedef struct Alloc_info
{
  struct Alloc_info *next;
  char *file;
  int line;
  unsigned long size;
  unsigned long calls;
} *ainfo;

static ainfo ainfos = NULL;
static region profile_region = NULL;

/* perror(s) then exit */
void pfail(const char *s)
{
  perror(s);
  exit(EXIT_FAILURE);
}

/**************************************************************************
 *                                                                        *
 * Log information about an allocation -- generic                         *
 *                                                                        *
 **************************************************************************/

static int registered_exit = 0;

static ainfo find_ainfo(char *file, int line)
{
  ainfo ai;

  for (ai = ainfos; ai; ai = ai->next)
    if (line == ai->line && !strcmp(file, ai->file))
      return ai;

  if (!registered_exit)
    {
      if (atexit(profile))
	fprintf(stderr, "Registration of profile at exit failed\n");
      registered_exit = 1;
    }

  if (!profile_region)
    profile_region = newregion();
  ai = ralloc(profile_region, struct Alloc_info);
  ai->file = file;
  ai->line = line;
  ai->size = 0;
  ai->calls = 0;
  ai->next = ainfos;
  ainfos = ai;
  return ai;
}

/**************************************************************************
 *                                                                        *
 * Log information about an allocation -- GCC                             *
 *                                                                        *
 * WARNING:  This code uses __builtin_return_address, a non-portable      *
 * feature of gcc, to trace the call chain back.   You'll also get ugly   *
 * output unless the addr2line (in GNU binutils) is installed.            *
 *                                                                        *
 * ANOTHER WARNING:  The depths hard-coded in find_cinfo are only correct *
 * if find_cinfo is inlined.  Ack!                                        *
 *                                                                        *
 **************************************************************************/

#define REGION_PROFILE_DEPTH 6
#undef TRACE_STACK
#if defined(__GNUC__) && defined(__OPTIMIZE__) && REGION_PROFILE_DEPTH > 1
#define TRACE_STACK
#endif

#ifdef TRACE_STACK

#if REGION_PROFILE_DEPTH > 6
#error "REGION_PROFILE_DEPTH must be less than 6.  See find_cinfo()."
#endif

typedef struct Call_info
{
  struct Call_info *next;
  void **stack;         /* Array holding the call chain */
  unsigned long size;
  unsigned long calls;
} *cinfo;

static cinfo cinfos = NULL;

/* Find the current call chain and return a pointer to our status for
   it, or allocate a new entry if there is none. */
static cinfo find_cinfo(void)
{
  void *calls[REGION_PROFILE_DEPTH];
  int i;
  cinfo ci;

  /* Compute the call chain.  This is an awful hack. */
  i = 0;
  if (i < REGION_PROFILE_DEPTH)
    {
      if (__builtin_frame_address(2) == 0) goto finish;
      calls[i++] = __builtin_return_address(2);
    }
  if (i < REGION_PROFILE_DEPTH)
    {
      if (__builtin_frame_address(3) == 0) goto finish;
      calls[i++] = __builtin_return_address(3);
    }
  if (i < REGION_PROFILE_DEPTH)
    {
      if (__builtin_frame_address(4) == 0) goto finish;
      calls[i++] = __builtin_return_address(4);
    }
  if (i < REGION_PROFILE_DEPTH)
    {
      if (__builtin_frame_address(5) == 0) goto finish;
      calls[i++] = __builtin_return_address(5);
    }
  if (i < REGION_PROFILE_DEPTH)
    {
      if (__builtin_frame_address(6) == 0) goto finish;
      calls[i++] = __builtin_return_address(6);
    }
  if (i < REGION_PROFILE_DEPTH)
    {
      if (__builtin_frame_address(7) == 0) goto finish;
      calls[i++] = __builtin_return_address(7);
    }
  /* Add more if you want a higher call-depth (why would you?) */
 finish:
  while (i < REGION_PROFILE_DEPTH)
    calls[i++] = "top";

  /* Find it */
  for (ci = cinfos; ci; ci = ci->next)
    if (!memcmp(calls, ci->stack, REGION_PROFILE_DEPTH*sizeof(void *)))
      return ci;

  if (!profile_region)
    profile_region = newregion();
  ci = ralloc(profile_region, struct Call_info);
  ci->stack = rarrayalloc(profile_region, REGION_PROFILE_DEPTH, void *);
  memcpy(ci->stack, calls, REGION_PROFILE_DEPTH*sizeof(void *));
  ci->size = 0;
  ci->calls = 0;
  ci->next = cinfos;
  cinfos = ci;
  return ci;

}
#endif

static void add_alloc(char *file, int line, int size)
{
  ainfo ai = find_ainfo(file, line);
  ai->calls++;
  ai->size += size;
#ifdef TRACE_STACK
  {
    cinfo ci;

    ci = find_cinfo();
    ci->calls++;
    ci->size += size;
  }
#endif
}

/**************************************************************************
 *                                                                        *
 * Intercept and log calls to region library                              *
 *                                                                        *
 **************************************************************************/

void *profile_typed_ralloc(region r, size_t size, type_t type, char *file,
			   int line)
{
  add_alloc(file, line, size);
  return typed_ralloc(r, size, type);
}

void *profile_typed_rarrayalloc(region r, size_t n, size_t size, type_t type,
				char *file, int line)
{
  add_alloc(file, line, n*size);
  return typed_rarrayalloc(r, n, size, type);
}

void *profile_typed_rarrayextend(region r, void *old, size_t n, size_t size,
				 type_t type, char *file, int line)
{
  add_alloc(file, line, n*size); /* XXX: Fix */
  return typed_rarrayextend(r, old, n, size, type);
}

char *profile_rstralloc(region r, size_t size, char *file, int line)
{
  add_alloc(file, line, size);
  return rstralloc(r, size);
}

char *profile_rstralloc0(region r, size_t size, char *file, int line)
{
  add_alloc(file, line, size);
  return rstralloc0(r, size);
}

char *profile_rstrdup(region r, const char *s, char *file, int line)
{
  add_alloc(file, line, strlen(s));
  return rstrdup(r, s);
}

char *profile_rstrextend(region r, const char *old, size_t newsize,
			 char *file, int line)
{
  add_alloc(file, line, newsize); /* XXX: Fix */
  return rstrextend(r, old, newsize);
}

char *profile_rstrextend0(region r, const char *old, size_t newsize,
			  char *file, int line)
{
  add_alloc(file, line, newsize); /* XXX: Fix */
  return rstrextend0(r, old, newsize);
}

void *profile__rcralloc_small0(region r, size_t size, char *file, int line)
{
  add_alloc(file, line, size);
  return __rc_ralloc_small0(r, size);
}

/**************************************************************************
 *                                                                        *
 * Display results -- generic                                             *
 *                                                                        *
 **************************************************************************/

static FILE *out = NULL;

/* Generic list -- used for generic sorting.  Note that next field is
   at the top. */
typedef struct List
{
  struct List *next;
} *list;

/* Sort a list.  cmp should sort in reverse order. */
static list sort_list(list l, int (*cmp)(const void *, const void *))
{
  list cur, result;
  list *sorted;
  int i, length;
  region temp_region;

  /* Compute length of list */
  for (cur = l, length = 0; cur; cur = cur->next, length++);

  temp_region = newregion();
  sorted = rarrayalloc(temp_region, length, list *);
  for (cur = l, i = 0; cur; cur = cur->next)
    sorted[i++] = cur;
  qsort(sorted, length, sizeof(list *), cmp);

  result = NULL;
  for (i = 0; i < length; i++)
    {
      cur = result;
      result = sorted[i];
      result->next = cur;
    }
  deleteregion(temp_region);
  return result;
}


typedef struct File_info
{
  struct File_info *next;
  char *file;
  unsigned long size;
  unsigned long calls;
  unsigned long sites;
} *finfo;

static finfo finfos = NULL;

static int finfo_cmp(const void *a, const void *b)
{
  finfo *afi = (finfo *) a;
  finfo *bfi = (finfo *) b;
  return (*afi)->size - (*bfi)->size;  /* Reverse order */
}

static void print_finfos(void)
{
  finfo fi;
  unsigned long size, sites, calls;

  finfos = (finfo) sort_list((list) finfos, finfo_cmp);
  size = sites = calls = 0;
  fprintf(out, "        Bytes | Sites |    Calls | File\n");
  fprintf(out, "  ------------+-------+----------+---------------------\n");
  for (fi = finfos; fi; fi = fi->next)
    {
      size += fi->size;
      sites += fi->sites;
      calls += fi->calls;
      fprintf(out, " %12lu | %5lu | %8lu | %s\n",
	      fi->size, fi->sites, fi->calls, fi->file);
    }
  fprintf(out, "  ------------+-------+----------+---------------------\n");
    fprintf(out, " %12lu | %5lu | %8lu | Total\n",
	    size, sites, calls);

}

static int ainfo_cmp(const void *a, const void *b)
{
  ainfo *afi = (ainfo *) a;
  ainfo *bfi = (ainfo *) b;
  return (*afi)->size - (*bfi)->size;  /* Reverse order */
}

static void print_ainfos(void)
{
  ainfo ai;

  unsigned long size, calls;

  ainfos = (ainfo) sort_list((list) ainfos, ainfo_cmp);
  size = calls = 0;
  fprintf(out, "        Bytes |    Calls | Site\n");
  fprintf(out, "  ------------+----------+---------------------\n");
  for (ai = ainfos; ai; ai = ai->next)
    {
      size += ai->size;
      calls += ai->calls;
      fprintf(out, " %12lu | %8lu | %s:%d\n",
	      ai->size, ai->calls, ai->file, ai->line);
    }
  fprintf(out, "  ------------+----------+---------------------\n");
    fprintf(out, " %12lu | %8lu | Total\n",
	    size, calls);
}

static finfo find_finfo(char *file)
{
  finfo fi;

  for (fi = finfos; fi; fi = fi->next)
    if (!strcmp(file, fi->file))
      return fi;

  fi = ralloc(profile_region, struct File_info);
  fi->file = file;
  fi->size = 0;
  fi->calls = 0;
  fi->sites = 0;
  fi->next = finfos;
  finfos = fi;
  return fi;
}

static void gather_finfo(void)
{
  ainfo ai;

  for (ai = ainfos; ai; ai = ai->next)
    {
      finfo fi = find_finfo(ai->file);
      fi->size += ai->size;
      fi->calls += ai->calls;
      fi->sites++;
    }
}

/**************************************************************************
 *                                                                        *
 * Display results -- GCC                                                 *
 *                                                                        *
 **************************************************************************/

#ifdef TRACE_STACK

pid_t child_pid = 0;
int child_in[2], child_out[2]; /* pipes to child process */

static void start_prettiness(void)
{
  if (pipe(child_in) || pipe(child_out))
    pfail("Unable to open pipe to child process");
  if (!(child_pid = fork()))
    {
      /* Child process */
      pid_t parent_pid;
      char filename[64];

      if (dup2(child_in[0], STDIN_FILENO) == -1)
	pfail("Unable to open pipe from parent");
      close(child_in[0]);
      close(child_in[1]);
      if (dup2(child_out[1], STDOUT_FILENO) == -1)
	pfail("Unable to open pipe to parent");
      close(child_out[0]);
      close(child_out[1]);

      parent_pid = getppid();
      snprintf(filename, 64, "/proc/%d/exe", parent_pid);
      filename[63] = '\0';
      execlp("addr2line", "addr2line", "-s", "-e", filename, NULL);
      fprintf(stderr, "Unable to fork addr2line\n");
      exit(EXIT_FAILURE);
    }
  else
    {
      close(child_in[0]);
      close(child_out[1]);
    }
}

/* Turn p into a file:line string */
static char *prettify(void *p)
{
#define BUFSIZE 1024
  static char buf[BUFSIZE];
  int size;

  /*printf("To child: %p\n", p);*/
  size = snprintf(buf, BUFSIZE, "%p\n", p);
  write(child_in[1], buf, size);
  size = read(child_out[0], buf, BUFSIZE - 1);
  if (!size)
    pfail("Unable to read from child process");
  buf[size-1] = '\0'; /* Kill \n */
  /*printf("Read: [%s]\n", buf);*/
  return buf;
}

static void end_prettiness(void)
{
  if (child_pid)
    kill(child_pid, SIGHUP);
}

static int cinfo_cmp(const void *a, const void *b)
{
  cinfo *aci = (cinfo *) a;
  cinfo *bci = (cinfo *) b;
  return (*aci)->size - (*bci)->size;  /* Reverse order */
}

/* Print the call chain information out to a file. */
static void print_cinfos(void)
{
  cinfo ci;
  unsigned long size, calls;
  int i;

  cinfos = (cinfo) sort_list((list) cinfos, cinfo_cmp);
  size = calls = 0;
  start_prettiness();
  fprintf(out, "        Bytes |    Calls | Call Stack\n");
  fprintf(out, "  ------------+----------+---------------------\n");
  for (ci = cinfos; ci; ci = ci->next)
    {
      size += ci->size;
      calls += ci->calls;
      fprintf(out, " %12lu | %8lu | ", ci->size, ci->calls);
      for (i = 0; i < REGION_PROFILE_DEPTH; i++)
	fprintf(out, "%s ", prettify(ci->stack[i]));
      fprintf(out, "\n");
    }
  fprintf(out, "  ------------+----------+---------------------\n");
    fprintf(out, " %12lu | %8lu | Total\n",
	    size, calls);
    end_prettiness();
}
#endif


void profile(void)
{
  if (profile_region == NULL)
    return;

  gather_finfo();

  if (!(out = fopen("profile.out", "w")))
    pfail("Unable to open profile.out");

  fprintf(out, "---------------------------\n");
  fprintf(out, "Region Library Memory Usage\n");
  fprintf(out, "---------------------------\n\n");

  print_finfos();
  fprintf(out, "\n");
  print_ainfos();
#ifdef TRACE_STACK
  fprintf(out, "\n");
  print_cinfos();
#endif

  fclose(out);
}
