/*-------------------------------------------------------------------------
 *
 * freespace.c
 *	  POSTGRES free space map for quickly finding free space in relations
 *
 *
 * Portions Copyright (c) 1996-2002, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * IDENTIFICATION
 *	  $Header: /cvsroot/pgsql/src/backend/storage/freespace/freespace.c,v 1.12 2002/06/20 20:29:34 momjian Exp $
 *
 *
 * NOTES:
 *
 * The only really interesting aspect of this code is the heuristics for
 * deciding how much information we can afford to keep about each relation,
 * given that we have a limited amount of workspace in shared memory.
 * These currently work as follows:
 *
 * The number of distinct relations tracked is limited by a configuration
 * variable (MaxFSMRelations).	When this would be exceeded, we discard the
 * least frequently used relation.	A previously-unknown relation is always
 * entered into the map with useCount 1 on first reference, even if this
 * causes an existing entry with higher useCount to be discarded.  This may
 * cause a little bit of thrashing among the bottom entries in the list,
 * but if we didn't do it then there'd be no way for a relation not in the
 * map to get in once the map is full.	Note we allow a relation to be in the
 * map even if no pages are currently stored for it: this allows us to track
 * its useCount & threshold, which may eventually go high enough to give it
 * priority for page storage.
 *
 * The total number of pages tracked is also set by a configuration variable
 * (MaxFSMPages).  We allocate these dynamically among the known relations,
 * giving preference to the more-frequently-referenced relations and those
 * with smaller tuples.  This is done by a thresholding mechanism: for each
 * relation we keep track of a current target threshold, and allow only pages
 * with free space >= threshold to be stored in the map.  The threshold starts
 * at BLCKSZ/2 (a somewhat arbitrary choice) for a newly entered relation.
 * On each GetFreeSpace reference, the threshold is moved towards the
 * request size using a slow exponential moving average; this means that
 * for heavily used relations, the threshold will approach the average
 * freespace request size (average tuple size).  Whenever we run out of
 * storage space in the map, we double the threshold of all existing relations
 * (but not to more than BLCKSZ, so as to prevent runaway thresholds).
 * Infrequently used relations will thus tend to have large thresholds.
 *
 * XXX this thresholding mechanism is experimental; need to see how well
 * it works in practice.
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include <limits.h>

#include "storage/freespace.h"
#include "storage/itemid.h"
#include "storage/lwlock.h"
#include "storage/shmem.h"


/*
 * Shared free-space-map objects
 *
 * Note: we handle pointers to these items as pointers, not as SHMEM_OFFSETs.
 * This assumes that all processes accessing the map will have the shared
 * memory segment mapped at the same place in their address space.
 */
typedef struct FSMHeader FSMHeader;
typedef struct FSMRelation FSMRelation;
typedef struct FSMChunk FSMChunk;

/* Header for whole map */
struct FSMHeader
{
	HTAB	   *relHash;		/* hashtable of FSMRelation entries */
	FSMRelation *relList;		/* FSMRelations in useCount order */
	FSMRelation *relListTail;	/* tail of FSMRelation list */
	int			numRels;		/* number of FSMRelations now in use */
	FSMChunk   *freeChunks;		/* linked list of currently-free chunks */
	int			numFreeChunks;	/* number of free chunks */
};

/*
 * Per-relation struct --- this is an entry in the shared hash table.
 * The hash key is the RelFileNode value (hence, we look at the physical
 * relation ID, not the logical ID, which is appropriate).
 */
struct FSMRelation
{
	RelFileNode key;			/* hash key (must be first) */
	FSMRelation *nextRel;		/* next rel in useCount order */
	FSMRelation *priorRel;		/* prior rel in useCount order */
	int			useCount;		/* use count for prioritizing rels */
	Size		threshold;		/* minimum amount of free space to keep */
	int			nextPage;		/* index (from 0) to start next search at */
	int			numPages;		/* total number of pages we have info
								 * about */
	int			numChunks;		/* number of FSMChunks allocated to rel */
	FSMChunk   *relChunks;		/* linked list of page info chunks */
};

/*
 * Info about individual pages in a relation is stored in chunks to reduce
 * allocation overhead.  Note that we allow any chunk of a relation's list
 * to be partly full, not only the last chunk; this speeds deletion of
 * individual page entries.  When the total number of unused slots reaches
 * CHUNKPAGES, we compact out the unused slots so that we can return a chunk
 * to the freelist; but there's no point in doing the compaction before that.
 */

#define CHUNKPAGES	32			/* each chunk can store this many pages */

struct FSMChunk
{
	FSMChunk   *next;			/* linked-list link */
	int			numPages;		/* number of pages described here */
	BlockNumber pages[CHUNKPAGES];		/* page numbers within relation */
	ItemLength	bytes[CHUNKPAGES];		/* free space available on each
										 * page */
};


