root/usr/src/lib/libdtrace_jni/java/src/org/opensolaris/os/dtrace/Distribution.java
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */

/*
 * Copyright 2008 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 *
 * ident        "%Z%%M% %I%     %E% SMI"
 */
package org.opensolaris.os.dtrace;

import java.util.*;
import java.io.*;
import java.beans.*;

/**
 * A frequency distribution aggregated by the DTrace {@code quantize()}
 * or {@code lquantize()} action.  Each aggregated value falls into a
 * range known as a bucket and counts toward the frequency of that
 * bucket.  Bucket ranges are consecutive, with the maximum of one
 * bucket's range always one less than the minimum of the next bucket's
 * range.  By convention each bucket is identified by the minimum of its
 * range.
 *
 * @author Tom Erickson
 */
public abstract class Distribution implements AggregationValue,
       Iterable <Distribution.Bucket>, Serializable
{
    static final long serialVersionUID = 1186243118882654932L;

    /** @serial */
    private List <Bucket> buckets;
    private transient double total;
    private transient boolean initialized;

    /**
     * Package level access, called by subclasses LinearDistribution and
     * LogDistribution, but not available outside the API.
     *
     * @param base  the lower bound of this distribution, or zero if not
     * applicable
     * @param constant  the constant term of the distribution function
     * used to calculate the lower bound of any bucket given the lower
     * bound of the previous bucket, for example the step in a linear
     * distribution or the log base in a logarithmic distribution.
     * @param frequencies  for each bucket, the number of aggregated
     * values falling into that bucket's range; each element must be a
     * positive integer
     * @throws NullPointerException if frequencies is null
     * @throws IllegalArgumentException if any element of frequencies
     * does not have the expected range as defined by checkBucketRange()
     */
    Distribution(long base, long constant, long[] frequencies)
    {
        total = 0;
        long frequency;
        for (int i = 0, len = frequencies.length; i < len; ++i) {
            frequency = frequencies[i];
            total += frequency;
        }

        buckets = createBuckets(base, constant, frequencies);
        initialized = true;
    }

    /**
     * Supports XML persistence of subclasses.  Sub-class implementation
     * must call initialize() after setting any state specific to that
     * subclass for determining bucket ranges.
     *
     * @throws NullPointerException if frequencies is null
     * @throws IllegalArgumentException if any element of frequencies
     * does not have the expected range as defined by checkBucketRange()
     */
    Distribution(List <Bucket> frequencies)
    {
        // defensively copy frequencies list
        int len = frequencies.size();
        // don't need gratuitous capacity % added by constructor that
        // takes a Collection argument; list will not be modified
        buckets = new ArrayList <Bucket> (len);
        buckets.addAll(frequencies);
    }

    final void
    initialize()
    {
        // Called by constructor and readObject() (deserialization).
        // 1. Check class invariants, throw exception if deserialized
        //    state inconsistent with a Distribution that can result
        //    from the public constructor.
        // 2. Compute total (transient property derived from buckets)
        total = 0;
        long frequency;
        Bucket bucket;
        int len = buckets.size();
        for (int i = 0; i < len; ++i) {
            bucket = buckets.get(i);
            frequency = bucket.getFrequency();
            // relies on package-private getBucketRange()
            // implementation
            checkBucketRange(i, len, bucket);
            total += frequency;
        }
        initialized = true;
    }

    // Must be called by public instance methods (since the AbstractList
    // methods all depend on get() and size(), it is sufficient to call
    // checkInit() only in those inherited methods).
    private void
    checkInit()
    {
        if (!initialized) {
            throw new IllegalStateException("Uninitialized");
        }
    }

    /**
     * Gets a two element array: the first elelemt is the range minimum
     * (inclusive), the second element is the range maximum (inclusive).
     * Implemented by subclasses LinearDistribution and LogDistribution
     * to define bucket ranges for this distribution and not available
     * outside the API.  Used by the private general purpose constructor
     * called from native code.  Implementation must not use
     * subclass-specific state, since subclass state has not yet been
     * allocated.
     *
     * @see #Distribution(long base, long constant, long[] frequencies)
     */
    abstract long[]
    getBucketRange(int i, int len, long base, long constant);

    /**
     * Used by public constructors and deserialization only after
     * state specific to the subclass is available to the method.
     */
    abstract long[]
    getBucketRange(int i, int len);

    private List <Distribution.Bucket>
    createBuckets(long base, long constant, long[] frequencies)
    {
        int len = frequencies.length;
        Bucket bucket;
        List <Bucket> buckets = new ArrayList <Bucket> (len);
        long min; // current bucket
        long max; // next bucket minus one
        long[] range; // two element array: { min, max }

        for (int i = 0; i < len; ++i) {
            range = getBucketRange(i, len, base, constant);
            min = range[0];
            max = range[1];
            bucket = new Distribution.Bucket(min, max, frequencies[i]);
            buckets.add(bucket);
        }

        return buckets;
    }

    /**
     * Validates that bucket has the expected range for the given bucket
     * index.  Uses {@code base} and {@code constant} constructor args
     * to check invariants specific to each subclass, since state
     * derived from these args in a subclass is not yet available in the
     * superclass constructor.
     *
     * @throws IllegalArgumentException if bucket does not have the
     * expected range for the given bucket index {@code i}
     */
    private void
    checkBucketRange(int i, int bucketCount, Distribution.Bucket bucket,
            long base, long constant)
    {
        long[] range = getBucketRange(i, bucketCount, base, constant);
        checkBucketRange(i, bucket, range);
    }

    private void
    checkBucketRange(int i, int bucketCount, Distribution.Bucket bucket)
    {
        long[] range = getBucketRange(i, bucketCount);
        checkBucketRange(i, bucket, range);
    }

    private void
    checkBucketRange(int i, Distribution.Bucket bucket, long[] range)
    {
        long min = range[0];
        long max = range[1];

        if (bucket.getMin() != min) {
            throw new IllegalArgumentException("bucket min " +
                    bucket.getMin() + " at index " + i + ", expected " + min);
        }
        if (bucket.getMax() != max) {
            throw new IllegalArgumentException("bucket max " +
                    bucket.getMax() + " at index " + i + ", expected " + max);
        }
    }

    /**
     * Gets a modifiable list of this distribution's buckets ordered by
     * bucket range.  Modifying the returned list has no effect on this
     * distribution.  Supports XML persistence.
     *
     * @return a modifiable list of this distribution's buckets ordered
     * by bucket range
     */
    public List <Bucket>
    getBuckets()
    {
        checkInit();
        return new ArrayList <Bucket> (buckets);
    }

    /**
     * Gets a read-only {@code List} view of this distribution.
     *
     * @return a read-only {@code List} view of this distribution
     */
    public List <Bucket>
    asList()
    {
        checkInit();
        return Collections. <Bucket> unmodifiableList(buckets);
    }

    /**
     * Gets the number of buckets in this distribution.
     *
     * @return non-negative bucket count
     */
    public int
    size()
    {
        checkInit();
        return buckets.size();
    }

    /**
     * Gets the bucket at the given distribution index (starting at
     * zero).
     *
     * @return non-null distribution bucket at the given zero-based
     * index
     */
    public Bucket
    get(int index)
    {
        checkInit();
        return buckets.get(index);
    }

    /**
     * Gets an iterator over the buckets of this distribution.
     *
     * @return an iterator over the buckets of this distribution
     */
    public Iterator<Bucket>
    iterator()
    {
        checkInit();
        return buckets.iterator();
    }

    /**
     * Compares the specified object with this {@code Distribution}
     * instance for equality.  Defines equality as having the same
     * buckets with the same values.
     *
     * @return {@code true} if and only if the specified object is of
     * type {@code Distribution} and both instances have the same size
     * and equal buckets at corresponding distribution indexes
     */
    public boolean
    equals(Object o)
    {
        checkInit();
        if (o instanceof Distribution) {
            Distribution d = (Distribution)o;
            return buckets.equals(d.buckets);
        }
        return false;
    }

    /**
     * Overridden to ensure that equals instances have equal hash codes.
     */
    public int
    hashCode()
    {
        checkInit();
        return buckets.hashCode();
    }

    /**
     * Gets the total frequency across all buckets.
     *
     * @return sum of the frequency of all buckets in this distribution
     */
    public double
    getTotal()
    {
        checkInit();
        return total;
    }

    /**
     * Gets the numeric value of this distribution used to compare
     * distributions by overall magnitude, defined as the sum total of
     * each bucket's frequency times the minimum of its range.
     */
    public abstract Number getValue();

    /**
     * Called by native code
     */
    private void
    normalizeBuckets(long normal)
    {
        for (Bucket b : buckets) {
            b.frequency /= normal;
        }
    }

    /**
     * A range inclusive at both endpoints and a count of aggregated
     * values that fall in that range.  Buckets in a {@link
     * Distribution} are consecutive, such that the max of one bucket is
     * always one less than the min of the next bucket (or {@link
     * Long#MAX_VALUE} if it is the last bucket in the {@code
     * Distribution}).
     * <p>
     * Immutable.  Supports persistence using {@link java.beans.XMLEncoder}.
     */
    public static final class Bucket implements Serializable {
        static final long serialVersionUID = 4863264115375406295L;

        /** @serial */
        private final long min;
        /** @serial */
        private final long max;
        /** @serial */
        private long frequency; // non-final so native code can normalize

        static {
            try {
                BeanInfo info = Introspector.getBeanInfo(Bucket.class);
                PersistenceDelegate persistenceDelegate =
                        new DefaultPersistenceDelegate(
                        new String[] {"min", "max", "frequency"})
                {
                    /*
                     * Need to prevent DefaultPersistenceDelegate from using
                     * overridden equals() method, resulting in a
                     * StackOverFlowError.  Revert to PersistenceDelegate
                     * implementation.  See
                     * http://forum.java.sun.com/thread.jspa?threadID=
                     * 477019&tstart=135
                     */
                    protected boolean
                    mutatesTo(Object oldInstance, Object newInstance)
                    {
                        return (newInstance != null && oldInstance != null &&
                                (oldInstance.getClass() ==
                                newInstance.getClass()));
                    }
                };
                BeanDescriptor d = info.getBeanDescriptor();
                d.setValue("persistenceDelegate", persistenceDelegate);
            } catch (IntrospectionException e) {
                System.out.println(e);
            }
        }

        /**
         * Creates a distribution bucket with the given range and
         * frequency.
         *
         * @param rangeMinimumInclusive sets the lower bound (inclusive)
         * returned by {@link #getMin()}
         * @param rangeMaximumInclusive sets the upper bound (inclusive)
         * returned by {@link #getMax()}
         * @param valuesInRange sets the value frequency in this
         * bucket's range returned by {@link #getFrequency()}
         * @throws IllegalArgumentException if {@code
         * rangeMaximumInclusive} is less than {@code
         * rangeMinimumInclusive}
         */
        public
        Bucket(long rangeMinimumInclusive, long rangeMaximumInclusive,
                long valuesInRange)
        {
            if (rangeMaximumInclusive < rangeMinimumInclusive) {
                throw new IllegalArgumentException("upper bound " +
                        rangeMaximumInclusive + " is less than lower bound " +
                        rangeMinimumInclusive);
            }

            min = rangeMinimumInclusive;
            max = rangeMaximumInclusive;
            frequency = valuesInRange;
        }

        /**
         * Gets the lower bound of this bucket's range (inclusive).
         */
        public long
        getMin()
        {
            return min;
        }

        /**
         * Gets the upper bound of this bucket's range (inclusive).
         */
        public long
        getMax()
        {
            return max;
        }

        /**
         * Gets the number of values in a {@link Distribution} that fall
         * into the range defined by this bucket.
         */
        public long
        getFrequency()
        {
            return frequency;
        }

        /**
         * Compares the specified object with this distribution bucket
         * for equality.  Defines equality of two distribution buckets
         * as having the same range and the same frequency.
         *
         * @return false if the specified object is not a {@code
         * Distribution.Bucket}
         */
        @Override
        public boolean
        equals(Object o)
        {
            if (o instanceof Bucket) {
                Bucket b = (Bucket)o;
                return ((min == b.min) &&
                        (max == b.max) &&
                        (frequency == b.frequency));
            }
            return false;
        }

        /**
         * Overridden to ensure that equal buckets have equal hashcodes.
         */
        @Override
        public int
        hashCode()
        {
            int hash = 17;
            hash = (37 * hash) + ((int)(min ^ (min >>> 32)));
            hash = (37 * hash) + ((int)(max ^ (max >>> 32)));
            hash = (37 * hash) + ((int)(frequency ^ (frequency >>> 32)));
            return hash;
        }

        private void
        readObject(ObjectInputStream s)
                throws IOException, ClassNotFoundException
        {
            s.defaultReadObject();
            // check class invariants (as constructor does)
            if (max < min) {
                throw new InvalidObjectException("upper bound " +
                        max + " is less than lower bound " + min);
            }
        }

        /**
         * Gets a string representation of this distribution bucket
         * useful for logging and not intended for display.  The exact
         * details of the representation are unspecified and subject to
         * change, but the following format may be regarded as typical:
         * <pre><code>
         * class-name[property1 = value1, property2 = value2]
         * </code></pre>
         */
        public String
        toString()
        {
            StringBuilder buf = new StringBuilder();
            buf.append(Bucket.class.getName());
            buf.append("[min = ");
            buf.append(min);
            buf.append(", max = ");
            buf.append(max);
            buf.append(", frequency = ");
            buf.append(frequency);
            buf.append(']');
            return buf.toString();
        }
    }

    /**
     * Gets a list of buckets of interest by excluding empty buckets at
     * both ends of the distribution.  Leaves one empty bucket on each
     * end if possible to convey the distribution context more
     * effectively in a display.
     *
     * @return an unmodifiable sublist that includes the range starting
     * from the first bucket with a non-zero frequency and ending with
     * the last bucket with a non-zero frequency, plus one empty bucket
     * before and after that range if possible
     */
    public List <Bucket>
    getDisplayRange()
    {
        checkInit();
        int min = -1;
        int max = -1;
        int len = size();
        Bucket b;
        // Get first non-empty bucket
        for (int i = 0; i < len; ++i) {
            b = buckets.get(i);
            if (b.getFrequency() > 0L) {
                min = i;
                break;
            }
        }
        if (min < 0) {
            return Collections. <Bucket> emptyList();
        }
        // Get last non-empty bucket
        for (int i = (len - 1); i >= 0; --i) {
            b = buckets.get(i);
            if (b.getFrequency() > 0L) {
                max = i;
                break;
            }
        }
        // If possible, pad non-empty range with one empty bucket at
        // each end.
        if (min > 0) {
            --min;
        }
        if (max < (len - 1)) {
            ++max;
        }

        // subList inclusive at low index, exclusive at high index
        return Collections. <Bucket>
                unmodifiableList(buckets).subList(min, max + 1);
    }

    private void
    readObject(ObjectInputStream s)
            throws IOException, ClassNotFoundException
    {
        s.defaultReadObject();

        // Defensively copy buckets _before_ validating.  Subclass
        // validates by calling initialize() after reading any state
        // specific to that subclass needed for validation.
        int len = buckets.size();
        ArrayList <Bucket> copy = new ArrayList <Bucket> (len);
        copy.addAll(buckets);
        buckets = copy;
    }

    /**
     * Gets a string representation of this {@code Distribution} useful
     * for logging and not intended for display.  The exact details of
     * the representation are unspecified and subject to change, but the
     * following format may be regarded as typical:
     * <pre><code>
     * class-name[property1 = value1, property2 = value2]
     * </code></pre>
     */
    public String
    toString()
    {
        checkInit();
        StringBuilder buf = new StringBuilder();
        buf.append(Distribution.class.getName());
        buf.append("[buckets = ");
        List <Bucket> list = getDisplayRange();
        if (list.isEmpty()) {
            buf.append("<empty>");
        } else {
            buf.append(Arrays.toString(getDisplayRange().toArray()));
        }
        buf.append(", total = ");
        buf.append(getTotal());
        buf.append(']');
        return buf.toString();
    }
}