/** * Copyright 2013 the original author or authors. * * Licensed under the Apache License, Version 2.0 the "License"; * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ package io.neba.core.resourcemodels.metadata; import static java.lang.Math.max; import static java.lang.System.currentTimeMillis; import static java.util.Arrays.copyOf; import static java.util.Arrays.fill; /** * Represents statistical data regarding the usage of a {@link io.neba.api.annotations.ResourceModel}. * This implementation is intentionally not thread-safe, i.e. the calculations will loose accuracy in case the * statics are modified concurrently. This is intended: We can give up some precision in favor of a massive performance gain, * as this model is accessed hundreds of times per request, e.g. during page rendering.<br /> * However, it is expected that the statistics still provide an accurate picture and gain * precision as time passes and data accumulates. * * @author Olaf Otto */ public class ResourceModelStatistics { private final long since = currentTimeMillis(); // This will consume approx 4 Byte * 16 + c // (= 68 Byte + constant management overhead for the array) of main memory. // The mapping duration intervals are of the form [0, 1), [1, 2), [2, 4), [4, 8), // i.e. use a base-2 exponential to build frequency groups up to [2^(i - 1), 2^i) where i // is the size of the frequency table. private final int[] mappingDurationFrequencies = new int[16]; private final int[] indexBoundaries = new int[mappingDurationFrequencies.length]; private long instantiations; private long mappings; private long cacheHits; public ResourceModelStatistics() { reset(); int boundary = 1; for (int i = 0; i < indexBoundaries.length; ++i) { indexBoundaries[i] = boundary; boundary *= 2; } } /** * Clears all collected statistical data. */ public void reset() { fill(this.mappingDurationFrequencies, 0); this.instantiations = 0; this.mappings = 0; this.cacheHits = 0; } /** * @return The age of these statistics in terms of {@link System#currentTimeMillis()}. */ public long getSince() { return since; } /** * @return The number of times this resource model instantiated. */ public long getInstantiations() { return instantiations; } /** * Increments the number of times the resource model was instantiated. * * @return this instance. */ public ResourceModelStatistics countInstantiation() { ++this.instantiations; return this; } /** * Increments the number of subsequent mappings. * * @return this instance. */ public ResourceModelStatistics countSubsequentMapping() { ++this.mappings; return this; } /** * @return The number of types a {@link io.neba.api.resourcemodels.ResourceModelCache} contained an instance * of the resource model. */ public long getCacheHits() { return cacheHits; } /** * Increment the number of cache hits for this model. * * @return this instance. */ public ResourceModelStatistics countCacheHit() { ++this.cacheHits; return this; } /** * @return the total number of recorded subsequent resource-to-resourcemodel mappings * that occurred during the mapping of this model. */ public long getNumberOfMappings() { return this.mappings; } /** * Adds the mapping with the duration to the statistics. * * @return this instance. */ public ResourceModelStatistics countMappingDuration(int durationInMs) { // The right-hand interval boundaries are pre-calculated. We start at the smallest (1) of the // interval [0, 1). Thus, when our value is less than the boundary, we know it is in the current interval (i), // as the right-hand boundary is exclusive. for (int i = 0; i < this.indexBoundaries.length; ++i) { if (durationInMs < this.indexBoundaries[i]) { ++this.mappingDurationFrequencies[i]; return this; } } // The mapping duration time exceeds the frequency table boundaries. // Fallback: count it as the longest possible duration ++this.mappingDurationFrequencies[this.mappingDurationFrequencies.length - 1]; return this; } /** * @return the average mapping duration of all {@link #countMappingDuration(int) counted mappings} in ms. */ public double getAverageMappingDuration() { return getTotalMappingDuration() / (double) max(getNumberOfMappingDurationSamples(), 1); } /** * @return the sum of all recorded mapping durations in ms, * i.e. summed up averages of the mapping duration frequency table interval means. */ public double getTotalMappingDuration() { double totalDuration = 0L; double leftBoundary = 0; for (int i = 0; i < this.mappingDurationFrequencies.length; ++i) { double rightBoundary = this.indexBoundaries[i]; double intervalMean = (leftBoundary + rightBoundary) / 2; totalDuration += intervalMean * this.mappingDurationFrequencies[i]; leftBoundary = this.indexBoundaries[i]; } return totalDuration; } /** * @return the median of the mapping durations based on a frequency table. */ public double getMappingDurationMedian() { double median; long numberOfSamples = getNumberOfMappingDurationSamples(); if (numberOfSamples == 0) { return 0; } if (numberOfSamples % 2 == 0) { // Even number of occurrences: The median is the average of the two center most elements (around the 50% mark) long sample = (numberOfSamples / 2L); // Obtain mapping depth x1, x2 at the center double[] depths = mappingDurationOfSampleAndSuccessor(sample); median = (depths[0] + depths[1]) / 2D; } else { // Odd number of occurrences: The median is defined by the sample representing the 50% mark. long sample = (numberOfSamples + 1) / 2L; return mappingDurationOf(sample); } return median; } /** * @return The maximum {@link #countMappingDuration(int) recorded mapping duration} of this resource model in ms. */ public double getMaximumMappingDuration() { for (int i = this.mappingDurationFrequencies.length - 1; i >= 0; --i) { if (this.mappingDurationFrequencies[i] != 0) { double leftHandBoundary = i == 0 ? 0 : this.indexBoundaries[i - 1]; double rightHandBoundary = this.indexBoundaries[i]; return (leftHandBoundary + rightHandBoundary) / 2D; } } return 0; } /** * @return The minimum {@link #countMappingDuration(int) recorded mapping duration} of this resource model in ms. */ public double getMinimumMappingDuration() { for (int i = 0; i < this.mappingDurationFrequencies.length; ++i) { if (this.mappingDurationFrequencies[i] != 0) { double leftHandBoundary = i == 0 ? 0 : this.indexBoundaries[i - 1]; double rightHandBoundary = this.indexBoundaries[i]; return (leftHandBoundary + rightHandBoundary) / 2D; } } return 0; } /** * @return the summed up mapping duration counts, i.e. all frequencies (number of times) any mapping time was recorded. */ private long getNumberOfMappingDurationSamples() { long sum = 0L; for (int samples : this.mappingDurationFrequencies) { sum += samples; } return sum; } /** * @return the mapping duration corresponding to the nth measured duration. * Example: if the duration was measured seven times, get the position of the third sample from the * ordered frequency table; it represents the duration which has 50% of all mapping durations below * and 50% of all mapping durations above: <code>[0 1 2 (3) 4 5 5]</code>. */ private double mappingDurationOf(long nthSample) { int index = 0; for (long sum = 0; sum < nthSample; ++index) { sum += this.mappingDurationFrequencies[index]; } // subtract one: the for-loop always post-increments index. double leftBoundary = index < 2 ? 0 : this.indexBoundaries[index - 2]; double rightBoundary = this.indexBoundaries[index - 1]; return (leftBoundary + rightBoundary) / 2D; } /** * @see #mappingDurationOf(long). Also returns the duration of the sample succeeding the given sample. */ private double[] mappingDurationOfSampleAndSuccessor(long nthSample) { int x1 = -1, x2 = -1; int index = 0; long samples = 0; do { samples += this.mappingDurationFrequencies[index]; if (x1 == -1 && samples >= nthSample) { x1 = index; } if (samples >= nthSample + 1) { x2 = index; break; } ++index; } while (index < this.mappingDurationFrequencies.length); // [n1, n2) -> mean is (n1 + n2) / 2 double leftHandBoundaryX1 = x1 == 0? 0 : this.indexBoundaries[x1 - 1]; double rightHandBoundaryX1 = this.indexBoundaries[x1]; double leftHandBoundaryX2 = x2 == 0? 0 : this.indexBoundaries[x2 - 1]; double rightHandBoundaryX2 = this.indexBoundaries[x2]; // The boundaries are doubles as the intervals [0, 1) and [1, 2) have fraction means (0.5, 1.5) double durationX1 = (leftHandBoundaryX1 + rightHandBoundaryX1) / 2; double durationX2 = (leftHandBoundaryX2 + rightHandBoundaryX2) / 2; return new double[]{durationX1, durationX2}; } public int[] getMappingDurationFrequencies() { return copyOf(this.mappingDurationFrequencies, this.mappingDurationFrequencies.length); } public int[] getMappingDurationIntervalBoundaries() { return copyOf(this.indexBoundaries, this.indexBoundaries.length); } }