int			MaxFSMRelations;	/* these are set by guc.c */
int			MaxFSMPages;

static FSMHeader *FreeSpaceMap; /* points to FSMHeader in shared memory */


static FSMRelation *lookup_fsm_rel(RelFileNode *rel);
static FSMRelation *create_fsm_rel(RelFileNode *rel);
static void delete_fsm_rel(FSMRelation *fsmrel);
static void link_fsm_rel_after(FSMRelation *fsmrel, FSMRelation *oldrel);
static void unlink_fsm_rel(FSMRelation *fsmrel);
static void free_chunk_chain(FSMChunk *fchunk);
static BlockNumber find_free_space(FSMRelation *fsmrel, Size spaceNeeded);
static void fsm_record_free_space(FSMRelation *fsmrel, BlockNumber page,
					  Size spaceAvail);
static bool lookup_fsm_page_entry(FSMRelation *fsmrel, BlockNumber page,
					  FSMChunk **outChunk, int *outChunkRelIndex);
static bool insert_fsm_page_entry(FSMRelation *fsmrel,
					  BlockNumber page, Size spaceAvail,
					  FSMChunk *chunk, int chunkRelIndex);
static bool push_fsm_page_entry(BlockNumber page, Size spaceAvail,
					FSMChunk *chunk, int chunkRelIndex);
static void delete_fsm_page_entry(FSMRelation *fsmrel, FSMChunk *chunk,
					  int chunkRelIndex);
static void compact_fsm_page_list(FSMRelation *fsmrel);
static void acquire_fsm_free_space(void);


/*
 * Exported routines
 */


/*
 * InitFreeSpaceMap -- Initialize the freespace module.
 *
 * This must be called once during shared memory initialization.
 * It builds the empty free space map table.  FreeSpaceLock must also be
 * initialized at some point, but is not touched here --- we assume there is
 * no need for locking, since only the calling process can be accessing shared
 * memory as yet.
 */
void
InitFreeSpaceMap(void)
{
	HASHCTL		info;
	FSMChunk   *chunks,
			   *prevchunk;
	int			nchunks;

	/* Create table header */
	FreeSpaceMap = (FSMHeader *) ShmemAlloc(sizeof(FSMHeader));
	if (FreeSpaceMap == NULL)
		elog(FATAL, "Insufficient shared memory for free space map");
	MemSet(FreeSpaceMap, 0, sizeof(FSMHeader));

	/* Create hashtable for FSMRelations */
	info.keysize = sizeof(RelFileNode);
	info.entrysize = sizeof(FSMRelation);
	info.hash = tag_hash;

	FreeSpaceMap->relHash = ShmemInitHash("Free Space Map Hash",
										  MaxFSMRelations / 10,
										  MaxFSMRelations,
										  &info,
										  (HASH_ELEM | HASH_FUNCTION));

	if (!FreeSpaceMap->relHash)
		elog(FATAL, "Insufficient shared memory for free space map");

	/* Allocate FSMChunks and fill up the free-chunks list */
	nchunks = (MaxFSMPages - 1) / CHUNKPAGES + 1;
	FreeSpaceMap->numFreeChunks = nchunks;

	chunks = (FSMChunk *) ShmemAlloc(nchunks * sizeof(FSMChunk));
	if (chunks == NULL)
		elog(FATAL, "Insufficient shared memory for free space map");

	prevchunk = NULL;
	while (nchunks-- > 0)
	{
		chunks->next = prevchunk;
		prevchunk = chunks;
		chunks++;
	}
	FreeSpaceMap->freeChunks = prevchunk;
}

/*
 * Estimate amount of shmem space needed for FSM.
 */
int
FreeSpaceShmemSize(void)
{
	int			size;
	int			nchunks;

	/* table header */
	size = MAXALIGN(sizeof(FSMHeader));

	/* hash table, including the FSMRelation objects */
	size += hash_estimate_size(MaxFSMRelations, sizeof(FSMRelation));

	/* FSMChunk objects */
	nchunks = (MaxFSMPages - 1) / CHUNKPAGES + 1;

	size += MAXALIGN(nchunks * sizeof(FSMChunk));

	return size;
}

/*
 * GetPageWithFreeSpace - try to find a page in the given relation with
 *		at least the specified amount of free space.
 *
 * If successful, return the block number; if not, return InvalidBlockNumber.
 *
 * The caller must be prepared for the possibility that the returned page
 * will turn out to have too little space available by the time the caller
 * gets a lock on it.  In that case, the caller should report the actual
 * amount of free space available on that page (via RecordFreeSpace) and
 * then try again.	If InvalidBlockNumber is returned, extend the relation.
 */
