diff --git a/jrugged-core/src/main/java/org/fishwife/jrugged/FunctionalStatisticallyBalancedPerformanceMonitor.java b/jrugged-core/src/main/java/org/fishwife/jrugged/FunctionalStatisticallyBalancedPerformanceMonitor.java new file mode 100644 index 00000000..85272364 --- /dev/null +++ b/jrugged-core/src/main/java/org/fishwife/jrugged/FunctionalStatisticallyBalancedPerformanceMonitor.java @@ -0,0 +1,629 @@ +/* PerformanceMonitor.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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 org.fishwife.jrugged; + +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +/** + * The {@link FunctionalStatisticallyBalancedPerformanceMonitor} is a convenience wrapper for + * gathering a slew of useful operational metrics about a service, + * including moving averages for latency and request rate over + * various time windows (last minute, last hour, last day). + * + * The intended use is for a client to use the "Decorator" design + * pattern that decorates an existing service with this wrapper. + * Portions of this object can then be exposed via JMX, for example + * to allow for operational polling. + */ +public class FunctionalStatisticallyBalancedPerformanceMonitor implements ServiceWrapper { + + private static final String WRAP_MSG = + "org.fishwife.jrugged.PerformanceMonitor.WRAPPED"; + + private boolean Balance = false; + + private final long startupMillis = System.currentTimeMillis(); + + private static final long ONE_MINUTE_MILLIS = 60L * 1000L; + private static final long ONE_HOUR_MILLIS = ONE_MINUTE_MILLIS * 60L; + private static final long ONE_DAY_MILLIS = ONE_HOUR_MILLIS * 24L; + + private final RequestCounter requestCounter = new RequestCounter(); + private final FlowMeter flowMeter = new FlowMeter(requestCounter); + + private MovingAverage averageSuccessLatencyLastMinute; + private MovingAverage averageSuccessLatencyLastHour; + private MovingAverage averageSuccessLatencyLastDay; + private MovingAverage averageFailureLatencyLastMinute; + private MovingAverage averageFailureLatencyLastHour; + private MovingAverage averageFailureLatencyLastDay; + private MovingAverage totalRequestsPerSecondLastMinute; + private MovingAverage successRequestsPerSecondLastMinute; + private MovingAverage failureRequestsPerSecondLastMinute; + private MovingAverage totalRequestsPerSecondLastHour; + private MovingAverage successRequestsPerSecondLastHour; + private MovingAverage failureRequestsPerSecondLastHour; + private MovingAverage totalRequestsPerSecondLastDay; + private MovingAverage successRequestsPerSecondLastDay; + private MovingAverage failureRequestsPerSecondLastDay; + + private SampledQuantile lifetimeSuccessLatencyQuantile = new SampledQuantile(); + private SampledQuantile lifetimeFailureLatencyQuantile = new SampledQuantile(); + + public StatisticallyBalancedSampledQuantile successLatencyQuantileStatBalancedSample = new StatisticallyBalancedSampledQuantile(1, 300L, TimeUnit.SECONDS,true); + public SampledQuantile successLatencyQuantileNonBalancedSample = new SampledQuantile(125, 300L, TimeUnit.SECONDS); + public SampledQuantile successLatencyQuantilePopulation = new SampledQuantile(1000,300L, TimeUnit.SECONDS); + + private SampledQuantile failureLatencyQuantileLastMinute = new SampledQuantile(60L, TimeUnit.SECONDS); + private SampledQuantile failureLatencyQuantileLastHour = new SampledQuantile(3600L, TimeUnit.SECONDS); + private SampledQuantile failureLatencyQuantileLastDay = new SampledQuantile(86400L, TimeUnit.SECONDS); + + private long lifetimeMaxSuccessMillis; + private long lifetimeMaxFailureMillis; + + /** Default constructor. */ + public FunctionalStatisticallyBalancedPerformanceMonitor() { + createMovingAverages(); + } + + + private void createMovingAverages() { + averageSuccessLatencyLastMinute = new MovingAverage(ONE_MINUTE_MILLIS); + averageSuccessLatencyLastHour = new MovingAverage(ONE_HOUR_MILLIS); + averageSuccessLatencyLastDay = new MovingAverage(ONE_DAY_MILLIS); + averageFailureLatencyLastMinute = new MovingAverage(ONE_MINUTE_MILLIS); + averageFailureLatencyLastHour = new MovingAverage(ONE_HOUR_MILLIS); + averageFailureLatencyLastDay = new MovingAverage(ONE_DAY_MILLIS); + + totalRequestsPerSecondLastMinute = new MovingAverage(ONE_MINUTE_MILLIS); + successRequestsPerSecondLastMinute = new MovingAverage(ONE_MINUTE_MILLIS); + failureRequestsPerSecondLastMinute = new MovingAverage(ONE_MINUTE_MILLIS); + + totalRequestsPerSecondLastHour = new MovingAverage(ONE_HOUR_MILLIS); + successRequestsPerSecondLastHour = new MovingAverage(ONE_HOUR_MILLIS); + failureRequestsPerSecondLastHour = new MovingAverage(ONE_HOUR_MILLIS); + + totalRequestsPerSecondLastDay = new MovingAverage(ONE_DAY_MILLIS); + successRequestsPerSecondLastDay = new MovingAverage(ONE_DAY_MILLIS); + failureRequestsPerSecondLastDay = new MovingAverage(ONE_DAY_MILLIS); + } + + private void recordRequest() { + double[] rates = flowMeter.sample(); + totalRequestsPerSecondLastMinute.update(rates[0]); + totalRequestsPerSecondLastHour.update(rates[0]); + totalRequestsPerSecondLastDay.update(rates[0]); + + successRequestsPerSecondLastMinute.update(rates[1]); + successRequestsPerSecondLastHour.update(rates[1]); + successRequestsPerSecondLastDay.update(rates[1]); + + failureRequestsPerSecondLastMinute.update(rates[2]); + failureRequestsPerSecondLastHour.update(rates[2]); + failureRequestsPerSecondLastDay.update(rates[2]); + } + + private void recordSuccess(LatencyTracker latencyTracker) { + long successMillis = latencyTracker.getLastSuccessMillis(); + averageSuccessLatencyLastMinute.update(successMillis); + averageSuccessLatencyLastHour.update(successMillis); + averageSuccessLatencyLastDay.update(successMillis); + lifetimeSuccessLatencyQuantile.addSample(successMillis); + successLatencyQuantileStatBalancedSample.addSample(successMillis); + successLatencyQuantileNonBalancedSample.addSample(successMillis); + successLatencyQuantilePopulation.addSample(successMillis); + lifetimeMaxSuccessMillis = + (successMillis > lifetimeMaxSuccessMillis) ? + successMillis : lifetimeMaxSuccessMillis; + recordRequest(); + } + + private void recordFailure(LatencyTracker latencyTracker) { + long failureMillis = latencyTracker.getLastFailureMillis(); + averageFailureLatencyLastMinute.update(failureMillis); + averageFailureLatencyLastHour.update(failureMillis); + averageFailureLatencyLastDay.update(failureMillis); + lifetimeFailureLatencyQuantile.addSample(failureMillis); + failureLatencyQuantileLastMinute.addSample(failureMillis); + failureLatencyQuantileLastHour.addSample(failureMillis); + failureLatencyQuantileLastDay.addSample(failureMillis); + lifetimeMaxFailureMillis = + (failureMillis > lifetimeMaxFailureMillis) ? + failureMillis : lifetimeMaxFailureMillis; + recordRequest(); + } + + public T invoke(final Callable c) throws Exception { + final LatencyTracker latencyTracker = new LatencyTracker(); + try { + T result = requestCounter.invoke(new Callable() { + public T call() throws Exception { + return latencyTracker.invoke(c); + } + }); + recordSuccess(latencyTracker); + return result; + } catch (Exception e) { + recordFailure(latencyTracker); + if (WRAP_MSG.equals(e.getMessage())) { + throw (Exception)e.getCause(); + } else { + throw e; + } + } + } + + public void invoke(final Runnable r) throws Exception { + final LatencyTracker latencyTracker = new LatencyTracker(); + try { + requestCounter.invoke(new Runnable() { + public void run() { + try { + latencyTracker.invoke(r); + } catch (Exception e) { + throw new RuntimeException(WRAP_MSG, e); + } + } + }); + recordSuccess(latencyTracker); + } catch (RuntimeException re) { + recordFailure(latencyTracker); + if (WRAP_MSG.equals(re.getMessage())) { + throw (Exception)re.getCause(); + } else { + throw re; + } + } + } + + public T invoke(final Runnable r, T result) throws Exception { + this.invoke(r); + return result; + } + + /** + * Returns the average latency in milliseconds of a successful request, + * as measured over the last minute. + * @return double + */ + public double getAverageSuccessLatencyLastMinute() { + return averageSuccessLatencyLastMinute.getAverage(); + } + + /** + * Returns the average latency in milliseconds of a successful request, + * as measured over the last hour. + * @return double + */ + public double getAverageSuccessLatencyLastHour() { + return averageSuccessLatencyLastHour.getAverage(); + } + + /** + * Returns the average latency in milliseconds of a successful request, + * as measured over the last day. + * @return double + */ + public double getAverageSuccessLatencyLastDay() { + return averageSuccessLatencyLastDay.getAverage(); + } + + /** + * Returns the average latency in milliseconds of a failed request, + * as measured over the last minute. + * @return double + */ + public double getAverageFailureLatencyLastMinute() { + return averageFailureLatencyLastMinute.getAverage(); + } + + /** + * Returns the average latency in milliseconds of a failed request, + * as measured over the last hour. + * @return double + */ + public double getAverageFailureLatencyLastHour() { + return averageFailureLatencyLastHour.getAverage(); + } + + /** + * Returns the average latency in milliseconds of a failed request, + * as measured over the last day. + * @return double + */ + public double getAverageFailureLatencyLastDay() { + return averageFailureLatencyLastDay.getAverage(); + } + + /** + * Returns the average request rate in requests per second of + * all requests, as measured over the last minute. + * @return double + */ + public double getTotalRequestsPerSecondLastMinute() { + return totalRequestsPerSecondLastMinute.getAverage(); + } + + /** + * Returns the average request rate in requests per second of + * successful requests, as measured over the last minute. + * @return double + */ + public double getSuccessRequestsPerSecondLastMinute() { + return successRequestsPerSecondLastMinute.getAverage(); + } + + /** + * Returns the average request rate in requests per second of + * failed requests, as measured over the last minute. + * @return double + */ + public double getFailureRequestsPerSecondLastMinute() { + return failureRequestsPerSecondLastMinute.getAverage(); + } + + /** + * Returns the average request rate in requests per second of + * all requests, as measured over the last hour. + * @return double + */ + public double getTotalRequestsPerSecondLastHour() { + return totalRequestsPerSecondLastHour.getAverage(); + } + + /** + * Returns the average request rate in requests per second of + * successful requests, as measured over the last hour. + * @return double + */ + public double getSuccessRequestsPerSecondLastHour() { + return successRequestsPerSecondLastHour.getAverage(); + } + + /** Returns the average request rate in requests per second of + * failed requests, as measured over the last hour. + * @return double + */ + public double getFailureRequestsPerSecondLastHour() { + return failureRequestsPerSecondLastHour.getAverage(); + } + + /** + * Returns the average request rate in requests per second of + * all requests, as measured over the last day. + * @return double + */ + public double getTotalRequestsPerSecondLastDay() { + return totalRequestsPerSecondLastDay.getAverage(); + } + + /** + * Returns the average request rate in requests per second of + * successful requests, as measured over the last day. + * @return double + */ + public double getSuccessRequestsPerSecondLastDay() { + return successRequestsPerSecondLastDay.getAverage(); + } + + /** + * Returns the average request rate in requests per second of + * failed requests, as measured over the last day. + * @return double + */ + public double getFailureRequestsPerSecondLastDay() { + return failureRequestsPerSecondLastDay.getAverage(); + } + + /** + * Returns the average request rate in requests per second of + * all requests, as measured since this object was initialized. + * @return double + */ + public double getTotalRequestsPerSecondLifetime() { + long deltaT = System.currentTimeMillis() - startupMillis; + return (((double)requestCounter.sample()[0])/(double)deltaT) * 1000; + } + + /** + * Returns the average request rate in requests per second of + * successful requests, as measured since this object was + * initialized. + * @return double + */ + public double getSuccessRequestsPerSecondLifetime() { + long deltaT = System.currentTimeMillis() - startupMillis; + return (((double)requestCounter.sample()[1])/(double)deltaT) * 1000; + } + + /** + * Returns the average request rate in requests per second of + * failed requests, as measured since this object was + * initialized. + * @return double + */ + public double getFailureRequestsPerSecondLifetime() { + long deltaT = System.currentTimeMillis() - startupMillis; + return (((double)requestCounter.sample()[2])/(double)deltaT) * 1000; + } + + /** + * Returns the underlying request counter that this performance + * monitor is using. This can be used in conjunction with + * {@link org.fishwife.jrugged.PercentErrPerTimeFailureInterpreter}. + * + * @return RequestCounter the request count tracker class + */ + public RequestCounter getRequestCounter() { + return this.requestCounter; + } + + /** + * Returns the total number of requests seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor}. + * @return long + */ + public long getRequestCount() { + return requestCounter.sample()[0]; + } + + /** + * Returns the number of successful requests seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor}. + * @return long + */ + public long getSuccessCount() { + return requestCounter.sample()[1]; + } + + /** + * Returns the number of failed requests seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor}. + * @return long + */ + public long getFailureCount() { + return requestCounter.sample()[2]; + } + + /** Returns the median latency seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor} for successful requests. + * @return latency in milliseconds + */ + public long getMedianPercentileSuccessLatencyLifetime() { + return lifetimeSuccessLatencyQuantile.getPercentile(50); + } + + /** Returns the 95th-percentile latency seen by this + * {@link FunctionalStatisticallyBalancedPerformanceMonitor} for successful requests. + * @return latency in milliseconds + */ + public long get95thPercentileSuccessLatencyLifetime() { + return lifetimeSuccessLatencyQuantile.getPercentile(95); + } + + /** Returns the 99th-percentile latency seen by this + * {@link FunctionalStatisticallyBalancedPerformanceMonitor} for successful requests. + * @return latency in milliseconds + */ + public long get99thPercentileSuccessLatencyLifetime() { + return lifetimeSuccessLatencyQuantile.getPercentile(99); + } + + /** Returns the maximum latency seen by this + * {@link FunctionalStatisticallyBalancedPerformanceMonitor} for successful requests. + * @return latency in milliseconds + */ + public long getMaxSuccessLatencyLifetime() { + return lifetimeMaxSuccessMillis; + } + + /** Returns the median latency seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor} for successful requests over the + * last minute. + * @return latency in milliseconds + */ + public long getMedianPercentileSuccessLatencyLastMinute() { + return successLatencyQuantileStatBalancedSample.getPercentile(50); + } + + /** Returns the 95th-percentile latency seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor} for successful requests over the last + * minute. + * @return latency in milliseconds + */ + public long get95thPercentileSuccessLatencyLastMinute() { + return successLatencyQuantileStatBalancedSample.getPercentile(95); + } + + /** Returns the 99th-percentile latency seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor} for successful requests over the last + * minute. + * @return latency in milliseconds + */ + public long get99thPercentileSuccessLatencyLastMinute() { + return successLatencyQuantileStatBalancedSample.getPercentile(99); + } + + /** Returns the median latency seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor} for successful requests over the + * last hour. + * @return latency in milliseconds + */ + public long getMedianPercentileSuccessfulLatencyLastHour() { + return successLatencyQuantileNonBalancedSample.getPercentile(50); + } + + /** Returns the 95th-percentile latency seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor} for successful requests over the last + * hour. + * @return latency in milliseconds + */ + public long get95thPercentileSuccessLatencyLastHour() { + return successLatencyQuantileNonBalancedSample.getPercentile(95); + } + + /** Returns the 99th-percentile latency seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor} for successful requests over the last + * hour. + * @return latency in milliseconds + */ + public long get99thPercentileSuccessLatencyLastHour() { + return successLatencyQuantileNonBalancedSample.getPercentile(99); + } + + /** Returns the median latency seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor} for successful requests over the + * last day. + * @return latency in milliseconds + */ + public long getMedianPercentileSuccessLatencyLastDay() { + return successLatencyQuantilePopulation.getPercentile(50); + } + + /** Returns the 95th-percentile latency seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor} for successful requests over the last + * day. + * @return latency in milliseconds + */ + public long get95thPercentileSuccessLatencyLastDay() { + return successLatencyQuantilePopulation.getPercentile(95); + } + + /** Returns the 99th-percentile latency seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor} for successful requests over the last + * hour. + * @return latency in milliseconds + */ + public long get99thPercentileSuccessLatencyLastDay() { + return successLatencyQuantilePopulation.getPercentile(99); + } + + + /** Returns the median latency seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor} for failed requests. + * @return latency in milliseconds + */ + public long getMedianPercentileFailureLatencyLifetime() { + return lifetimeFailureLatencyQuantile.getPercentile(50); + } + + /** Returns the 95th-percentile latency seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor} for failed requests. + * @return latency in milliseconds + */ + public long get95thPercentileFailureLatencyLifetime() { + return lifetimeFailureLatencyQuantile.getPercentile(95); + } + + /** Returns the 99th-percentile latency seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor} for failed requests. + * @return latency in milliseconds + */ + public long get99thPercentileFailureLatencyLifetime() { + return lifetimeFailureLatencyQuantile.getPercentile(99); + } + + /** Returns the maximum latency seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor} for failed requests. + * @return latency in milliseconds + */ + public long getMaxFailureLatencyLifetime() { + return lifetimeMaxFailureMillis; + } + + /** Returns the median latency seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor} for failed requests over the + * last minute. + * @return latency in milliseconds + */ + public long getMedianPercentileFailureLatencyLastMinute() { + return failureLatencyQuantileLastMinute.getPercentile(50); + } + + /** Returns the 95th-percentile latency seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor} for failed requests over the last + * minute. + * @return latency in milliseconds + */ + public long get95thPercentileFailureLatencyLastMinute() { + return failureLatencyQuantileLastMinute.getPercentile(95); + } + + /** Returns the 99th-percentile latency seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor} for failed requests over the last + * minute. + * @return latency in milliseconds + */ + public long get99thPercentileFailureLatencyLastMinute() { + return failureLatencyQuantileLastMinute.getPercentile(99); + } + + /** Returns the median latency seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor} for failed requests over the + * last hour. + * @return latency in milliseconds + */ + public long getMedianPercentileFailureLatencyLastHour() { + return failureLatencyQuantileLastHour.getPercentile(50); + } + + /** Returns the 95th-percentile latency seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor} for failed requests over the last + * hour. + * @return latency in milliseconds + */ + public long get95thPercentileFailureLatencyLastHour() { + return failureLatencyQuantileLastHour.getPercentile(95); + } + + /** Returns the 99th-percentile latency seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor} for failed requests over the last + * hour. + * @return latency in milliseconds + */ + public long get99thPercentileFailureLatencyLastHour() { + return failureLatencyQuantileLastHour.getPercentile(99); + } + + /** Returns the median latency seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor} for failed requests over the + * last day. + * @return latency in milliseconds + */ + public long getMedianPercentileFailureLatencyLastDay() { + return failureLatencyQuantileLastDay.getPercentile(50); + } + + /** Returns the 95th-percentile latency seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor} for failed requests over the last + * day. + * @return latency in milliseconds + */ + public long get95thPercentileFailureLatencyLastDay() { + return failureLatencyQuantileLastDay.getPercentile(95); + } + + /** Returns the 99th-percentile latency seen by this {@link + * FunctionalStatisticallyBalancedPerformanceMonitor} for failed requests over the last + * hour. + * @return latency in milliseconds + */ + public long get99thPercentileFailureLatencyLastDay() { + return failureLatencyQuantileLastDay.getPercentile(99); + } +} \ No newline at end of file diff --git a/jrugged-core/src/main/java/org/fishwife/jrugged/StatisticallyBalancedSampledQuantile.java b/jrugged-core/src/main/java/org/fishwife/jrugged/StatisticallyBalancedSampledQuantile.java new file mode 100644 index 00000000..3e4502f4 --- /dev/null +++ b/jrugged-core/src/main/java/org/fishwife/jrugged/StatisticallyBalancedSampledQuantile.java @@ -0,0 +1,282 @@ +/* StatisticallyBalancedPerformanceMonitor.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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 org.fishwife.jrugged; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** + * The {@link StatisticallyBalancedSampledQuantile} provides a way to compute approximate quantile measurements + * across a set of samples reported to an instance. By default, these samples are taken across the instance's + * lifetime, but a window can be configured to keep samples across just that trailing time span (for example, + * getting a quantile across the last minute). We let the user specify the amount of memory they want the sample + * quantile to take up. We use an algorithm that keeps a fixed maximum number of samples that selects uniformly + * from all reported samples so far (thus representing a statistically appropriate sampling of the entire + * population). For the windowed version, we keep track of how many samples had been seen over twentieths of the + * window on a rolling basis to ensure that samples are chosen appropriately. + */ +public class StatisticallyBalancedSampledQuantile { + + private boolean statisticalBalance = false; + + private static final int NUM_WINDOW_SEGMENTS = 20; + private static final int DEFAULT_MEM_SIZE = 2; + + private List samples = new ArrayList(); + + private AtomicLong samplesSeen = new AtomicLong(0L); + private int maxSamples = DEFAULT_MEM_SIZE*125; + private Long windowMillis; + + private LinkedList windowSegments; + Random rand = new Random(); + + /** + * Creates a SampleQuantile that keeps a + * default number of samples (according to the default memory) + * within its lifetime. + */ + public StatisticallyBalancedSampledQuantile() { + this(DEFAULT_MEM_SIZE, true); + } + + /** + * Creates a SampleQuantile that keeps a + * given maximum number of samples based off a memory constraint + * across its lifetime. + * + * @param max_Mem the maximum number of memory in kilobytes to allocate + * to samples. Each kilobyte holding a 125 pieces of data + * @param balance Provide a statistical balance to the quantile + */ + public StatisticallyBalancedSampledQuantile(int max_Mem, boolean balance) { + + statisticalBalance = balance; + if (statisticalBalance) { + this.maxSamples = max_Mem * 125; + } + else{ + maxSamples = 200; + } + + } + + /** + * Creates a SampleQuantile that keeps a + * default number of samples based off a memory constraint + * across the specified time window. + * + * @param windowLength size of time window to hold onto samples + * @param units indication of what time units windowLength is specified in + * @param balance Provide a statistical balance to the quantile + */ + public StatisticallyBalancedSampledQuantile(long windowLength, TimeUnit units, boolean balance) { + this(DEFAULT_MEM_SIZE, windowLength, units, balance); + } + + /** + * Creates a SampleQuantile that keeps a + * given maximum number of samples based off a memory constraint + * across the specified time window. + * + * @param max_Mem the maximum number of samples to keep inside of windowLength + * @param windowLength size of time window to hold onto samples + * @param units indication of what time units windowLength is specified in + * @param balance Provide a statistical balance to the quantile + */ + public StatisticallyBalancedSampledQuantile(int max_Mem, long windowLength, TimeUnit units, boolean balance) { + this(max_Mem, windowLength, units, System.currentTimeMillis(), balance); + + } + + /** + * Creates a SampleQuantile that keeps a + * given maximum number of samples based off a memory constraint + * across the specified time window. + * + * @param max_Mem the maximum number of samples to keep inside of windowLength + * @param windowLength size of time window to hold onto samples + * @param units indication of what time units windowLength is specified in + * @param now current time + * @param balance Provide a statistical balance to the quantile + */ + StatisticallyBalancedSampledQuantile(int max_Mem, long windowLength, TimeUnit units, long now, boolean balance) { + statisticalBalance = balance; + this.maxSamples = max_Mem * 125; + setWindowMillis(windowLength, units); + windowSegments = new LinkedList(); + windowSegments.offer(new Sample(samplesSeen.get(), now)); + } + + private void setWindowMillis(long windowLength, TimeUnit units) { + switch(units) { + case NANOSECONDS: windowMillis = windowLength / 1000000; break; + case MICROSECONDS: windowMillis = windowLength / 1000; break; + case MILLISECONDS: windowMillis = windowLength; break; + case SECONDS: windowMillis = windowLength * 1000; break; + default: throw new IllegalArgumentException("Unknown TimeUnit specified"); + } + } + + /** + * Returns the median of the samples seen thus far. + * + * @return long The median measurement + */ + public long getMedian() { + return getPercentile(50); + } + + /** + * Returns the ith percentile of the samples seen + * thus far. This is equivalent to getQuantile(i,100). + * + * @param i must be 0 < i < 100 + * @return i-th percentile, or 0 if there is no data + * @throws StatisticallyBalancedSampledQuantile.QuantileOutOfBoundsException if i <= 0 or i >= 100 + */ + public long getPercentile(int i) { + return getPercentile(i, System.currentTimeMillis()); + } + + long getPercentile(int i, long now) { + return getQuantile(i, 100, now); + } + + /** + * Returns the kth q-quantile of the samples + * seen thus far. + * + * @param q must be >= 2 + * @param k must be 0 < k < q + * @return k-th q-quantile, or 0 if there is no data + * @throws StatisticallyBalancedSampledQuantile.QuantileOutOfBoundsException if k <= 0 or k >= q + */ + public long getQuantile(int k, int q) { + return getQuantile(k, q, System.currentTimeMillis()); + } + + long getQuantile(int k, int q, long now) { + if (k <= 0 || k >= q) throw new QuantileOutOfBoundsException(); + List validSamples = getValidSamples(now); + if (validSamples.size() == 0) return 0; + Collections.sort(validSamples); + double targetIndex = (validSamples.size() * k) / (q * 1.0); + if (validSamples.size() % 2 == 1) { + return validSamples.get((int)Math.ceil(targetIndex) - 1).data; + } + int i0 = (int)Math.floor(targetIndex) - 1; + return (validSamples.get(i0).data + validSamples.get(i0+1).data) / 2; + } + + private List getValidSamples(long now) { + if (windowMillis == null) return samples; + long deadline = now - windowMillis; + List validSamples = new ArrayList(); + for(Sample sample : samples) { + if (sample.timestamp >= deadline) { + validSamples.add(sample); + } + } + return validSamples; + } + + /** + * Reports the number of samples currently held by this + * SampleQuantile. + * + * @return int + */ + public int getNumSamples() { + return samples.size(); + } + + /** + * Reports a sample measurement to be included in the quantile + * calculations. + * + * @param l specific measurement + */ + public void addSample(long l) { + addSample(l, System.currentTimeMillis()); + } + + private synchronized void updateWindowSegments(long now) { + if (windowMillis == null) return; + long deadline = now - windowMillis; + long segmentSize = windowMillis / NUM_WINDOW_SEGMENTS; + while(windowSegments.size() > 0 && windowSegments.peek().timestamp < deadline) { + windowSegments.remove(); + } + long mostRecentSegmentTimestamp = (windowSegments.size() > 0) ? + windowSegments.getLast().timestamp : 0L; + if (windowSegments.size() == 0 + || now - mostRecentSegmentTimestamp > segmentSize) { + windowSegments.offer(new Sample(samplesSeen.get(), now)); + } + } + + private long getEffectiveSamplesSeen() { + if (windowMillis == null) return samplesSeen.get(); + return (samplesSeen.get() - windowSegments.getFirst().data); + } + + void addSample(long l, long now) { + double addratio = maxSamples * 1.0 / getEffectiveSamplesSeen(); + + if (statisticalBalance){ + addratio = .5; + } + samplesSeen.getAndIncrement(); + updateWindowSegments(now); + if (samples.size() < maxSamples) { + samples.add(new Sample(l, now)); + return; + } + if (rand.nextDouble() < addratio) { + int idx = rand.nextInt(maxSamples); + samples.set(idx, new Sample(l, now)); + } + } + + private static class Sample implements Comparable { + public long data; + public long timestamp; + + public Sample(long data, long timestamp) { + this.data = data; + this.timestamp = timestamp; + } + + public int compareTo(Sample other) { + if (other.data > data) return -1; + if (other.data < data) return 1; + if (other.timestamp > timestamp) return -1; + if (other.timestamp < timestamp) return 1; + return 0; + } + } + + public static class QuantileOutOfBoundsException extends RuntimeException { + private static final long serialVersionUID = 1L; + } +} \ No newline at end of file diff --git a/jrugged-core/src/test/java/org/fishwife/jrugged/FunctionalTestStatisticallyBalancedSampledQuantile.java b/jrugged-core/src/test/java/org/fishwife/jrugged/FunctionalTestStatisticallyBalancedSampledQuantile.java new file mode 100644 index 00000000..3f2070f5 --- /dev/null +++ b/jrugged-core/src/test/java/org/fishwife/jrugged/FunctionalTestStatisticallyBalancedSampledQuantile.java @@ -0,0 +1,84 @@ +/* TestPerformanceMonitor.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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 org.fishwife.jrugged; + +import org.junit.Test; +import sun.misc.Perf; + +import java.io.IOException; +import java.net.CookieStore; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + +import static org.junit.Assert.assertEquals; + +public class FunctionalTestStatisticallyBalancedSampledQuantile { + + @Test + public void testRunnableWithResultReturnsResultOnSuccess() throws Exception { + FunctionalStatisticallyBalancedPerformanceMonitor perfMon = new FunctionalStatisticallyBalancedPerformanceMonitor(); + + Integer returnResult = 5000; + + for (int i = 0; i < 300; i++){ + perfMon.invoke(new ConstantSuccessPerformer(1)); + } + + System.out.println(perfMon.get95thPercentileSuccessLatencyLastMinute()); + + System.out.println(perfMon.get95thPercentileSuccessLatencyLastHour()); + + System.out.println(perfMon.get95thPercentileSuccessLatencyLastDay()); + + // assertEquals(returnResult, callResult); + } + + public class ConstantSuccessPerformer implements Runnable { + + private int _totalNumberOfTimesToLoop; + + public ConstantSuccessPerformer(int howManyTimesToLoop) { + _totalNumberOfTimesToLoop = howManyTimesToLoop; + } + + public void run() { + for (long i = 0; i < _totalNumberOfTimesToLoop; i++) { + try { + URL url = new URL("http://www.yahoo.com"); + try { + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + + con.setRequestMethod("GET"); + + //add request header + con.setRequestProperty("User-Agent", "Chrome 41.0"); + + int responseCode = con.getResponseCode(); + } + catch (IOException e){ + System.out.println("bad io"); + } + } + catch (MalformedURLException m){ + System.out.println("Bad url"); + } + } + } + } + +} \ No newline at end of file diff --git a/jrugged-core/src/test/java/org/fishwife/jrugged/TestStatisticallyBalancedSampledQuantile.java b/jrugged-core/src/test/java/org/fishwife/jrugged/TestStatisticallyBalancedSampledQuantile.java new file mode 100644 index 00000000..e0a2a8c9 --- /dev/null +++ b/jrugged-core/src/test/java/org/fishwife/jrugged/TestStatisticallyBalancedSampledQuantile.java @@ -0,0 +1,209 @@ +/* TestSampledQuantile.java + * + * Copyright 2009-2019 Comcast Interactive Media, LLC. + * + * 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 org.fishwife.jrugged; + +import org.junit.Before; +import org.junit.Test; + +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.*; + +public class TestStatisticallyBalancedSampledQuantile { + + private StatisticallyBalancedSampledQuantile impl; + + @Before + public void setUp() { + impl = new StatisticallyBalancedSampledQuantile(); + } + + @Test + public void quantileWithNoSamplesShouldReturnZero() { + assertEquals(0, impl.getPercentile(50)); + } + + @Test + public void quantileWithOneSampleShouldReturnThatSample() { + impl.addSample(42); + assertEquals(42, impl.getPercentile(50)); + } + + @Test + public void medianOfThreeSamplesIsMiddleSample() { + impl.addSample(42); + impl.addSample(41); + impl.addSample(43); + assertEquals(42, impl.getPercentile(50)); + } + + @Test + public void medianOfFiveSamplesWithRepeatsStillWorks() { + impl.addSample(41); + impl.addSample(43); + impl.addSample(42); + impl.addSample(41); + impl.addSample(43); + assertEquals(42, impl.getPercentile(50)); + } + + @Test + public void medianOfTwoSamplesIsTheirAverage() { + impl.addSample(41); + impl.addSample(43); + assertEquals(42, impl.getPercentile(50)); + } + + @Test + public void canGetMedianAsExpressedInQuantiles() { + impl.addSample(42); + impl.addSample(41); + impl.addSample(43); + assertEquals(42, impl.getQuantile(1,2)); + } + + @Test + public void canGetMedianDirectly() { + impl.addSample(42); + impl.addSample(41); + impl.addSample(43); + assertEquals(42, impl.getMedian()); + } + + @Test + public void zerothQuantileShouldThrowException() { + impl.addSample(41); + try { + impl.getQuantile(0,7); + fail("should have thrown exception"); + } catch (StatisticallyBalancedSampledQuantile.QuantileOutOfBoundsException expected) { + } + } + + @Test + public void qthQuantileShouldThrowException() { + impl.addSample(41); + try { + impl.getQuantile(7,7); + fail("should have thrown exception"); + } catch (StatisticallyBalancedSampledQuantile.QuantileOutOfBoundsException expected) { + } + } + + @Test + public void canSpecifyMaxSamples() { + impl = new StatisticallyBalancedSampledQuantile(1, true); + for(int i=0; i<200; i++) impl.addSample(0); + assertEquals(125, impl.getNumSamples()); + } + + @Test + public void canSpecifyCurrentTimeWhenAddingSample() { + impl.addSample(41, System.currentTimeMillis()); + } + + @Test + public void ignoresSamplesOutsideOfSpecifiedSecondWindow() { + impl = new StatisticallyBalancedSampledQuantile(60, TimeUnit.SECONDS, true); + long now = System.currentTimeMillis(); + impl.addSample(7, now - 90 * 1000L); + impl.addSample(42, now); + assertEquals(42, impl.getPercentile(50, now+1)); + } + + @Test + public void ignoresSamplesOutsideOfSpecifiedNanosecondWindow() { + impl = new StatisticallyBalancedSampledQuantile(60 * 1000000000L, TimeUnit.NANOSECONDS, true); + long now = System.currentTimeMillis(); + impl.addSample(7, now - 90 * 1000L); + impl.addSample(42, now); + assertEquals(42, impl.getPercentile(50, now+1)); + } + + @Test + public void ignoresSamplesOutsideOfSpecifiedMicrosecondWindow() { + impl = new StatisticallyBalancedSampledQuantile(60 * 1000000L, TimeUnit.MICROSECONDS, true); + long now = System.currentTimeMillis(); + impl.addSample(7, now - 90 * 1000L); + impl.addSample(42, now); + assertEquals(42, impl.getPercentile(50, now+1)); + } + + @Test + public void ignoresSamplesOutsideOfSpecifiedMillisecondWindow() { + impl = new StatisticallyBalancedSampledQuantile(60 * 1000L, TimeUnit.MILLISECONDS, true); + long now = System.currentTimeMillis(); + impl.addSample(7, now - 90 * 1000L); + impl.addSample(42, now); + assertEquals(42, impl.getPercentile(50, now+1)); + } + + @Test + public void ignoresSamplesOutsideOfSpecifiedMinuteWindow() { + impl = new StatisticallyBalancedSampledQuantile(60L, TimeUnit.SECONDS, true); + long now = System.currentTimeMillis(); + impl.addSample(7, now - 90 * 1000L); + impl.addSample(42, now); + assertEquals(42, impl.getPercentile(50, now+1)); + } + + @Test + public void ignoresSamplesOutsideOfSpecifiedHourWindow() { + impl = new StatisticallyBalancedSampledQuantile(3600L, TimeUnit.SECONDS, true); + long now = System.currentTimeMillis(); + impl.addSample(7, now - 5400 * 1000L); + impl.addSample(42, now); + assertEquals(42, impl.getPercentile(50, now+1)); + } + + @Test + public void ignoresSamplesOutsideOfSpecifiedDayWindow() { + impl = new StatisticallyBalancedSampledQuantile(86400L, TimeUnit.SECONDS, true); + long now = System.currentTimeMillis(); + impl.addSample(7, now - 2 * 24 * 3600 * 1000L); + impl.addSample(42, now); + assertEquals(42, impl.getPercentile(50, now+1)); + } + + @Test + public void windowedSamplingWorks() { + long t0 = System.currentTimeMillis(); + impl = new StatisticallyBalancedSampledQuantile(1, 60L, TimeUnit.SECONDS, t0, true); + for(int t=0; t<30 * 1000; t++) { + impl.addSample(1L, t0 + t); + } + long t1 = t0 + 30 * 1000L; + assertEquals(1L, impl.getPercentile(50, t1)); + + for(int t=0; t<60*1000; t++) { + impl.addSample(2L, t1 + t); + } + long t2 = t1 + 60 * 1000L; + assertEquals(2L, impl.getPercentile(50, t2)); + impl.addSample(3L, t2+1); + } + + @Test + public void windowedSamplingHandlesLongTimesBetweenSamples() { + long t0 = System.currentTimeMillis(); + impl = new StatisticallyBalancedSampledQuantile(10, 60L, TimeUnit.SECONDS, t0, true); + impl.addSample(1L, t0 + 1); + long t1 = t0 + 90 * 1000L; + impl.addSample(2L, t1); + assertEquals(2L, impl.getPercentile(50, t1)); + } +} \ No newline at end of file