/* * Copyright 2015-2016 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 org.glowroot.common.model; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.List; import java.util.zip.DataFormatException; import com.google.common.annotations.VisibleForTesting; import com.google.protobuf.ByteString; import org.HdrHistogram.Histogram; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.glowroot.wire.api.model.AggregateOuterClass.Aggregate; public class LazyHistogram { private static final int HISTOGRAM_SIGNIFICANT_DIGITS = 5; private static final int MAX_VALUES = 1024; private long[] values = new long[8]; private int size; private boolean sorted; private @MonotonicNonNull Histogram histogram; public LazyHistogram() {} // special constructor, histogram created this way cannot be further mutated public LazyHistogram(Aggregate.Histogram hist) { ByteString encodedBytes = hist.getEncodedBytes(); if (encodedBytes.isEmpty()) { List<Long> orderedRawValues = hist.getOrderedRawValueList(); values = new long[orderedRawValues.size()]; for (int i = 0; i < values.length; i++) { values[i] = orderedRawValues.get(i); } size = values.length; } else { histogram = Histogram.decodeFromByteBuffer(encodedBytes.asReadOnlyByteBuffer(), 0); } } public Aggregate.Histogram toProto(ScratchBuffer scratchBuffer) { Aggregate.Histogram.Builder builder = Aggregate.Histogram.newBuilder(); if (histogram == null) { if (!sorted) { // sort values before storing so don't have to sort each time later when calculating // percentiles sortValues(); } for (int i = 0; i < size; i++) { builder.addOrderedRawValue(values[i]); } } else { ByteBuffer buffer = scratchBuffer.getBuffer(histogram.getNeededByteBufferCapacity()); buffer.clear(); histogram.encodeIntoByteBuffer(buffer); int size = buffer.position(); buffer.flip(); builder.setEncodedBytes(ByteString.copyFrom(buffer, size)); } return builder.build(); } public void merge(Aggregate.Histogram toBeMergedHistogram) throws DataFormatException { ByteString encodedBytes = toBeMergedHistogram.getEncodedBytes(); if (encodedBytes.isEmpty()) { for (long rawValue : toBeMergedHistogram.getOrderedRawValueList()) { add(rawValue); } } else { if (histogram == null) { convertValuesToHistogram(); } histogram.add(Histogram.decodeFromByteBuffer(encodedBytes.asReadOnlyByteBuffer(), 0)); } } public void merge(LazyHistogram toBeMergedHistogram) { if (toBeMergedHistogram.histogram == null) { for (int i = 0; i < toBeMergedHistogram.size; i++) { add(toBeMergedHistogram.values[i]); } } else { if (histogram == null) { convertValuesToHistogram(); } histogram.add(toBeMergedHistogram.histogram); } } public long getValueAtPercentile(double percentile) { if (histogram == null) { if (size == 0) { // this is consistent with HdrHistogram behavior return 0; } if (!sorted) { sortValues(); } if (percentile == 0) { // support "0th" percentile to mean the smallest tracked percentile return values[0]; } return values[(int) Math.ceil(size * percentile / 100) - 1]; } return histogram.getValueAtPercentile(percentile); } @VisibleForTesting public void add(long value) { ensureCapacity(size + 1); if (histogram != null) { histogram.recordValue(value); } else { values[size++] = value; sorted = false; } } private void ensureCapacity(int capacity) { if (histogram != null) { return; } if (capacity > MAX_VALUES) { convertValuesToHistogram(); return; } if (capacity > values.length) { // at least double in size long[] temp = new long[Math.max(size * 2, capacity)]; System.arraycopy(values, 0, temp, 0, size); values = temp; } } @EnsuresNonNull("histogram") private void convertValuesToHistogram() { // tracking nanoseconds, but only at microsecond precision (to save histogram space) histogram = new Histogram(1000, 2000, HISTOGRAM_SIGNIFICANT_DIGITS); histogram.setAutoResize(true); for (int i = 0; i < size; i++) { histogram.recordValue(values[i]); } values = new long[0]; } private void sortValues() { Arrays.sort(values, 0, size); sorted = true; } public static class ScratchBuffer { private @MonotonicNonNull ByteBuffer buffer; ByteBuffer getBuffer(int capacity) { if (buffer == null || buffer.capacity() < capacity) { buffer = ByteBuffer.allocate(capacity); } return buffer; } } }