BlockNumber
GetPageWithFreeSpace(RelFileNode *rel, Size spaceNeeded)
{
	FSMRelation *fsmrel;
	BlockNumber freepage;

	LWLockAcquire(FreeSpaceLock, LW_EXCLUSIVE);

	/*
	 * We always add a rel to the hashtable when it is inquired about.
	 */
	fsmrel = create_fsm_rel(rel);

	/*
	 * Adjust the threshold towards the space request.	This essentially
	 * implements an exponential moving average with an equivalent period
	 * of about 63 requests.  Ignore silly requests, however, to ensure
	 * that the average stays in bounds.
	 *
	 * In theory, if the threshold increases here we should immediately
	 * delete any pages that fall below the new threshold.	In practice it
	 * seems OK to wait until we have a need to compact space.
	 */
	if (spaceNeeded > 0 && spaceNeeded < BLCKSZ)
	{
		int			cur_avg = (int) fsmrel->threshold;

		cur_avg += ((int) spaceNeeded - cur_avg) / 32;
		fsmrel->threshold = (Size) cur_avg;
	}
	freepage = find_free_space(fsmrel, spaceNeeded);
	LWLockRelease(FreeSpaceLock);
	return freepage;
}

/*
 * RecordFreeSpace - record the amount of free space available on a page.
 *
 * The FSM is at liberty to forget about the page instead, if the amount of
 * free space is too small to be interesting.  The only guarantee is that
 * a subsequent request to get a page with more than spaceAvail space free
 * will not return this page.
 */
void
RecordFreeSpace(RelFileNode *rel, BlockNumber page, Size spaceAvail)
{
	FSMRelation *fsmrel;

	/* Sanity check: ensure spaceAvail will fit into ItemLength */
	AssertArg(spaceAvail < BLCKSZ);

	LWLockAcquire(FreeSpaceLock, LW_EXCLUSIVE);

	/*
	 * We choose not to add rels to the hashtable unless they've been
	 * inquired about with GetPageWithFreeSpace.  Also, a Record operation
	 * does not increase the useCount or change threshold, only Get does.
	 */
	fsmrel = lookup_fsm_rel(rel);
	if (fsmrel)
		fsm_record_free_space(fsmrel, page, spaceAvail);
	LWLockRelease(FreeSpaceLock);
}

/*
 * RecordAndGetPageWithFreeSpace - combo form to save one lock and
 *		hash table lookup cycle.
 */
BlockNumber
RecordAndGetPageWithFreeSpace(RelFileNode *rel,
							  BlockNumber oldPage,
							  Size oldSpaceAvail,
							  Size spaceNeeded)
{
	FSMRelation *fsmrel;
	BlockNumber freepage;

	/* Sanity check: ensure spaceAvail will fit into ItemLength */
	AssertArg(oldSpaceAvail < BLCKSZ);

	LWLockAcquire(FreeSpaceLock, LW_EXCLUSIVE);

	/*
	 * We always add a rel to the hashtable when it is inquired about.
	 */
	fsmrel = create_fsm_rel(rel);

	/*
	 * Adjust the threshold towards the space request, same as in
	 * GetPageWithFreeSpace.
	 *
	 * Note that we do this before storing data for oldPage, which means this
	 * isn't exactly equivalent to Record followed by Get; but it seems
	 * appropriate to adjust the threshold first.
	 */
	if (spaceNeeded > 0 && spaceNeeded < BLCKSZ)
	{
		int			cur_avg = (int) fsmrel->threshold;

		cur_avg += ((int) spaceNeeded - cur_avg) / 32;
		fsmrel->threshold = (Size) cur_avg;
	}
	/* Do the Record */
	fsm_record_free_space(fsmrel, oldPage, oldSpaceAvail);
	/* Do the Get */
	freepage = find_free_space(fsmrel, spaceNeeded);
	LWLockRelease(FreeSpaceLock);
	return freepage;
}

/*
 * MultiRecordFreeSpace - record available-space info about multiple pages
 *		of a relation in one call.
 *
 * First, if minPage <= maxPage, the FSM must discard any entries it has for
 * pages in that page number range (inclusive).  This allows obsolete info
 * to be discarded.  Second, if nPages > 0, record the page numbers and free
 * space amounts in the given arrays.  As with RecordFreeSpace, the FSM is at
 * liberty to discard some of the information.	However, it *must* discard
 * previously stored info in the minPage..maxPage range (for example, this
 * case is used to remove info about deleted pages during relation truncation).
 */
