/*****************************************************************************
*  (c) Copyright 1996,1997 Metapath Software Corporation
*
*  Permission to use, copy, modify, and distribute this software and its
*  documentation for any purpose and without fee is hereby granted,
*  provided that the above copyright notice appear in all copies and that
*  both that copyright notice and this permission notice appear in
*  supporting documentation.
*
*  This file is provided AS IS with no warranties of any kind.  The author
*  shall have no liability with respect to the infringement of copyrights,
*  trade secrets or any patents by this file or any part thereof.  In no
*  event will the author be liable for any lost revenue or profits or
*  other special, indirect and consequential damages.
*
*  Author:  David A. Hansen
*  Created: 28-MAR-1996
*
*  Change History:
*  22-JUL-96   D.Hansen    Fixed some bugs and added length check.
*  01-AUG-96   D.Hansen    Added Usage dump on SIGHUP
*  17-JUL-97   D.Hansen    Removed dependencies for use with xlock
*
*  Description:
*     This module replaces the standard malloc/free routines with more
*     enhanced/robust version to aid in catching memory bugs.
*
*****************************************************************************/

/*-
   It's still a little crude, but it works.  So the next thing I need to do is
   add some more refinement.  First on my list is to figure out a way to
   translate the caller's address into something useful, like a symbol.
   Currently you have to be in gdb and use something like:

   gdb> x <addr>

   Oh, and don't forget to link with debugging, LDFLAGS=-g

   To build, I just put memcheck.c in the xlock subdirectory and hand edited

   the Makefile and ../mode/Makefile to include it.  You'll probably want to
   create a debug subdirectory and figure out a way to eloquently get
   configure to build with it.

   Also, there is no comparison/growth detection utility.  Maybe that's
   something you can add.  Basically, you send HUP signals to the process
   every so often and it will dump the memory users and amount they have
   allocated.  After running and HUPing for a while, a script could analyze
   the output and determine which caller addresses are continuing to consume
   memory.

   Also, the method for determining the caller's address is probably specific
   to Intel machines since I use a trick that is based on the way the frame is
   stacked.  It may work on Sun with a little playing around with the
   reference variable, like the last variable in the parameter list instead of
   the first, or maybe by changing the reference to add 1 long word instead of
   subtract 1 long word.  If you look at the variable caller_addr, you'll see
   what I'm talking about.  It all depends on things like whether the stack
   grows up or down, etc.  I would try it first without changing anything and
   use gdb or dbx or whatever to see if the addresses translate into
   appropriate symbols.  In any case, it will always be very machine
   dependent.  Not much one can do about that.

   Finally, what we probably want to do in the long run is create a script
   that runs xlock randomly through all the modes on a given interval and at
   the same interval issues SIGHUPs to the process to take a snapshot of the
   used memory.  Then after letting it iterate through all the modes several
   times, another script can post-process the output looking for memory growth
   and bad memory users.  (gdb> handle SIGHUP print pass nostop) (ifndebug
   around the SIGHUP handler in xlock.c).

   I've also thought about adding another signal catcher to snapshot a stable
   allocation.  In other words, once xlock is started, issue a SIGUSR1 or
   something, and then all subsequent SIGHUPs would print deviations from the
   initial SIGUSR1.  Of course, that would produce a different list for every
   mode, but it would delete the common mallocs done at process
   initialization.  Sometimes it's also hard to tell if a library is doing a
   one time permanent malloc and just reusing it later.  I think MesaGL does
   this. */

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <signal.h>
#include <time.h>

#ifdef ULONG
#undef ULONG
#endif
#define ULONG unsigned long

extern char *ProgramName;
extern pid_t ProgramPID;
extern void *sbrk(int incr);

typedef struct mem_struct {
	struct mem_hdr {
		ULONG       check_mark;
		ULONG       chunk_size;
		ULONG       used_length;
		struct mem_struct *next;
		void       *caller;
	} hdr;

	/* allocate space for the marker at the end of the data */
	/* but return the address of data */
	unsigned char data[2];

} mem_type;


