/*-
* See the file LICENSE for redistribution information.
*
* Copyright (c) 2002-2005
*      Sleepycat Software.  All rights reserved.
*
* $Id: MemoryBudget.java,v 1.25 2004/12/22 14:11:30 linda Exp $
*/

package com.sleepycat.je.dbi;

import java.util.Iterator;

import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.EnvironmentStats;
import com.sleepycat.je.StatsConfig;
import com.sleepycat.je.config.EnvironmentParams;
import com.sleepycat.je.latch.Latch;
import com.sleepycat.je.tree.BIN;
import com.sleepycat.je.tree.DBIN;
import com.sleepycat.je.tree.DIN;
import com.sleepycat.je.tree.IN;

/**
 * MemoryBudget calculates the available memory for JE and how to apportion
 * it between cache and log buffers. It is meant to centralize all memory 
 * calculations. Objects that ask for memory budgets should get settings from
 * this class, rather than using the configuration parameter values directly.
 */
public class MemoryBudget implements EnvConfigObserver {

    /*
     * Object overheads. These are set statically with advance measurements.
     * Java doesn't provide a way of assessing object size dynamically. These
     * overheads will not be precise, but are close enough to let the system
     * behave predictably.
     */
    /* General Java objects */
    public final static int LONG_OVERHEAD = 16;
    public final static int BYTE_ARRAY_OVERHEAD = 16;
    public final static int OBJECT_OVERHEAD = 8;
    public final static int ARRAY_ITEM_OVERHEAD = 4;
    public final static int HASHMAP_OVERHEAD = 120;
    public final static int HASHMAP_ENTRY_OVERHEAD = 24;
    public final static int HASHSET_OVERHEAD = 130;
    public final static int HASHSET_ENTRY_OVERHEAD = 24;
    public final static int TWOHASHMAPS_OVERHEAD = 240;

    /* Node overheads */
    public final static int LN_OVERHEAD = 24;
    public final static int DUPCOUNTLN_OVERHEAD = 40;
    public final static int BIN_FIXED_OVERHEAD = 464;
    public final static int DIN_FIXED_OVERHEAD = 376;
    public final static int DBIN_FIXED_OVERHEAD = 480;
    public final static int IN_FIXED_OVERHEAD = 305;
    public final static int KEY_OVERHEAD = 16;
    public final static int LSN_SIZE = 8;

    /* Lock overheads. */
    public final static int LOCK_OVERHEAD = 32;
    public final static int LOCKINFO_OVERHEAD = 16;
    /* 
     * Txn memory is the size for the txn (135) + a hashmap entry
     * overhead for being part of the transaction table. 
     */
    public final static int TXN_OVERHEAD = 160; 

    /* 
     * The in-memory size of Checkpointer.CheckpointReference, calculated
     * manually by the Sizeof program.
     */
    public final static int CHECKPOINT_REFERENCE_SIZE = 32 +
        HASHSET_ENTRY_OVERHEAD;

    private final static long MIN_MAX_MEMORY_SIZE = 1024;

    private final static long N_64MB = (1 << 26);

    /*
     * Note that this class contains long fields that are accessed by multiple
     * threads, and access to these fields is intentionally not synchronized.
     * Although inaccuracies may result, correcting them is not worth the cost
     * of synchronizing every time we adjust the cacheMemoryUsage.
     */

    /* 
     * environmentMemoryUsage is the per-environment unlatched, inaccurate
     * count used to trigger eviction.
     */
    private long cacheMemoryUsage;

    /* 
     * Memory available to JE, based on je.maxMemory and the memory available
     * to this process.
     */
    private long maxMemory;
    private long criticalThreshold; // experimental mark for sync eviction.
                           
    /* Memory available to log buffers. */
    private long logBufferBudget;

    /* Memory to hold internal nodes, controlled by the evictor. */
    private long treeBudget;
    
    /* 
     * Overheads that are a function of node capacity.
     */
    private long inOverhead;
    private long binOverhead;
    private long dinOverhead;
    private long dbinOverhead;

    private EnvironmentImpl envImpl;