void
MultiRecordFreeSpace(RelFileNode *rel,
					 BlockNumber minPage,
					 BlockNumber maxPage,
					 int nPages,
					 BlockNumber *pages,
					 Size *spaceAvail)
{
	FSMRelation *fsmrel;
	int			i;

	LWLockAcquire(FreeSpaceLock, LW_EXCLUSIVE);
	fsmrel = lookup_fsm_rel(rel);
	if (fsmrel)
	{
		/*
		 * Remove entries in specified range
		 */
		if (minPage <= maxPage)
		{
			FSMChunk   *chunk;
			int			chunkRelIndex;
			bool		done;

			/* Use lookup to locate first entry >= minPage */
			lookup_fsm_page_entry(fsmrel, minPage, &chunk, &chunkRelIndex);
			/* Set free space to 0 for each page within range */
			done = false;
			while (chunk && !done)
			{
				int			numPages = chunk->numPages;

				for (; chunkRelIndex < numPages; chunkRelIndex++)
				{
					if (chunk->pages[chunkRelIndex] > maxPage)
					{
						done = true;
						break;
					}
					chunk->bytes[chunkRelIndex] = 0;
				}
				chunk = chunk->next;
				chunkRelIndex = 0;
			}
			/* Now compact out the zeroed entries */
			compact_fsm_page_list(fsmrel);
		}

		/*
		 * Add new entries, if appropriate.
		 *
		 * XXX we could probably be smarter about this than doing it
		 * completely separately for each one.	FIXME later.
		 *
		 * One thing we can do is short-circuit the process entirely if a
		 * page (a) has too little free space to be recorded, and (b) is
		 * within the minPage..maxPage range --- then we deleted any old
		 * entry above, and we aren't going to make a new one. This is
		 * particularly useful since in most cases, all the passed pages
		 * will in fact be in the minPage..maxPage range.
		 */
		for (i = 0; i < nPages; i++)
		{
			BlockNumber page = pages[i];
			Size		avail = spaceAvail[i];

			if (avail >= fsmrel->threshold ||
				page < minPage || page > maxPage)
				fsm_record_free_space(fsmrel, page, avail);
		}
	}
	LWLockRelease(FreeSpaceLock);
}

/*
 * FreeSpaceMapForgetRel - forget all about a relation.
 *
 * This is called when a relation is deleted.  Although we could just let
 * the rel age out of the map, it's better to reclaim and reuse the space
 * sooner.
 */
void
FreeSpaceMapForgetRel(RelFileNode *rel)
{
	FSMRelation *fsmrel;

	LWLockAcquire(FreeSpaceLock, LW_EXCLUSIVE);
	fsmrel = lookup_fsm_rel(rel);
	if (fsmrel)
		delete_fsm_rel(fsmrel);
	LWLockRelease(FreeSpaceLock);
}

/*
 * FreeSpaceMapForgetDatabase - forget all relations of a database.
 *
 * This is called during DROP DATABASE.  As above, might as well reclaim
 * map space sooner instead of later.
 *
 * XXX when we implement tablespaces, target Oid will need to be tablespace
 * ID not database ID.
 */
void
FreeSpaceMapForgetDatabase(Oid dbid)
{
	FSMRelation *fsmrel,
			   *nextrel;

	LWLockAcquire(FreeSpaceLock, LW_EXCLUSIVE);
	for (fsmrel = FreeSpaceMap->relList; fsmrel; fsmrel = nextrel)
	{
		nextrel = fsmrel->nextRel;		/* in case we delete it */
		if (fsmrel->key.tblNode == dbid)
			delete_fsm_rel(fsmrel);
	}
	LWLockRelease(FreeSpaceLock);
}


/*
 * Internal routines.  These all assume the caller holds the FreeSpaceLock.
 */

/*
 * Lookup a relation in the hash table.  If not present, return NULL.
 * The relation's useCount is not changed.
 */
static FSMRelation *
lookup_fsm_rel(RelFileNode *rel)
{
	FSMRelation *fsmrel;

	fsmrel = (FSMRelation *) hash_search(FreeSpaceMap->relHash,
										 (void *) rel,
										 HASH_FIND,
										 NULL);
	if (!fsmrel)
		return NULL;

	return fsmrel;
}

/*
 * Lookup a relation in the hash table, creating an entry if not present.
 *
 * On successful lookup, the relation's useCount is incremented, possibly
 * causing it to move up in the priority list.
 */
static FSMRelation *
create_fsm_rel(RelFileNode *rel)
{
	FSMRelation *fsmrel,
			   *oldrel;
	bool		found;

	fsmrel = (FSMRelation *) hash_search(FreeSpaceMap->relHash,
										 (void *) rel,
										 HASH_ENTER,
										 &found);
	if (!fsmrel)
		elog(ERROR, "FreeSpaceMap hashtable out of memory");

	if (!found)
	{
		/* New hashtable entry, initialize it (hash_search set the key) */
		fsmrel->useCount = 1;
		fsmrel->threshold = BLCKSZ / 2; /* starting point for new entry */
		fsmrel->nextPage = 0;
		fsmrel->numPages = 0;
		fsmrel->numChunks = 0;
		fsmrel->relChunks = NULL;
		/* Discard lowest-priority existing rel, if we are over limit */
		if (FreeSpaceMap->numRels >= MaxFSMRelations)
			delete_fsm_rel(FreeSpaceMap->relListTail);

		/*
		 * Add new entry in front of any others with useCount 1 (since it
		 * is more recently used than them).
		 */
		oldrel = FreeSpaceMap->relListTail;
		while (oldrel && oldrel->useCount <= 1)
			oldrel = oldrel->priorRel;
		link_fsm_rel_after(fsmrel, oldrel);
		FreeSpaceMap->numRels++;
	}
	else
	{
		int			myCount;

		/* Existing entry, advance its useCount */
		if (++(fsmrel->useCount) >= INT_MAX / 2)
		{
			/* When useCounts threaten to overflow, reduce 'em all 2X */
			for (oldrel = FreeSpaceMap->relList;
				 oldrel != NULL;
				 oldrel = oldrel->nextRel)
				oldrel->useCount >>= 1;
		}
		/* If warranted, move it up the priority list */
		oldrel = fsmrel->priorRel;
		myCount = fsmrel->useCount;
		if (oldrel && oldrel->useCount <= myCount)
		{
			unlink_fsm_rel(fsmrel);
			while (oldrel && oldrel->useCount <= myCount)
				oldrel = oldrel->priorRel;
			link_fsm_rel_after(fsmrel, oldrel);
		}
	}

	return fsmrel;
}