#define USED_MARKER   ((ULONG) 0xBABECAFE)
#define FREE_MARKER   ((ULONG) 0xDEADBEEF)
#define HEAD_MARKER   ((ULONG) 0xFACEF00D)
#define EOD_MARKER    ((ULONG) 0xEC)

#define SBRK_MIN      4096
#define SPLIT_MIN     32

static mem_type *free_head = NULL;
static mem_type *malloc_head = NULL;
static int  reentrancy_check = 0;
static int  first_time = 1;
static FILE *dump_file;
static char dump_fname[256];

static struct sigaction hup_action;
static struct sigaction old_action;

static ULONG total_count;
static ULONG total_size;
static ULONG total_chunk;
static ULONG caller_count;
static ULONG caller_size;
static ULONG caller_chunk;
static time_t hup_time;
static char time_str[256];

/*-------------------------------------------------------------------------*/
static void
message(char *msgstr)
{
	if (dump_file == NULL)
		return;
	(void) time(&hup_time);
	(void) strftime(time_str, sizeof (time_str), "%d-%b-%y %H:%M:%S",
			localtime(&hup_time));
	(void) fprintf(dump_file, "%s - %s (%d): %s\n",
		       time_str, ProgramName, (int) ProgramPID, msgstr);
	(void) fflush(dump_file);
}				/* message */


/*-------------------------------------------------------------------------*/
static void
hup_handler(int interrupt)
{
	mem_type   *t1;
	mem_type   *t2;

	if (dump_file == NULL)
		return;
	if (reentrancy_check > 1) {
		message("Memory allocation list is currently unaccessible");
		return;
	}
	message("malloc/free usage dump:");

	(void) fprintf(dump_file,
		       "Dumping on interrupt %d.\n", interrupt);
	(void) fprintf(dump_file,
		       "=================================================\n");
	(void) fprintf(dump_file,
		       "Caller      |   Number  |    Size   |    Heap   |\n");
	(void) fprintf(dump_file,
		       "------------|-----------|-----------|-----------|\n");
	total_count = 0;
	total_size = 0;
	total_chunk = 0;
	for (t1 = malloc_head; t1; t1 = t1->hdr.next) {
		total_count++;
		total_size += t1->hdr.used_length;
		total_chunk += t1->hdr.chunk_size;
		if (t1->hdr.check_mark == HEAD_MARKER) {
			t1->hdr.check_mark = USED_MARKER;
			continue;
		}
		caller_count = 1;
		caller_size = t1->hdr.used_length;
		caller_chunk = t1->hdr.chunk_size;
		for (t2 = t1->hdr.next; t2; t2 = t2->hdr.next) {
			if (t2->hdr.caller == t1->hdr.caller) {
				t2->hdr.check_mark = HEAD_MARKER;
				caller_count++;
				caller_size += t2->hdr.used_length;
				caller_chunk += t2->hdr.chunk_size;
			}
		}
		(void) fprintf(dump_file, "0x%08lX: |%10lu |%10lu |%10lu |\n",
			       (ULONG) t1->hdr.caller,
			       caller_count, caller_size, caller_chunk);
	}
	(void) fprintf(dump_file,
		       "------------|-----------|-----------|-----------|\n");
	(void) fprintf(dump_file, "totals:     |%10lu |%10lu |%10lu |\n\n",
		       total_count, total_size, total_chunk);
	(void) fflush(dump_file);
}				/* hup_handler */