    MemoryBudget(EnvironmentImpl envImpl,
                 DbConfigManager configManager) 
        throws DatabaseException {

        this.envImpl = envImpl;

        /* Request notification of mutable property changes. */
        envImpl.addConfigObserver(this);

        /* Peform first time budget initialization. */
        reset(configManager);

        /*
         * Calculate IN and BIN overheads, which are a function of
         * capacity. These values are stored in this class so that they
         * can be calculated once per environment. The logic to do the
         * calculations is left in the respective node classes so it
         * can be properly the domain of those objects.
         */
        inOverhead = IN.computeOverhead(configManager);
        binOverhead = BIN.computeOverhead(configManager);
        dinOverhead = DIN.computeOverhead(configManager);
        dbinOverhead = DBIN.computeOverhead(configManager);
    }

    /**
     * Respond to config updates.
     */
    public void envConfigUpdate(DbConfigManager configManager) 
        throws DatabaseException {

        /*
         * Reinitialize the cache budget and the log buffer pool, in that
         * order.  Do not reset the log buffer pool if the log buffer budget
         * hasn't changed, since that is expensive and may cause I/O.
         */
        long oldLogBufferBudget = logBufferBudget;
        reset(configManager);
        if (oldLogBufferBudget != logBufferBudget) {
            envImpl.getLogManager().resetPool(configManager);
        }
    }

    /**
     * Initialize at construction time and when the cache is resized.
     */
    private void reset(DbConfigManager configManager)
        throws DatabaseException {

        /* 
         * Calculate the total memory allotted to JE.
         * 1. If je.maxMemory is specified, use that. Check that it's
         * not more than the jvm memory.
         * 2. Otherwise, take je.maxMemoryPercent * JVM max memory.
         */
        long newMaxMemory =
            configManager.getLong(EnvironmentParams.MAX_MEMORY);
        long jvmMemory = getRuntimeMaxMemory();

        if (newMaxMemory != 0) {
            /* Application specified a cache size number, validate it. */
            if (jvmMemory < newMaxMemory) {
                throw new IllegalArgumentException
                    (EnvironmentParams.MAX_MEMORY.getName() +
                     " has a value of " + newMaxMemory +
                     " but the JVM is only configured for " +
                     jvmMemory +
                     ". Consider using je.maxMemoryPercent.");
            }
            if (newMaxMemory < MIN_MAX_MEMORY_SIZE) {
                throw new IllegalArgumentException
                    (EnvironmentParams.MAX_MEMORY.getName() +
                     " is " + newMaxMemory +
                     " which less than the minimum: " +
                     MIN_MAX_MEMORY_SIZE);
            }
        } else {

            /*
             * When no explicit cache size is specified and the JVM memory size
             * is unknown, assume a default sized (64 MB) heap.  This produces
             * a reasonable cache size when no heap size is known.
             */
            if (jvmMemory == Long.MAX_VALUE) {
                jvmMemory = N_64MB;
            }

            /* Use the configured percentage of the JVM memory size. */
            int maxMemoryPercent =
                configManager.getInt(EnvironmentParams.MAX_MEMORY_PERCENT);
            newMaxMemory = (maxMemoryPercent * jvmMemory)/100;
        }

        /*
         * Calculate the memory budget for log buffering.
	 * If the LOG_MEM_SIZE parameter is not set, start by using
         * 7% (1/16th) of the cache size. If it is set, use that
         * explicit setting.
         * 
         * No point in having more log buffers than the maximum size. If
         * this starting point results in overly large log buffers,
         * reduce the log buffer budget again.
         */
        long newLogBufferBudget =
            configManager.getLong(EnvironmentParams.LOG_MEM_SIZE);	    
        if (newLogBufferBudget == 0) {
	    newLogBufferBudget = newMaxMemory >> 4;
	} else if (newLogBufferBudget > newMaxMemory/2) {
            newLogBufferBudget = newMaxMemory/2;
        }

        /* 
         * We have a first pass at the log buffer budget. See what
         * size log buffers result. Don't let them be too big, it would
         * be a waste.
         */
        int numBuffers =
	    configManager.getInt(EnvironmentParams.NUM_LOG_BUFFERS);
        long startingBufferSize = newLogBufferBudget / numBuffers; 
        int logBufferSize =
            configManager.getInt(EnvironmentParams.LOG_BUFFER_MAX_SIZE);
        if (startingBufferSize > logBufferSize) {
            startingBufferSize = logBufferSize;
            newLogBufferBudget = numBuffers * startingBufferSize;
        }

        long newCriticalThreshold =
            (newMaxMemory * 
             envImpl.getConfigManager().getInt(
                  EnvironmentParams.EVICTOR_CRITICAL_PERCENTAGE))/100;

        /* 
         * If all has gone well, update the budget fields.  Once the log buffer
         * budget is determined, the remainder of the memory is left for tree
         * nodes.
         */
        maxMemory = newMaxMemory;
        criticalThreshold = newCriticalThreshold;
        logBufferBudget = newLogBufferBudget;
        treeBudget = newMaxMemory - newLogBufferBudget;
    }