/*
 * Remove an existing FSMRelation entry.
 */
static void
delete_fsm_rel(FSMRelation *fsmrel)
{
	FSMRelation *result;

	free_chunk_chain(fsmrel->relChunks);
	unlink_fsm_rel(fsmrel);
	FreeSpaceMap->numRels--;
	result = (FSMRelation *) hash_search(FreeSpaceMap->relHash,
										 (void *) &(fsmrel->key),
										 HASH_REMOVE,
										 NULL);
	if (!result)
		elog(ERROR, "FreeSpaceMap hashtable corrupted");
}

/*
 * Link a FSMRelation into the priority list just after the given existing
 * entry (or at the head of the list, if oldrel is NULL).
 */
static void
link_fsm_rel_after(FSMRelation *fsmrel, FSMRelation *oldrel)
{
	if (oldrel == NULL)
	{
		/* insert at head */
		fsmrel->priorRel = NULL;
		fsmrel->nextRel = FreeSpaceMap->relList;
		FreeSpaceMap->relList = fsmrel;
		if (fsmrel->nextRel != NULL)
			fsmrel->nextRel->priorRel = fsmrel;
		else
			FreeSpaceMap->relListTail = fsmrel;
	}
	else
	{
		/* insert after oldrel */
		fsmrel->priorRel = oldrel;
		fsmrel->nextRel = oldrel->nextRel;
		oldrel->nextRel = fsmrel;
		if (fsmrel->nextRel != NULL)
			fsmrel->nextRel->priorRel = fsmrel;
		else
			FreeSpaceMap->relListTail = fsmrel;
	}
}

/*
 * Delink a FSMRelation from the priority list.
 */
static void
unlink_fsm_rel(FSMRelation *fsmrel)
{
	if (fsmrel->priorRel != NULL)
		fsmrel->priorRel->nextRel = fsmrel->nextRel;
	else
		FreeSpaceMap->relList = fsmrel->nextRel;
	if (fsmrel->nextRel != NULL)
		fsmrel->nextRel->priorRel = fsmrel->priorRel;
	else
		FreeSpaceMap->relListTail = fsmrel->priorRel;
}

/*
 * Return all the FSMChunks in the chain starting at fchunk to the freelist.
 * (Caller must handle unlinking them from wherever they were.)
 */
static void
free_chunk_chain(FSMChunk *fchunk)
{
	int			nchunks;
	FSMChunk   *lchunk;

	if (fchunk == NULL)
		return;
	nchunks = 1;
	lchunk = fchunk;
	while (lchunk->next != NULL)
	{
		nchunks++;
		lchunk = lchunk->next;
	}
	lchunk->next = FreeSpaceMap->freeChunks;
	FreeSpaceMap->freeChunks = fchunk;
	FreeSpaceMap->numFreeChunks += nchunks;
}

/*
 * Look to see if a page with at least the specified amount of space is
 * available in the given FSMRelation.	If so, return its page number,
 * and advance the nextPage counter so that the next inquiry will return
 * a different page if possible.  Return InvalidBlockNumber if no success.
 */