/*-------------------------------------------------------------------------*/
static void *
allocate_memory(ULONG length, void *caller_addr)
{
	mem_type   *temp;
	mem_type   *cnew;
	mem_type   *last;
	ULONG       req_size;
	ULONG       incr;

	if (first_time) {
		/* install SIGHUP handler to dump usage info */
		first_time = 0;
		(void) sprintf(dump_fname,
			     "memdiag.%s-%d", ProgramName, (int) ProgramPID);
		dump_file = fopen(dump_fname, "a");

		message("malloc/free diagnostics started");

		(void) sigaction(SIGHUP, NULL, &old_action);
		if (old_action.sa_handler == SIG_DFL) {
			hup_action.sa_handler = hup_handler;
#ifdef _INCLUDE_HPUX_SOURCE
			hup_action.sa_flags = SA_RESETHAND;	/* Just getting it to compile */
#else
			hup_action.sa_flags = SA_RESTART;
#endif
			(void) sigaction(SIGHUP, &hup_action, NULL);
			message("Installed SIGHUP handler for usage dump");
		} else {
			message("Another SIGHUP handler already installed");
		}
	}
	if (++reentrancy_check > 1) {
		message("MALLOC - reentrancy detected");
		*(ULONG *) 1 = 1L;
	}
	/* round length up to next long word boundary */
	req_size = (length + 3) & ~3;

	/* add in the mem_type overhead */
	req_size += sizeof (mem_type);

	/* check the current list of free space */
	last = NULL;
	for (temp = free_head; temp != NULL; temp = temp->hdr.next) {
		if (temp->hdr.check_mark != FREE_MARKER) {
			message("MALLOC - corrupt free list");
			*(ULONG *) 1 = 1L;
		}
		if (temp->hdr.chunk_size >= req_size)
			break;
		last = temp;
	}

	/* no free space large enough, lets sbrk some more */
	if (temp == NULL) {
		/* round up to the next page boundary */
		incr = (req_size + SBRK_MIN) & ~SBRK_MIN;
		temp = (mem_type *) sbrk(incr);
		if (temp == NULL) {
			message("MALLOC - no memory available");
			*(ULONG *) 1 = 1L;
			return;
		}
		temp->hdr.check_mark = FREE_MARKER;
		temp->hdr.chunk_size = incr;
		temp->hdr.caller = NULL;
		temp->hdr.next = NULL;
	}
	/* if space is large enough to split */
	if ((temp->hdr.chunk_size - req_size) > SPLIT_MIN) {
		cnew = (mem_type *) ((char *) temp + req_size);
		cnew->hdr.check_mark = FREE_MARKER;
		cnew->hdr.chunk_size = temp->hdr.chunk_size - req_size;
		cnew->hdr.caller = NULL;
		cnew->hdr.next = temp->hdr.next;
		temp->hdr.next = cnew;
		temp->hdr.chunk_size = req_size;
	}
	/* remove block from the free list */
	if (last == NULL)
		free_head = temp->hdr.next;
	else
		last->hdr.next = temp->hdr.next;

	/* add block to the malloc list */
	temp->hdr.next = malloc_head;
	malloc_head = temp;

	temp->hdr.caller = caller_addr;
	temp->hdr.check_mark = USED_MARKER;
	temp->hdr.used_length = length;
	temp->data[length] = EOD_MARKER;

	reentrancy_check--;

	return ((void *) temp->data);

}				/* allocate_memory */



/*-------------------------------------------------------------------------*/
void       *
malloc(ULONG length)
{
	void       *caller_addr = (void *) *((ULONG *) & length - 1);

	return (allocate_memory(length, caller_addr));

}				/* malloc */



