#ifndef JEMALLOC_INTERNAL_DECAY_H
#define JEMALLOC_INTERNAL_DECAY_H

#include "jemalloc/internal/jemalloc_preamble.h"
#include "jemalloc/internal/mutex.h"
#include "jemalloc/internal/smoothstep.h"

#define DECAY_UNBOUNDED_TIME_TO_PURGE ((uint64_t) - 1)

/*
 * The decay_t computes the number of pages we should purge at any given time.
 * Page allocators inform a decay object when pages enter a decay-able state
 * (i.e. dirty or muzzy), and query it to determine how many pages should be
 * purged at any given time.
 *
 * This is mostly a single-threaded data structure and doesn't care about
 * synchronization at all; it's the caller's responsibility to manage their
 * synchronization on their own.  There are two exceptions:
 * 1) It's OK to racily call decay_ms_read (i.e. just the simplest state query).
 * 2) The mtx and purging fields live (and are initialized) here, but are
 *    logically owned by the page allocator.  This is just a convenience (since
 *    those fields would be duplicated for both the dirty and muzzy states
 *    otherwise).
 */
typedef struct decay_s decay_t;
struct decay_s {
	/* Synchronizes all non-atomic fields. */
	malloc_mutex_t mtx;
	/*
	 * True if a thread is currently purging the extents associated with
	 * this decay structure.
	 */
	bool purging;
	/*
	 * Approximate time in milliseconds from the creation of a set of unused
	 * dirty pages until an equivalent set of unused dirty pages is purged
	 * and/or reused.
	 */
	atomic_zd_t time_ms;
	/* time / SMOOTHSTEP_NSTEPS. */
	nstime_t interval;
	/*
	 * Time at which the current decay interval logically started.  We do
	 * not actually advance to a new epoch until sometime after it starts
	 * because of scheduling and computation delays, and it is even possible
	 * to completely skip epochs.  In all cases, during epoch advancement we
	 * merge all relevant activity into the most recently recorded epoch.
	 */
	nstime_t epoch;
	/* Deadline randomness generator. */
	uint64_t jitter_state;
	/*
	 * Deadline for current epoch.  This is the sum of interval and per
	 * epoch jitter which is a uniform random variable in [0..interval).
	 * Epochs always advance by precise multiples of interval, but we
	 * randomize the deadline to reduce the likelihood of arenas purging in
	 * lockstep.
	 */
	nstime_t deadline;
	/*
	 * The number of pages we cap ourselves at in the current epoch, per
	 * decay policies.  Updated on an epoch change.  After an epoch change,
	 * the caller should take steps to try to purge down to this amount.
	 */
	size_t npages_limit;
	/*
	 * Number of unpurged pages at beginning of current epoch.  During epoch
	 * advancement we use the delta between arena->decay_*.nunpurged and
	 * ecache_npages_get(&arena->ecache_*) to determine how many dirty pages,
	 * if any, were generated.
	 */
	size_t nunpurged;
	/*
	 * Trailing log of how many unused dirty pages were generated during
	 * each of the past SMOOTHSTEP_NSTEPS decay epochs, where the last
	 * element is the most recent epoch.  Corresponding epoch times are
	 * relative to epoch.
	 *
	 * Updated only on epoch advance, triggered by
	 * decay_maybe_advance_epoch, below.
	 */
	size_t backlog[SMOOTHSTEP_NSTEPS];

	/* Peak number of pages in associated extents.  Used for debug only. */
	uint64_t ceil_npages;
};

/*
 * The current decay time setting.  This is the only public access to a decay_t
 * that's allowed without holding mtx.
 */
static inline ssize_t
decay_ms_read(const decay_t *decay) {
	return atomic_load_zd(&decay->time_ms, ATOMIC_RELAXED);
}

/*
 * See the comment on the struct field -- the limit on pages we should allow in
 * this decay state this epoch.
 */
static inline size_t
decay_npages_limit_get(const decay_t *decay) {
	return decay->npages_limit;
}

/* How many unused dirty pages were generated during the last epoch. */
static inline size_t
decay_epoch_npages_delta(const decay_t *decay) {
	return decay->backlog[SMOOTHSTEP_NSTEPS - 1];
}

/*
 * Current epoch duration, in nanoseconds.  Given that new epochs are started
 * somewhat haphazardly, this is not necessarily exactly the time between any
 * two calls to decay_maybe_advance_epoch; see the comments on fields in the
 * decay_t.
 */
static inline uint64_t
decay_epoch_duration_ns(const decay_t *decay) {
	return nstime_ns(&decay->interval);
}

static inline bool
decay_immediately(const decay_t *decay) {
	ssize_t decay_ms = decay_ms_read(decay);
	return decay_ms == 0;
}

static inline bool
decay_disabled(const decay_t *decay) {
	ssize_t decay_ms = decay_ms_read(decay);
	return decay_ms < 0;
}

/* Returns true if decay is enabled and done gradually. */
static inline bool
decay_gradually(const decay_t *decay) {
	ssize_t decay_ms = decay_ms_read(decay);
	return decay_ms > 0;
}

/*
 * Returns true if the passed in decay time setting is valid.
 * < -1 : invalid
 * -1   : never decay
 *  0   : decay immediately
 *  > 0 : some positive decay time, up to a maximum allowed value of
 *  NSTIME_SEC_MAX * 1000, which corresponds to decaying somewhere in the early
 *  27th century.  By that time, we expect to have implemented alternate purging
 *  strategies.
 */
bool decay_ms_valid(ssize_t decay_ms);

/*
 * As a precondition, the decay_t must be zeroed out (as if with memset).
 *
 * Returns true on error.
 */
bool decay_init(decay_t *decay, nstime_t *cur_time, ssize_t decay_ms);

/*
 * Given an already-initialized decay_t, reinitialize it with the given decay
 * time.  The decay_t must have previously been initialized (and should not then
 * be zeroed).
 */
void decay_reinit(decay_t *decay, nstime_t *cur_time, ssize_t decay_ms);

/*
 * Compute how many of 'npages_new' pages we would need to purge in 'time'.
 */
uint64_t decay_npages_purge_in(
    decay_t *decay, nstime_t *time, size_t npages_new);

/* Returns true if the epoch advanced and there are pages to purge. */
bool decay_maybe_advance_epoch(
    decay_t *decay, nstime_t *new_time, size_t current_npages);

/*
 * Calculates wait time until a number of pages in the interval
 * [0.5 * npages_threshold .. 1.5 * npages_threshold] should be purged.
 *
 * Returns number of nanoseconds or DECAY_UNBOUNDED_TIME_TO_PURGE in case of
 * indefinite wait.
 */
uint64_t decay_ns_until_purge(
    decay_t *decay, size_t npages_current, uint64_t npages_threshold);

#endif /* JEMALLOC_INTERNAL_DECAY_H */