static BlockNumber
find_free_space(FSMRelation *fsmrel, Size spaceNeeded)
{
	int			pagesToCheck,	/* outer loop counter */
				pageIndex,		/* current page index relative to relation */
				chunkRelIndex;	/* current page index relative to curChunk */
	FSMChunk   *curChunk;

	pageIndex = fsmrel->nextPage;
	/* Last operation may have left nextPage pointing past end */
	if (pageIndex >= fsmrel->numPages)
		pageIndex = 0;
	curChunk = fsmrel->relChunks;
	chunkRelIndex = pageIndex;

	for (pagesToCheck = fsmrel->numPages; pagesToCheck > 0; pagesToCheck--)
	{
		/*
		 * Make sure we are in the right chunk.  First time through, we
		 * may have to advance through multiple chunks; subsequent loops
		 * should do this at most once.
		 */
		while (chunkRelIndex >= curChunk->numPages)
		{
			chunkRelIndex -= curChunk->numPages;
			curChunk = curChunk->next;
		}
		/* Check the next page */
		if ((Size) curChunk->bytes[chunkRelIndex] >= spaceNeeded)
		{
			fsmrel->nextPage = pageIndex + 1;
			return curChunk->pages[chunkRelIndex];
		}
		/* Advance pageIndex and chunkRelIndex, wrapping around if needed */
		if (++pageIndex >= fsmrel->numPages)
		{
			pageIndex = 0;
			curChunk = fsmrel->relChunks;
			chunkRelIndex = 0;
		}
		else
			chunkRelIndex++;
	}

	return InvalidBlockNumber;	/* nothing found */
}

/*
 * fsm_record_free_space - guts of RecordFreeSpace, which is also used by
 * RecordAndGetPageWithFreeSpace.
 */
static void
fsm_record_free_space(FSMRelation *fsmrel, BlockNumber page, Size spaceAvail)
{
	FSMChunk   *chunk;
	int			chunkRelIndex;

	if (lookup_fsm_page_entry(fsmrel, page, &chunk, &chunkRelIndex))
	{
		/* Found an existing entry for page; update or delete it */
		if (spaceAvail >= fsmrel->threshold)
			chunk->bytes[chunkRelIndex] = (ItemLength) spaceAvail;
		else
			delete_fsm_page_entry(fsmrel, chunk, chunkRelIndex);
	}
	else
	{
		/*
		 * No existing entry; add one if spaceAvail exceeds threshold.
		 *
		 * CORNER CASE: if we have to do acquire_fsm_free_space then our own
		 * threshold will increase, possibly meaning that we shouldn't
		 * store the page after all.  Loop to redo the test if that
		 * happens.  The loop also covers the possibility that
		 * acquire_fsm_free_space must be executed more than once to free
		 * any space (ie, thresholds must be more than doubled).
		 */
		while (spaceAvail >= fsmrel->threshold)
		{
			if (insert_fsm_page_entry(fsmrel, page, spaceAvail,
									  chunk, chunkRelIndex))
				break;
			/* No space, acquire some and recheck threshold */
			acquire_fsm_free_space();
			if (spaceAvail < fsmrel->threshold)
				break;

			/*
			 * Need to redo the lookup since our own page list may well
			 * have lost entries, so position is not correct anymore.
			 */
			if (lookup_fsm_page_entry(fsmrel, page,
									  &chunk, &chunkRelIndex))
				elog(ERROR, "fsm_record_free_space: unexpected match");
		}
	}
}

/*
 * Look for an entry for a specific page (block number) in a FSMRelation.
 * Returns TRUE if a matching entry exists, else FALSE.
 *
 * The output arguments *outChunk, *outChunkRelIndex are set to indicate where
 * the entry exists (if TRUE result) or could be inserted (if FALSE result).
 * *chunk is set to NULL if there is no place to insert, ie, the entry would
 * need to be added to a new chunk.
 */
static bool
lookup_fsm_page_entry(FSMRelation *fsmrel, BlockNumber page,
					  FSMChunk **outChunk, int *outChunkRelIndex)
{
	FSMChunk   *chunk;
	int			chunkRelIndex;

	for (chunk = fsmrel->relChunks; chunk; chunk = chunk->next)
	{
		int			numPages = chunk->numPages;

		/* Can skip the chunk quickly if page must be after last in chunk */
		if (numPages > 0 && page <= chunk->pages[numPages - 1])
		{
			for (chunkRelIndex = 0; chunkRelIndex < numPages; chunkRelIndex++)
			{
				if (page <= chunk->pages[chunkRelIndex])
				{
					*outChunk = chunk;
					*outChunkRelIndex = chunkRelIndex;
					return (page == chunk->pages[chunkRelIndex]);
				}
			}
			/* Should not get here, given above test */
			Assert(false);
		}

		/*
		 * If we are about to fall off the end, and there's space
		 * available in the end chunk, return a pointer to it.
		 */
		if (chunk->next == NULL && numPages < CHUNKPAGES)
		{
			*outChunk = chunk;
			*outChunkRelIndex = numPages;
			return false;
		}
	}

	/*
	 * Adding the page would require a new chunk (or, perhaps, compaction
	 * of available free space --- not my problem here).
	 */
	*outChunk = NULL;
	*outChunkRelIndex = 0;
	return false;
}

/*
 * Insert a new page entry into a FSMRelation's list at given position
 * (chunk == NULL implies at end).
 *
 * If there is no space available to insert the entry, return FALSE.
 */
