/*
** dobjgc.cpp
** The garbage collector. Based largely on Lua's.
**
**---------------------------------------------------------------------------
** Copyright 2008-2022 Marisa Heit
** All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
**
** 1. Redistributions of source code must retain the above copyright
**    notice, this list of conditions and the following disclaimer.
** 2. Redistributions in binary form must reproduce the above copyright
**    notice, this list of conditions and the following disclaimer in the
**    documentation and/or other materials provided with the distribution.
** 3. The name of the author may not be used to endorse or promote products
**    derived from this software without specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**---------------------------------------------------------------------------
**
*/
/******************************************************************************
* Copyright (C) 1994-2008 Lua.org, PUC-Rio.  All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************************************************************/

// HEADER FILES ------------------------------------------------------------

#include "dobject.h"

#include "c_dispatch.h"
#include "menu.h"
#include "stats.h"
#include "printf.h"
#include "cmdlib.h"

// MACROS ------------------------------------------------------------------

/*
@@ DEFAULT_GCPAUSE defines the default pause between garbage-collector cycles
@* as a percentage.
** CHANGE it if you want the GC to run faster or slower (higher values
** mean larger pauses which mean slower collection.) You can also change
** this value dynamically.
*/
#define DEFAULT_GCPAUSE		150	// 150% (wait for memory to increase by half before next GC)

/*
@@ DEFAULT_GCMUL defines the default speed of garbage collection relative to
@* memory allocation as a percentage.
** CHANGE it if you want to change the granularity of the garbage
** collection. (Higher values mean coarser collections. 0 represents
** infinity, where each step performs a full collection.) You can also
** change this value dynamically.
*/
#ifndef _DEBUG
#define DEFAULT_GCMUL		600 // GC runs gcmul% the speed of memory allocation
#else
// Higher in debug builds to account for the extra time spent freeing objects
#define DEFAULT_GCMUL		800
#endif

// Minimum step size
#define GCMINSTEPSIZE		(sizeof(DObject) * 16)

// Sweeps traverse objects in chunks of this size
#define GCSWEEPGRANULARITY	40

// Cost of deleting an object
#ifndef _DEBUG
#define GCDELETECOST		75
#else
// Freeing memory is much more costly in debug builds
#define GCDELETECOST		230
#endif

// Cost of destroying an object
#define GCDESTROYCOST		15

// TYPES -------------------------------------------------------------------

class FAveragizer
{
	// Number of allocations to track
	static inline constexpr unsigned HistorySize = 512;

	size_t History[HistorySize];
	size_t TotalAmount;
	int TotalCount;
	unsigned NewestPos;

public:
	FAveragizer();
	void AddAlloc(size_t alloc);
	size_t GetAverage();
};

struct FStepStats
{
	cycle_t Clock[GC::GCS_COUNT];
	size_t BytesCovered[GC::GCS_COUNT];
	int Count[GC::GCS_COUNT];

	void Format(FString &out);
	void Reset();
};

// EXTERNAL FUNCTION PROTOTYPES --------------------------------------------

// PUBLIC FUNCTION PROTOTYPES ----------------------------------------------

// PRIVATE FUNCTION PROTOTYPES ---------------------------------------------

static size_t CalcStepSize();

// EXTERNAL DATA DECLARATIONS ----------------------------------------------

// PUBLIC DATA DEFINITIONS -------------------------------------------------

