0

Initial commit

This commit is contained in:
eleksir
2022-09-04 02:34:54 +03:00
commit d9de157ad8
1017 changed files with 407553 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
.vscode/
.idea/
.DS_Store
coverage.txt
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Test example output
example.logV2.hlog
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/

21
vendor/github.com/HdrHistogram/hdrhistogram-go/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Coda Hale
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,45 @@
# Go parameters
GOCMD=GO111MODULE=on go
GOBUILD=$(GOCMD) build
GOINSTALL=$(GOCMD) install
GOCLEAN=$(GOCMD) clean
GOTEST=$(GOCMD) test
GOGET=$(GOCMD) get
GOMOD=$(GOCMD) mod
GOFMT=$(GOCMD) fmt
GODOC=godoc
.PHONY: all test coverage
all: test
checkfmt:
@echo 'Checking gofmt';\
bash -c "diff -u <(echo -n) <(gofmt -d .)";\
EXIT_CODE=$$?;\
if [ "$$EXIT_CODE" -ne 0 ]; then \
echo '$@: Go files must be formatted with gofmt'; \
fi && \
exit $$EXIT_CODE
lint:
$(GOGET) github.com/golangci/golangci-lint/cmd/golangci-lint
golangci-lint run
get:
$(GOGET) -v ./...
fmt:
$(GOFMT) ./...
test: get fmt
$(GOTEST) -count=1 ./...
coverage: get test
$(GOTEST) -count=1 -race -coverprofile=coverage.txt -covermode=atomic .
benchmark: get
$(GOTEST) -bench=. -benchmem
godoc:
$(GODOC)

View File