static bool
insert_fsm_page_entry(FSMRelation *fsmrel, BlockNumber page, Size spaceAvail,
					  FSMChunk *chunk, int chunkRelIndex)
{
	/* Outer loop handles retry after compacting rel's page list */
	for (;;)
	{
		if (fsmrel->numPages >= fsmrel->numChunks * CHUNKPAGES)
		{
			/* No free space within chunk list, so need another chunk */
			FSMChunk   *newChunk;

			if ((newChunk = FreeSpaceMap->freeChunks) == NULL)
				return false;		/* can't do it */
			FreeSpaceMap->freeChunks = newChunk->next;
			FreeSpaceMap->numFreeChunks--;
			newChunk->next = NULL;
			newChunk->numPages = 0;
			if (fsmrel->relChunks == NULL)
				fsmrel->relChunks = newChunk;
			else
			{
				FSMChunk   *priorChunk = fsmrel->relChunks;

				while (priorChunk->next != NULL)
					priorChunk = priorChunk->next;
				priorChunk->next = newChunk;
			}
			fsmrel->numChunks++;
			if (chunk == NULL)
			{
				/* Original search found that new page belongs at end */
				chunk = newChunk;
				chunkRelIndex = 0;
			}
		}

		/* Try to insert it the easy way, ie, just move down subsequent data */
		if (chunk &&
			push_fsm_page_entry(page, spaceAvail, chunk, chunkRelIndex))
		{
			fsmrel->numPages++;
			fsmrel->nextPage++;		/* don't return same page twice running */
			return true;
		}

		/*
		 * There is space available, but evidently it's before the place where
		 * the page entry needs to go.	Compact the list and try again. This
		 * will require us to redo the search for the appropriate place.
		 * Furthermore, compact_fsm_page_list deletes empty end chunks, so
		 * we may need to repeat the action of grabbing a new end chunk.
		 */
		compact_fsm_page_list(fsmrel);
		if (lookup_fsm_page_entry(fsmrel, page, &chunk, &chunkRelIndex))
			elog(ERROR, "insert_fsm_page_entry: entry already exists!");
	}
}

/*
 * Auxiliary routine for insert_fsm_page_entry: try to push entries to the
 * right to insert at chunk/chunkRelIndex.	Return TRUE if successful.
 * Note that the FSMRelation's own fields are not updated.
 */
static bool
push_fsm_page_entry(BlockNumber page, Size spaceAvail,
					FSMChunk *chunk, int chunkRelIndex)
{
	int			i;

	if (chunk->numPages >= CHUNKPAGES)
	{
		if (chunk->next == NULL)
			return false;		/* no space */
		/* try to push chunk's last item to next chunk */
		if (!push_fsm_page_entry(chunk->pages[CHUNKPAGES - 1],
								 chunk->bytes[CHUNKPAGES - 1],
								 chunk->next, 0))
			return false;
		/* successfully pushed it */
		chunk->numPages--;
	}
	for (i = chunk->numPages; i > chunkRelIndex; i--)
	{
		chunk->pages[i] = chunk->pages[i - 1];
		chunk->bytes[i] = chunk->bytes[i - 1];
	}
	chunk->numPages++;
	chunk->pages[chunkRelIndex] = page;
	chunk->bytes[chunkRelIndex] = (ItemLength) spaceAvail;
	return true;
}

/*
 * Delete one page entry from a FSMRelation's list.  Compact free space
 * in the list, but only if a chunk can be returned to the freelist.
 */
static void
delete_fsm_page_entry(FSMRelation *fsmrel, FSMChunk *chunk, int chunkRelIndex)
{
	int			i,
				lim;

	Assert(chunk && chunkRelIndex >= 0 && chunkRelIndex < chunk->numPages);
	/* Compact out space in this chunk */
	lim = --chunk->numPages;
	for (i = chunkRelIndex; i < lim; i++)
	{
		chunk->pages[i] = chunk->pages[i + 1];
		chunk->bytes[i] = chunk->bytes[i + 1];
	}
	/* Compact the whole list if a chunk can be freed */
	fsmrel->numPages--;
	if (fsmrel->numPages <= (fsmrel->numChunks - 1) * CHUNKPAGES)
		compact_fsm_page_list(fsmrel);
}

/*
 * Remove any pages with free space less than the current threshold for the
 * FSMRelation, compact out free slots in non-last chunks, and return any
 * completely freed chunks to the freelist.
 */