namespace GC
{
size_t AllocBytes;
size_t RunningAllocBytes;
size_t RunningDeallocBytes;
size_t Threshold;
size_t Estimate;
DObject *Gray;
DObject *Root;
DObject *SoftRoots;
DObject **SweepPos;
DObject *ToDestroy;
uint32_t CurrentWhite = OF_White0 | OF_Fixed;
EGCState State = GCS_Pause;
int Pause = DEFAULT_GCPAUSE;
int StepMul = DEFAULT_GCMUL;
FStepStats StepStats;
FStepStats PrevStepStats;
bool FinalGC;
bool HadToDestroy;

// PRIVATE DATA DEFINITIONS ------------------------------------------------

static FAveragizer AllocHistory;// Tracks allocation rate over time
static cycle_t GCTime;			// Track time spent in GC

// CODE --------------------------------------------------------------------

//==========================================================================
//
// CheckGC
// 
// Check if it's time to collect, and do a collection step if it is.
// Also does some bookkeeping. Should be called fairly consistantly.
//
//==========================================================================

void CheckGC()
{
	AllocHistory.AddAlloc(RunningAllocBytes);
	RunningAllocBytes = 0;
	if (State > GCS_Pause || AllocBytes >= Threshold)
	{
		Step();
	}
}

//==========================================================================
//
// SetThreshold
//
// Sets the new threshold after a collection is finished.
//
//==========================================================================

void SetThreshold()
{
	Threshold = (std::min(Estimate, AllocBytes) / 100) * Pause;
}

//==========================================================================
//
// PropagateMark
//
// Marks the top-most gray object black and marks all objects it points to
// gray.
//
//==========================================================================

size_t PropagateMark()
{
	DObject *obj = Gray;
	assert(obj->IsGray());
	obj->Gray2Black();
	Gray = obj->GCNext;
	return !(obj->ObjectFlags & OF_EuthanizeMe) ? obj->PropagateMark() :
		obj->GetClass()->Size;
}

//==========================================================================
//
// SweepObjects
//
// Runs a limited sweep on the object list, returning the number of bytes
// swept.
//
//==========================================================================

static size_t SweepObjects(size_t count)
{
	DObject *curr;
	int deadmask = OtherWhite();
	size_t swept = 0;

	while ((curr = *SweepPos) != nullptr && count-- > 0)
	{
		swept += curr->GetClass()->Size;
		if ((curr->ObjectFlags ^ OF_WhiteBits) & deadmask)	// not dead?
		{
			assert(!curr->IsDead() || (curr->ObjectFlags & OF_Fixed));
			curr->MakeWhite();	// make it white (for next cycle)
			SweepPos = &curr->ObjNext;
		}
		else
		{
			assert(curr->IsDead());
			if (!(curr->ObjectFlags & OF_EuthanizeMe))
			{	// The object must be destroyed before it can be deleted.
				curr->GCNext = ToDestroy;
				ToDestroy = curr;
				SweepPos = &curr->ObjNext;
			}
			else
			{	// must erase 'curr'
				*SweepPos = curr->ObjNext;
				curr->ObjectFlags |= OF_Cleanup;
				curr->~DObject();
				M_Free(curr);
				swept += GCDELETECOST;
			}
		}
	}
	return swept;
}

//==========================================================================
//
// DestroyObjects
//
// Destroys up to count objects on a list linked on GCNext, returning the
// size of objects destroyed, for updating the estimate.
//
//==========================================================================

static size_t DestroyObjects(size_t count)
{
	DObject *curr;
	size_t bytes_destroyed = 0;

	while ((curr = ToDestroy) != nullptr && count-- > 0)
	{
		// Note that we cannot assume here that the object has not yet been destroyed.
		// If destruction happens as the result of another object's destruction we may
		// get entries here that have been destroyed already if that owning object was
		// first in the list.
		if (!(curr->ObjectFlags & OF_EuthanizeMe))
		{
			bytes_destroyed += curr->GetClass()->Size + GCDESTROYCOST;
			ToDestroy = curr->GCNext;
			curr->GCNext = nullptr;
			curr->Destroy();
		}
		else
		{
			ToDestroy = curr->GCNext;
			curr->GCNext = nullptr;
		}
	}
	return bytes_destroyed;
}

//==========================================================================
//
// Mark
//
// Mark a single object gray.
//
//==========================================================================

void Mark(DObject **obj)
{
	DObject *lobj = *obj;

	//assert(lobj == nullptr || !(lobj->ObjectFlags & OF_Released));
	if (lobj != nullptr && !(lobj->ObjectFlags & OF_Released))
	{
		if (lobj->ObjectFlags & OF_EuthanizeMe)
		{
			*obj = (DObject *)NULL;
		}
		else if (lobj->IsWhite())
		{
			lobj->White2Gray();
			lobj->GCNext = Gray;
			Gray = lobj;
		}
	}
}

//==========================================================================
//
// MarkArray
//
// Mark an array of objects gray.
//
//==========================================================================

void MarkArray(DObject **obj, size_t count)
{
	for (size_t i = 0; i < count; ++i)
	{
		Mark(obj[i]);
	}
}

//==========================================================================
//
// CalcStepSize
//
// Decide how big a step should be, based on the current allocation rate.
//
//==========================================================================

static size_t CalcStepSize()
{
	size_t avg = AllocHistory.GetAverage();
	return std::max<size_t>(GCMINSTEPSIZE, avg * StepMul / 100);
}

//==========================================================================
//
// MarkRoot
//
// Mark the root set of objects.
//
//==========================================================================

TArray<GCMarkerFunc> markers;
void AddMarkerFunc(GCMarkerFunc func)
{
	if (markers.Find(func) == markers.Size())
		markers.Push(func);
}

static void MarkRoot()
{
	PrevStepStats = StepStats;
	StepStats.Reset();

	Gray = nullptr;

	for (auto func : markers) func();

	// Mark soft roots.
	if (SoftRoots != nullptr)
	{
		DObject **probe = &SoftRoots->ObjNext;
		while (*probe != nullptr)
		{
			DObject *soft = *probe;
			probe = &soft->ObjNext;
			if ((soft->ObjectFlags & (OF_Rooted | OF_EuthanizeMe)) == OF_Rooted)
			{
				Mark(soft);
			}
		}
	}
	// Time to propagate the marks.
	State = GCS_Propagate;
}

//==========================================================================
//
// Atomic
//
// If there were any propagations that needed to be done atomicly, they
// would go here. It also sets things up for the sweep state.
//
//==========================================================================

static void Atomic()
{
	// Flip current white
	CurrentWhite = OtherWhite();
	SweepPos = &Root;
	State = GCS_Sweep;
	Estimate = AllocBytes;
}

//==========================================================================
//
// SweepDone
// 
// Sets up the Destroy phase, if there are any dead objects that haven't
// been destroyed yet, or skips to the Done state.
//
//==========================================================================

static void SweepDone()
{
	HadToDestroy = ToDestroy != nullptr;
	State = HadToDestroy ? GCS_Destroy : GCS_Done;
}

//==========================================================================
//
// SingleStep
//
// Performs one step of the collector.
//
//==========================================================================

static size_t SingleStep()
{
	switch (State)
	{
	case GCS_Pause:
		MarkRoot();		// Start a new collection
		return 0;

	case GCS_Propagate:
		if (Gray != nullptr)
		{
			return PropagateMark();
		}
		else
		{ // no more gray objects
			Atomic();	// finish mark phase
			return 0;
		}

	case GCS_Sweep: {
		RunningDeallocBytes = 0;
		size_t swept = SweepObjects(GCSWEEPGRANULARITY);
		Estimate -= RunningDeallocBytes;
		if (*SweepPos == nullptr)
		{ // Nothing more to sweep?
			SweepDone();
		}
		return swept;
	  }

	case GCS_Destroy: {
		size_t destroy_size;
		destroy_size = DestroyObjects(GCSWEEPGRANULARITY);
		Estimate -= destroy_size;
		if (ToDestroy == nullptr)
		{ // Nothing more to destroy?
			State = GCS_Done;
		}
		return destroy_size;
	  }

	case GCS_Done:
		State = GCS_Pause;		// end collection
		SetThreshold();
		return 0;

	default:
		assert(0);
		return 0;
	}
}

//==========================================================================
//
// Step
//
// Performs enough single steps to cover <StepSize> bytes of memory.
// Some of those bytes might be "fake" to account for the cost of freeing
// or destroying object.
//
//==========================================================================

void Step()
{
	GCTime.ResetAndClock();

	auto enter_state = State;
	StepStats.Count[enter_state]++;
	StepStats.Clock[enter_state].Clock();

	size_t did = 0;
	size_t lim = CalcStepSize();

	do
	{
		size_t done = SingleStep();
		did += done;
		if (done < lim)
		{
			lim -= done;
		}
		else
		{
			lim = 0;
		}
		if (State != enter_state)
		{
			// Finish stats on old state
			StepStats.Clock[enter_state].Unclock();
			StepStats.BytesCovered[enter_state] += did;

			// Start stats on new state
			did = 0;
			enter_state = State;
			StepStats.Clock[enter_state].Clock();
			StepStats.Count[enter_state]++;
		}
	} while (lim && State != GCS_Pause);

	StepStats.Clock[enter_state].Unclock();
	StepStats.BytesCovered[enter_state] += did;
	GCTime.Unclock();
}

//==========================================================================
//
// FullGC
//
// Collects everything in one fell swoop.
//
//==========================================================================

void FullGC()
{
	bool ContinueCheck = true;
	while (ContinueCheck)
	{
		ContinueCheck = false;
		if (State <= GCS_Propagate)
		{
			// Reset sweep mark to sweep all elements (returning them to white)
			SweepPos = &Root;
			// Reset other collector lists
			Gray = nullptr;
			State = GCS_Sweep;
		}
		// Finish any pending GC stages
		while (State != GCS_Pause)
		{
			SingleStep();
		}
		// Loop until everything that can be destroyed and freed is
		do
		{
			MarkRoot();
			while (State != GCS_Pause)
			{
				SingleStep();
			}
			ContinueCheck |= HadToDestroy;
		} while (HadToDestroy);
	}
}

//==========================================================================
//
// Barrier
//
// Implements a write barrier to maintain the invariant that a black node
// never points to a white node by making the node pointed at gray.
//
//==========================================================================

void Barrier(DObject *pointing, DObject *pointed)
{
	assert(pointing == nullptr || (pointing->IsBlack() && !pointing->IsDead()));
	assert(pointed->IsWhite() && !pointed->IsDead());
	assert(State != GCS_Destroy && State != GCS_Pause);
	assert(!(pointed->ObjectFlags & OF_Released));	// if a released object gets here, something must be wrong.
	if (pointed->ObjectFlags & OF_Released) return;	// don't do anything with non-GC'd objects.
	// The invariant only needs to be maintained in the propagate state.
	if (State == GCS_Propagate)
	{
		pointed->White2Gray();
		pointed->GCNext = Gray;
		Gray = pointed;
	}
	// In other states, we can mark the pointing object white so this
	// barrier won't be triggered again, saving a few cycles in the future.
	else if (pointing != nullptr)
	{
		pointing->MakeWhite();
	}
}

void DelSoftRootHead()
{
	if (SoftRoots != nullptr)
	{
		// Don't let the destructor print a warning message
		SoftRoots->ObjectFlags |= OF_YesReallyDelete;
		SoftRoots->~DObject();
		M_Free(SoftRoots);
	}
	SoftRoots = nullptr;
}

//==========================================================================
//
// AddSoftRoot
//
// Marks an object as a soft root. A soft root behaves exactly like a root
// in MarkRoot, except it can be added at run-time.
//
//==========================================================================

void AddSoftRoot(DObject *obj)
{
	DObject **probe;

	// Are there any soft roots yet?
	if (SoftRoots == nullptr)
	{
		// Create a new object to root the soft roots off of, and stick
		// it at the end of the object list, so we know that anything
		// before it is not a soft root.
		SoftRoots = Create<DObject>();
		SoftRoots->ObjectFlags |= OF_Fixed;
		probe = &Root;
		while (*probe != nullptr)
		{
			probe = &(*probe)->ObjNext;
		}
		Root = SoftRoots->ObjNext;
		SoftRoots->ObjNext = nullptr;
		*probe = SoftRoots;
	}
	// Mark this object as rooted and move it after the SoftRoots marker.
	probe = &Root;
	while (*probe != nullptr && *probe != obj)
	{
		probe = &(*probe)->ObjNext;
	}
	*probe = (*probe)->ObjNext;
	obj->ObjNext = SoftRoots->ObjNext;
	SoftRoots->ObjNext = obj;
	obj->ObjectFlags |= OF_Rooted;
	WriteBarrier(obj);
}

//==========================================================================
//
// DelSoftRoot
//
// Unroots an object so that it must be reachable or it will get collected.
//
//==========================================================================

void DelSoftRoot(DObject *obj)
{
	DObject **probe;

	if (obj == nullptr || !(obj->ObjectFlags & OF_Rooted))
	{ // Not rooted, so nothing to do.
		return;
	}
	obj->ObjectFlags &= ~OF_Rooted;
	// Move object out of the soft roots part of the list.
	probe = &SoftRoots;
	while (*probe != nullptr && *probe != obj)
	{
		probe = &(*probe)->ObjNext;
	}
	if (*probe == obj)
	{
		*probe = obj->ObjNext;
		obj->ObjNext = Root;
		Root = obj;
	}
}

}