    /**
     * Returns Runtime.maxMemory(), accounting for a MacOS bug.
     * May return Long.MAX_VALUE if there is no inherent limit.
     * Used by unit tests as well as by this class.
     */
    public static long getRuntimeMaxMemory() {

        /* Runtime.maxMemory is unreliable on MacOS Java 1.4.2. */
        if ("Mac OS X".equals(System.getProperty("os.name"))) {
            String jvmVersion = System.getProperty("java.version");
            if (jvmVersion != null && jvmVersion.startsWith("1.4.2")) {
                return Long.MAX_VALUE; /* Undetermined heap size. */
            }
        }

        return Runtime.getRuntime().maxMemory();
    }

    /**
     * Initialize the starting environment memory state 
     */
    void initCacheMemoryUsage() 
        throws DatabaseException {

        long totalSize = 0;
        INList inList = envImpl.getInMemoryINs();

        inList.latchMajor();
        try {
            Iterator iter = inList.iterator();
            while (iter.hasNext()) {
                IN in = (IN) iter.next();
                long size = in.getInMemorySize();
                totalSize += size;
            }
        } finally {
            inList.releaseMajorLatch();
        }
        assert Latch.countLatchesHeld() == 0;
        cacheMemoryUsage = totalSize;
    }

    /**
     * Update the environment wide count, wake up the evictor if necessary.
     * @param increment note that increment may be negative.
     */
    public void updateCacheMemoryUsage(long increment) {
        cacheMemoryUsage += increment;
        if (cacheMemoryUsage > treeBudget) {
            envImpl.alertEvictor();
        }
    }

    public long accumulateNewUsage(IN in, long newSize) {
        return in.getInMemorySize() + newSize;
    }

    public void refreshCacheMemoryUsage(long newSize) {
        cacheMemoryUsage = newSize;
    }

    public long getCacheMemoryUsage() {
        return cacheMemoryUsage;
    }

    public long getLogBufferBudget() {
        return logBufferBudget;
    }

    public long getMaxMemory() {
        return maxMemory;
    }

    public long getCriticalThreshold() {
        return criticalThreshold;
    }

    public long getTreeBudget() {
        return treeBudget;
    }

    public long getINOverhead() {
        return inOverhead;
    }

    public long getBINOverhead() {
        return binOverhead;
    }

    public long getDINOverhead() {
        return dinOverhead;
    }

    public long getDBINOverhead() {
        return dbinOverhead;
    }

    /**
     * Returns the memory size occupied by a byte array of a given length,
     * not including BYTE_ARRAY_OVERHEAD.
     */
    public static int byteArraySize(int arrayLen) {

        /*
         * BYTE_ARRAY_OVERHEAD accounts for 4 bytes of data.  Data larger than
         * 4 bytes is allocated in 8 byte increments.
         */
        if (arrayLen > 4) {
            return ((arrayLen - 4 + 7) / 8) * 8;
        } else {
            return 0;
        }
    }

    void loadStats(StatsConfig config, EnvironmentStats stats) {
        stats.setCacheDataBytes(getCacheMemoryUsage());
    }
}