@@ -0,0 +1,78 @@
hdrhistogram-go
===============
<a href="https://pkg.go.dev/github.com/HdrHistogram/hdrhistogram-go"><img src="https://pkg.go.dev/badge/github.com/HdrHistogram/hdrhistogram-go" alt="PkgGoDev"></a>
[![Gitter](https://badges.gitter.im/Join_Chat.svg)](https://gitter.im/HdrHistogram/HdrHistogram)
![Test](https://github.com/HdrHistogram/hdrhistogram-go/workflows/Test/badge.svg?branch=master)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/HdrHistogram/hdrhistogram-go/blob/master/LICENSE)
[![Codecov](https://codecov.io/gh/HdrHistogram/hdrhistogram-go/branch/master/graph/badge.svg)](https://codecov.io/gh/HdrHistogram/hdrhistogram-go)
A pure Go implementation of the [HDR Histogram](https://github.com/HdrHistogram/HdrHistogram).
> A Histogram that supports recording and analyzing sampled data value counts
> across a configurable integer value range with configurable value precision
> within the range. Value precision is expressed as the number of significant
> digits in the value recording, and provides control over value quantization
> behavior across the value range and the subsequent value resolution at any
> given level.
For documentation, check [godoc](https://pkg.go.dev/github.com/HdrHistogram/hdrhistogram-go).
## Getting Started
### Installing
Use `go get` to retrieve the hdrhistogram-go implementation and to add it to your `GOPATH` workspace, or project's Go module dependencies.
```go
go get github.com/HdrHistogram/hdrhistogram-go
```
To update the implementation use `go get -u` to retrieve the latest version of the hdrhistogram.
```go
go get github.com/HdrHistogram/hdrhistogram-go
```
### Go Modules
If you are using Go modules, your `go get` will default to the latest tagged
release version of the histogram. To get a specific release version, use
`@<tag>` in your `go get` command.
```go
go get github.com/HdrHistogram/hdrhistogram-go@v0.9.0
```
To get the latest HdrHistogram/hdrhistogram-go master repository change use `@latest`.
```go
go get github.com/HdrHistogram/hdrhistogram-go@latest
```
### Repo transfer and impact on go dependencies
-------------------------------------------
This repository has been transferred under the github HdrHstogram umbrella with the help from the orginal
author in Sept 2020. The main reasons are to group all implementations under the same roof and to provide more active contribution
from the community as the orginal repository was archived several years ago.
Unfortunately such URL change will break go applications that depend on this library
directly or indirectly, as discussed [here](https://github.com/HdrHistogram/hdrhistogram-go/issues/30#issuecomment-696365251).
The dependency URL should be modified to point to the new repository URL.
The tag "v0.9.0" was applied at the point of transfer and will reflect the exact code that was frozen in the
original repository.
If you are using Go modules, you can update to the exact point of transfter using the `@v0.9.0` tag in your `go get` command.
```
go mod edit -replace github.com/codahale/hdrhistogram=github.com/HdrHistogram/hdrhistogram-go@v0.9.0
```
## Credits
-------
Many thanks for Coda Hale for contributing the initial implementation and transfering the repository here.

782
vendor/github.com/HdrHistogram/hdrhistogram-go/hdr.go generated vendored Normal file
View File

@@ -0,0 +1,782 @@
// Package hdrhistogram provides an implementation of Gil Tene's HDR Histogram
// data structure. The HDR Histogram allows for fast and accurate analysis of
// the extreme ranges of data with non-normal distributions, like latency.
package hdrhistogram
import (
"fmt"
"io"
"math"
"math/bits"
"sort"
)
// A Bracket is a part of a cumulative distribution.
type Bracket struct {
Quantile float64
Count, ValueAt int64
}
// A Snapshot is an exported view of a Histogram, useful for serializing them.
// A Histogram can be constructed from it by passing it to Import.
type Snapshot struct {
LowestTrackableValue int64
HighestTrackableValue int64
SignificantFigures int64
Counts []int64
}
// A Histogram is a lossy data structure used to record the distribution of
// non-normally distributed data (like latency) with a high degree of accuracy
// and a bounded degree of precision.
type Histogram struct {
lowestDiscernibleValue int64
highestTrackableValue int64
unitMagnitude int64
significantFigures int64
subBucketHalfCountMagnitude int32
subBucketHalfCount int32
subBucketMask int64
subBucketCount int32
bucketCount int32
countsLen int32
totalCount int64
counts []int64
startTimeMs int64
endTimeMs int64
tag string
}
func (h *Histogram) Tag() string {
return h.tag
}
func (h *Histogram) SetTag(tag string) {
h.tag = tag
}
func (h *Histogram) EndTimeMs() int64 {
return h.endTimeMs
}
func (h *Histogram) SetEndTimeMs(endTimeMs int64) {
h.endTimeMs = endTimeMs
}
func (h *Histogram) StartTimeMs() int64 {
return h.startTimeMs
}
func (h *Histogram) SetStartTimeMs(startTimeMs int64) {
h.startTimeMs = startTimeMs
}
// Construct a Histogram given the Lowest and Highest values to be tracked and a number of significant decimal digits.
//
// Providing a lowestDiscernibleValue is useful in situations where the units used for the histogram's values are
// much smaller that the minimal accuracy required.
// E.g. when tracking time values stated in nanosecond units, where the minimal accuracy required is a microsecond,
// the proper value for lowestDiscernibleValue would be 1000.
//
// Note: the numberOfSignificantValueDigits must be [1,5]. If lower than 1 the numberOfSignificantValueDigits will be
// forced to 1, and if higher than 5 the numberOfSignificantValueDigits will be forced to 5.
func New(lowestDiscernibleValue, highestTrackableValue int64, numberOfSignificantValueDigits int) *Histogram {
if numberOfSignificantValueDigits < 1 {
numberOfSignificantValueDigits = 1
} else if numberOfSignificantValueDigits > 5 {
numberOfSignificantValueDigits = 5
}
if lowestDiscernibleValue < 1 {
lowestDiscernibleValue = 1
}
// Given a 3 decimal point accuracy, the expectation is obviously for "+/- 1 unit at 1000". It also means that
// it's "ok to be +/- 2 units at 2000". The "tricky" thing is that it is NOT ok to be +/- 2 units at 1999. Only
// starting at 2000. So internally, we need to maintain single unit resolution to 2x 10^decimalPoints.
largestValueWithSingleUnitResolution := 2 * math.Pow10(numberOfSignificantValueDigits)
// We need to maintain power-of-two subBucketCount (for clean direct indexing) that is large enough to
// provide unit resolution to at least largestValueWithSingleUnitResolution. So figure out
// largestValueWithSingleUnitResolution's nearest power-of-two (rounded up), and use that:
subBucketCountMagnitude := int32(math.Ceil(math.Log2(float64(largestValueWithSingleUnitResolution))))
subBucketHalfCountMagnitude := subBucketCountMagnitude
if subBucketHalfCountMagnitude < 1 {
subBucketHalfCountMagnitude = 1
}
subBucketHalfCountMagnitude--
unitMagnitude := int32(math.Floor(math.Log2(float64(lowestDiscernibleValue))))
if unitMagnitude < 0 {
unitMagnitude = 0
}
subBucketCount := int32(math.Pow(2, float64(subBucketHalfCountMagnitude)+1))
subBucketHalfCount := subBucketCount / 2
subBucketMask := int64(subBucketCount-1) << uint(unitMagnitude)
// determine exponent range needed to support the trackable value with no
// overflow:
smallestUntrackableValue := int64(subBucketCount) << uint(unitMagnitude)
bucketsNeeded := getBucketsNeededToCoverValue(smallestUntrackableValue, highestTrackableValue)
bucketCount := bucketsNeeded
countsLen := (bucketCount + 1) * (subBucketCount / 2)
return &Histogram{
lowestDiscernibleValue: lowestDiscernibleValue,
highestTrackableValue: highestTrackableValue,
unitMagnitude: int64(unitMagnitude),
significantFigures: int64(numberOfSignificantValueDigits),
subBucketHalfCountMagnitude: subBucketHalfCountMagnitude,
subBucketHalfCount: subBucketHalfCount,
subBucketMask: subBucketMask,
subBucketCount: subBucketCount,
bucketCount: bucketCount,
countsLen: countsLen,
totalCount: 0,
counts: make([]int64, countsLen),
startTimeMs: 0,
endTimeMs: 0,
tag: "",
}
}
func getBucketsNeededToCoverValue(smallestUntrackableValue int64, maxValue int64) int32 {
// always have at least 1 bucket
bucketsNeeded := int32(1)
for smallestUntrackableValue < maxValue {
if smallestUntrackableValue > (math.MaxInt64 / 2) {
// next shift will overflow, meaning that bucket could represent values up to ones greater than
// math.MaxInt64, so it's the last bucket
return bucketsNeeded + 1
}
smallestUntrackableValue <<= 1
bucketsNeeded++
}
return bucketsNeeded
}
// ByteSize returns an estimate of the amount of memory allocated to the
// histogram in bytes.
//
// N.B.: This does not take into account the overhead for slices, which are
// small, constant, and specific to the compiler version.
func (h *Histogram) ByteSize() int {
return 6*8 + 5*4 + len(h.counts)*8
}
func (h *Histogram) getNormalizingIndexOffset() int32 {
return 1
}
// Merge merges the data stored in the given histogram with the receiver,
// returning the number of recorded values which had to be dropped.
func (h *Histogram) Merge(from *Histogram) (dropped int64) {
i := from.rIterator()
for i.next() {
v := i.valueFromIdx
c := i.countAtIdx
if h.RecordValues(v, c) != nil {
dropped += c
}
}
return
}
// TotalCount returns total number of values recorded.
func (h *Histogram) TotalCount() int64 {
return h.totalCount
}
// Max returns the approximate maximum recorded value.
func (h *Histogram) Max() int64 {
var max int64
i := h.iterator()
for i.next() {
if i.countAtIdx != 0 {
max = i.highestEquivalentValue
}
}
return h.highestEquivalentValue(max)
}
// Min returns the approximate minimum recorded value.
func (h *Histogram) Min() int64 {
var min int64
i := h.iterator()
for i.next() {
if i.countAtIdx != 0 && min == 0 {
min = i.highestEquivalentValue
break
}
}
return h.lowestEquivalentValue(min)
}
// Mean returns the approximate arithmetic mean of the recorded values.
func (h *Histogram) Mean() float64 {
if h.totalCount == 0 {
return 0
}
var total int64
i := h.iterator()
for i.next() {
if i.countAtIdx != 0 {
total += i.countAtIdx * h.medianEquivalentValue(i.valueFromIdx)
}
}
return float64(total) / float64(h.totalCount)
}
// StdDev returns the approximate standard deviation of the recorded values.
func (h *Histogram) StdDev() float64 {
if h.totalCount == 0 {
return 0
}
mean := h.Mean()
geometricDevTotal := 0.0
i := h.iterator()
for i.next() {
if i.countAtIdx != 0 {
dev := float64(h.medianEquivalentValue(i.valueFromIdx)) - mean
geometricDevTotal += (dev * dev) * float64(i.countAtIdx)
}
}
return math.Sqrt(geometricDevTotal / float64(h.totalCount))
}
// Reset deletes all recorded values and restores the histogram to its original
// state.
func (h *Histogram) Reset() {
h.totalCount = 0
for i := range h.counts {
h.counts[i] = 0
}
}
// RecordValue records the given value, returning an error if the value is out
// of range.
func (h *Histogram) RecordValue(v int64) error {
return h.RecordValues(v, 1)
}
// RecordCorrectedValue records the given value, correcting for stalls in the
// recording process. This only works for processes which are recording values
// at an expected interval (e.g., doing jitter analysis). Processes which are
// recording ad-hoc values (e.g., latency for incoming requests) can't take
// advantage of this.
func (h *Histogram) RecordCorrectedValue(v, expectedInterval int64) error {
if err := h.RecordValue(v); err != nil {
return err
}
if expectedInterval <= 0 || v <= expectedInterval {
return nil
}
missingValue := v - expectedInterval
for missingValue >= expectedInterval {
if err := h.RecordValue(missingValue); err != nil {
return err
}
missingValue -= expectedInterval
}
return nil
}
// RecordValues records n occurrences of the given value, returning an error if
// the value is out of range.
func (h *Histogram) RecordValues(v, n int64) error {
idx := h.countsIndexFor(v)
if idx < 0 || int(h.countsLen) <= idx {
return fmt.Errorf("value %d is too large to be recorded", v)
}
h.setCountAtIndex(idx, n)
return nil
}
func (h *Histogram) setCountAtIndex(idx int, n int64) {
h.counts[idx] += n
h.totalCount += n
}
// ValueAtQuantile returns the largest value that (100% - percentile) of the overall recorded value entries
// in the histogram are either larger than or equivalent to.
//
// The passed quantile must be a float64 value in [0.0 .. 100.0]
// Note that two values are "equivalent" if `ValuesAreEquivalent(value1,value2)` would return true.
//
// Returns 0 if no recorded values exist.
func (h *Histogram) ValueAtQuantile(q float64) int64 {
return h.ValueAtPercentile(q)
}
// ValueAtPercentile returns the largest value that (100% - percentile) of the overall recorded value entries
// in the histogram are either larger than or equivalent to.
//
// The passed percentile must be a float64 value in [0.0 .. 100.0]
// Note that two values are "equivalent" if `ValuesAreEquivalent(value1,value2)` would return true.
//
// Returns 0 if no recorded values exist.
func (h *Histogram) ValueAtPercentile(percentile float64) int64 {
if percentile > 100 {
percentile = 100
}
countAtPercentile := int64(((percentile / 100) * float64(h.totalCount)) + 0.5)
valueFromIdx := h.getValueFromIdxUpToCount(countAtPercentile)
if percentile == 0.0 {
return h.lowestEquivalentValue(valueFromIdx)
}
return h.highestEquivalentValue(valueFromIdx)
}
func (h *Histogram) getValueFromIdxUpToCount(countAtPercentile int64) int64 {
var countToIdx int64
var valueFromIdx int64
var subBucketIdx int32 = -1
var bucketIdx int32
bucketBaseIdx := h.getBucketBaseIdx(bucketIdx)
for {
if countToIdx >= countAtPercentile {
break
}
// increment bucket
subBucketIdx++
if subBucketIdx >= h.subBucketCount {
subBucketIdx = h.subBucketHalfCount
bucketIdx++
bucketBaseIdx = h.getBucketBaseIdx(bucketIdx)
}
countToIdx += h.getCountAtIndexGivenBucketBaseIdx(bucketBaseIdx, subBucketIdx)
valueFromIdx = int64(subBucketIdx) << uint(int64(bucketIdx)+h.unitMagnitude)
}
return valueFromIdx
}
// ValueAtPercentiles, given an slice of percentiles returns a map containing for each passed percentile,
// the largest value that (100% - percentile) of the overall recorded value entries
// in the histogram are either larger than or equivalent to.
//
// Each element in the given an slice of percentiles must be a float64 value in [0.0 .. 100.0]
// Note that two values are "equivalent" if `ValuesAreEquivalent(value1,value2)` would return true.
//
// Returns a map of 0's if no recorded values exist.
func (h *Histogram) ValueAtPercentiles(percentiles []float64) (values map[float64]int64) {
sort.Float64s(percentiles)
totalQuantilesToCalculate := len(percentiles)
values = make(map[float64]int64, totalQuantilesToCalculate)
countAtPercentiles := make([]int64, totalQuantilesToCalculate)
for i, percentile := range percentiles {
if percentile > 100 {
percentile = 100
}
values[percentile] = 0
countAtPercentiles[i] = int64(((percentile / 100) * float64(h.totalCount)) + 0.5)
}
total := int64(0)
currentQuantileSlicePos := 0
i := h.iterator()
for currentQuantileSlicePos < totalQuantilesToCalculate && i.nextCountAtIdx(h.totalCount) {
total += i.countAtIdx
for currentQuantileSlicePos < totalQuantilesToCalculate && total >= countAtPercentiles[currentQuantileSlicePos] {
currentPercentile := percentiles[currentQuantileSlicePos]
if currentPercentile == 0.0 {
values[currentPercentile] = h.lowestEquivalentValue(i.valueFromIdx)
} else {
values[currentPercentile] = h.highestEquivalentValue(i.valueFromIdx)
}
currentQuantileSlicePos++
}
}
return
}
// Determine if two values are equivalent with the histogram's resolution.
// Where "equivalent" means that value samples recorded for any two
// equivalent values are counted in a common total count.
func (h *Histogram) ValuesAreEquivalent(value1, value2 int64) (result bool) {
result = h.lowestEquivalentValue(value1) == h.lowestEquivalentValue(value2)
return
}
// CumulativeDistribution returns an ordered list of brackets of the
// distribution of recorded values.
func (h *Histogram) CumulativeDistribution() []Bracket {
var result []Bracket
i := h.pIterator(1)
for i.next() {
result = append(result, Bracket{
Quantile: i.percentile,
Count: i.countToIdx,
ValueAt: i.highestEquivalentValue,
})
}
return result
}
// SignificantFigures returns the significant figures used to create the
// histogram
func (h *Histogram) SignificantFigures() int64 {
return h.significantFigures
}
// LowestTrackableValue returns the lower bound on values that will be added
// to the histogram
func (h *Histogram) LowestTrackableValue() int64 {
return h.lowestDiscernibleValue
}
// HighestTrackableValue returns the upper bound on values that will be added
// to the histogram
func (h *Histogram) HighestTrackableValue() int64 {
return h.highestTrackableValue
}
// Histogram bar for plotting
type Bar struct {
From, To, Count int64
}
// Pretty print as csv for easy plotting
func (b Bar) String() string {
return fmt.Sprintf("%v, %v, %v\n", b.From, b.To, b.Count)
}
// Distribution returns an ordered list of bars of the
// distribution of recorded values, counts can be normalized to a probability
func (h *Histogram) Distribution() (result []Bar) {
i := h.iterator()
for i.next() {
result = append(result, Bar{
Count: i.countAtIdx,
From: h.lowestEquivalentValue(i.valueFromIdx),
To: i.highestEquivalentValue,
})
}
return result
}
// Equals returns true if the two Histograms are equivalent, false if not.
func (h *Histogram) Equals(other *Histogram) bool {
switch {
case
h.lowestDiscernibleValue != other.lowestDiscernibleValue,
h.highestTrackableValue != other.highestTrackableValue,
h.unitMagnitude != other.unitMagnitude,
h.significantFigures != other.significantFigures,
h.subBucketHalfCountMagnitude != other.subBucketHalfCountMagnitude,
h.subBucketHalfCount != other.subBucketHalfCount,
h.subBucketMask != other.subBucketMask,
h.subBucketCount != other.subBucketCount,
h.bucketCount != other.bucketCount,
h.countsLen != other.countsLen,
h.totalCount != other.totalCount:
return false
default:
for i, c := range h.counts {
if c != other.counts[i] {
return false
}
}
}
return true
}
// Export returns a snapshot view of the Histogram. This can be later passed to
// Import to construct a new Histogram with the same state.
func (h *Histogram) Export() *Snapshot {
return &Snapshot{
LowestTrackableValue: h.lowestDiscernibleValue,
HighestTrackableValue: h.highestTrackableValue,
SignificantFigures: h.significantFigures,
Counts: append([]int64(nil), h.counts...), // copy
}
}
// Import returns a new Histogram populated from the Snapshot data (which the
// caller must stop accessing).
func Import(s *Snapshot) *Histogram {
h := New(s.LowestTrackableValue, s.HighestTrackableValue, int(s.SignificantFigures))
h.counts = s.Counts
totalCount := int64(0)
for i := int32(0); i < h.countsLen; i++ {
countAtIndex := h.counts[i]
if countAtIndex > 0 {
totalCount += countAtIndex
}
}
h.totalCount = totalCount
return h
}
func (h *Histogram) iterator() *iterator {
return &iterator{
h: h,
subBucketIdx: -1,
}
}
func (h *Histogram) rIterator() *rIterator {
return &rIterator{
iterator: iterator{
h: h,
subBucketIdx: -1,
},
}
}
func (h *Histogram) pIterator(ticksPerHalfDistance int32) *pIterator {
return &pIterator{
iterator: iterator{
h: h,
subBucketIdx: -1,
},
ticksPerHalfDistance: ticksPerHalfDistance,
}
}
func (h *Histogram) sizeOfEquivalentValueRange(v int64) int64 {
bucketIdx := h.getBucketIndex(v)
return h.sizeOfEquivalentValueRangeGivenBucketIdx(v, bucketIdx)
}
func (h *Histogram) sizeOfEquivalentValueRangeGivenBucketIdx(v int64, bucketIdx int32) int64 {
subBucketIdx := h.getSubBucketIdx(v, bucketIdx)
adjustedBucket := bucketIdx
if subBucketIdx >= h.subBucketCount {
adjustedBucket++
}
return int64(1) << uint(h.unitMagnitude+int64(adjustedBucket))
}
func (h *Histogram) valueFromIndex(bucketIdx, subBucketIdx int32) int64 {
return int64(subBucketIdx) << uint(int64(bucketIdx)+h.unitMagnitude)
}
func (h *Histogram) lowestEquivalentValue(v int64) int64 {
bucketIdx := h.getBucketIndex(v)
return h.lowestEquivalentValueGivenBucketIdx(v, bucketIdx)
}
func (h *Histogram) lowestEquivalentValueGivenBucketIdx(v int64, bucketIdx int32) int64 {
subBucketIdx := h.getSubBucketIdx(v, bucketIdx)
return h.valueFromIndex(bucketIdx, subBucketIdx)
}
func (h *Histogram) nextNonEquivalentValue(v int64) int64 {
bucketIdx := h.getBucketIndex(v)
return h.lowestEquivalentValueGivenBucketIdx(v, bucketIdx) + h.sizeOfEquivalentValueRangeGivenBucketIdx(v, bucketIdx)
}
func (h *Histogram) highestEquivalentValue(v int64) int64 {
return h.nextNonEquivalentValue(v) - 1
}
func (h *Histogram) medianEquivalentValue(v int64) int64 {
return h.lowestEquivalentValue(v) + (h.sizeOfEquivalentValueRange(v) >> 1)
}
func (h *Histogram) getCountAtIndex(bucketIdx, subBucketIdx int32) int64 {
return h.counts[h.countsIndex(bucketIdx, subBucketIdx)]
}
func (h *Histogram) getCountAtIndexGivenBucketBaseIdx(bucketBaseIdx, subBucketIdx int32) int64 {
return h.counts[bucketBaseIdx+subBucketIdx-h.subBucketHalfCount]
}
func (h *Histogram) countsIndex(bucketIdx, subBucketIdx int32) int32 {
return h.getBucketBaseIdx(bucketIdx) + subBucketIdx - h.subBucketHalfCount
}
func (h *Histogram) getBucketBaseIdx(bucketIdx int32) int32 {
return (bucketIdx + 1) << uint(h.subBucketHalfCountMagnitude)
}
// return the lowest (and therefore highest precision) bucket index that can represent the value
// Calculates the number of powers of two by which the value is greater than the biggest value that fits in
// bucket 0. This is the bucket index since each successive bucket can hold a value 2x greater.
func (h *Histogram) getBucketIndex(v int64) int32 {
var pow2Ceiling = int64(64 - bits.LeadingZeros64(uint64(v|h.subBucketMask)))
return int32(pow2Ceiling - int64(h.unitMagnitude) -
int64(h.subBucketHalfCountMagnitude+1))
}
// For bucketIndex 0, this is just value, so it may be anywhere in 0 to subBucketCount.
// For other bucketIndex, this will always end up in the top half of subBucketCount: assume that for some bucket
// k > 0, this calculation will yield a value in the bottom half of 0 to subBucketCount. Then, because of how
// buckets overlap, it would have also been in the top half of bucket k-1, and therefore would have
// returned k-1 in getBucketIndex(). Since we would then shift it one fewer bits here, it would be twice as big,
// and therefore in the top half of subBucketCount.
func (h *Histogram) getSubBucketIdx(v int64, idx int32) int32 {
return int32(v >> uint(int64(idx)+int64(h.unitMagnitude)))
}
func (h *Histogram) countsIndexFor(v int64) int {
bucketIdx := h.getBucketIndex(v)
subBucketIdx := h.getSubBucketIdx(v, bucketIdx)
return int(h.countsIndex(bucketIdx, subBucketIdx))
}
func (h *Histogram) getIntegerToDoubleValueConversionRatio() float64 {
return 1.0
}
type iterator struct {
h *Histogram
bucketIdx, subBucketIdx int32
countAtIdx, countToIdx, valueFromIdx int64
highestEquivalentValue int64
}
// nextCountAtIdx does not update the iterator highestEquivalentValue in order to optimize cpu usage.
func (i *iterator) nextCountAtIdx(limit int64) bool {
if i.countToIdx >= limit {
return false
}
// increment bucket
i.subBucketIdx++
if i.subBucketIdx >= i.h.subBucketCount {
i.subBucketIdx = i.h.subBucketHalfCount
i.bucketIdx++
}
if i.bucketIdx >= i.h.bucketCount {
return false
}
i.countAtIdx = i.h.getCountAtIndex(i.bucketIdx, i.subBucketIdx)
i.countToIdx += i.countAtIdx
i.valueFromIdx = i.h.valueFromIndex(i.bucketIdx, i.subBucketIdx)
return true
}
// Returns the next element in the iteration.
func (i *iterator) next() bool {
if !i.nextCountAtIdx(i.h.totalCount) {
return false
}
i.highestEquivalentValue = i.h.highestEquivalentValue(i.valueFromIdx)
return true
}
type rIterator struct {
iterator
countAddedThisStep int64
}
func (r *rIterator) next() bool {
for r.iterator.next() {
if r.countAtIdx != 0 {
r.countAddedThisStep = r.countAtIdx
return true
}
}
return false
}
type pIterator struct {
iterator
seenLastValue bool
ticksPerHalfDistance int32
percentileToIteratorTo float64
percentile float64
}
func (p *pIterator) next() bool {
if !(p.countToIdx < p.h.totalCount) {
if p.seenLastValue {
return false
}
p.seenLastValue = true
p.percentile = 100
return true
}
if p.subBucketIdx == -1 && !p.iterator.next() {
return false
}
var done = false
for !done {
currentPercentile := (100.0 * float64(p.countToIdx)) / float64(p.h.totalCount)
if p.countAtIdx != 0 && p.percentileToIteratorTo <= currentPercentile {
p.percentile = p.percentileToIteratorTo
halfDistance := math.Trunc(math.Pow(2, math.Trunc(math.Log2(100.0/(100.0-p.percentileToIteratorTo)))+1))
percentileReportingTicks := float64(p.ticksPerHalfDistance) * halfDistance
p.percentileToIteratorTo += 100.0 / percentileReportingTicks
return true
}
done = !p.iterator.next()
}
return true
}
// CumulativeDistribution returns an ordered list of brackets of the
// distribution of recorded values.
func (h *Histogram) CumulativeDistributionWithTicks(ticksPerHalfDistance int32) []Bracket {
var result []Bracket
i := h.pIterator(ticksPerHalfDistance)
for i.next() {
result = append(result, Bracket{
Quantile: i.percentile,
Count: i.countToIdx,
ValueAt: int64(i.highestEquivalentValue),
})
}
return result
}
// Output the percentiles distribution in a text format
func (h *Histogram) PercentilesPrint(writer io.Writer, ticksPerHalfDistance int32, valueScale float64) (outputWriter io.Writer, err error) {
outputWriter = writer
dist := h.CumulativeDistributionWithTicks(ticksPerHalfDistance)
_, err = outputWriter.Write([]byte(" Value\tPercentile\tTotalCount\t1/(1-Percentile)\n\n"))
if err != nil {
return
}
for _, slice := range dist {
percentile := slice.Quantile / 100.0
inverted_percentile := 1.0 / (1.0 - percentile)
var inverted_percentile_string = fmt.Sprintf("%12.2f", inverted_percentile)
// Given that other language implementations display inf (instead of Go's +Inf)
// we want to be as close as possible to them
if math.IsInf(inverted_percentile, 1) {
inverted_percentile_string = fmt.Sprintf("%12s", "inf")
}
_, err = outputWriter.Write([]byte(fmt.Sprintf("%12.3f %12f %12d %s\n", float64(slice.ValueAt)/valueScale, percentile, slice.Count, inverted_percentile_string)))
if err != nil {
return
}
}
footer := fmt.Sprintf("#[Mean = %12.3f, StdDeviation = %12.3f]\n#[Max = %12.3f, Total count = %12d]\n#[Buckets = %12d, SubBuckets = %12d]\n",
h.Mean()/valueScale,
h.StdDev()/valueScale,
float64(h.Max())/valueScale,
h.TotalCount(),
h.bucketCount,
h.subBucketCount,
)
_, err = outputWriter.Write([]byte(footer))
return
}

View File

@@ -0,0 +1,261 @@
// Histograms are encoded using the HdrHistogram V2 format which is based on an adapted ZigZag LEB128 encoding where:
// consecutive zero counters are encoded as a negative number representing the count of consecutive zeros
// non zero counter values are encoded as a positive number
// A typical histogram (2 digits precision 1 usec to 1 day range) can be encoded in less than the typical MTU size of 1500 bytes.
package hdrhistogram
import (
"bytes"
"compress/zlib"
"encoding/base64"
"encoding/binary"
"fmt"
"io/ioutil"
)
const (
V2EncodingCookieBase int32 = 0x1c849303
V2CompressedEncodingCookieBase int32 = 0x1c849304
encodingCookie int32 = V2EncodingCookieBase | 0x10
compressedEncodingCookie int32 = V2CompressedEncodingCookieBase | 0x10
ENCODING_HEADER_SIZE = 40
)
// Encode returns a snapshot view of the Histogram.
// The snapshot is compact binary representations of the state of the histogram.
// They are intended to be used for archival or transmission to other systems for further analysis.
func (h *Histogram) Encode(version int32) (buffer []byte, err error) {
switch version {
case V2CompressedEncodingCookieBase:
buffer, err = h.dumpV2CompressedEncoding()
default:
err = fmt.Errorf("The provided enconding version %d is not supported.", version)
}
return
}
// Decode returns a new Histogram by decoding it from a String containing
// a base64 encoded compressed histogram representation.
func Decode(encoded []byte) (rh *Histogram, err error) {
var decoded []byte
decoded, err = base64.StdEncoding.DecodeString(string(encoded))
if err != nil {
return
}
rbuf := bytes.NewBuffer(decoded[0:8])
r32 := make([]int32, 2)
err = binary.Read(rbuf, binary.BigEndian, &r32)
if err != nil {
return
}
Cookie := r32[0] & ^0xf0
lengthOfCompressedContents := r32[1]
if Cookie != V2CompressedEncodingCookieBase {
err = fmt.Errorf("Encoding not supported, only V2 is supported. Got %d want %d", Cookie, V2CompressedEncodingCookieBase)
return
}
decodeLengthOfCompressedContents := int32(len(decoded[8:]))
if lengthOfCompressedContents > decodeLengthOfCompressedContents {
err = fmt.Errorf("The compressed contents buffer is smaller than the lengthOfCompressedContents. Got %d want %d", decodeLengthOfCompressedContents, lengthOfCompressedContents)
return
}
rh, err = decodeCompressedFormat(decoded[8:8+lengthOfCompressedContents], ENCODING_HEADER_SIZE)
return
}
// internal method to encode an histogram in V2 Compressed format
func (h *Histogram) dumpV2CompressedEncoding() (outBuffer []byte, err error) {
// final buffer
buf := new(bytes.Buffer)
err = binary.Write(buf, binary.BigEndian, compressedEncodingCookie)
if err != nil {
return
}
toCompress, err := h.encodeIntoByteBuffer()
if err != nil {
return
}
uncompressedBytes := toCompress.Bytes()
var b bytes.Buffer
w, err := zlib.NewWriterLevel(&b, zlib.BestCompression)
if err != nil {
return
}
_, err = w.Write(uncompressedBytes)
if err != nil {
return
}
w.Close()
// LengthOfCompressedContents
compressedContents := b.Bytes()
err = binary.Write(buf, binary.BigEndian, int32(len(compressedContents)))
if err != nil {
return
}
err = binary.Write(buf, binary.BigEndian, compressedContents)
if err != nil {
return
}
outBuffer = []byte(base64.StdEncoding.EncodeToString(buf.Bytes()))
return
}
func (h *Histogram) encodeIntoByteBuffer() (*bytes.Buffer, error) {
countsBytes, err := h.fillBufferFromCountsArray()
if err != nil {
return nil, err
}
toCompress := new(bytes.Buffer)
err = binary.Write(toCompress, binary.BigEndian, encodingCookie) // 0-3
if err != nil {
return nil, err
}
err = binary.Write(toCompress, binary.BigEndian, int32(len(countsBytes))) // 3-7
if err != nil {
return nil, err
}
err = binary.Write(toCompress, binary.BigEndian, h.getNormalizingIndexOffset()) // 8-11
if err != nil {
return nil, err
}
err = binary.Write(toCompress, binary.BigEndian, int32(h.significantFigures)) // 12-15
if err != nil {
return nil, err
}
err = binary.Write(toCompress, binary.BigEndian, h.lowestDiscernibleValue) // 16-23
if err != nil {
return nil, err
}
err = binary.Write(toCompress, binary.BigEndian, h.highestTrackableValue) // 24-31
if err != nil {
return nil, err
}
err = binary.Write(toCompress, binary.BigEndian, h.getIntegerToDoubleValueConversionRatio()) // 32-39
if err != nil {
return nil, err
}
err = binary.Write(toCompress, binary.BigEndian, countsBytes)
if err != nil {
return nil, err
}
return toCompress, err
}
func decodeCompressedFormat(compressedContents []byte, headerSize int) (rh *Histogram, err error) {
b := bytes.NewReader(compressedContents)
z, err := zlib.NewReader(b)
if err != nil {
return
}
defer z.Close()
decompressedSlice, err := ioutil.ReadAll(z)
if err != nil {
return
}
decompressedSliceLen := int32(len(decompressedSlice))
cookie, PayloadLength, _, NumberOfSignificantValueDigits, LowestTrackableValue, HighestTrackableValue, _, err := decodeDeCompressedHeaderFormat(decompressedSlice[0:headerSize])
if err != nil {
return
}
if cookie != V2EncodingCookieBase {
err = fmt.Errorf("Encoding not supported, only V2 is supported. Got %d want %d", cookie, V2EncodingCookieBase)
return
}
actualPayloadLen := decompressedSliceLen - int32(headerSize)
if PayloadLength != actualPayloadLen {
err = fmt.Errorf("PayloadLength should have the same size of the actual payload. Got %d want %d", actualPayloadLen, PayloadLength)
return
}
rh = New(LowestTrackableValue, HighestTrackableValue, int(NumberOfSignificantValueDigits))
payload := decompressedSlice[headerSize:]
err = fillCountsArrayFromSourceBuffer(payload, rh)
return rh, err
}
func fillCountsArrayFromSourceBuffer(payload []byte, rh *Histogram) (err error) {
var payloadSlicePos = 0
var dstIndex int64 = 0
var n int
var count int64
var zerosCount int64
for payloadSlicePos < len(payload) {
count, n, err = zig_zag_decode_i64(payload[payloadSlicePos:])
if err != nil {
return
}
payloadSlicePos += n
if count < 0 {
zerosCount = -count
dstIndex += zerosCount
} else {
rh.setCountAtIndex(int(dstIndex), count)
dstIndex += 1
}
}
return
}
func (rh *Histogram) fillBufferFromCountsArray() (buffer []byte, err error) {
buf := new(bytes.Buffer)
// V2 encoding format uses a ZigZag LEB128-64b9B encoded long. Positive values are counts,
// while negative values indicate a repeat zero counts.
var countsLimit int32 = int32(rh.countsIndexFor(rh.Max()) + 1)
var srcIndex int32 = 0
for srcIndex < countsLimit {
count := rh.counts[srcIndex]
srcIndex++
var zeros int64 = 0
// check for contiguous zeros
if count == 0 {
zeros = 1
for srcIndex < countsLimit && 0 == rh.counts[srcIndex] {
zeros++
srcIndex++
}
}
if zeros > 1 {
err = binary.Write(buf, binary.BigEndian, zig_zag_encode_i64(-zeros))
if err != nil {
return
}
} else {
err = binary.Write(buf, binary.BigEndian, zig_zag_encode_i64(count))
if err != nil {
return
}
}
}
buffer = buf.Bytes()
return
}
func decodeDeCompressedHeaderFormat(decoded []byte) (Cookie int32, PayloadLength int32, NormalizingIndexOffSet int32, NumberOfSignificantValueDigits int32, LowestTrackableValue int64, HighestTrackableValue int64, IntegerToDoubleConversionRatio float64, err error) {
rbuf := bytes.NewBuffer(decoded[0:40])
r32 := make([]int32, 4)
r64 := make([]int64, 2)
err = binary.Read(rbuf, binary.BigEndian, &r32)
if err != nil {
return
}
err = binary.Read(rbuf, binary.BigEndian, &r64)
if err != nil {
return
}
err = binary.Read(rbuf, binary.BigEndian, &IntegerToDoubleConversionRatio)
if err != nil {
return
}
Cookie = r32[0] & ^0xf0
PayloadLength = r32[1]
NormalizingIndexOffSet = r32[2]
NumberOfSignificantValueDigits = r32[3]
LowestTrackableValue = r64[0]
HighestTrackableValue = r64[1]
return
}

View File

@@ -0,0 +1,209 @@
package hdrhistogram
import (
"bufio"
"io"
"math"
"regexp"
"strconv"
"strings"
)
type HistogramLogReader struct {
log *bufio.Reader
startTimeSec float64
observedStartTime bool
baseTimeSec float64
observedBaseTime bool
// scanner handling state
absolute bool
rangeStartTimeSec float64
rangeEndTimeSec float64
observedMax bool
rangeObservedMax int64
observedMin bool
rangeObservedMin int64
reStartTime *regexp.Regexp
reBaseTime *regexp.Regexp
reHistogramInterval *regexp.Regexp
}
func (hlr *HistogramLogReader) ObservedMin() bool {
return hlr.observedMin
}
func (hlr *HistogramLogReader) ObservedMax() bool {
return hlr.observedMax
}
// Returns the overall observed max limit ( up to the current point ) of the read histograms
func (hlr *HistogramLogReader) RangeObservedMax() int64 {
return hlr.rangeObservedMax
}
// Returns the overall observed min limit ( up to the current point ) of the read histograms
func (hlr *HistogramLogReader) RangeObservedMin() int64 {
return hlr.rangeObservedMin
}
func NewHistogramLogReader(log io.Reader) *HistogramLogReader {
//# "#[StartTime: %f (seconds since epoch), %s]\n"
reStartTime, _ := regexp.Compile(`#\[StartTime: ([\d\.]*)`)
//# "#[BaseTime: %f (seconds since epoch)]\n"
reBaseTime, _ := regexp.Compile(`#\[BaseTime: ([\d\.]*)`)
//# 0.127,1.007,2.769,HISTFAAAAEV42pNpmSz...
//# Tag=A,0.127,1.007,2.769,HISTFAAAAEV42pNpmSz
//# "%f,%f,%f,%s\n"
reHistogramInterval, _ := regexp.Compile(`([\d\.]*),([\d\.]*),([\d\.]*),(.*)`)
//
reader := bufio.NewReader(log)
return &HistogramLogReader{log: reader,
startTimeSec: 0.0,
observedStartTime: false,
baseTimeSec: 0.0,
observedBaseTime: false,
reStartTime: reStartTime,
reBaseTime: reBaseTime,
reHistogramInterval: reHistogramInterval,
rangeObservedMin: math.MaxInt64,
observedMin: false,
rangeObservedMax: math.MinInt64,
observedMax: false,
}
}
func (hlr *HistogramLogReader) NextIntervalHistogram() (histogram *Histogram, err error) {
return hlr.NextIntervalHistogramWithRange(0.0, math.MaxFloat64, true)
}
func (hlr *HistogramLogReader) NextIntervalHistogramWithRange(rangeStartTimeSec, rangeEndTimeSec float64, absolute bool) (histogram *Histogram, err error) {
hlr.rangeStartTimeSec = rangeStartTimeSec
hlr.rangeEndTimeSec = rangeEndTimeSec
hlr.absolute = absolute
return hlr.decodeNextIntervalHistogram()
}
func (hlr *HistogramLogReader) decodeNextIntervalHistogram() (histogram *Histogram, err error) {
var line string
var tag string = ""
var logTimeStampInSec float64
var intervalLengthSec float64
for {
line, err = hlr.log.ReadString('\n')
if err != nil {
if err == io.EOF {
err = nil
break
}
break
}
if line[0] == '#' {
matchRes := hlr.reStartTime.FindStringSubmatch(line)
if len(matchRes) > 0 {
hlr.startTimeSec, err = strconv.ParseFloat(matchRes[1], 64)
if err != nil {
return
}
hlr.observedStartTime = true
continue
}
matchRes = hlr.reBaseTime.FindStringSubmatch(line)
if len(matchRes) > 0 {
hlr.baseTimeSec, err = strconv.ParseFloat(matchRes[1], 64)
if err != nil {
return
}
hlr.observedBaseTime = true
continue
}
continue
}
if strings.HasPrefix(line, "Tag=") {
commaPos := strings.Index(line, ",")
tag = line[4:commaPos]
line = line[commaPos+1:]
}
matchRes := hlr.reHistogramInterval.FindStringSubmatch(line)
if len(matchRes) >= 1 {
// Decode: startTimestamp, intervalLength, maxTime, histogramPayload
// Timestamp is expected to be in seconds
logTimeStampInSec, err = strconv.ParseFloat(matchRes[1], 64)
if err != nil {
return
}
intervalLengthSec, err = strconv.ParseFloat(matchRes[2], 64)
if err != nil {
return
}
cpayload := matchRes[4]
// No explicit start time noted. Use 1st observed time:
if !hlr.observedStartTime {
hlr.startTimeSec = logTimeStampInSec
hlr.observedStartTime = true
}
// No explicit base time noted.
// Deduce from 1st observed time (compared to start time):
if !hlr.observedBaseTime {
// Criteria Note: if log timestamp is more than a year in
// the past (compared to StartTime),
// we assume that timestamps in the log are not absolute
if logTimeStampInSec < (hlr.startTimeSec - (365 * 24 * 3600.0)) {
hlr.baseTimeSec = hlr.startTimeSec
} else {
hlr.baseTimeSec = 0.0
}
hlr.observedBaseTime = true
}
absoluteStartTimeStampSec := logTimeStampInSec + hlr.baseTimeSec
offsetStartTimeStampSec := absoluteStartTimeStampSec + hlr.startTimeSec
// Timestamp length is expect to be in seconds
absoluteEndTimeStampSec := absoluteStartTimeStampSec + intervalLengthSec
var startTimeStampToCheckRangeOn float64
if hlr.absolute {
startTimeStampToCheckRangeOn = absoluteStartTimeStampSec
} else {
startTimeStampToCheckRangeOn = offsetStartTimeStampSec
}
if startTimeStampToCheckRangeOn < hlr.rangeStartTimeSec {
continue
}
if startTimeStampToCheckRangeOn > hlr.rangeEndTimeSec {
return
}
histogram, err = Decode([]byte(cpayload))
if err != nil {
return
}
if histogram.Max() > hlr.rangeObservedMax {
hlr.rangeObservedMax = histogram.Max()
}
if histogram.Min() < hlr.rangeObservedMin {
hlr.rangeObservedMin = histogram.Min()
}
histogram.SetStartTimeMs(int64(absoluteStartTimeStampSec * 1000.0))
histogram.SetEndTimeMs(int64(absoluteEndTimeStampSec * 1000.0))
if tag != "" {
histogram.SetTag(tag)
}
return
}
}
return
}

View File

@@ -0,0 +1,141 @@
//The log format encodes into a single file, multiple histograms with optional shared meta data.
package hdrhistogram
import (
"fmt"
"io"
"regexp"
"time"
)
const HISTOGRAM_LOG_FORMAT_VERSION = "1.3"
const MsToNsRatio float64 = 1000000.0
type HistogramLogOptions struct {
startTimeStampSec float64
endTimeStampSec float64
maxValueUnitRatio float64
}
func DefaultHistogramLogOptions() *HistogramLogOptions {
return &HistogramLogOptions{0, 0, MsToNsRatio}
}
type HistogramLogWriter struct {
baseTime int64
log io.Writer
}
// Return the current base time offset
func (lw *HistogramLogWriter) BaseTime() int64 {
return lw.baseTime
}
// Set a base time to subtract from supplied histogram start/end timestamps when
// logging based on histogram timestamps.
// baseTime is expected to be in msec since the epoch, as histogram start/end times
// are typically stamped with absolute times in msec since the epoch.
func (lw *HistogramLogWriter) SetBaseTime(baseTime int64) {
lw.baseTime = baseTime
}
func NewHistogramLogWriter(log io.Writer) *HistogramLogWriter {
return &HistogramLogWriter{baseTime: 0, log: log}
}
// Output an interval histogram, using the start/end timestamp indicated in the histogram, and the [optional] tag associated with the histogram.
// The histogram start and end timestamps are assumed to be in msec units
//
// By convention, histogram start/end time are generally stamped with absolute times in msec
// since the epoch. For logging with absolute time stamps, the base time would remain zero ( default ).
// For logging with relative time stamps (time since a start point), the base time should be set with SetBaseTime(baseTime int64)
//
// The max value in the histogram will be reported scaled down by a default maxValueUnitRatio of 1000000.0 (which is the msec : nsec ratio).
// If you need to specify a different start/end timestamp or a different maxValueUnitRatio you should use OutputIntervalHistogramWithLogOptions(histogram *Histogram, logOptions *HistogramLogOptions)
func (lw *HistogramLogWriter) OutputIntervalHistogram(histogram *Histogram) (err error) {
return lw.OutputIntervalHistogramWithLogOptions(histogram, nil)
}
// Output an interval histogram, with the given timestamp information and the [optional] tag associated with the histogram
//
// If you specify non-nil logOptions, and non-zero start timestamp, the the specified timestamp information will be used, and the start timestamp information in the actual histogram will be ignored.
// If you specify non-nil logOptions, and non-zero start timestamp, the the specified timestamp information will be used, and the end timestamp information in the actual histogram will be ignored.
// If you specify non-nil logOptions, The max value reported with the interval line will be scaled by the given maxValueUnitRatio,
// otherwise a default maxValueUnitRatio of 1,000,000 (which is the msec : nsec ratio) will be used.
//
// By convention, histogram start/end time are generally stamped with absolute times in msec
// since the epoch. For logging with absolute time stamps, the base time would remain zero ( default ).
// For logging with relative time stamps (time since a start point), the base time should be set with SetBaseTime(baseTime int64)
func (lw *HistogramLogWriter) OutputIntervalHistogramWithLogOptions(histogram *Histogram, logOptions *HistogramLogOptions) (err error) {
tag := histogram.Tag()
var match bool
tagStr := ""
if tag != "" {
match, err = regexp.MatchString(".[, \\r\\n].", tag)
if err != nil {
return
}
if match {
err = fmt.Errorf("Tag string cannot contain commas, spaces, or line breaks. Used tag: %s", tag)
return
}
tagStr = fmt.Sprintf("Tag=%s,", tag)
}
var usedStartTime float64 = float64(histogram.StartTimeMs())
var usedEndTime float64 = float64(histogram.EndTimeMs())
var maxValueUnitRatio float64 = MsToNsRatio
if logOptions != nil {
if logOptions.startTimeStampSec != 0 {
usedStartTime = logOptions.startTimeStampSec
}
if logOptions.endTimeStampSec != 0 {
usedEndTime = logOptions.endTimeStampSec
}
maxValueUnitRatio = logOptions.maxValueUnitRatio
}
startTime := usedStartTime - float64(lw.baseTime)/1000.0
endTime := usedEndTime - float64(lw.baseTime)/1000.0
maxValueAsDouble := float64(histogram.Max()) / maxValueUnitRatio
cpayload, err := histogram.Encode(V2CompressedEncodingCookieBase)
if err != nil {
return
}
_, err = lw.log.Write([]byte(fmt.Sprintf("%s%f,%f,%f,%s\n", tagStr, startTime, endTime, maxValueAsDouble, string(cpayload))))
return
}
// Log a start time in the log.
// Start time is represented as seconds since epoch with up to 3 decimal places. Line starts with the leading text '#[StartTime:'
func (lw *HistogramLogWriter) OutputStartTime(start_time_msec int64) (err error) {
secs := start_time_msec / 1000
iso_str := time.Unix(secs, start_time_msec%int64(1000)*int64(1000000000)).Format(time.RFC3339)
_, err = lw.log.Write([]byte(fmt.Sprintf("#[StartTime: %d (seconds since epoch), %s]\n", secs, iso_str)))
return
}
// Log a base time in the log.
// Base time is represented as seconds since epoch with up to 3 decimal places. Line starts with the leading text '#[BaseTime:'
func (lw *HistogramLogWriter) OutputBaseTime(base_time_msec int64) (err error) {
secs := base_time_msec / 1000
_, err = lw.log.Write([]byte(fmt.Sprintf("#[Basetime: %d (seconds since epoch)]\n", secs)))
return
}
// Log a comment to the log.
// A comment is any line that leads with '#' that is not matched by the BaseTime or StartTime formats. Comments are ignored when parsed.
func (lw *HistogramLogWriter) OutputComment(comment string) (err error) {
_, err = lw.log.Write([]byte(fmt.Sprintf("#%s\n", comment)))
return
}
// Output a legend line to the log.
// Human readable column headers. Ignored when parsed.
func (lw *HistogramLogWriter) OutputLegend() (err error) {
_, err = lw.log.Write([]byte("\"StartTimestamp\",\"Interval_Length\",\"Interval_Max\",\"Interval_Compressed_Histogram\"\n"))
return
}
// Output a log format version to the log.
func (lw *HistogramLogWriter) OutputLogFormatVersion() (err error) {
return lw.OutputComment(fmt.Sprintf("[Histogram log format version %s]", HISTOGRAM_LOG_FORMAT_VERSION))
}

View File

@@ -0,0 +1,45 @@
package hdrhistogram
// A WindowedHistogram combines histograms to provide windowed statistics.
type WindowedHistogram struct {
idx int
h []Histogram
m *Histogram
Current *Histogram
}
// NewWindowed creates a new WindowedHistogram with N underlying histograms with
// the given parameters.
func NewWindowed(n int, minValue, maxValue int64, sigfigs int) *WindowedHistogram {
w := WindowedHistogram{
idx: -1,
h: make([]Histogram, n),
m: New(minValue, maxValue, sigfigs),
}
for i := range w.h {
w.h[i] = *New(minValue, maxValue, sigfigs)
}
w.Rotate()
return &w
}
// Merge returns a histogram which includes the recorded values from all the
// sections of the window.
func (w *WindowedHistogram) Merge() *Histogram {
w.m.Reset()
for _, h := range w.h {
w.m.Merge(&h)
}
return w.m
}
// Rotate resets the oldest histogram and rotates it to be used as the current
// histogram.
func (w *WindowedHistogram) Rotate() {
w.idx++
w.Current = &w.h[w.idx%len(w.h)]
w.Current.Reset()
}

View File

@@ -0,0 +1,133 @@
package hdrhistogram
import "fmt"
const truncatedErrStr = "Truncated compressed histogram decode. Expected minimum length of %d bytes and got %d."
// Read an LEB128 ZigZag encoded long value from the given buffer
func zig_zag_decode_i64(buf []byte) (signedValue int64, n int, err error) {
buflen := len(buf)
if buflen < 1 {
return 0, 0, nil
}
var value = uint64(buf[0]) & 0x7f
n = 1
if (buf[0] & 0x80) != 0 {
if buflen < 2 {
err = fmt.Errorf(truncatedErrStr, 2, buflen)
return
}
value |= uint64(buf[1]) & 0x7f << 7
n = 2
if (buf[1] & 0x80) != 0 {
if buflen < 3 {
err = fmt.Errorf(truncatedErrStr, 3, buflen)
return
}
value |= uint64(buf[2]) & 0x7f << 14
n = 3
if (buf[2] & 0x80) != 0 {
if buflen < 4 {
err = fmt.Errorf(truncatedErrStr, 4, buflen)
return
}
value |= uint64(buf[3]) & 0x7f << 21
n = 4
if (buf[3] & 0x80) != 0 {
if buflen < 5 {
err = fmt.Errorf(truncatedErrStr, 5, buflen)
return
}
value |= uint64(buf[4]) & 0x7f << 28
n = 5
if (buf[4] & 0x80) != 0 {
if buflen < 6 {
err = fmt.Errorf(truncatedErrStr, 6, buflen)
return
}
value |= uint64(buf[5]) & 0x7f << 35
n = 6
if (buf[5] & 0x80) != 0 {
if buflen < 7 {
err = fmt.Errorf(truncatedErrStr, 7, buflen)
return
}
value |= uint64(buf[6]) & 0x7f << 42
n = 7
if (buf[6] & 0x80) != 0 {
if buflen < 8 {
err = fmt.Errorf(truncatedErrStr, 8, buflen)
return
}
value |= uint64(buf[7]) & 0x7f << 49
n = 8
if (buf[7] & 0x80) != 0 {
if buflen < 9 {
err = fmt.Errorf(truncatedErrStr, 9, buflen)
return
}
value |= uint64(buf[8]) << 56
n = 9
}
}
}
}
}
}
}
}
signedValue = int64((value >> 1) ^ -(value & 1))
return
}
// Writes a int64_t value to the given buffer in LEB128 ZigZag encoded format
// ZigZag encoding maps signed integers to unsigned integers so that numbers with a small
// absolute value (for instance, -1) have a small varint encoded value too.
// It does this in a way that "zig-zags" back and forth through the positive and negative integers,
// so that -1 is encoded as 1, 1 is encoded as 2, -2 is encoded as 3, and so on.
func zig_zag_encode_i64(signedValue int64) (buffer []byte) {
buffer = make([]byte, 0)
var value = uint64((signedValue << 1) ^ (signedValue >> 63))
if value>>7 == 0 {
buffer = append(buffer, byte(value))
} else {
buffer = append(buffer, byte((value&0x7F)|0x80))
if value>>14 == 0 {
buffer = append(buffer, byte(value>>7))
} else {
buffer = append(buffer, byte((value>>7)|0x80))
if value>>21 == 0 {
buffer = append(buffer, byte(value>>14))
} else {
buffer = append(buffer, byte((value>>14)|0x80))
if value>>28 == 0 {
buffer = append(buffer, byte(value>>21))
} else {
buffer = append(buffer, byte((value>>21)|0x80))
if value>>35 == 0 {
buffer = append(buffer, byte(value>>28))
} else {
buffer = append(buffer, byte((value>>28)|0x80))
if value>>42 == 0 {
buffer = append(buffer, byte(value>>35))
} else {
buffer = append(buffer, byte((value>>35)|0x80))
if value>>49 == 0 {
buffer = append(buffer, byte(value>>42))
} else {
buffer = append(buffer, byte((value>>42)|0x80))
if value>>56 == 0 {
buffer = append(buffer, byte(value>>49))
} else {
buffer = append(buffer, byte((value>>49)|0x80))
buffer = append(buffer, byte(value>>56))
}
}
}
}
}
}
}
}
return
}