//==========================================================================
//
// FAveragizer - Constructor
//
//==========================================================================

FAveragizer::FAveragizer()
{
	NewestPos = 0;
	TotalAmount = 0;
	TotalCount = 0;
	memset(History, 0, sizeof(History));
}

//==========================================================================
//
// FAveragizer :: AddAlloc
//
//==========================================================================

void FAveragizer::AddAlloc(size_t alloc)
{
	NewestPos = (NewestPos + 1) & (HistorySize - 1);
	if (TotalCount < (int)HistorySize)
	{
		TotalCount++;
	}
	else
	{
		TotalAmount -= History[NewestPos];
	}
	History[NewestPos] = alloc;
	TotalAmount += alloc;
}

//==========================================================================
//
// FAveragizer :: GetAverage
//
//==========================================================================

size_t FAveragizer::GetAverage()
{
	return TotalCount != 0 ? TotalAmount / TotalCount : 0;
}

//==========================================================================
//
// STAT gc
//
// Provides information about the current garbage collector state.
//
//==========================================================================

ADD_STAT(gc)
{
	static const char *StateStrings[] = {
		"  Pause  ",
		"Propagate",
		"  Sweep  ",
		" Destroy ",
		"  Done   "
	};
	FString out;
	double time = GC::State != GC::GCS_Pause ? GC::GCTime.TimeMS() : 0;

	GC::PrevStepStats.Format(out);
	out << "\n";
	GC::StepStats.Format(out);
	out.AppendFormat("\n%.2fms [%s] Rate:%3zuK (%3zuK)  Alloc:%6zuK  Est:%6zuK  Thresh:%6zuK",
		time,
		StateStrings[GC::State],
		(GC::AllocHistory.GetAverage() + 1023) >> 10,
		(GC::CalcStepSize() + 1023) >> 10,
		(GC::AllocBytes + 1023) >> 10,
		(GC::Estimate + 1023) >> 10,
		(GC::Threshold + 1023) >> 10);
	return out;
}