/*-------------------------------------------------------------------------*/
void
free(void *ptr)
{
	mem_type   *cur;
	mem_type   *temp;
	mem_type   *last;

	/* Don't try to free null */
	if (ptr == NULL) {
		message("FREE - NULL pointer");
		*(ULONG *) 1 = 1L;
	}
	if (++reentrancy_check > 1) {
		message("FREE - reentrancy detected");
		*(ULONG *) 1 = 1L;
	}
	/* subtract off mem_type header */
	cur = (mem_type *) ((char *) ptr - sizeof (struct mem_hdr));

	/* check data length integrity */
	if (cur->data[cur->hdr.used_length] != EOD_MARKER) {
		message("FREE - end of data corrupted");
		*(ULONG *) 1 = 1L;
	}
	/* find the current memory in the malloc list */
	last = NULL;
	for (temp = malloc_head; temp != NULL; temp = temp->hdr.next) {
		if (temp->hdr.check_mark != USED_MARKER) {
			message("FREE - corrupt malloc list");
			*(ULONG *) 1 = 1L;
		}
		if (temp == cur)
			break;
		last = temp;
	}

	if (temp == NULL) {
		message("FREE - pointer not found");
		*(ULONG *) 1 = 1L;
	}
	/* remove block from the malloc list */
	if (last == NULL)
		malloc_head = temp->hdr.next;
	else
		last->hdr.next = temp->hdr.next;

	cur->hdr.check_mark = FREE_MARKER;

	/* add block by insertion sort to the free list */
	last = NULL;
	for (temp = free_head; temp != NULL; temp = temp->hdr.next) {
		if (temp > cur)
			break;
		last = temp;
	}
	cur->hdr.next = temp;
	if (last == NULL)
		free_head = cur;
	else
		last->hdr.next = cur;

	/* do garbage collection */

	/* forward chunk reconciliation */
	if (cur->hdr.next != NULL) {
		temp = (mem_type *) ((char *) cur + cur->hdr.chunk_size);
		if (temp == cur->hdr.next) {
			cur->hdr.next = temp->hdr.next;
			cur->hdr.chunk_size += temp->hdr.chunk_size;
			temp->hdr.check_mark = 0L;
			temp->hdr.next = NULL;
		}
	}
	/* reverse chunk reconciliation */
	if (last != NULL) {
		temp = (mem_type *) ((char *) last + last->hdr.chunk_size);
		if (temp == cur) {
			last->hdr.next = temp->hdr.next;
			last->hdr.chunk_size += temp->hdr.chunk_size;
			temp->hdr.check_mark = 0L;
			temp->hdr.next = NULL;
		}
	}
	reentrancy_check--;

}				/* free */



/*-------------------------------------------------------------------------*/
void       *
calloc(ULONG nelem, ULONG length)
{
	void       *caller_addr = (void *) *((ULONG *) & nelem - 1);
	register void *temp;

	length *= nelem;
	temp = allocate_memory(length, caller_addr);
	(void) memset(temp, 0, length);
	return (temp);

}				/* calloc */



/*-------------------------------------------------------------------------*/
void       *
realloc(void *ptr, ULONG new_length)
{
	void       *caller_addr = (void *) *((ULONG *) & ptr - 1);
	mem_type   *temp;
	ULONG       alloc_length;
	void       *cnew;

	if (new_length == 0) {
		if (ptr)
			free(ptr);
		return (NULL);
	}
	if (ptr == NULL)
		return (allocate_memory(new_length, caller_addr));

	temp = (mem_type *) ((char *) ptr - sizeof (struct mem_hdr));

	if (temp->hdr.check_mark != USED_MARKER) {
		message("REALLOC - corrupt malloc list");
		*(ULONG *) 1 = 1L;
	}
	alloc_length = temp->hdr.chunk_size - sizeof (mem_type);

	if (new_length <= alloc_length) {
		/* check data length integrity */
		if (temp->data[temp->hdr.used_length] != EOD_MARKER) {
			message("FREE - end of data corrupted");
			*(ULONG *) 1 = 1L;
		}
		/* update the info */
		temp->hdr.used_length = new_length;
		temp->data[new_length] = EOD_MARKER;
	} else {
		/* we need a new chunk */
		cnew = allocate_memory(new_length, caller_addr);
		(void) memcpy(cnew, ptr, temp->hdr.used_length);
		free(ptr);
		ptr = cnew;
	}

	return (ptr);

}				/* realloc */