static void
compact_fsm_page_list(FSMRelation *fsmrel)
{
	Size		threshold = fsmrel->threshold;
	FSMChunk   *srcChunk,
			   *dstChunk;
	int			srcIndex,
				dstIndex,
				dstPages,
				dstChunkCnt;

	srcChunk = dstChunk = fsmrel->relChunks;
	srcIndex = dstIndex = 0;
	dstPages = 0;				/* total pages kept */
	dstChunkCnt = 1;			/* includes current dstChunk */

	while (srcChunk != NULL)
	{
		int			srcPages = srcChunk->numPages;

		while (srcIndex < srcPages)
		{
			if ((Size) srcChunk->bytes[srcIndex] >= threshold)
			{
				if (dstIndex >= CHUNKPAGES)
				{
					/*
					 * At this point srcChunk must be pointing to a later
					 * chunk, so it's OK to overwrite dstChunk->numPages.
					 */
					dstChunk->numPages = dstIndex;
					dstChunk = dstChunk->next;
					dstChunkCnt++;
					dstIndex = 0;
				}
				dstChunk->pages[dstIndex] = srcChunk->pages[srcIndex];
				dstChunk->bytes[dstIndex] = srcChunk->bytes[srcIndex];
				dstIndex++;
				dstPages++;
			}
			srcIndex++;
		}
		srcChunk = srcChunk->next;
		srcIndex = 0;
	}

	if (dstPages == 0)
	{
		/* No chunks to be kept at all */
		fsmrel->nextPage = 0;
		fsmrel->numPages = 0;
		fsmrel->numChunks = 0;
		fsmrel->relChunks = NULL;
		free_chunk_chain(dstChunk);
	}
	else
	{
		/* we deliberately don't change nextPage here */
		fsmrel->numPages = dstPages;
		fsmrel->numChunks = dstChunkCnt;
		dstChunk->numPages = dstIndex;
		free_chunk_chain(dstChunk->next);
		dstChunk->next = NULL;
	}
}

/*
 * Acquire some free space by raising the thresholds of all FSMRelations.
 *
 * Note there is no guarantee as to how much space will be freed by a single
 * invocation; caller may repeat if necessary.
 */
static void
acquire_fsm_free_space(void)
{
	FSMRelation *fsmrel;

	for (fsmrel = FreeSpaceMap->relList; fsmrel; fsmrel = fsmrel->nextRel)
	{
		fsmrel->threshold *= 2;
		/* Limit thresholds to BLCKSZ so they can't get ridiculously large */
		if (fsmrel->threshold > BLCKSZ)
			fsmrel->threshold = BLCKSZ;
		/* Release any pages that don't meet the new threshold */
		compact_fsm_page_list(fsmrel);
	}
}


#ifdef FREESPACE_DEBUG
/*
 * Dump contents of freespace map for debugging.
 *
 * We assume caller holds the FreeSpaceLock, or is otherwise unconcerned
 * about other processes.
 */
void
DumpFreeSpace(void)
{
	FSMRelation *fsmrel;
	FSMRelation *prevrel = NULL;
	int			relNum = 0;
	FSMChunk   *chunk;
	int			chunkRelIndex;
	int			nChunks;
	int			nPages;

	for (fsmrel = FreeSpaceMap->relList; fsmrel; fsmrel = fsmrel->nextRel)
	{
		relNum++;
		fprintf(stderr, "Map %d: rel %u/%u useCount %d threshold %u nextPage %d\nMap= ",
				relNum, fsmrel->key.tblNode, fsmrel->key.relNode,
				fsmrel->useCount, fsmrel->threshold, fsmrel->nextPage);
		nChunks = nPages = 0;
		for (chunk = fsmrel->relChunks; chunk; chunk = chunk->next)
		{
			int			numPages = chunk->numPages;

			nChunks++;
			for (chunkRelIndex = 0; chunkRelIndex < numPages; chunkRelIndex++)
			{
				nPages++;
				fprintf(stderr, " %u:%u",
						chunk->pages[chunkRelIndex],
						chunk->bytes[chunkRelIndex]);
			}
		}
		fprintf(stderr, "\n");
		/* Cross-check local counters and list links */
		if (nPages != fsmrel->numPages)
			fprintf(stderr, "DumpFreeSpace: %d pages in rel, but numPages = %d\n",
					nPages, fsmrel->numPages);
		if (nChunks != fsmrel->numChunks)
			fprintf(stderr, "DumpFreeSpace: %d chunks in rel, but numChunks = %d\n",
					nChunks, fsmrel->numChunks);
		if (prevrel != fsmrel->priorRel)
			fprintf(stderr, "DumpFreeSpace: broken list links\n");
		prevrel = fsmrel;
	}
	if (prevrel != FreeSpaceMap->relListTail)
		fprintf(stderr, "DumpFreeSpace: broken list links\n");
	/* Cross-check global counters */
	if (relNum != FreeSpaceMap->numRels)
		fprintf(stderr, "DumpFreeSpace: %d rels in list, but numRels = %d\n",
				relNum, FreeSpaceMap->numRels);
	nChunks = 0;
	for (chunk = FreeSpaceMap->freeChunks; chunk; chunk = chunk->next)
		nChunks++;
	if (nChunks != FreeSpaceMap->numFreeChunks)
		fprintf(stderr, "DumpFreeSpace: %d chunks in list, but numFreeChunks = %d\n",
				nChunks, FreeSpaceMap->numFreeChunks);
}

#endif   /* FREESPACE_DEBUG */