//==========================================================================
//
// FStepStats :: Reset
//
//==========================================================================

void FStepStats::Reset()
{
	for (unsigned i = 0; i < countof(Count); ++i)
	{
		Count[i] = 0;
		BytesCovered[i] = 0;
		Clock[i].Reset();
	}
}

//==========================================================================
//
// FStepStats :: Format
//
// Appends its stats to the given FString.
//
//==========================================================================

void FStepStats::Format(FString &out)
{
	// Because everything in the default green is hard to distinguish,
	// each stage has its own color.
	for (int i = GC::GCS_Propagate; i < GC::GCS_Done; ++i)
	{
		int count = Count[i];
		double time = Clock[i].TimeMS();
		out.AppendFormat(TEXTCOLOR_ESCAPESTR "%c[%c%6zuK %4d*%.2fms]",
			"-NKB"[i],	/* Color codes */
			"-PSD"[i],	/* Stage prefixes: (P)ropagate, (S)weep, (D)estroy */
			(BytesCovered[i] + 1023) >> 10, count, count != 0 ? time / count : time);
	}
	out << TEXTCOLOR_GREEN;
}

//==========================================================================
//
// CCMD gc
//
// Controls various aspects of the collector.
//
//==========================================================================

CCMD(gc)
{
	if (argv.argc() == 1)
	{
		Printf ("Usage: gc stop|now|full|count|pause [size]|stepmul [size]\n");
		return;
	}
	if (stricmp(argv[1], "stop") == 0)
	{
		GC::Threshold = ~(size_t)0 - 2;
	}
	else if (stricmp(argv[1], "now") == 0)
	{
		GC::Threshold = GC::AllocBytes;
	}
	else if (stricmp(argv[1], "full") == 0)
	{
		GC::FullGC();
	}
	else if (stricmp(argv[1], "count") == 0)
	{
		int cnt = 0;
		for (DObject *obj = GC::Root; obj; obj = obj->ObjNext, cnt++);
		Printf("%d active objects counted\n", cnt);
	}
	else if (stricmp(argv[1], "pause") == 0)
	{
		if (argv.argc() == 2)
		{
			Printf ("Current GC pause is %d\n", GC::Pause);
		}
		else
		{
			GC::Pause = max(1,atoi(argv[2]));
		}
	}
	else if (stricmp(argv[1], "stepmul") == 0)
	{
		if (argv.argc() == 2)
		{
			Printf ("Current GC stepmul is %d\n", GC::StepMul);
		}
		else
		{
			GC::StepMul = max(100, atoi(argv[2]));
		}
	}
}

