build(deps): bump github.com/prometheus/client_golang

Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.23.2 to 1.24.1.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/v1.24.1/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.23.2...v1.24.1)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-version: 1.24.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
dependabot[bot]
2026-08-03 07:47:49 +00:00
committed by Ralf Haferkamp
parent 639e3657cd
commit 7c2cb14a80
78 changed files with 5137 additions and 866 deletions

8
go.mod
View File

@@ -69,7 +69,7 @@ require (
github.com/orcaman/concurrent-map v1.0.0
github.com/pkg/errors v0.9.1
github.com/pkg/xattr v0.4.12
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_golang v1.24.1
github.com/r3labs/sse/v2 v2.10.0
github.com/riandyrn/otelchi v0.12.3
github.com/rogpeppe/go-internal v1.15.0
@@ -255,7 +255,7 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/juliangruber/go-intersect v1.1.0 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/compress v1.19.1 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
github.com/kovidgoyal/go-parallel v1.1.1 // indirect
@@ -325,8 +325,8 @@ require (
github.com/pquerna/cachecontrol v0.2.0 // indirect
github.com/prometheus/alertmanager v0.33.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/prometheus/common v0.70.1 // indirect
github.com/prometheus/procfs v0.21.1 // indirect
github.com/prometheus/statsd_exporter v0.22.8 // indirect
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
github.com/rs/xid v1.6.0 // indirect

16
go.sum
View File

@@ -715,8 +715,8 @@ github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
@@ -1016,8 +1016,8 @@ github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqr
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
github.com/prometheus/client_model v0.0.0-20170216185247-6f3806018612/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
@@ -1037,8 +1037,8 @@ github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/common v0.35.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY=
github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc=
github.com/prometheus/procfs v0.0.0-20170703101242-e645f4e5aaa8/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
@@ -1049,8 +1049,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI=
github.com/prometheus/statsd_exporter v0.22.8 h1:Qo2D9ZzaQG+id9i5NYNGmbf1aa/KxKbB9aKfMS+Yib0=
github.com/prometheus/statsd_exporter v0.22.8/go.mod h1:/DzwbTEaFTE0Ojz5PqcSk6+PFHOPWGxdXVr6yC8eFOM=

View File

@@ -27,6 +27,18 @@ Use the links above for more information on each.
# changelog
* Jul 1st, 2026 [1.19.0](https://github.com/klauspost/compress/releases/tag/v1.19.0)
* zstd: Add true concurrent stream encodingin https://github.com/klauspost/compress/pull/1136
* zstd: arm64 decoder asm by @lizthegrey in https://github.com/klauspost/compress/pull/1160
* flate: Add inflate checkpoints in https://github.com/klauspost/compress/pull/1154
* zstd: avoid unused BuildDict encoder allocation by @snissn in https://github.com/klauspost/compress/pull/1147
* snappy/s2: Limit length of varint in `decodedLen` by @eustas in https://github.com/klauspost/compress/pull/1148
* gzhttp: match qvalue parameter case-insensitively (RFC 7231) by @z9z in https://github.com/klauspost/compress/pull/1149
* zip: add NameDecoder callback for legacy encoding rewrite by @SAY-5 in https://github.com/klauspost/compress/pull/1150
* huff0: Allow building tables from histogram in https://github.com/klauspost/compress/pull/1155
* huff0: Allow building table from oversized histogram in https://github.com/klauspost/compress/pull/1156
* s2sx: Clean symlink targets in https://github.com/klauspost/compress/pull/1163
* Feb 9th, 2026 [1.18.4](https://github.com/klauspost/compress/releases/tag/v1.18.4)
* gzhttp: Add zstandard to server handler wrapper https://github.com/klauspost/compress/pull/1121
* zstd: Add ResetWithOptions to encoder/decoder https://github.com/klauspost/compress/pull/1122

View File

@@ -28,9 +28,10 @@ type dictDecoder struct {
hist []byte // Sliding window history
// Invariant: 0 <= rdPos <= wrPos <= len(hist)
wrPos int // Current output position in buffer
rdPos int // Have emitted hist[:rdPos] already
full bool // Has a full window length been written yet?
wrPos int // Current output position in buffer
rdPos int // Have emitted hist[:rdPos] already
flushed int64 // Total bytes returned by readFlush since init
full bool // Has a full window length been written yet?
}
// init initializes dictDecoder to have a sliding window dictionary of the given
@@ -167,11 +168,22 @@ loop:
return dstPos - dstBase
}
// appendWindow appends the current sliding window (up to len(hist) most recent
// bytes, oldest first) to dst.
func (dd *dictDecoder) appendWindow(dst []byte) []byte {
if dd.full {
dst = append(dst, dd.hist[dd.wrPos:]...)
return append(dst, dd.hist[:dd.wrPos]...)
}
return append(dst, dd.hist[:dd.wrPos]...)
}
// readFlush returns a slice of the historical buffer that is ready to be
// emitted to the user. The data returned by readFlush must be fully consumed
// before calling any other dictDecoder methods.
func (dd *dictDecoder) readFlush() []byte {
toRead := dd.hist[dd.rdPos:dd.wrPos]
dd.flushed += int64(len(toRead))
dd.rdPos = dd.wrPos
if dd.wrPos == len(dd.hist) {
dd.wrPos, dd.rdPos = 0, 0
@@ -179,3 +191,9 @@ func (dd *dictDecoder) readFlush() []byte {
}
return toRead
}
// decoded reports the total number of bytes written into the dictionary since
// init (i.e. excluding any preset dict bytes).
func (dd *dictDecoder) decoded() int64 {
return dd.flushed + int64(dd.wrPos-dd.rdPos)
}

View File

@@ -342,6 +342,11 @@ type decompressor struct {
final bool
flushMode flushMode
cb func(InflateCheckpoint)
cp InflateCheckpoint
hasCP bool // WithResumeFrom was supplied
uncOffset int64 // baseline uncompressed offset (from a resume checkpoint)
cpBuf []byte
}
func (f *decompressor) nextBlock() {
@@ -676,6 +681,18 @@ func (f *decompressor) finishBlock() {
f.toRead = f.dict.readFlush()
}
if f.cb != nil {
bitPos := f.roffset*8 - int64(f.nb)
f.cpBuf = f.dict.appendWindow(f.cpBuf[:0])
f.cb(InflateCheckpoint{
UncompressedOffset: f.uncOffset + f.dict.decoded(),
CompressedOffset: bitPos / 8,
Final: f.final,
BitOffset: uint8(bitPos & 7),
Window: f.cpBuf,
})
}
f.step = nextBlock
}
@@ -806,6 +823,45 @@ func (f *decompressor) Reset(r io.Reader, dict []byte) error {
return nil
}
// ResetCP will adjust the input to the provided checkpoint.
// It is assumed the input stream is forwarded to cp.CompressedOffset.
func (f *decompressor) ResetCP(r io.Reader, cp InflateCheckpoint) error {
*f = decompressor{
r: makeReader(r),
bits: f.bits,
codebits: f.codebits,
h1: f.h1,
h2: f.h2,
dict: f.dict,
step: nextBlock,
cpBuf: f.cpBuf,
}
return f.applyCP(cp)
}
// applyCP seeds the decompressor state from a resume checkpoint:
// loads the sliding window, sets the absolute compressed/uncompressed
// offsets, and skips cp.BitOffset bits into the first input byte so
// the next decode aligns with the start of a deflate block.
func (f *decompressor) applyCP(cp InflateCheckpoint) error {
f.dict.init(maxMatchOffset, cp.Window)
f.roffset = cp.CompressedOffset
f.uncOffset = cp.UncompressedOffset
f.final = cp.Final
f.b = 0
f.nb = 0
if cp.BitOffset > 0 {
c, err := f.r.ReadByte()
if err != nil {
return noEOF(err)
}
f.roffset++
f.b = uint32(c) >> cp.BitOffset
f.nb = 8 - uint(cp.BitOffset)
}
return nil
}
type ReaderOpt func(*decompressor)
// WithPartialBlock tells decompressor to return after each block,
@@ -823,6 +879,36 @@ func WithDict(dict []byte) ReaderOpt {
}
}
// InflateCheckpoint provides a resumable checkpoint for inflate.
type InflateCheckpoint struct {
UncompressedOffset int64 // Byte offset in the decompressed stream
CompressedOffset int64 // Byte offset in the compressed stream
Final bool // True if this is the final block
BitOffset uint8 // 0-7 bits
Window []byte // 32KB sliding window dictionary
}
// WithEobCallback will call the provided function after each block
// with the current gzip checkpoint.
// After returning the provided window can no longer be referenced.
// The callback will not be triggered after a block is marked "final".
// The callback is not retained after Reset.
func WithEobCallback(cb func(InflateCheckpoint)) ReaderOpt {
return func(f *decompressor) {
f.cb = cb
}
}
// WithResumeFrom will adjust the input to the provided checkpoint.
// It is assumed the input stream is forwarded to the provided offset.
// The checkpoint is removed when Reset is called.
func WithResumeFrom(cp InflateCheckpoint) ReaderOpt {
return func(f *decompressor) {
f.cp = cp
f.hasCP = true
}
}
// NewReaderOpts returns new reader with provided options
func NewReaderOpts(r io.Reader, opts ...ReaderOpt) io.ReadCloser {
fixedHuffmanDecoderInit()
@@ -838,6 +924,12 @@ func NewReaderOpts(r io.Reader, opts ...ReaderOpt) io.ReadCloser {
opt(&f)
}
if f.hasCP {
if err := f.applyCP(f.cp); err != nil {
f.err = err
}
}
return &f
}

View File

@@ -10,6 +10,15 @@ import (
"strings"
)
func peekBufio(fr *bufio.Reader) ([]byte, error) {
if fr.Buffered() == 0 {
if _, err := fr.Peek(1); err != nil && fr.Buffered() == 0 {
return nil, err
}
}
return fr.Peek(fr.Buffered())
}
// Decode a single Huffman block from f.
// hl and hd are the Huffman states for the lit/length values
// and the distance values, respectively. If hd == nil, using the
@@ -527,6 +536,8 @@ func (f *decompressor) huffmanBufioReader() {
// but is smart enough to keep local variables in registers, so use nb and b,
// inline call to moreBits and reassign b,nb back to f on return.
fnb, fb, dict := f.nb, f.b, &f.dict
pbuf, _ := fr.Peek(fr.Buffered())
pos := 0
switch f.stepState {
case stateInit:
@@ -548,12 +559,19 @@ readLiteral:
n := uint(f.hl.maxRead)
for {
for fnb < n {
c, err := fr.ReadByte()
if err != nil {
f.b, f.nb = fb, fnb
f.err = noEOF(err)
return
if pos >= len(pbuf) {
fr.Discard(pos)
var err error
pbuf, err = peekBufio(fr)
pos = 0
if len(pbuf) == 0 {
f.b, f.nb = fb, fnb
f.err = noEOF(err)
return
}
}
c := pbuf[pos]
pos++
f.roffset++
fb |= uint32(c) << (fnb & regSizeMaskUint32)
fnb += 8
@@ -566,6 +584,7 @@ readLiteral:
}
if n <= fnb {
if n == 0 {
fr.Discard(pos)
f.b, f.nb = fb, fnb
if debugDecode {
fmt.Println("huffsym: n==0")
@@ -586,6 +605,7 @@ readLiteral:
case v < 256:
dict.writeByte(byte(v))
if dict.availWrite() == 0 {
fr.Discard(pos)
f.toRead = dict.readFlush()
f.step = huffmanBufioReader
f.stepState = stateInit
@@ -594,6 +614,7 @@ readLiteral:
}
goto readLiteral
case v == 256:
fr.Discard(pos)
f.b, f.nb = fb, fnb
f.finishBlock()
return
@@ -605,15 +626,22 @@ readLiteral:
length = int(val.length) + 3
n := uint(val.extra)
for fnb < n {
c, err := fr.ReadByte()
if err != nil {
f.b, f.nb = fb, fnb
if debugDecode {
fmt.Println("morebits n>0:", err)
if pos >= len(pbuf) {
fr.Discard(pos)
var err error
pbuf, err = peekBufio(fr)
pos = 0
if len(pbuf) == 0 {
f.b, f.nb = fb, fnb
if debugDecode {
fmt.Println("morebits n>0:", err)
}
f.err = err
return
}
f.err = err
return
}
c := pbuf[pos]
pos++
f.roffset++
fb |= uint32(c) << (fnb & regSizeMaskUint32)
fnb += 8
@@ -622,6 +650,7 @@ readLiteral:
fb >>= n & regSizeMaskUint32
fnb -= n
default:
fr.Discard(pos)
if debugDecode {
fmt.Println(v, ">= maxNumLit")
}
@@ -633,15 +662,22 @@ readLiteral:
var dist uint32
if f.hd == nil {
for fnb < 5 {
c, err := fr.ReadByte()
if err != nil {
f.b, f.nb = fb, fnb
if debugDecode {
fmt.Println("morebits f.nb<5:", err)
if pos >= len(pbuf) {
fr.Discard(pos)
var err error
pbuf, err = peekBufio(fr)
pos = 0
if len(pbuf) == 0 {
f.b, f.nb = fb, fnb
if debugDecode {
fmt.Println("morebits f.nb<5:", err)
}
f.err = err
return
}
f.err = err
return
}
c := pbuf[pos]
pos++
f.roffset++
fb |= uint32(c) << (fnb & regSizeMaskUint32)
fnb += 8
@@ -660,12 +696,19 @@ readLiteral:
// inline call to moreBits and reassign b,nb back to f on return.
for {
for fnb < n {
c, err := fr.ReadByte()
if err != nil {
f.b, f.nb = fb, fnb
f.err = noEOF(err)
return
if pos >= len(pbuf) {
fr.Discard(pos)
var err error
pbuf, err = peekBufio(fr)
pos = 0
if len(pbuf) == 0 {
f.b, f.nb = fb, fnb
f.err = noEOF(err)
return
}
}
c := pbuf[pos]
pos++
f.roffset++
fb |= uint32(c) << (fnb & regSizeMaskUint32)
fnb += 8
@@ -678,6 +721,7 @@ readLiteral:
}
if n <= fnb {
if n == 0 {
fr.Discard(pos)
f.b, f.nb = fb, fnb
if debugDecode {
fmt.Println("huffsym: n==0")
@@ -701,15 +745,22 @@ readLiteral:
// have 1 bit in bottom of dist, need nb more.
extra := (dist & 1) << (nb & regSizeMaskUint32)
for fnb < nb {
c, err := fr.ReadByte()
if err != nil {
f.b, f.nb = fb, fnb
if debugDecode {
fmt.Println("morebits f.nb<nb:", err)
if pos >= len(pbuf) {
fr.Discard(pos)
var err error
pbuf, err = peekBufio(fr)
pos = 0
if len(pbuf) == 0 {
f.b, f.nb = fb, fnb
if debugDecode {
fmt.Println("morebits f.nb<nb:", err)
}
f.err = err
return
}
f.err = err
return
}
c := pbuf[pos]
pos++
f.roffset++
fb |= uint32(c) << (fnb & regSizeMaskUint32)
fnb += 8
@@ -720,6 +771,7 @@ readLiteral:
dist = 1<<((nb+1)&regSizeMaskUint32) + 1 + extra
// slower: dist = bitMask32[nb+1] + 2 + extra
default:
fr.Discard(pos)
f.b, f.nb = fb, fnb
if debugDecode {
fmt.Println("dist too big:", dist, maxNumDist)
@@ -730,6 +782,7 @@ readLiteral:
// No check on length; encoding can be prescient.
if dist > uint32(dict.histSize()) {
fr.Discard(pos)
f.b, f.nb = fb, fnb
if debugDecode {
fmt.Println("dist > dict.histSize():", dist, dict.histSize())
@@ -752,6 +805,7 @@ copyHistory:
f.copyLen -= cnt
if dict.availWrite() == 0 || f.copyLen > 0 {
fr.Discard(pos)
f.toRead = dict.readFlush()
f.step = huffmanBufioReader // We need to continue this work
f.stepState = stateDict

View File

@@ -0,0 +1,168 @@
package huff0
import "errors"
// BuildCTable builds a Huffman compression table from a precomputed symbol
// histogram and installs it as the previous (reuse) table on s.
//
// After this call:
// - EstimateSize/CanUseTable can probe the table against other histograms.
// - Compress1X/Compress4X with Reuse = ReusePolicyMust will encode without
// emitting a new table header.
// - TransferCTable can hand the table to a sibling Scratch.
//
// count[i] is the number of occurrences of symbol i. The histogram must have
// at least 2 distinct non-zero symbols; ErrUseRLE is returned for a single
// symbol and an error is returned for an empty histogram.
func (s *Scratch) BuildCTable(count *[256]uint32) error {
if s == nil {
return errors.New("huff0: BuildCTable on nil Scratch")
}
if count == nil {
return errors.New("huff0: nil count passed to BuildCTable")
}
var err error
s, err = s.prepare(nil)
if err != nil {
return err
}
s.count = *count
var total, maxCount int
var symLen uint16
for i, v := range s.count {
total += int(v)
if int(v) > maxCount {
maxCount = int(v)
}
if v != 0 {
symLen = uint16(i) + 1
}
}
if total == 0 {
return errors.New("huff0: empty histogram")
}
if symLen < 2 || maxCount == total {
return ErrUseRLE
}
// huff0's internal rank table assumes total ≤ BlockSizeMax (it uses
// highBit32(count+1) + 1 as a rank index into a fixed-size array).
// Histograms summed across multiple blocks can exceed that; scale the
// counts down preserving the distribution. Non-zero entries round up so
// rare symbols stay representable.
if total > BlockSizeMax {
shift := uint(0)
for total>>shift > BlockSizeMax {
shift++
}
round := uint32(1<<shift) - 1
var newTotal, newMax int
for i, v := range s.count {
if v == 0 {
continue
}
scaled := (v + round) >> shift
if scaled == 0 {
scaled = 1
}
s.count[i] = scaled
newTotal += int(scaled)
if int(scaled) > newMax {
newMax = int(scaled)
}
}
total = newTotal
maxCount = newMax
if maxCount == total {
return ErrUseRLE
}
}
s.symbolLen = symLen
s.maxCount = maxCount
s.srcLen = total
if err := s.buildCTable(); err != nil {
return err
}
if cap(s.prevTable) < len(s.cTable) {
s.prevTable = make(cTable, 0, maxSymbolValue+1)
}
s.prevTable = s.prevTable[:len(s.cTable)]
copy(s.prevTable, s.cTable)
s.prevTableLog = s.actualTableLog
// Force the next Compress* to recount from real input.
s.clearCount = true
s.maxCount = 0
return nil
}
// EstimateSize returns an estimated compressed payload size in bytes for the
// supplied histogram using the table currently stored in prevTable. It returns
// -1 when the table cannot encode every non-zero symbol of hist (i.e. when
// CanUseTable would return false). The estimate excludes the table header.
func (s *Scratch) EstimateSize(hist *[256]uint32) int {
if s == nil || hist == nil || len(s.prevTable) == 0 {
return -1
}
pt := s.prevTable
nbBits := uint32(7)
for i, v := range hist {
if v == 0 {
continue
}
if i >= len(pt) || pt[i].nBits == 0 {
return -1
}
nbBits += uint32(pt[i].nBits) * v
}
return int(nbBits >> 3)
}
// CanUseTable reports whether the table in prevTable can encode every
// non-zero symbol present in hist.
func (s *Scratch) CanUseTable(hist *[256]uint32) bool {
if s == nil || hist == nil || len(s.prevTable) == 0 {
return false
}
pt := s.prevTable
for i, v := range hist {
if v == 0 {
continue
}
if i >= len(pt) || pt[i].nBits == 0 {
return false
}
}
return true
}
// AppendTable serializes the table currently stored in prevTable (e.g. as
// installed by BuildCTable or carried over from a previous Compress call)
// into a self-delimiting zstd-style header and appends it to dst. The
// returned slice can be parsed back by ReadTable.
func (s *Scratch) AppendTable(dst []byte) ([]byte, error) {
if s == nil || len(s.prevTable) == 0 {
return dst, errors.New("huff0: AppendTable with empty table")
}
// cTable.write reads s.actualTableLog, s.symbolLen, s.huffWeight, s.fse
// and writes into s.Out. Save/restore Out so we don't disturb in-flight
// compression buffers.
saveOut := s.Out
saveTL := s.actualTableLog
saveSL := s.symbolLen
if s.fse == nil {
// Lazily init in case AppendTable is called on a fresh Scratch.
if _, err := s.prepare(nil); err != nil {
return dst, err
}
saveOut = s.Out
}
s.Out = s.Out[:0]
s.actualTableLog = s.prevTableLog
s.symbolLen = uint16(len(s.prevTable))
if err := s.prevTable.write(s); err != nil {
s.Out, s.actualTableLog, s.symbolLen = saveOut, saveTL, saveSL
return dst, err
}
dst = append(dst, s.Out...)
s.Out, s.actualTableLog, s.symbolLen = saveOut, saveTL, saveSL
return dst, nil
}

View File

@@ -31,7 +31,7 @@ func DecodedLen(src []byte) (int, error) {
// that the length header occupied.
func decodedLen(src []byte) (blockLen, headerLen int, err error) {
v, n := binary.Uvarint(src)
if n <= 0 || v > 0xffffffff {
if n <= 0 || n > 5 || v > 0xffffffff {
return 0, 0, ErrCorrupt
}

View File

@@ -35,7 +35,7 @@ func DecodedLen(src []byte) (int, error) {
// that the length header occupied.
func decodedLen(src []byte) (blockLen, headerLen int, err error) {
v, n := binary.Uvarint(src)
if n <= 0 || v > 0xffffffff {
if n <= 0 || n > 5 || v > 0xffffffff {
return 0, 0, ErrCorrupt
}

View File

@@ -56,10 +56,12 @@ func NewDict(dict []byte) *Dict {
if len(dict) < MinDictSize || len(dict) > MaxDictSize {
return nil
}
d.repeat = int(r)
if d.repeat > len(dict) {
// Compare as uint64: int(r) would wrap negative for r > MaxInt64,
// slipping past the bounds check and causing an OOB read in encode.
if r > uint64(len(dict)) {
return nil
}
d.repeat = int(r)
return &d
}

View File

@@ -981,7 +981,7 @@ searchDict:
cv = load64(src, s)
continue
}
} else if uint32(cv>>(checkRep*8)) == load32(src, s-repeat+checkRep) {
} else if repeat > 0 && uint32(cv>>(checkRep*8)) == load32(src, s-repeat+checkRep) {
base := s + checkRep
// Extend back
for i := base - repeat; base > nextEmit && i > 0 && src[i-1] == src[base-1]; {

View File

@@ -25,7 +25,7 @@ type betterTables struct {
sTable [betterShortTableSize]uint32
}
var betterTablePool = sync.Pool{New: func() interface{} { return &betterTables{} }}
var betterTablePool = sync.Pool{New: func() any { return &betterTables{} }}
// betterSnappyTables holds better-snappy compression hash tables.
type betterSnappyTables struct {
@@ -33,7 +33,7 @@ type betterSnappyTables struct {
sTable [betterShortTableSize]uint32
}
var betterSnappyTablePool = sync.Pool{New: func() interface{} { return &betterSnappyTables{} }}
var betterSnappyTablePool = sync.Pool{New: func() any { return &betterSnappyTables{} }}
// bestTables holds best compression hash tables.
type bestTables struct {
@@ -41,7 +41,7 @@ type bestTables struct {
sTable [bestShortTableSize]uint64
}
var bestTablePool = sync.Pool{New: func() interface{} { return &bestTables{} }}
var bestTablePool = sync.Pool{New: func() any { return &bestTables{} }}
// getBetterTables gets a zeroed betterTables from the pool.
func getBetterTables() *betterTables {

View File

@@ -216,13 +216,18 @@ func (r *Reader) skippable(tmp []byte, n int, allowEOF bool, id uint8) (ok bool)
return r.err == nil
}
if rs, ok := r.r.(io.ReadSeeker); ok {
_, err := rs.Seek(int64(n), io.SeekCurrent)
if err == nil {
return true
}
if err == io.ErrUnexpectedEOF || (r.err == io.EOF && !allowEOF) {
r.err = ErrCorrupt
return false
if cur, err := rs.Seek(0, io.SeekCurrent); err == nil {
if end, err := rs.Seek(0, io.SeekEnd); err == nil {
if cur+int64(n) <= end {
if _, err := rs.Seek(cur+int64(n), io.SeekStart); err == nil {
return true
}
}
if _, err := rs.Seek(cur, io.SeekStart); err != nil {
r.err = ErrCorrupt
return false
}
}
}
}
for n > 0 {

View File

@@ -0,0 +1,130 @@
// Copyright 2011 The Snappy-Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package snappy
import "encoding/binary"
// DecodeStrict returns the decoded form of src, like Decode.
//
// Unlike Decode, which delegates to the s2 decoder and accepts the s2
// "repeat last offset" extension (a copy whose encoded offset is 0),
// DecodeStrict uses a strict standard-Snappy block decoder. Offset 0 is not
// valid in the standard Snappy block format, so such a copy is rejected with
// ErrCorrupt. This makes DecodeStrict match github.com/golang/snappy and the
// C++ reference decoder, at the cost of being slower than Decode.
//
// The returned slice may be a sub-slice of dst if dst was large enough to
// hold the entire decoded block. Otherwise, a newly allocated slice will be
// returned.
//
// The dst and src must not overlap. It is valid to pass a nil dst.
//
// DecodeStrict handles the Snappy block format, not the Snappy stream format.
func DecodeStrict(dst, src []byte) ([]byte, error) {
v, n := binary.Uvarint(src)
if n <= 0 || n > 5 || v > 0xffffffff {
return nil, ErrCorrupt
}
const wordSize = 32 << (^uint(0) >> 32 & 1)
if wordSize == 32 && v > 0x7fffffff {
return nil, ErrTooLarge
}
dLen := int(v)
if dLen <= cap(dst) {
dst = dst[:dLen]
} else {
dst = make([]byte, dLen)
}
if decodeStrict(dst, src[n:]) != 0 {
return nil, ErrCorrupt
}
return dst, nil
}
func decodeStrict(dst, src []byte) int {
var d, s, offset, length int
for s < len(src) {
switch src[s] & 0x03 {
case 0x00:
x := uint32(src[s] >> 2)
switch {
case x < 60:
s++
case x == 60:
s += 2
if uint(s) > uint(len(src)) {
return 1
}
x = uint32(src[s-1])
case x == 61:
s += 3
if uint(s) > uint(len(src)) {
return 1
}
x = uint32(src[s-2]) | uint32(src[s-1])<<8
case x == 62:
s += 4
if uint(s) > uint(len(src)) {
return 1
}
x = uint32(src[s-3]) | uint32(src[s-2])<<8 | uint32(src[s-1])<<16
case x == 63:
s += 5
if uint(s) > uint(len(src)) {
return 1
}
x = uint32(src[s-4]) | uint32(src[s-3])<<8 | uint32(src[s-2])<<16 | uint32(src[s-1])<<24
}
length = int(x) + 1
if length <= 0 || length > len(dst)-d || length > len(src)-s {
return 1
}
copy(dst[d:], src[s:s+length])
d += length
s += length
continue
case 0x01:
s += 2
if uint(s) > uint(len(src)) {
return 1
}
length = 4 + int(src[s-2])>>2&0x7
offset = int(uint32(src[s-2])&0xe0<<3 | uint32(src[s-1]))
case 0x02:
s += 3
if uint(s) > uint(len(src)) {
return 1
}
length = 1 + int(src[s-3])>>2
offset = int(uint32(src[s-2]) | uint32(src[s-1])<<8)
case 0x03:
s += 5
if uint(s) > uint(len(src)) {
return 1
}
length = 1 + int(src[s-5])>>2
offset = int(uint32(src[s-4]) | uint32(src[s-3])<<8 | uint32(src[s-2])<<16 | uint32(src[s-1])<<24)
}
if offset <= 0 || d < offset || length > len(dst)-d {
return 1
}
if offset >= length {
copy(dst[d:d+length], dst[d-offset:])
d += length
continue
}
a := dst[d : d+length]
b := dst[d-offset:]
b = b[:len(a)]
for i := range a {
a[i] = b[i]
}
d += length
}
if d != len(dst) {
return 1
}
return 0
}

View File

@@ -75,14 +75,47 @@ The above is fine for big encodes. However, whenever possible try to *reuse* the
To reuse the encoder, you can use the `Reset(io.Writer)` function to change to another output.
This will allow the encoder to reuse all resources and avoid wasteful allocations.
Currently stream encoding has 'light' concurrency, meaning up to 2 goroutines can be working on part
of a stream. This is independent of the `WithEncoderConcurrency(n)`, but that is likely to change
By default, stream encoding has 'light' concurrency, meaning up to 2 goroutines can be working on part
of a stream. This is independent of the `WithEncoderConcurrency(n)`, but that is likely to change
in the future. So if you want to limit concurrency for future updates, specify the concurrency
you would like.
If you would like stream encoding to be done without spawning async goroutines, use `WithEncoderConcurrency(1)`
which will compress input as each block is completed, blocking on writes until each has completed.
#### Parallel Stream Compression
For maximum throughput on large streams, use `WithConcurrentBlocks(true)` together with
`WithEncoderConcurrency(n)` where n is the number of CPU cores you want to use.
This splits the input into large sections (jobs) that are compressed simultaneously by multiple goroutines,
similar to how the C zstd library does multithreaded compression.
```Go
enc, err := zstd.NewWriter(out,
zstd.WithEncoderLevel(zstd.SpeedDefault),
zstd.WithEncoderConcurrency(runtime.GOMAXPROCS(0)),
zstd.WithConcurrentBlocks(true),
)
```
Each non-first job receives an overlap prefix from the previous job for match context,
so compression ratio is only marginally affected. Output is flushed in order,
producing a valid single-frame zstd stream.
Benchmark on 1.8GB GOB stream (AMD Ryzen 9 9950X):
| Level | 1 thread | 4 threads | 16 threads | 1T ratio | 16T ratio |
|---------|:----------:|:------------------:|:-------------------:|:--------:|:---------:|
| fastest | 783 MB/s | 2950 MB/s (3.8×) | 6939 MB/s (8.9×) | 12.24% | 12.26% |
| default | 728 MB/s | 2533 MB/s (3.5×) | 5340 MB/s (7.3×) | 10.67% | 10.68% |
| better | 434 MB/s | 1105 MB/s (2.5×) | 2206 MB/s (5.1×) | 9.14% | 9.21% |
| best | 129 MB/s | 367 MB/s (2.8×) | 884 MB/s (6.8×) | 8.48% | 8.63% |
Notes:
* Not compatible with dictionary encoding.
* `Flush()` dispatches the current partial job, so latency-sensitive callers can force output.
* `EncodeAll` is unaffected — it uses its own concurrency via the encoder pool.
You can specify your desired compression level using `WithEncoderLevel()` option. Currently only pre-defined
compression settings can be specified.

View File

@@ -230,7 +230,7 @@ func BuildDict(o BuildDictOptions) ([]byte, error) {
}
block := blockEnc{lowMem: false}
block.init()
enc := encoder(&bestFastEncoder{fastBase: fastBase{maxMatchOff: int32(maxMatchLen), bufferReset: math.MaxInt32 - int32(maxMatchLen*2), lowMem: false}})
var enc encoder
if o.Level != 0 {
eOpts := encoderOptions{
level: o.Level,
@@ -242,6 +242,7 @@ func BuildDict(o BuildDictOptions) ([]byte, error) {
enc = eOpts.encoder()
} else {
o.Level = SpeedBestCompression
enc = encoder(&bestFastEncoder{fastBase: fastBase{maxMatchOff: int32(maxMatchLen), bufferReset: math.MaxInt32 - int32(maxMatchLen*2), lowMem: false}})
}
var (
remain [256]int

View File

@@ -128,6 +128,34 @@ func (e *fastBase) matchlen(s, t int32, src []byte) int32 {
return int32(matchLen(src[s:], src[t:]))
}
// resetBasePrefix resets the encoder state and loads prefix as initial history.
// This is used for parallel job encoding where non-first jobs need overlap context.
// Rep offsets are set to defaults [1,4,8] (invalidated, matching C behavior).
func (e *fastBase) resetBasePrefix(prefix []byte) {
if e.blk == nil {
e.blk = &blockEnc{lowMem: e.lowMem}
e.blk.init()
} else {
e.blk.reset(nil)
}
e.blk.initNewEncode()
if e.crc == nil {
e.crc = xxhash.New()
} else {
e.crc.Reset()
}
e.blk.dictLitEnc = nil
e.ensureHist(len(prefix) + maxCompressedBlockSize)
// Bump cur so old table entries fall outside the window.
// When cur >= bufferReset, leave it; the first Encode call
// will shift/clear tables, preserving valid prefix entries.
if e.cur < e.bufferReset {
e.cur += e.maxMatchOff + int32(len(e.hist))
}
e.hist = e.hist[:0]
e.hist = append(e.hist, prefix...)
}
// Reset the encoding table.
func (e *fastBase) resetBase(d *dict, singleBlock bool) {
if e.blk == nil {

View File

@@ -551,3 +551,18 @@ func (e *bestFastEncoder) Reset(d *dict, singleBlock bool) {
// Reset table to initial state
copy(e.table[:], e.dictTable)
}
func (e *bestFastEncoder) ResetPrefix(prefix []byte) {
e.resetBasePrefix(prefix)
if len(prefix) < 8 {
return
}
end := e.cur + int32(len(prefix)) - 8
for i := e.cur; i < end; i++ {
cv := load6432(prefix, i-e.cur)
h := hashLen(cv, bestLongTableBits, bestLongLen)
e.longTable[h] = prevEntry{offset: i, prev: e.longTable[h].offset}
h0 := hashLen(cv, bestShortTableBits, bestShortLen)
e.table[h0] = prevEntry{offset: i, prev: e.table[h0].offset}
}
}

View File

@@ -1096,6 +1096,20 @@ func (e *betterFastEncoder) Reset(d *dict, singleBlock bool) {
}
}
func (e *betterFastEncoder) ResetPrefix(prefix []byte) {
e.resetBasePrefix(prefix)
if len(prefix) < 8 {
return
}
end := e.cur + int32(len(prefix)) - 8
for i := e.cur; i < end; i += 2 {
cv := load6432(prefix, i-e.cur)
h := hashLen(cv, betterLongTableBits, betterLongLen)
e.longTable[h] = prevEntry{offset: i, prev: e.longTable[h].offset}
e.table[hashLen(cv>>8, betterShortTableBits, betterShortLen)] = tableEntry{val: uint32(cv >> 8), offset: i + 1}
}
}
// ResetDict will reset and set a dictionary if not nil
func (e *betterFastEncoderDict) Reset(d *dict, singleBlock bool) {
e.resetBase(d, singleBlock)
@@ -1229,6 +1243,10 @@ func (e *betterFastEncoderDict) Reset(d *dict, singleBlock bool) {
e.allDirty = false
}
func (e *betterFastEncoderDict) ResetPrefix([]byte) {
panic("ResetPrefix not supported for dict encoders")
}
func (e *betterFastEncoderDict) markLongShardDirty(entryNum uint32) {
e.longTableShardDirty[entryNum/betterLongTableShardSize] = true
}

View File

@@ -1037,6 +1037,18 @@ func (e *doubleFastEncoder) Reset(d *dict, singleBlock bool) {
}
}
func (e *doubleFastEncoder) ResetPrefix(prefix []byte) {
e.fastEncoder.ResetPrefix(prefix)
if len(prefix) < 8 {
return
}
end := e.cur + int32(len(prefix)) - 8
for i := e.cur + 1; i < end; i += 2 {
cv := load6432(prefix, i-e.cur)
e.longTable[hashLen(cv, dFastLongTableBits, dFastLongLen)] = tableEntry{val: uint32(cv), offset: i}
}
}
// ResetDict will reset and set a dictionary if not nil
func (e *doubleFastEncoderDict) Reset(d *dict, singleBlock bool) {
allDirty := e.allDirty
@@ -1102,6 +1114,10 @@ func (e *doubleFastEncoderDict) Reset(d *dict, singleBlock bool) {
}
}
func (e *doubleFastEncoderDict) ResetPrefix([]byte) {
panic("ResetPrefix not supported for dict encoders")
}
func (e *doubleFastEncoderDict) markLongShardDirty(entryNum uint32) {
e.longTableShardDirty[entryNum/dLongTableShardSize] = true
}

View File

@@ -797,6 +797,19 @@ func (e *fastEncoder) Reset(d *dict, singleBlock bool) {
}
}
func (e *fastEncoder) ResetPrefix(prefix []byte) {
e.resetBasePrefix(prefix)
if len(prefix) < 8 {
return
}
end := e.cur + int32(len(prefix)) - 8
// Index every 4th
for i := e.cur + 1; i < end; i += 4 {
cv := load6432(prefix, i-e.cur)
e.table[hashLen(cv, tableBits, tableFastHashLen)] = tableEntry{val: uint32(cv), offset: i}
}
}
// ResetDict will reset and set a dictionary if not nil
func (e *fastEncoderDict) Reset(d *dict, singleBlock bool) {
e.resetBase(d, singleBlock)
@@ -866,6 +879,10 @@ func (e *fastEncoderDict) Reset(d *dict, singleBlock bool) {
e.allDirty = false
}
func (e *fastEncoderDict) ResetPrefix([]byte) {
panic("ResetPrefix not supported for dict encoders")
}
func (e *fastEncoderDict) markAllShardsDirty() {
e.allDirty = true
}

352
vendor/github.com/klauspost/compress/zstd/enc_jobs.go generated vendored Normal file
View File

@@ -0,0 +1,352 @@
// Copyright 2019+ Klaus Post. All rights reserved.
// License information can be found in the LICENSE file.
// Based on work by Yann Collet, released under BSD License.
package zstd
import (
"fmt"
rdebug "runtime/debug"
"sync"
)
type encJob struct {
prefix []byte // overlap from previous job (nil for first)
input []byte // job's own input data (swapped from filling)
last bool // last block of last job gets last=true
output []byte // compressed blocks (filled by worker)
err error // encoding error
done chan struct{} // closed when complete
}
type jobState struct {
jobSize int
overlapSize int
filling []byte // accumulates input up to jobSize
nextPrefix []byte // overlap prefix prepared for the next dispatched job
jobSeq int // next job sequence number
jobCh chan *encJob // dispatch to workers
resultCh chan *encJob // ordered results to flusher
workerWg sync.WaitGroup
flusherWg sync.WaitGroup
mu sync.Mutex
flushedSeq int // last flushed sequence number
cond *sync.Cond
flusherErr error
started bool
inputPool sync.Pool // *[]byte buffers of jobSize cap
outputPool sync.Pool // *[]byte buffers for compressed output
overlapPool sync.Pool // *[]byte buffers for overlap prefixes
}
func (e *Encoder) startJobWorkers() {
js := &e.state.jobs
n := e.o.concurrent
js.jobCh = make(chan *encJob, n)
js.resultCh = make(chan *encJob, n)
js.flushedSeq = 0
js.cond = sync.NewCond(&js.mu)
// Workers borrow encoders from the shared e.encoders pool per-job.
// Ensure the pool is initialized before any worker tries to borrow.
e.init.Do(e.initialize)
for range n {
js.workerWg.Add(1)
go e.jobWorker()
}
js.flusherWg.Add(1)
go e.jobFlusher()
js.started = true
}
func (e *Encoder) jobWorker() {
js := &e.state.jobs
defer js.workerWg.Done()
for job := range js.jobCh {
enc := <-e.encoders
e.compressJob(enc, job)
e.encoders <- enc
close(job.done)
}
}
func (e *Encoder) compressJob(enc encoder, job *encJob) {
defer func() {
if r := recover(); r != nil {
job.err = fmt.Errorf("panic in parallel job: %v", r)
rdebug.PrintStack()
}
}()
if len(job.prefix) > 0 {
enc.ResetPrefix(job.prefix)
} else {
enc.Reset(nil, false)
}
data := job.input
if len(data) == 0 && job.last {
blk := enc.Block()
blk.reset(nil)
blk.last = true
blk.encodeRaw(nil)
job.output = append(job.output, blk.output...)
return
}
blk := enc.Block()
for len(data) > 0 {
todo := data
if len(todo) > e.o.blockSize {
todo = todo[:e.o.blockSize]
}
data = data[len(todo):]
blk.pushOffsets()
enc.Encode(blk, todo)
blk.last = len(data) == 0 && job.last
err := blk.encode(todo, e.o.noEntropy, !e.o.allLitEntropy)
if err != nil {
job.err = err
return
}
job.output = append(job.output, blk.output...)
blk.reset(nil)
}
}
func (js *jobState) getInputBuf(size int) []byte {
if v := js.inputPool.Get(); v != nil {
bp := v.(*[]byte)
b := *bp
if cap(b) >= size {
return b[:0]
}
}
return make([]byte, 0, size)
}
func (js *jobState) putInputBuf(b []byte) {
if cap(b) > 0 {
b = b[:0]
js.inputPool.Put(&b)
}
}
func (js *jobState) getOutputBuf(size int) []byte {
if v := js.outputPool.Get(); v != nil {
bp := v.(*[]byte)
b := *bp
if cap(b) >= size {
return b[:0]
}
}
return make([]byte, 0, size)
}
func (js *jobState) putOutputBuf(b []byte) {
if cap(b) > 0 {
b = b[:0]
js.outputPool.Put(&b)
}
}
func (js *jobState) getOverlapBuf(size int) []byte {
if v := js.overlapPool.Get(); v != nil {
bp := v.(*[]byte)
b := *bp
if cap(b) >= size {
return b[:size]
}
}
return make([]byte, size)
}
func (js *jobState) putOverlapBuf(b []byte) {
if cap(b) > 0 {
b = b[:0]
js.overlapPool.Put(&b)
}
}
func (e *Encoder) jobFlusher() {
js := &e.state.jobs
defer js.flusherWg.Done()
for job := range js.resultCh {
<-job.done
// Worker has fully exited compressJob, so the prefix is no longer
// in use. Return it to the pool regardless of outcome.
if job.prefix != nil {
js.putOverlapBuf(job.prefix)
job.prefix = nil
}
if job.err != nil {
js.mu.Lock()
js.flusherErr = job.err
js.cond.Broadcast()
js.mu.Unlock()
for range js.resultCh {
}
return
}
if len(job.output) > 0 {
_, err := e.state.w.Write(job.output)
if err != nil {
js.mu.Lock()
js.flusherErr = err
js.cond.Broadcast()
js.mu.Unlock()
for range js.resultCh {
}
return
}
e.state.nWritten += int64(len(job.output))
}
// Return buffers to pools.
js.putInputBuf(job.input)
js.putOutputBuf(job.output)
job.input = nil
job.output = nil
js.mu.Lock()
js.flushedSeq++
js.cond.Broadcast()
js.mu.Unlock()
}
}
func (e *Encoder) shutdownJobWorkers() {
js := &e.state.jobs
if !js.started {
return
}
close(js.jobCh)
js.workerWg.Wait()
close(js.resultCh)
js.flusherWg.Wait()
js.started = false
}
// waitAllJobs blocks until all dispatched jobs have been flushed.
func (e *Encoder) waitAllJobs() {
js := &e.state.jobs
if !js.started {
return
}
js.mu.Lock()
for js.flushedSeq < js.jobSeq && js.flusherErr == nil {
js.cond.Wait()
}
js.mu.Unlock()
}
func (e *Encoder) dispatchJob(final bool) error {
s := &e.state
js := &s.jobs
js.mu.Lock()
fErr := js.flusherErr
js.mu.Unlock()
if fErr != nil {
return fErr
}
if !s.headerWritten {
// Single-block optimization: fall through to encodeAll path.
if final && len(js.filling) > 0 && len(js.filling) <= e.o.blockSize {
s.current = e.encodeAll(s.encoder, js.filling, s.current[:0])
var n2 int
n2, s.err = s.w.Write(s.current)
if s.err != nil {
return s.err
}
s.nWritten += int64(n2)
s.nInput += int64(len(js.filling))
s.current = s.current[:0]
js.filling = js.filling[:0]
s.headerWritten = true
s.fullFrameWritten = true
s.eofWritten = true
return nil
}
if final && len(js.filling) == 0 && !e.o.fullZero {
s.headerWritten = true
s.fullFrameWritten = true
s.eofWritten = true
return nil
}
var tmp [maxHeaderSize]byte
fh := frameHeader{
ContentSize: uint64(s.frameContentSize),
WindowSize: uint32(s.encoder.WindowSize(s.frameContentSize)),
SingleSegment: false,
Checksum: e.o.crc,
DictID: 0,
}
dst := fh.appendTo(tmp[:0])
var n2 int
n2, s.err = s.w.Write(dst)
if s.err != nil {
return s.err
}
s.nWritten += int64(n2)
s.headerWritten = true
}
if len(js.filling) == 0 && !final {
return nil
}
if !js.started {
e.startJobWorkers()
}
// Estimate output size for pooled buffer.
outputEst := max(len(js.filling)/2, 512)
job := &encJob{
last: final,
done: make(chan struct{}),
output: js.getOutputBuf(outputEst),
}
// Each job owns its prefix slice; the flusher returns it to the pool
// after <-job.done, so workers and dispatch never share a buffer.
if js.nextPrefix != nil {
job.prefix = js.nextPrefix
js.nextPrefix = nil
}
// Build the next job's prefix from the tail of this job's input.
if !final && len(js.filling) > 0 {
overlapLen := min(js.overlapSize, len(js.filling))
np := js.getOverlapBuf(overlapLen)
copy(np, js.filling[len(js.filling)-overlapLen:])
js.nextPrefix = np
}
// Swap filling buffer into job — zero-copy for the input data.
job.input = js.filling
js.filling = js.getInputBuf(js.jobSize)
s.nInput += int64(len(job.input))
js.jobSeq++
if final {
s.eofWritten = true
}
js.resultCh <- job
js.jobCh <- job
return nil
}

View File

@@ -38,6 +38,7 @@ type encoder interface {
WindowSize(size int64) int32
UseBlock(*blockEnc)
Reset(d *dict, singleBlock bool)
ResetPrefix(prefix []byte)
}
type encoderState struct {
@@ -60,6 +61,9 @@ type encoderState struct {
wg sync.WaitGroup
// This waitgroup indicates we have a block encoding/writing.
wWg sync.WaitGroup
// Parallel job state (used when concurrentBlocks is enabled).
jobs jobState
}
// NewWriter will create a new Zstandard encoder.
@@ -74,6 +78,9 @@ func NewWriter(w io.Writer, opts ...EOption) (*Encoder, error) {
return nil, err
}
}
if e.o.concurrentBlocks && (e.o.dict != nil || e.o.concurrent <= 1) {
e.o.concurrentBlocks = false
}
if w != nil {
e.Reset(w)
}
@@ -95,12 +102,31 @@ func (e *Encoder) initialize() {
// as a new, independent stream.
func (e *Encoder) Reset(w io.Writer) {
s := &e.state
if e.o.concurrentBlocks {
e.shutdownJobWorkers()
js := &s.jobs
js.jobSize = e.o.jobSize()
js.overlapSize = e.o.overlapSize()
// js.filling is allocated lazily on first Write/ReadFrom so callers
// that only use EncodeAll don't pay the (up to ~32 MB) jobSize cost.
js.filling = js.filling[:0]
if js.nextPrefix != nil {
js.putOverlapBuf(js.nextPrefix)
js.nextPrefix = nil
}
js.jobSeq = 0
js.flushedSeq = 0
js.flusherErr = nil
js.started = false
}
s.wg.Wait()
s.wWg.Wait()
if cap(s.filling) == 0 {
s.filling = make([]byte, 0, e.o.blockSize)
}
if e.o.concurrent > 1 {
if e.o.concurrent > 1 && !e.o.concurrentBlocks {
if cap(s.current) == 0 {
s.current = make([]byte, 0, e.o.blockSize)
}
@@ -145,6 +171,9 @@ func (e *Encoder) ResetWithOptions(w io.Writer, opts ...EOption) error {
}
}
hasDict := e.o.dict != nil
if e.o.concurrentBlocks && hasDict {
e.o.concurrentBlocks = false
}
if hadDict != hasDict {
// Dict presence changed — encoder type must be recreated.
e.state.encoder = nil
@@ -176,6 +205,49 @@ func (e *Encoder) Write(p []byte) (n int, err error) {
if s.eofWritten {
return 0, ErrEncoderClosed
}
if e.o.concurrentBlocks {
return e.writeJobs(p)
}
return e.writeBlocks(p)
}
func (e *Encoder) writeJobs(p []byte) (n int, err error) {
s := &e.state
js := &s.jobs
jobSize := js.jobSize
if cap(js.filling) == 0 && len(p) > 0 {
js.filling = make([]byte, 0, jobSize)
}
for len(p) > 0 {
if len(p)+len(js.filling) < jobSize {
if e.o.crc {
_, _ = s.encoder.CRC().Write(p)
}
js.filling = append(js.filling, p...)
return n + len(p), nil
}
add := p
if len(p)+len(js.filling) > jobSize {
add = add[:jobSize-len(js.filling)]
}
if e.o.crc {
_, _ = s.encoder.CRC().Write(add)
}
js.filling = append(js.filling, add...)
p = p[len(add):]
n += len(add)
if len(js.filling) < jobSize {
return n, nil
}
if err := e.dispatchJob(false); err != nil {
return n, err
}
}
return n, nil
}
func (e *Encoder) writeBlocks(p []byte) (n int, err error) {
s := &e.state
for len(p) > 0 {
if len(p)+len(s.filling) < e.o.blockSize {
if e.o.crc {
@@ -374,6 +446,10 @@ func (e *Encoder) ReadFrom(r io.Reader) (n int64, err error) {
println("Using ReadFrom")
}
if e.o.concurrentBlocks {
return e.readFromJobs(r)
}
// Flush any current writes.
if len(e.state.filling) > 0 {
if err := e.nextBlock(false); err != nil {
@@ -387,7 +463,6 @@ func (e *Encoder) ReadFrom(r io.Reader) (n int64, err error) {
if e.o.crc {
_, _ = e.state.encoder.CRC().Write(src[:n2])
}
// src is now the unfilled part...
src = src[n2:]
n += int64(n2)
switch err {
@@ -420,15 +495,63 @@ func (e *Encoder) ReadFrom(r io.Reader) (n int64, err error) {
}
}
func (e *Encoder) readFromJobs(r io.Reader) (n int64, err error) {
js := &e.state.jobs
jobSize := js.jobSize
// Flush any current filling.
if len(js.filling) > 0 {
if err := e.dispatchJob(false); err != nil {
return 0, err
}
}
if cap(js.filling) < jobSize {
js.filling = make([]byte, 0, jobSize)
}
js.filling = js.filling[:jobSize]
src := js.filling
for {
n2, err := r.Read(src)
if e.o.crc {
_, _ = e.state.encoder.CRC().Write(src[:n2])
}
src = src[n2:]
n += int64(n2)
switch err {
case io.EOF:
js.filling = js.filling[:len(js.filling)-len(src)]
return n, nil
case nil:
default:
e.state.err = err
return n, err
}
if len(src) > 0 {
continue
}
if err = e.dispatchJob(false); err != nil {
return n, err
}
if cap(js.filling) < jobSize {
js.filling = make([]byte, 0, jobSize)
}
js.filling = js.filling[:jobSize]
src = js.filling
}
}
// Flush will send the currently written data to output
// and block until everything has been written.
// This should only be used on rare occasions where pushing the currently queued data is critical.
func (e *Encoder) Flush() error {
s := &e.state
if e.o.concurrentBlocks {
return e.flushJobs()
}
if len(s.filling) > 0 {
err := e.nextBlock(false)
if err != nil {
// Ignore Flush after Close.
if errors.Is(s.err, ErrEncoderClosed) {
return nil
}
@@ -438,7 +561,6 @@ func (e *Encoder) Flush() error {
s.wg.Wait()
s.wWg.Wait()
if s.err != nil {
// Ignore Flush after Close.
if errors.Is(s.err, ErrEncoderClosed) {
return nil
}
@@ -447,6 +569,20 @@ func (e *Encoder) Flush() error {
return s.writeErr
}
func (e *Encoder) flushJobs() error {
js := &e.state.jobs
if len(js.filling) > 0 {
if err := e.dispatchJob(false); err != nil {
return err
}
}
e.waitAllJobs()
js.mu.Lock()
fErr := js.flusherErr
js.mu.Unlock()
return fErr
}
// Close will flush the final output and close the stream.
// The function will block until everything has been written.
// The Encoder can still be re-used after calling this.
@@ -455,12 +591,16 @@ func (e *Encoder) Close() error {
if s.encoder == nil {
return nil
}
if e.o.concurrentBlocks {
return e.closeJobs()
}
if s.w == nil {
if len(s.filling) == 0 && !s.headerWritten && !s.eofWritten && s.nInput == 0 {
return nil
}
return errors.New("zstd: encoder has no writer")
}
err := e.nextBlock(true)
if err != nil {
if errors.Is(s.err, ErrEncoderClosed) {
@@ -511,6 +651,68 @@ func (e *Encoder) Close() error {
return s.err
}
func (e *Encoder) closeJobs() error {
s := &e.state
js := &s.jobs
if errors.Is(s.err, ErrEncoderClosed) {
return nil
}
if s.w == nil {
if len(js.filling) == 0 && !s.headerWritten && !s.eofWritten && s.nInput == 0 {
return nil
}
return errors.New("zstd: encoder has no writer")
}
if err := e.dispatchJob(true); err != nil {
e.shutdownJobWorkers()
if errors.Is(s.err, ErrEncoderClosed) {
return nil
}
return err
}
if s.frameContentSize > 0 && s.nInput != s.frameContentSize {
e.shutdownJobWorkers()
return fmt.Errorf("frame content size %d given, but %d bytes was written", s.frameContentSize, s.nInput)
}
if s.fullFrameWritten {
e.shutdownJobWorkers()
s.err = ErrEncoderClosed
return nil
}
e.shutdownJobWorkers()
if js.flusherErr != nil {
return js.flusherErr
}
// Write CRC
if e.o.crc {
var tmp [4]byte
_, s.err = s.w.Write(s.encoder.AppendCRC(tmp[:0]))
s.nWritten += 4
}
// Add padding
if s.err == nil && e.o.pad > 0 {
add := calcSkippableFrame(s.nWritten, int64(e.o.pad))
frame, err := skippableFrame(js.filling[:0], add, rand.Reader)
if err != nil {
return err
}
_, s.err = s.w.Write(frame)
}
if s.err == nil {
s.err = ErrEncoderClosed
return nil
}
return s.err
}
// EncodeAll will encode all input in src and append it to dst.
// This function can be called concurrently, but each call will only run on a single goroutine.
// If empty input is given, nothing is returned, unless WithZeroFrames is specified.

View File

@@ -14,22 +14,23 @@ type EOption func(*encoderOptions) error
// options retains accumulated state of multiple options.
type encoderOptions struct {
resetOpt bool
concurrent int
level EncoderLevel
single *bool
pad int
blockSize int
windowSize int
crc bool
fullZero bool
noEntropy bool
allLitEntropy bool
customWindow bool
customALEntropy bool
customBlockSize bool
lowMem bool
dict *dict
resetOpt bool
concurrent int
level EncoderLevel
single *bool
pad int
blockSize int
windowSize int
crc bool
fullZero bool
noEntropy bool
allLitEntropy bool
customWindow bool
customALEntropy bool
customBlockSize bool
lowMem bool
dict *dict
concurrentBlocks bool
}
func (o *encoderOptions) setDefault() {
@@ -333,6 +334,42 @@ func WithLowerEncoderMem(b bool) EOption {
}
}
// WithConcurrentBlocks enables job-based parallel compression for streams.
// When enabled and concurrent > 1, input is split into large sections (jobs)
// that are compressed simultaneously by multiple goroutines.
// Each non-first job receives an overlap prefix from the previous job for match context.
// Output is flushed in order, producing a valid single-frame zstd stream.
//
// Currently disabled when used with dictionary encoding.
// Cannot be changed with ResetWithOptions.
func WithConcurrentBlocks(b bool) EOption {
return func(o *encoderOptions) error {
if o.resetOpt && b != o.concurrentBlocks {
return errors.New("WithConcurrentBlocks cannot be changed on Reset")
}
o.concurrentBlocks = b
return nil
}
}
// jobSize returns the input section size per parallel job.
func (o *encoderOptions) jobSize() int {
s := max(o.windowSize*4, 512<<10)
return s
}
// overlapSize returns the overlap prefix size for parallel jobs.
func (o *encoderOptions) overlapSize() int {
switch o.level {
case SpeedBestCompression:
return o.windowSize / 2
case SpeedBetterCompression:
return o.windowSize / 4
default:
return o.windowSize / 8
}
}
// WithEncoderDict allows to register a dictionary that will be used for the encode.
//
// The slice dict must be in the [dictionary format] produced by

View File

@@ -1,4 +1,4 @@
// Code generated by command: go run gen_fse.go -out ../fse_decoder_amd64.s -pkg=zstd. DO NOT EDIT.
// Code generated by command: go run gen_fse.go -out ../fse_decoder.s -arch amd64,arm64 -pkg=zstd. DO NOT EDIT.
//go:build !appengine && !noasm && gc && !noasm

View File

@@ -0,0 +1,153 @@
// Code generated by command: go run gen_fse.go -out ../fse_decoder.s -arch amd64,arm64 -pkg=zstd. DO NOT EDIT.
// EXPERIMENTAL arm64 output lowered from an amd64 avo program.
//go:build arm64 && !appengine && !noasm && gc && !noasm
// func buildDtable_asm(s *fseDecoder, ctx *buildDtableAsmContext) int
TEXT ·buildDtable_asm(SB), $0-24
MOVD ctx+8(FP), R1
MOVD s+0(FP), R6
// Load values
MOVBU 4098(R6), R2
MOVD $0, R0
MOVD $1, R16
LSL R2, R16, R16
ORR R16, R0, R0
MOVD (R1), R3
MOVD 16(R1), R5
SUB $1, R0, R7
MOVD 8(R1), R1
MOVHU 4096(R6), R6
// End load values
// Init, lay down lowprob symbols
MOVD $0, R8
JMP init_main_loop_condition
init_main_loop:
ADD R8<<1, R1, R15
MOVH (R15), R9
AND $0xffff, R9, R15
MOVD $-1, R16
AND $0xffff, R16, R16
CMP R16, R15
BNE do_not_update_high_threshold
ADD R7<<3, R5, R15
MOVB R8, 1(R15)
SUB $1, R7, R7
MOVD $0x0000000000000001, R9
do_not_update_high_threshold:
ADD R8<<1, R3, R15
MOVH R9, (R15)
ADD $1, R8, R8
init_main_loop_condition:
CMP R6, R8
BLT init_main_loop
// Spread symbols
// Calculate table step
MOVD R0, R8
LSR $0x01, R8, R8
MOVD R0, R9
LSR $0x03, R9, R9
ADD R9, R8, R8
ADD $3, R8, R8
// Fill add bits values
SUB $1, R0, R9
MOVD $0, R10
MOVD $0, R11
JMP spread_main_loop_condition
spread_main_loop:
MOVD $0, R12
ADD R11<<1, R1, R15
MOVH (R15), R13
JMP spread_inner_loop_condition
spread_inner_loop:
ADD R10<<3, R5, R15
MOVB R11, 1(R15)
adjust_position:
ADD R8, R10, R10
AND R9, R10, R10
CMP R7, R10
BGT adjust_position
ADD $1, R12, R12
spread_inner_loop_condition:
CMP R13, R12
BLT spread_inner_loop
ADD $1, R11, R11
spread_main_loop_condition:
CMP R6, R11
BLT spread_main_loop
TST R10, R10
BEQ spread_check_ok
MOVD ctx+8(FP), R0
MOVD R10, 24(R0)
MOVD $+1, R16
MOVD R16, ret+16(FP)
RET
spread_check_ok:
// Build Decoding table
MOVD $0, R6
build_table_main_table:
ADD R6<<3, R5, R15
MOVBU 1(R15), R1
ADD R1<<1, R3, R15
MOVHU (R15), R7
ADD $1, R7, R8
ADD R1<<1, R3, R15
MOVH R8, (R15)
MOVD R7, R8
CLZ R8, R16
MOVD $63, R8
SUB R16, R8, R8
MOVD R2, R1
SUB R8, R1, R1
LSL R1, R7, R7
SUB R0, R7, R7
ADD R6<<3, R5, R15
MOVB R1, (R15)
ADD R6<<3, R5, R15
MOVH R7, 2(R15)
CMP R0, R7
BLE build_table_check1_ok
MOVD ctx+8(FP), R1
MOVD R7, 24(R1)
MOVD R0, 32(R1)
MOVD $+2, R16
MOVD R16, ret+16(FP)
RET
build_table_check1_ok:
AND $0xff, R1, R15
AND $0xff, R1, R16
TST R16, R15
BNE build_table_check2_ok
AND $0xffff, R7, R15
AND $0xffff, R6, R16
CMP R16, R15
BNE build_table_check2_ok
MOVD ctx+8(FP), R0
MOVD R7, 24(R0)
MOVD R6, 32(R0)
MOVD $+3, R16
MOVD R16, ret+16(FP)
RET
build_table_check2_ok:
ADD $1, R6, R6
CMP R0, R6
BLT build_table_main_table
MOVD $+0, R16
MOVD R16, ret+16(FP)
RET

View File

@@ -1,4 +1,4 @@
//go:build amd64 && !appengine && !noasm && gc
//go:build (amd64 || arm64) && !appengine && !noasm && gc
package zstd
@@ -6,6 +6,10 @@ import (
"fmt"
)
// buildDtable_asm is generated by _generate/gen_fse.go and lowered to each
// architecture (amd64 by goasm, arm64 by the avo arm64 lowering printer). The
// Go side is identical across architectures, so it lives here.
type buildDtableAsmContext struct {
// inputs
stateTable *uint16
@@ -18,7 +22,7 @@ type buildDtableAsmContext struct {
errParam2 uint64
}
// buildDtable_asm is an x86 assembly implementation of fseDecoder.buildDtable.
// buildDtable_asm is an assembly implementation of fseDecoder.buildDtable.
// Function returns non-zero exit code on error.
//
//go:noescape

View File

@@ -1,4 +1,4 @@
//go:build !amd64 || appengine || !gc || noasm
//go:build (!amd64 && !arm64) || appengine || !gc || noasm
package zstd

View File

@@ -3,30 +3,47 @@
package zstd
import (
"fmt"
"io"
"github.com/klauspost/compress/internal/cpuinfo"
)
type decodeSyncAsmContext struct {
llTable []decSymbol
mlTable []decSymbol
ofTable []decSymbol
llState uint64
mlState uint64
ofState uint64
iteration int
litRemain int
out []byte
outPosition int
literals []byte
litPosition int
history []byte
windowSize int
ll int // set on error (not for all errors, please refer to _generate/gen.go)
ml int // set on error (not for all errors, please refer to _generate/gen.go)
mo int // set on error (not for all errors, please refer to _generate/gen.go)
// The shared decode/decodeSync/executeSimple wrappers and context structs live
// in seqdec_asm.go; this file only declares the amd64 asm routines and the
// dispatch helpers that pick the BMI2 / non-BMI2 (and 56-bit / safe) variant.
// sequenceDecs_decode implements the main loop of sequenceDecs in x86 asm.
//
// Please refer to seqdec_generic.go for the reference implementation.
//
//go:noescape
func sequenceDecs_decode_amd64(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext) int
// sequenceDecs_decode_56_amd64 implements the main loop of sequenceDecs in x86 asm.
//
//go:noescape
func sequenceDecs_decode_56_amd64(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext) int
// sequenceDecs_decode_bmi2 implements the main loop of sequenceDecs in x86 asm with BMI2 extensions.
//
//go:noescape
func sequenceDecs_decode_bmi2(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext) int
// sequenceDecs_decode_56_bmi2 implements the main loop of sequenceDecs in x86 asm with BMI2 extensions.
//
//go:noescape
func sequenceDecs_decode_56_bmi2(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext) int
// decodeAsm runs the sequenceDecs decode loop, choosing the BMI2 / 56-bit variant.
func decodeAsm(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext, lte56bits bool) int {
if cpuinfo.HasBMI2() {
if lte56bits {
return sequenceDecs_decode_56_bmi2(s, br, ctx)
}
return sequenceDecs_decode_bmi2(s, br, ctx)
}
if lte56bits {
return sequenceDecs_decode_56_amd64(s, br, ctx)
}
return sequenceDecs_decode_amd64(s, br, ctx)
}
// sequenceDecs_decodeSync_amd64 implements the main loop of sequenceDecs.decodeSync in x86 asm.
@@ -51,273 +68,18 @@ func sequenceDecs_decodeSync_safe_amd64(s *sequenceDecs, br *bitReader, ctx *dec
//go:noescape
func sequenceDecs_decodeSync_safe_bmi2(s *sequenceDecs, br *bitReader, ctx *decodeSyncAsmContext) int
// decode sequences from the stream with the provided history but without a dictionary.
func (s *sequenceDecs) decodeSyncSimple(hist []byte) (bool, error) {
if len(s.dict) > 0 {
return false, nil
}
if s.maxSyncLen == 0 && cap(s.out)-len(s.out) < maxCompressedBlockSize {
return false, nil
}
// FIXME: Using unsafe memory copies leads to rare, random crashes
// with fuzz testing. It is therefore disabled for now.
const useSafe = true
/*
useSafe := false
if s.maxSyncLen == 0 && cap(s.out)-len(s.out) < maxCompressedBlockSizeAlloc {
useSafe = true
}
if s.maxSyncLen > 0 && cap(s.out)-len(s.out)-compressedBlockOverAlloc < int(s.maxSyncLen) {
useSafe = true
}
if cap(s.literals) < len(s.literals)+compressedBlockOverAlloc {
useSafe = true
}
*/
br := s.br
maxBlockSize := min(s.windowSize, maxCompressedBlockSize)
ctx := decodeSyncAsmContext{
llTable: s.litLengths.fse.dt[:maxTablesize],
mlTable: s.matchLengths.fse.dt[:maxTablesize],
ofTable: s.offsets.fse.dt[:maxTablesize],
llState: uint64(s.litLengths.state.state),
mlState: uint64(s.matchLengths.state.state),
ofState: uint64(s.offsets.state.state),
iteration: s.nSeqs - 1,
litRemain: len(s.literals),
out: s.out,
outPosition: len(s.out),
literals: s.literals,
windowSize: s.windowSize,
history: hist,
}
s.seqSize = 0
startSize := len(s.out)
var errCode int
// decodeSyncAsm runs the decodeSync loop, choosing the BMI2 / safe variant.
func decodeSyncAsm(s *sequenceDecs, br *bitReader, ctx *decodeSyncAsmContext, safe bool) int {
if cpuinfo.HasBMI2() {
if useSafe {
errCode = sequenceDecs_decodeSync_safe_bmi2(s, br, &ctx)
} else {
errCode = sequenceDecs_decodeSync_bmi2(s, br, &ctx)
}
} else {
if useSafe {
errCode = sequenceDecs_decodeSync_safe_amd64(s, br, &ctx)
} else {
errCode = sequenceDecs_decodeSync_amd64(s, br, &ctx)
if safe {
return sequenceDecs_decodeSync_safe_bmi2(s, br, ctx)
}
return sequenceDecs_decodeSync_bmi2(s, br, ctx)
}
switch errCode {
case noError:
break
case errorMatchLenOfsMismatch:
return true, fmt.Errorf("zero matchoff and matchlen (%d) > 0", ctx.ml)
case errorMatchLenTooBig:
return true, fmt.Errorf("match len (%d) bigger than max allowed length", ctx.ml)
case errorMatchOffTooBig:
return true, fmt.Errorf("match offset (%d) bigger than current history (%d)",
ctx.mo, ctx.outPosition+len(hist)-startSize)
case errorNotEnoughLiterals:
return true, fmt.Errorf("unexpected literal count, want %d bytes, but only %d is available",
ctx.ll, ctx.litRemain+ctx.ll)
case errorOverread:
return true, io.ErrUnexpectedEOF
case errorNotEnoughSpace:
size := ctx.outPosition + ctx.ll + ctx.ml
if debugDecoder {
println("msl:", s.maxSyncLen, "cap", cap(s.out), "bef:", startSize, "sz:", size-startSize, "mbs:", maxBlockSize, "outsz:", cap(s.out)-startSize)
}
return true, fmt.Errorf("output bigger than max block size (%d)", maxBlockSize)
default:
return true, fmt.Errorf("sequenceDecs_decode returned erroneous code %d", errCode)
if safe {
return sequenceDecs_decodeSync_safe_amd64(s, br, ctx)
}
s.seqSize += ctx.litRemain
if s.seqSize > maxBlockSize {
return true, fmt.Errorf("output bigger than max block size (%d)", maxBlockSize)
}
err := br.close()
if err != nil {
printf("Closing sequences: %v, %+v\n", err, *br)
return true, err
}
s.literals = s.literals[ctx.litPosition:]
t := ctx.outPosition
s.out = s.out[:t]
// Add final literals
s.out = append(s.out, s.literals...)
if debugDecoder {
t += len(s.literals)
if t != len(s.out) {
panic(fmt.Errorf("length mismatch, want %d, got %d", len(s.out), t))
}
}
return true, nil
}
// --------------------------------------------------------------------------------
type decodeAsmContext struct {
llTable []decSymbol
mlTable []decSymbol
ofTable []decSymbol
llState uint64
mlState uint64
ofState uint64
iteration int
seqs []seqVals
litRemain int
}
const noError = 0
// error reported when mo == 0 && ml > 0
const errorMatchLenOfsMismatch = 1
// error reported when ml > maxMatchLen
const errorMatchLenTooBig = 2
// error reported when mo > available history or mo > s.windowSize
const errorMatchOffTooBig = 3
// error reported when the sum of literal lengths exeeceds the literal buffer size
const errorNotEnoughLiterals = 4
// error reported when capacity of `out` is too small
const errorNotEnoughSpace = 5
// error reported when bits are overread.
const errorOverread = 6
// sequenceDecs_decode implements the main loop of sequenceDecs in x86 asm.
//
// Please refer to seqdec_generic.go for the reference implementation.
//
//go:noescape
func sequenceDecs_decode_amd64(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext) int
// sequenceDecs_decode implements the main loop of sequenceDecs in x86 asm.
//
// Please refer to seqdec_generic.go for the reference implementation.
//
//go:noescape
func sequenceDecs_decode_56_amd64(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext) int
// sequenceDecs_decode implements the main loop of sequenceDecs in x86 asm with BMI2 extensions.
//
//go:noescape
func sequenceDecs_decode_bmi2(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext) int
// sequenceDecs_decode implements the main loop of sequenceDecs in x86 asm with BMI2 extensions.
//
//go:noescape
func sequenceDecs_decode_56_bmi2(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext) int
// decode sequences from the stream without the provided history.
func (s *sequenceDecs) decode(seqs []seqVals) error {
br := s.br
maxBlockSize := min(s.windowSize, maxCompressedBlockSize)
ctx := decodeAsmContext{
llTable: s.litLengths.fse.dt[:maxTablesize],
mlTable: s.matchLengths.fse.dt[:maxTablesize],
ofTable: s.offsets.fse.dt[:maxTablesize],
llState: uint64(s.litLengths.state.state),
mlState: uint64(s.matchLengths.state.state),
ofState: uint64(s.offsets.state.state),
seqs: seqs,
iteration: len(seqs) - 1,
litRemain: len(s.literals),
}
if debugDecoder {
println("decode: decoding", len(seqs), "sequences", br.remain(), "bits remain on stream")
}
s.seqSize = 0
lte56bits := s.maxBits+s.offsets.fse.actualTableLog+s.matchLengths.fse.actualTableLog+s.litLengths.fse.actualTableLog <= 56
var errCode int
if cpuinfo.HasBMI2() {
if lte56bits {
errCode = sequenceDecs_decode_56_bmi2(s, br, &ctx)
} else {
errCode = sequenceDecs_decode_bmi2(s, br, &ctx)
}
} else {
if lte56bits {
errCode = sequenceDecs_decode_56_amd64(s, br, &ctx)
} else {
errCode = sequenceDecs_decode_amd64(s, br, &ctx)
}
}
if errCode != 0 {
i := len(seqs) - ctx.iteration - 1
switch errCode {
case errorMatchLenOfsMismatch:
ml := ctx.seqs[i].ml
return fmt.Errorf("zero matchoff and matchlen (%d) > 0", ml)
case errorMatchLenTooBig:
ml := ctx.seqs[i].ml
return fmt.Errorf("match len (%d) bigger than max allowed length", ml)
case errorNotEnoughLiterals:
ll := ctx.seqs[i].ll
return fmt.Errorf("unexpected literal count, want %d bytes, but only %d is available", ll, ctx.litRemain+ll)
case errorOverread:
return io.ErrUnexpectedEOF
}
return fmt.Errorf("sequenceDecs_decode_amd64 returned erroneous code %d", errCode)
}
if ctx.litRemain < 0 {
return fmt.Errorf("literal count is too big: total available %d, total requested %d",
len(s.literals), len(s.literals)-ctx.litRemain)
}
s.seqSize += ctx.litRemain
if s.seqSize > maxBlockSize {
return fmt.Errorf("output bigger than max block size (%d)", maxBlockSize)
}
if debugDecoder {
println("decode: ", br.remain(), "bits remain on stream. code:", errCode)
}
err := br.close()
if err != nil {
printf("Closing sequences: %v, %+v\n", err, *br)
}
return err
}
// --------------------------------------------------------------------------------
type executeAsmContext struct {
seqs []seqVals
seqIndex int
out []byte
history []byte
literals []byte
outPosition int
litPosition int
windowSize int
return sequenceDecs_decodeSync_amd64(s, br, ctx)
}
// sequenceDecs_executeSimple_amd64 implements the main loop of sequenceDecs.executeSimple in x86 asm.
@@ -334,54 +96,10 @@ func sequenceDecs_executeSimple_amd64(ctx *executeAsmContext) bool
//go:noescape
func sequenceDecs_executeSimple_safe_amd64(ctx *executeAsmContext) bool
// executeSimple handles cases when dictionary is not used.
func (s *sequenceDecs) executeSimple(seqs []seqVals, hist []byte) error {
// Ensure we have enough output size...
if len(s.out)+s.seqSize+compressedBlockOverAlloc > cap(s.out) {
addBytes := s.seqSize + len(s.out) + compressedBlockOverAlloc
s.out = append(s.out, make([]byte, addBytes)...)
s.out = s.out[:len(s.out)-addBytes]
// executeSimpleAsm runs the executeSimple loop, choosing the safe variant.
func executeSimpleAsm(ctx *executeAsmContext, safe bool) bool {
if safe {
return sequenceDecs_executeSimple_safe_amd64(ctx)
}
if debugDecoder {
printf("Execute %d seqs with literals: %d into %d bytes\n", len(seqs), len(s.literals), s.seqSize)
}
var t = len(s.out)
out := s.out[:t+s.seqSize]
ctx := executeAsmContext{
seqs: seqs,
seqIndex: 0,
out: out,
history: hist,
outPosition: t,
litPosition: 0,
literals: s.literals,
windowSize: s.windowSize,
}
var ok bool
if cap(s.literals) < len(s.literals)+compressedBlockOverAlloc {
ok = sequenceDecs_executeSimple_safe_amd64(&ctx)
} else {
ok = sequenceDecs_executeSimple_amd64(&ctx)
}
if !ok {
return fmt.Errorf("match offset (%d) bigger than current history (%d)",
seqs[ctx.seqIndex].mo, ctx.outPosition+len(hist))
}
s.literals = s.literals[ctx.litPosition:]
t = ctx.outPosition
// Add final literals
copy(out[t:], s.literals)
if debugDecoder {
t += len(s.literals)
if t != len(out) {
panic(fmt.Errorf("length mismatch, want %d, got %d, ss: %d", len(out), t, s.seqSize))
}
}
s.out = out
return nil
return sequenceDecs_executeSimple_amd64(ctx)
}

View File

@@ -1,4 +1,4 @@
// Code generated by command: go run gen.go -out ../seqdec_amd64.s -pkg=zstd. DO NOT EDIT.
// Code generated by command: go run gen.go -out ../seqdec.s -arch amd64,arm64 -pkg=zstd. DO NOT EDIT.
//go:build !appengine && !noasm && gc && !noasm

View File

@@ -0,0 +1,70 @@
//go:build arm64 && !appengine && !noasm && gc
package zstd
// The shared decode/decodeSync/executeSimple wrappers and context structs live
// in seqdec_asm.go; this file only declares the arm64 asm routines (generated
// by the avo arm64 lowering printer) and the dispatch helpers. arm64 has no
// BMI2, so each helper selects only between the 56-bit / safe variants.
// sequenceDecs_decode_arm64 implements the main loop of sequenceDecs in arm64 asm.
//
// Please refer to seqdec_generic.go for the reference implementation.
//
//go:noescape
func sequenceDecs_decode_arm64(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext) int
// sequenceDecs_decode_56_arm64 implements the main loop of sequenceDecs in arm64 asm.
//
//go:noescape
func sequenceDecs_decode_56_arm64(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext) int
// decodeAsm runs the sequenceDecs decode loop, choosing the 56-bit variant.
func decodeAsm(s *sequenceDecs, br *bitReader, ctx *decodeAsmContext, lte56bits bool) int {
if lte56bits {
return sequenceDecs_decode_56_arm64(s, br, ctx)
}
return sequenceDecs_decode_arm64(s, br, ctx)
}
// sequenceDecs_decodeSync_arm64 implements the main loop of sequenceDecs.decodeSync in arm64 asm.
//
// Please refer to seqdec_generic.go for the reference implementation.
//
//go:noescape
func sequenceDecs_decodeSync_arm64(s *sequenceDecs, br *bitReader, ctx *decodeSyncAsmContext) int
// sequenceDecs_decodeSync_safe_arm64 does the same as above, but does not write more than output buffer.
//
//go:noescape
func sequenceDecs_decodeSync_safe_arm64(s *sequenceDecs, br *bitReader, ctx *decodeSyncAsmContext) int
// decodeSyncAsm runs the decodeSync loop, choosing the safe variant.
func decodeSyncAsm(s *sequenceDecs, br *bitReader, ctx *decodeSyncAsmContext, safe bool) int {
if safe {
return sequenceDecs_decodeSync_safe_arm64(s, br, ctx)
}
return sequenceDecs_decodeSync_arm64(s, br, ctx)
}
// sequenceDecs_executeSimple_arm64 implements the main loop of sequenceDecs.executeSimple in arm64 asm.
//
// Returns false if a match offset is too big.
//
// Please refer to seqdec_generic.go for the reference implementation.
//
//go:noescape
func sequenceDecs_executeSimple_arm64(ctx *executeAsmContext) bool
// Same as above, but with safe memcopies
//
//go:noescape
func sequenceDecs_executeSimple_safe_arm64(ctx *executeAsmContext) bool
// executeSimpleAsm runs the executeSimple loop, choosing the safe variant.
func executeSimpleAsm(ctx *executeAsmContext, safe bool) bool {
if safe {
return sequenceDecs_executeSimple_safe_arm64(ctx)
}
return sequenceDecs_executeSimple_arm64(ctx)
}

2705
vendor/github.com/klauspost/compress/zstd/seqdec_arm64.s generated vendored Normal file
View File

File diff suppressed because it is too large Load Diff

289
vendor/github.com/klauspost/compress/zstd/seqdec_asm.go generated vendored Normal file
View File

@@ -0,0 +1,289 @@
//go:build (amd64 || arm64) && !appengine && !noasm && gc
package zstd
import (
"fmt"
"io"
)
// This file holds the parts of the assembly sequence decoder that are identical
// across architectures: the context structs exchanged with the asm, the error
// codes, and the decode/decodeSync/executeSimple wrappers. Each architecture
// supplies the small dispatch helpers (decodeAsm, decodeSyncAsm,
// executeSimpleAsm) that select the concrete asm routine — amd64 also chooses a
// BMI2 variant, arm64 has a single implementation.
type decodeSyncAsmContext struct {
llTable []decSymbol
mlTable []decSymbol
ofTable []decSymbol
llState uint64
mlState uint64
ofState uint64
iteration int
litRemain int
out []byte
outPosition int
literals []byte
litPosition int
history []byte
windowSize int
ll int // set on error (not for all errors, please refer to _generate/gen.go)
ml int // set on error (not for all errors, please refer to _generate/gen.go)
mo int // set on error (not for all errors, please refer to _generate/gen.go)
}
type decodeAsmContext struct {
llTable []decSymbol
mlTable []decSymbol
ofTable []decSymbol
llState uint64
mlState uint64
ofState uint64
iteration int
seqs []seqVals
litRemain int
}
type executeAsmContext struct {
seqs []seqVals
seqIndex int
out []byte
history []byte
literals []byte
outPosition int
litPosition int
windowSize int
}
const noError = 0
// error reported when mo == 0 && ml > 0
const errorMatchLenOfsMismatch = 1
// error reported when ml > maxMatchLen
const errorMatchLenTooBig = 2
// error reported when mo > available history or mo > s.windowSize
const errorMatchOffTooBig = 3
// error reported when the sum of literal lengths exeeceds the literal buffer size
const errorNotEnoughLiterals = 4
// error reported when capacity of `out` is too small
const errorNotEnoughSpace = 5
// error reported when bits are overread.
const errorOverread = 6
// decode sequences from the stream with the provided history but without a dictionary.
func (s *sequenceDecs) decodeSyncSimple(hist []byte) (bool, error) {
if len(s.dict) > 0 {
return false, nil
}
if s.maxSyncLen == 0 && cap(s.out)-len(s.out) < maxCompressedBlockSize {
return false, nil
}
// FIXME: Using unsafe memory copies leads to rare, random crashes
// with fuzz testing. It is therefore disabled for now.
const useSafe = true
br := s.br
maxBlockSize := min(s.windowSize, maxCompressedBlockSize)
ctx := decodeSyncAsmContext{
llTable: s.litLengths.fse.dt[:maxTablesize],
mlTable: s.matchLengths.fse.dt[:maxTablesize],
ofTable: s.offsets.fse.dt[:maxTablesize],
llState: uint64(s.litLengths.state.state),
mlState: uint64(s.matchLengths.state.state),
ofState: uint64(s.offsets.state.state),
iteration: s.nSeqs - 1,
litRemain: len(s.literals),
out: s.out,
outPosition: len(s.out),
literals: s.literals,
windowSize: s.windowSize,
history: hist,
}
s.seqSize = 0
startSize := len(s.out)
errCode := decodeSyncAsm(s, br, &ctx, useSafe)
switch errCode {
case noError:
break
case errorMatchLenOfsMismatch:
return true, fmt.Errorf("zero matchoff and matchlen (%d) > 0", ctx.ml)
case errorMatchLenTooBig:
return true, fmt.Errorf("match len (%d) bigger than max allowed length", ctx.ml)
case errorMatchOffTooBig:
return true, fmt.Errorf("match offset (%d) bigger than current history (%d)",
ctx.mo, ctx.outPosition+len(hist)-startSize)
case errorNotEnoughLiterals:
return true, fmt.Errorf("unexpected literal count, want %d bytes, but only %d is available",
ctx.ll, ctx.litRemain+ctx.ll)
case errorOverread:
return true, io.ErrUnexpectedEOF
case errorNotEnoughSpace:
size := ctx.outPosition + ctx.ll + ctx.ml
if debugDecoder {
println("msl:", s.maxSyncLen, "cap", cap(s.out), "bef:", startSize, "sz:", size-startSize, "mbs:", maxBlockSize, "outsz:", cap(s.out)-startSize)
}
return true, fmt.Errorf("output bigger than max block size (%d)", maxBlockSize)
default:
return true, fmt.Errorf("sequenceDecs_decode returned erroneous code %d", errCode)
}
s.seqSize += ctx.litRemain
if s.seqSize > maxBlockSize {
return true, fmt.Errorf("output bigger than max block size (%d)", maxBlockSize)
}
err := br.close()
if err != nil {
printf("Closing sequences: %v, %+v\n", err, *br)
return true, err
}
s.literals = s.literals[ctx.litPosition:]
t := ctx.outPosition
s.out = s.out[:t]
// Add final literals
s.out = append(s.out, s.literals...)
if debugDecoder {
t += len(s.literals)
if t != len(s.out) {
panic(fmt.Errorf("length mismatch, want %d, got %d", len(s.out), t))
}
}
return true, nil
}
// decode sequences from the stream without the provided history.
func (s *sequenceDecs) decode(seqs []seqVals) error {
br := s.br
maxBlockSize := min(s.windowSize, maxCompressedBlockSize)
ctx := decodeAsmContext{
llTable: s.litLengths.fse.dt[:maxTablesize],
mlTable: s.matchLengths.fse.dt[:maxTablesize],
ofTable: s.offsets.fse.dt[:maxTablesize],
llState: uint64(s.litLengths.state.state),
mlState: uint64(s.matchLengths.state.state),
ofState: uint64(s.offsets.state.state),
seqs: seqs,
iteration: len(seqs) - 1,
litRemain: len(s.literals),
}
if debugDecoder {
println("decode: decoding", len(seqs), "sequences", br.remain(), "bits remain on stream")
}
s.seqSize = 0
lte56bits := s.maxBits+s.offsets.fse.actualTableLog+s.matchLengths.fse.actualTableLog+s.litLengths.fse.actualTableLog <= 56
errCode := decodeAsm(s, br, &ctx, lte56bits)
if errCode != 0 {
i := len(seqs) - ctx.iteration - 1
switch errCode {
case errorMatchLenOfsMismatch:
ml := ctx.seqs[i].ml
return fmt.Errorf("zero matchoff and matchlen (%d) > 0", ml)
case errorMatchLenTooBig:
ml := ctx.seqs[i].ml
return fmt.Errorf("match len (%d) bigger than max allowed length", ml)
case errorNotEnoughLiterals:
ll := ctx.seqs[i].ll
return fmt.Errorf("unexpected literal count, want %d bytes, but only %d is available", ll, ctx.litRemain+ll)
case errorOverread:
return io.ErrUnexpectedEOF
}
return fmt.Errorf("sequenceDecs_decode_amd64 returned erroneous code %d", errCode)
}
if ctx.litRemain < 0 {
return fmt.Errorf("literal count is too big: total available %d, total requested %d",
len(s.literals), len(s.literals)-ctx.litRemain)
}
s.seqSize += ctx.litRemain
if s.seqSize > maxBlockSize {
return fmt.Errorf("output bigger than max block size (%d)", maxBlockSize)
}
if debugDecoder {
println("decode: ", br.remain(), "bits remain on stream. code:", errCode)
}
err := br.close()
if err != nil {
printf("Closing sequences: %v, %+v\n", err, *br)
}
return err
}
// executeSimple handles cases when dictionary is not used.
func (s *sequenceDecs) executeSimple(seqs []seqVals, hist []byte) error {
// Ensure we have enough output size...
if len(s.out)+s.seqSize+compressedBlockOverAlloc > cap(s.out) {
addBytes := s.seqSize + len(s.out) + compressedBlockOverAlloc
s.out = append(s.out, make([]byte, addBytes)...)
s.out = s.out[:len(s.out)-addBytes]
}
if debugDecoder {
printf("Execute %d seqs with literals: %d into %d bytes\n", len(seqs), len(s.literals), s.seqSize)
}
var t = len(s.out)
out := s.out[:t+s.seqSize]
ctx := executeAsmContext{
seqs: seqs,
seqIndex: 0,
out: out,
history: hist,
outPosition: t,
litPosition: 0,
literals: s.literals,
windowSize: s.windowSize,
}
// useSafe avoids overwriting the output buffer when the literals slice has
// not been allocated with the required over-allocation slack.
useSafe := cap(s.literals) < len(s.literals)+compressedBlockOverAlloc
ok := executeSimpleAsm(&ctx, useSafe)
if !ok {
return fmt.Errorf("match offset (%d) bigger than current history (%d)",
seqs[ctx.seqIndex].mo, ctx.outPosition+len(hist))
}
s.literals = s.literals[ctx.litPosition:]
t = ctx.outPosition
// Add final literals
copy(out[t:], s.literals)
if debugDecoder {
t += len(s.literals)
if t != len(out) {
panic(fmt.Errorf("length mismatch, want %d, got %d, ss: %d", len(out), t, s.seqSize))
}
}
s.out = out
return nil
}

View File

@@ -1,4 +1,4 @@
//go:build !amd64 || appengine || !gc || noasm
//go:build (!amd64 && !arm64) || appengine || !gc || noasm
package zstd

View File

@@ -334,9 +334,10 @@ func decodeSnappy(blk *blockEnc, src []byte) error {
return errUnsupportedLiteralLength
}
//if length > snappyMaxBlockSize-d || uint32(length) > len(src)-s {
// return ErrSnappyCorrupt
//}
if length > len(src)-s {
println("length > len(src)-s", length, len(src)-s)
return ErrSnappyCorrupt
}
blk.literals = append(blk.literals, src[s:s+length]...)
//println(length, "litLen")

View File

@@ -90,7 +90,7 @@ loop:
s = skipSpace(s[1:])
}
}
return
return specs
}
func skipSpace(s string) (rest string) {

View File

@@ -85,11 +85,12 @@ type CounterVecOpts struct {
// Both internal tracking values are added up in the Write method. This has to
// be taken into account when it comes to precision and overflow behavior.
func NewCounter(opts CounterOpts) Counter {
desc := NewDesc(
desc := V2.NewDesc(
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help,
nil,
UnconstrainedLabels(nil),
opts.ConstLabels,
WithUnit(opts.Unit),
)
if opts.now == nil {
opts.now = time.Now
@@ -205,6 +206,7 @@ func (v2) NewCounterVec(opts CounterVecOpts) *CounterVec {
opts.Help,
opts.VariableLabels,
opts.ConstLabels,
WithUnit(opts.Unit),
)
if opts.now == nil {
opts.now = time.Now
@@ -349,10 +351,11 @@ type CounterFunc interface {
//
// Check out the ExampleGaugeFunc examples for the similar GaugeFunc.
func NewCounterFunc(opts CounterOpts, function func() float64) CounterFunc {
return newValueFunc(NewDesc(
return newValueFunc(V2.NewDesc(
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help,
nil,
UnconstrainedLabels(nil),
opts.ConstLabels,
WithUnit(opts.Unit),
), CounterValue, function)
}

View File

@@ -47,6 +47,8 @@ type Desc struct {
fqName string
// help provides some helpful information about this metric.
help string
// unit provides the unit of this metric.
unit string
// constLabelPairs contains precalculated DTO label pairs based on
// the constant labels.
constLabelPairs []*dto.LabelPair
@@ -66,6 +68,16 @@ type Desc struct {
err error
}
// DescOpt allows setting optional fields for NewDesc.
type DescOpt func(*Desc)
// WithUnit sets the unit for a Desc.
func WithUnit(unit string) DescOpt {
return func(d *Desc) {
d.unit = unit
}
}
// NewDesc allocates and initializes a new Desc. Errors are recorded in the Desc
// and will be reported on registration time. variableLabels and constLabels can
// be nil if no such labels should be set. fqName must not be empty.
@@ -89,14 +101,17 @@ func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *
//
// For constLabels, the label values are constant. Therefore, they are fully
// specified in the Desc. See the Collector example for a usage pattern.
func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, constLabels Labels) *Desc {
func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, constLabels Labels, opts ...DescOpt) *Desc {
d := &Desc{
fqName: fqName,
help: help,
variableLabels: variableLabels.compile(),
}
//nolint:staticcheck // TODO: Don't use deprecated model.NameValidationScheme.
if !model.NameValidationScheme.IsValidMetricName(fqName) {
for _, opt := range opts {
opt(d)
}
if !model.UTF8Validation.IsValidMetricName(fqName) {
d.err = fmt.Errorf("%q is not a valid metric name", fqName)
return d
}
@@ -150,11 +165,13 @@ func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, const
d.id = xxh.Sum64()
// Sort labelNames so that order doesn't matter for the hash.
sort.Strings(labelNames)
// Now hash together (in this order) the help string and the sorted
// Now hash together (in this order) the help string, the unit string and the sorted
// label names.
xxh.Reset()
xxh.WriteString(help)
xxh.Write(separatorByteSlice)
xxh.WriteString(d.unit)
xxh.Write(separatorByteSlice)
for _, labelName := range labelNames {
xxh.WriteString(labelName)
xxh.Write(separatorByteSlice)
@@ -182,6 +199,15 @@ func NewInvalidDesc(err error) *Desc {
}
}
// Err returns an error that occurred during construction, if any.
//
// Calling this method is optional. It can be used to detect construction
// errors early, before invoking other methods on the Desc. If an error is
// present, later operations may not behave as expected.
func (d *Desc) Err() error {
return d.err
}
func (d *Desc) String() string {
lpStrings := make([]string, 0, len(d.constLabelPairs))
for _, lp := range d.constLabelPairs {
@@ -202,9 +228,10 @@ func (d *Desc) String() string {
}
}
return fmt.Sprintf(
"Desc{fqName: %q, help: %q, constLabels: {%s}, variableLabels: {%s}}",
"Desc{fqName: %q, help: %q, unit: %q, constLabels: {%s}, variableLabels: {%s}}",
d.fqName,
d.help,
d.unit,
strings.Join(lpStrings, ","),
strings.Join(vlStrings, ","),
)

View File

@@ -47,14 +47,14 @@ func (e *expvarCollector) Collect(ch chan<- Metric) {
if expVar == nil {
continue
}
var v interface{}
var v any
labels := make([]string, len(desc.variableLabels.names))
if err := json.Unmarshal([]byte(expVar.String()), &v); err != nil {
ch <- NewInvalidMetric(desc, err)
continue
}
var processValue func(v interface{}, i int)
processValue = func(v interface{}, i int) {
var processValue func(v any, i int)
processValue = func(v any, i int) {
if i >= len(labels) {
copiedLabels := append(make([]string, 0, len(labels)), labels...)
switch v := v.(type) {
@@ -72,7 +72,7 @@ func (e *expvarCollector) Collect(ch chan<- Metric) {
ch <- m
return
}
vm, ok := v.(map[string]interface{})
vm, ok := v.(map[string]any)
if !ok {
return
}

View File

@@ -76,11 +76,12 @@ type GaugeVecOpts struct {
// scenarios for Gauges and Counters, where the former tends to be Set-heavy and
// the latter Inc-heavy.
func NewGauge(opts GaugeOpts) Gauge {
desc := NewDesc(
desc := V2.NewDesc(
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help,
nil,
UnconstrainedLabels(nil),
opts.ConstLabels,
WithUnit(opts.Unit),
)
result := &gauge{desc: desc, labelPairs: desc.constLabelPairs}
result.init(result) // Init self-collection.
@@ -163,6 +164,7 @@ func (v2) NewGaugeVec(opts GaugeVecOpts) *GaugeVec {
opts.Help,
opts.VariableLabels,
opts.ConstLabels,
WithUnit(opts.Unit),
)
return &GaugeVec{
MetricVec: NewMetricVec(desc, func(lvs ...string) Metric {
@@ -302,10 +304,11 @@ type GaugeFunc interface {
// value of 1. Example:
// https://github.com/prometheus/common/blob/8558a5b7db3c84fa38b4766966059a7bd5bfa2ee/version/info.go#L36-L56
func NewGaugeFunc(opts GaugeOpts, function func() float64) GaugeFunc {
return newValueFunc(NewDesc(
return newValueFunc(V2.NewDesc(
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help,
nil,
UnconstrainedLabels(nil),
opts.ConstLabels,
WithUnit(opts.Unit),
), GaugeValue, function)
}

View File

@@ -1,122 +0,0 @@
// Copyright 2021 The Prometheus 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.
//go:build !go1.17
// +build !go1.17
package prometheus
import (
"runtime"
"sync"
"time"
)
type goCollector struct {
base baseGoCollector
// ms... are memstats related.
msLast *runtime.MemStats // Previously collected memstats.
msLastTimestamp time.Time
msMtx sync.Mutex // Protects msLast and msLastTimestamp.
msMetrics memStatsMetrics
msRead func(*runtime.MemStats) // For mocking in tests.
msMaxWait time.Duration // Wait time for fresh memstats.
msMaxAge time.Duration // Maximum allowed age of old memstats.
}
// NewGoCollector is the obsolete version of collectors.NewGoCollector.
// See there for documentation.
//
// Deprecated: Use collectors.NewGoCollector instead.
func NewGoCollector() Collector {
msMetrics := goRuntimeMemStats()
msMetrics = append(msMetrics, struct {
desc *Desc
eval func(*runtime.MemStats) float64
valType ValueType
}{
// This metric is omitted in Go1.17+, see https://github.com/prometheus/client_golang/issues/842#issuecomment-861812034
desc: NewDesc(
memstatNamespace("gc_cpu_fraction"),
"The fraction of this program's available CPU time used by the GC since the program started.",
nil, nil,
),
eval: func(ms *runtime.MemStats) float64 { return ms.GCCPUFraction },
valType: GaugeValue,
})
return &goCollector{
base: newBaseGoCollector(),
msLast: &runtime.MemStats{},
msRead: runtime.ReadMemStats,
msMaxWait: time.Second,
msMaxAge: 5 * time.Minute,
msMetrics: msMetrics,
}
}
// Describe returns all descriptions of the collector.
func (c *goCollector) Describe(ch chan<- *Desc) {
c.base.Describe(ch)
for _, i := range c.msMetrics {
ch <- i.desc
}
}
// Collect returns the current state of all metrics of the collector.
func (c *goCollector) Collect(ch chan<- Metric) {
var (
ms = &runtime.MemStats{}
done = make(chan struct{})
)
// Start reading memstats first as it might take a while.
go func() {
c.msRead(ms)
c.msMtx.Lock()
c.msLast = ms
c.msLastTimestamp = time.Now()
c.msMtx.Unlock()
close(done)
}()
// Collect base non-memory metrics.
c.base.Collect(ch)
timer := time.NewTimer(c.msMaxWait)
select {
case <-done: // Our own ReadMemStats succeeded in time. Use it.
timer.Stop() // Important for high collection frequencies to not pile up timers.
c.msCollect(ch, ms)
return
case <-timer.C: // Time out, use last memstats if possible. Continue below.
}
c.msMtx.Lock()
if time.Since(c.msLastTimestamp) < c.msMaxAge {
// Last memstats are recent enough. Collect from them under the lock.
c.msCollect(ch, c.msLast)
c.msMtx.Unlock()
return
}
// If we are here, the last memstats are too old or don't exist. We have
// to wait until our own ReadMemStats finally completes. For that to
// happen, we have to release the lock.
c.msMtx.Unlock()
<-done
c.msCollect(ch, ms)
}
func (c *goCollector) msCollect(ch chan<- Metric, ms *runtime.MemStats) {
for _, i := range c.msMetrics {
ch <- MustNewConstMetric(i.desc, i.valType, i.eval(ms))
}
}

View File

@@ -98,7 +98,7 @@ type goCollector struct {
// snapshot is always produced by Collect.
mu sync.Mutex
// Contains all samples that has to retrieved from runtime/metrics (not all of them will be exposed).
// Contains all samples that have to be retrieved from runtime/metrics (not all of them will be exposed).
sampleBuf []metrics.Sample
// sampleMap allows lookup for MemStats metrics and runtime/metrics histograms for exact sums.
sampleMap map[string]*metrics.Sample
@@ -210,16 +210,26 @@ func NewGoCollector(opts ...func(o *internal.GoCollectorOptions)) Collector {
sampleBuf = append(sampleBuf, metrics.Sample{Name: d.Name})
sampleMap[d.Name] = &sampleBuf[len(sampleBuf)-1]
// Extract unit from the runtime/metrics name (e.g., "/gc/heap/allocs:bytes" -> "bytes")
// and sanitize to match Prometheus naming conventions (e.g., "cpu-seconds" -> "cpu_seconds")
var unit string
if idx := strings.IndexRune(d.Name, ':'); idx >= 0 {
unit = d.Name[idx+1:]
unit = strings.ReplaceAll(unit, "-", "_")
unit = strings.ReplaceAll(unit, "*", "_")
unit = strings.ReplaceAll(unit, "/", "_per_")
}
var m collectorMetric
if d.Kind == metrics.KindFloat64Histogram {
_, hasSum := opt.RuntimeMetricSumForHist[d.Name]
unit := d.Name[strings.IndexRune(d.Name, ':')+1:]
m = newBatchHistogram(
NewDesc(
V2.NewDesc(
BuildFQName(namespace, subsystem, name),
help,
UnconstrainedLabels(nil),
nil,
nil,
WithUnit(unit),
),
internal.RuntimeMetricsBucketsForUnit(bucketsMap[d.Name], unit),
hasSum,
@@ -230,6 +240,7 @@ func NewGoCollector(opts ...func(o *internal.GoCollectorOptions)) Collector {
Subsystem: subsystem,
Name: name,
Help: help,
Unit: unit,
},
)
} else {
@@ -238,6 +249,7 @@ func NewGoCollector(opts ...func(o *internal.GoCollectorOptions)) Collector {
Subsystem: subsystem,
Name: name,
Help: help,
Unit: unit,
})
}
metricSet = append(metricSet, m)

View File

@@ -378,6 +378,9 @@ type HistogramOpts struct {
// string.
Help string
// Unit provides the unit of this Histogram.
Unit string
// ConstLabels are used to attach fixed labels to this metric. Metrics
// with the same fully-qualified name must have the same label names in
// their ConstLabels.
@@ -522,11 +525,12 @@ type HistogramVecOpts struct {
// for each bucket.
func NewHistogram(opts HistogramOpts) Histogram {
return newHistogram(
NewDesc(
V2.NewDesc(
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help,
nil,
UnconstrainedLabels(nil),
opts.ConstLabels,
WithUnit(opts.Unit),
),
opts,
)
@@ -966,7 +970,7 @@ func (h *histogram) maybeReset(
// We are using the possibly mocked h.now() rather than
// time.Since(h.lastResetTime) to enable testing.
if h.nativeHistogramMinResetDuration == 0 || // No reset configured.
h.resetScheduled || // Do not interefere if a reset is already scheduled.
h.resetScheduled || // Do not interfere if a reset is already scheduled.
h.now().Sub(h.lastResetTime) < h.nativeHistogramMinResetDuration {
return false
}
@@ -1053,8 +1057,8 @@ func (h *histogram) maybeWidenZeroBucket(hot, cold *histogramCounts) bool {
atomic.StoreUint64(&cold.nativeHistogramZeroThresholdBits, math.Float64bits(newZeroThreshold))
// ...and then merge the newly deleted buckets into the wider zero
// bucket.
mergeAndDeleteOrAddAndReset := func(hotBuckets, coldBuckets *sync.Map) func(k, v interface{}) bool {
return func(k, v interface{}) bool {
mergeAndDeleteOrAddAndReset := func(hotBuckets, coldBuckets *sync.Map) func(k, v any) bool {
return func(k, v any) bool {
key := k.(int)
bucket := v.(*int64)
if key == smallestKey {
@@ -1107,8 +1111,8 @@ func (h *histogram) doubleBucketWidth(hot, cold *histogramCounts) {
// ...adjust the schema in the cold counts, too...
atomic.StoreInt32(&cold.nativeHistogramSchema, coldSchema)
// ...and then merge the cold buckets into the wider hot buckets.
merge := func(hotBuckets *sync.Map) func(k, v interface{}) bool {
return func(k, v interface{}) bool {
merge := func(hotBuckets *sync.Map) func(k, v any) bool {
return func(k, v any) bool {
key := k.(int)
bucket := v.(*int64)
// Adjust key to match the bucket to merge into.
@@ -1190,6 +1194,7 @@ func (v2) NewHistogramVec(opts HistogramVecOpts) *HistogramVec {
opts.Help,
opts.VariableLabels,
opts.ConstLabels,
WithUnit(opts.Unit),
)
return &HistogramVec{
MetricVec: NewMetricVec(desc, func(lvs ...string) Metric {
@@ -1476,7 +1481,7 @@ func pickSchema(bucketFactor float64) int32 {
func makeBuckets(buckets *sync.Map) ([]*dto.BucketSpan, []int64) {
var ii []int
buckets.Range(func(k, v interface{}) bool {
buckets.Range(func(k, v any) bool {
ii = append(ii, k.(int))
return true
})
@@ -1553,8 +1558,8 @@ func addToBucket(buckets *sync.Map, key int, increment int64) bool {
// according to the buckets ranged through. It then resets all buckets ranged
// through to 0 (but leaves them in place so that they don't need to get
// recreated on the next scrape).
func addAndReset(hotBuckets *sync.Map, bucketNumber *uint32) func(k, v interface{}) bool {
return func(k, v interface{}) bool {
func addAndReset(hotBuckets *sync.Map, bucketNumber *uint32) func(k, v any) bool {
return func(k, v any) bool {
bucket := v.(*int64)
if addToBucket(hotBuckets, k.(int), atomic.LoadInt64(bucket)) {
atomic.AddUint32(bucketNumber, 1)
@@ -1565,7 +1570,7 @@ func addAndReset(hotBuckets *sync.Map, bucketNumber *uint32) func(k, v interface
}
func deleteSyncMap(m *sync.Map) {
m.Range(func(k, v interface{}) bool {
m.Range(func(k, v any) bool {
m.Delete(k)
return true
})
@@ -1573,7 +1578,7 @@ func deleteSyncMap(m *sync.Map) {
func findSmallestKey(m *sync.Map) int {
result := math.MaxInt32
m.Range(func(k, v interface{}) bool {
m.Range(func(k, v any) bool {
key := k.(int)
if key < result {
result = key

View File

@@ -78,7 +78,7 @@ type OpCode struct {
// notion, pairing up elements that appear uniquely in each sequence.
// That, and the method here, appear to yield more intuitive difference
// reports than does diff. This method appears to be the least vulnerable
// to synching up on blocks of "junk lines", though (like blank lines in
// to syncing up on blocks of "junk lines", though (like blank lines in
// ordinary text files, or maybe "<P>" lines in HTML files). That may be
// because this is the only method of the 3 that has a *concept* of
// "junk" <wink>.
@@ -567,7 +567,7 @@ type UnifiedDiff struct {
func WriteUnifiedDiff(writer io.Writer, diff UnifiedDiff) error {
buf := bufio.NewWriter(writer)
defer buf.Flush()
wf := func(format string, args ...interface{}) error {
wf := func(format string, args ...any) error {
_, err := fmt.Fprintf(buf, format, args...)
return err
}

View File

@@ -184,6 +184,5 @@ func validateLabelValues(vals []string, expectedNumberOfValues int) error {
}
func checkLabelName(l string) bool {
//nolint:staticcheck // TODO: Don't use deprecated model.NameValidationScheme.
return model.NameValidationScheme.IsValidLabelName(l) && !strings.HasPrefix(l, reservedLabelPrefix)
return model.UTF8Validation.IsValidLabelName(l) && !strings.HasPrefix(l, reservedLabelPrefix)
}

View File

@@ -81,6 +81,9 @@ type Opts struct {
// string.
Help string
// Unit provides the unit of this metric as per https://prometheus.io/docs/specs/om
Unit string
// ConstLabels are used to attach fixed labels to this metric. Metrics
// with the same fully-qualified name must have the same label names in
// their ConstLabels.

View File

@@ -72,7 +72,13 @@ func getOpenFileCount() (float64, error) {
}
func (c *processCollector) processCollect(ch chan<- Metric) {
if procs, err := unix.SysctlKinfoProcSlice("kern.proc.pid", os.Getpid()); err == nil {
pid, err := c.pidFn()
if err != nil {
c.reportError(ch, nil, err)
return
}
if procs, err := unix.SysctlKinfoProcSlice("kern.proc.pid", pid); err == nil {
if len(procs) == 1 {
startTime := float64(procs[0].Proc.P_starttime.Nano() / 1e9)
ch <- MustNewConstMetric(c.startTime, GaugeValue, startTime)
@@ -84,6 +90,11 @@ func (c *processCollector) processCollect(ch chan<- Metric) {
c.reportError(ch, c.startTime, err)
}
if pid != os.Getpid() {
c.reportError(ch, nil, fmt.Errorf("collecting metrics for pid %d is not supported on darwin: process metrics collection is limited to the current process (pid %d)", pid, os.Getpid()))
return
}
// The proc structure returned by kern.proc.pid above has an Rusage member,
// but it is not filled in, so it needs to be fetched by getrusage(2). For
// that call, the UTime, STime, and Maxrss members are filled out, but not

View File

@@ -30,6 +30,10 @@ var (
procGetProcessMemoryInfo = modpsapi.NewProc("GetProcessMemoryInfo")
procGetProcessHandleCount = modkernel32.NewProc("GetProcessHandleCount")
openProcess = windows.OpenProcess
closeHandle = windows.CloseHandle
getProcessTimes = windows.GetProcessTimes
)
type processMemoryCounters struct {
@@ -79,10 +83,21 @@ func getProcessHandleCount(handle windows.Handle) (uint32, error) {
}
func (c *processCollector) processCollect(ch chan<- Metric) {
h := windows.CurrentProcess()
pid, err := c.pidFn()
if err != nil {
c.reportError(ch, nil, err)
return
}
h, err := openProcess(windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ, false, uint32(pid))
if err != nil {
c.reportError(ch, nil, err)
return
}
defer closeHandle(h)
var startTime, exitTime, kernelTime, userTime windows.Filetime
err := windows.GetProcessTimes(h, &startTime, &exitTime, &kernelTime, &userTime)
err = getProcessTimes(h, &startTime, &exitTime, &kernelTime, &userTime)
if err != nil {
c.reportError(ch, nil, err)
return

View File

@@ -37,10 +37,12 @@ import (
"fmt"
"io"
"net/http"
"slices"
"strconv"
"sync"
"time"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"
"github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil"
@@ -74,11 +76,118 @@ func defaultCompressionFormats() []Compression {
}
var gzipPool = sync.Pool{
New: func() interface{} {
New: func() any {
return gzip.NewWriter(nil)
},
}
// coalescingGatherer wraps a TransactionalGatherer to deduplicate concurrent
// Gather calls. When a Gather is already in flight, new callers join the
// existing cycle and receive the same result once it completes. The underlying
// done function is called exactly once, when the last joined caller releases.
//
// This prevents goroutine pile-up when the scrape rate is faster than the
// time collectors need to produce metrics.
type coalescingGatherer struct {
g prometheus.TransactionalGatherer
mu sync.Mutex
cycle *gatherCycle
}
// gatherCycle tracks a single in-flight Gather and all HTTP handlers sharing it.
type gatherCycle struct {
ready chan struct{} // closed when Gather completes; happens-before reads of mfs/err/done
mfs []*dto.MetricFamily // canonical result, set before ready is closed; callers get a slices.Clone, the element values stay shared and must not be mutated
err error // set before ready is closed
done func() // underlying done callback; set before ready is closed
refs int // number of handlers using this cycle; protected by coalescingGatherer.mu
}
var _ prometheus.TransactionalGatherer = (*coalescingGatherer)(nil) // compile-time interface check
// errGatherPanicked is returned to callers that joined an in-flight coalesced
// Gather whose underlying gatherer panicked. See the panic guard in Gather for
// why joiners receive this error instead of the panic itself.
var errGatherPanicked = errors.New("coalesced gather panicked")
func (c *coalescingGatherer) Gather() ([]*dto.MetricFamily, func(), error) {
c.mu.Lock()
if cy := c.cycle; cy != nil {
// c.cycle is non-nil while Gather runs or handlers are still consuming its results.
cy.refs++
c.mu.Unlock()
<-cy.ready
// Each caller gets its own slice header so it can filter or reorder
// without racing other callers sharing this cycle. The *dto.MetricFamily
// values remain shared and must not be mutated in place.
return slices.Clone(cy.mfs), c.releaseFunc(cy), cy.err
}
cy := &gatherCycle{
ready: make(chan struct{}),
done: func() {},
refs: 1,
}
c.cycle = cy
c.mu.Unlock()
// Guard against a panic in c.g.Gather. The common case, a panicking
// Collector, never reaches here: Registry.Gather recovers Collector panics
// and returns them as an error. This guard only covers the rare case where
// the wrapped gatherer itself panics.
//
// We deliberately do not recover: the leader's panic propagates and is
// handled by net/http exactly as it would be without coalescing. We only
// set cy.err before closing cy.ready so joiners waiting on <-cy.ready fail
// with that error instead of silently returning an empty, successful
// response, and we clear c.cycle so the next Gather starts a fresh cycle.
//
// The leader never runs its own releaseFunc on this path, so its ref is
// not decremented; that is harmless because the cycle is detached (c.cycle
// = nil) and cy.done is still the no-op set at construction (c.g.Gather
// panicked before assigning a real done). If cy.done is ever made non-nil
// before c.g.Gather runs, this path would need to release it.
panicked := true
defer func() {
if panicked {
c.mu.Lock()
if c.cycle == cy {
c.cycle = nil
}
c.mu.Unlock()
cy.err = errGatherPanicked // set before close: happens-before joiners' reads
close(cy.ready)
}
}()
cy.mfs, cy.done, cy.err = c.g.Gather()
panicked = false
close(cy.ready) // happens-before joiners' reads of cy.mfs/err/done
// Clone here too so cy.mfs stays the write-once canonical slice: joiners
// read it concurrently via slices.Clone, so the leader must not hand out
// (and potentially reorder) the same backing array.
return slices.Clone(cy.mfs), c.releaseFunc(cy), cy.err
}
// releaseFunc returns the done callback for one caller sharing cy.
// When the last caller releases, the underlying done is invoked and the
// cycle is cleared so the next Gather starts fresh.
func (c *coalescingGatherer) releaseFunc(cy *gatherCycle) func() {
return func() {
c.mu.Lock()
cy.refs--
if cy.refs > 0 {
c.mu.Unlock()
return
}
// Last caller.
if c.cycle == cy {
c.cycle = nil
}
c.mu.Unlock()
cy.done() // called outside the lock to avoid holding it during done
}
}
// Handler returns an http.Handler for the prometheus.DefaultGatherer, using
// default HandlerOpts, i.e. it reports the first error as an HTTP error, it has
// no error logging, and it applies compression if requested by the client.
@@ -89,6 +198,10 @@ var gzipPool = sync.Pool{
// metrics used for instrumentation will be shared between them, providing
// global scrape counts.
//
// The handler supports filtering metrics by name using the `name[]` query parameter.
// Multiple metric names can be specified by providing the parameter multiple times.
// When no name[] parameters are provided, all metrics are returned.
//
// This function is meant to cover the bulk of basic use cases. If you are doing
// anything that requires more customization (including using a non-default
// Gatherer, different instrumentation, and non-default HandlerOpts), use the
@@ -105,6 +218,10 @@ func Handler() http.Handler {
// Gatherers, with non-default HandlerOpts, and/or with custom (or no)
// instrumentation. Use the InstrumentMetricHandler function to apply the same
// kind of instrumentation as it is used by the Handler function.
//
// The handler supports filtering metrics by name using the `name[]` query parameter.
// Multiple metric names can be specified by providing the parameter multiple times.
// When no name[] parameters are provided, all metrics are returned.
func HandlerFor(reg prometheus.Gatherer, opts HandlerOpts) http.Handler {
return HandlerForTransactional(prometheus.ToTransactionalGatherer(reg), opts)
}
@@ -112,7 +229,15 @@ func HandlerFor(reg prometheus.Gatherer, opts HandlerOpts) http.Handler {
// HandlerForTransactional is like HandlerFor, but it uses transactional gather, which
// can safely change in-place returned *dto.MetricFamily before call to `Gather` and after
// call to `done` of that `Gather`.
//
// The handler supports filtering metrics by name using the `name[]` query parameter.
// Multiple metric names can be specified by providing the parameter multiple times.
// When no name[] parameters are provided, all metrics are returned.
func HandlerForTransactional(reg prometheus.TransactionalGatherer, opts HandlerOpts) http.Handler {
if opts.CoalesceGather {
reg = &coalescingGatherer{g: reg}
}
var (
inFlightSem chan struct{}
errCnt = prometheus.NewCounterVec(
@@ -214,12 +339,14 @@ func HandlerForTransactional(reg prometheus.TransactionalGatherer, opts HandlerO
rsp.Header().Set(contentEncodingHeader, encodingHeader)
}
var enc expfmt.Encoder
var (
enc expfmt.Encoder
encOpts []expfmt.EncoderOption
)
if opts.EnableOpenMetricsTextCreatedSamples {
enc = expfmt.NewEncoder(w, contentType, expfmt.WithCreatedLines())
} else {
enc = expfmt.NewEncoder(w, contentType)
encOpts = append(encOpts, expfmt.WithCreatedLines())
}
enc = expfmt.NewEncoder(w, contentType, encOpts...)
// handleError handles the error according to opts.ErrorHandling
// and returns true if we have to abort after the handling.
@@ -245,7 +372,24 @@ func HandlerForTransactional(reg prometheus.TransactionalGatherer, opts HandlerO
return false
}
// Build metric name filter set from query params (if any). The URL
// can be nil on hand-constructed requests.
var metricFilter map[string]struct{}
if req.URL != nil {
if metricNames := req.URL.Query()["name[]"]; len(metricNames) > 0 {
metricFilter = make(map[string]struct{}, len(metricNames))
for _, name := range metricNames {
metricFilter[name] = struct{}{}
}
}
}
for _, mf := range mfs {
if metricFilter != nil {
if _, ok := metricFilter[mf.GetName()]; !ok {
continue
}
}
if handleError(enc.Encode(mf)) {
return
}
@@ -353,7 +497,7 @@ const (
// log.Logger from the standard library implements this interface, and it is
// easy to implement by custom loggers, if they don't do so already anyway.
type Logger interface {
Println(v ...interface{})
Println(v ...any)
}
// HandlerOpts specifies options how to serve metrics via an http.Handler. The
@@ -400,6 +544,40 @@ type HandlerOpts struct {
// Service Unavailable and a suitable message in the body. If
// MaxRequestsInFlight is 0 or negative, no limit is applied.
MaxRequestsInFlight int
// CoalesceGather, if true, deduplicates concurrent Gather calls so that
// only one collection runs at a time. Additional requests that arrive
// while a Gather is in flight will receive the same result once it
// completes. This prevents goroutine pile-up when the scrape rate is
// faster than the time collectors need to produce metrics.
//
// When enabled, concurrent scrapers share a single metric snapshot per
// collection cycle. Each request receives its own copy of the returned
// slice, so filtering or reordering it (for example via name[] query
// parameters) is safe. The pointed-to MetricFamily values are still
// shared: the built-in handler only reads them, so this is safe in
// practice, but a custom TransactionalGatherer that mutates the returned
// families in place after Gather returns must not use this option.
//
// Because the snapshot is shared, a request that arrives while a cycle is
// in flight receives that cycle's result even though collection began
// before the request; two scrapers joined to one cycle observe the same
// timestamps rather than independently gathered data.
//
// Consider using CoalesceGather together with Timeout. Timeout bounds the
// client-facing response time and keeps at most one collection running at
// a time, but it does not cancel the underlying Gather: a joined request
// that times out still holds a MaxRequestsInFlight slot until the shared
// collection completes.
//
// Panic handling: a panicking Collector is already turned into an error by
// the registry, so joiners receive that error like any other. In the rare
// case where the wrapped gatherer itself panics, the panicking request's
// panic propagates as usual (handled by net/http), while requests that
// joined the same cycle receive an error rather than an empty response.
//
// NOTE: This option is experimental and may change or be removed in a
// future release.
CoalesceGather bool
// If handling a request takes longer than Timeout, it is responded to
// with 503 ServiceUnavailable and a suitable Message. No timeout is
// applied if Timeout is 0 or negative. Note that with the current
@@ -407,8 +585,9 @@ type HandlerOpts struct {
// described above (and even that only if sending of the body hasn't
// started yet), while the bulk work of gathering all the metrics keeps
// running in the background (with the eventual result to be thrown
// away). Until the implementation is improved, it is recommended to
// implement a separate timeout in potentially slow Collectors.
// away). When CoalesceGather is enabled, only one such background Gather
// can be in flight at a time. It is also recommended to implement a
// separate timeout in potentially slow Collectors.
Timeout time.Duration
// If true, the experimental OpenMetrics encoding is added to the
// possible options during content negotiation. Note that Prometheus
@@ -460,7 +639,7 @@ func httpError(rsp http.ResponseWriter, err error) {
// negotiateEncodingWriter reads the Accept-Encoding header from a request and
// selects the right compression based on an allow-list of supported
// compressions. It returns a writer implementing the compression and an the
// compressions. It returns a writer implementing the compression and the
// correct value that the caller can set in the response header.
func negotiateEncodingWriter(r *http.Request, rw io.Writer, compressions []string) (_ io.Writer, encodingHeaderValue string, closeWriter func(), _ error) {
if len(compressions) == 0 {

View File

@@ -75,10 +75,10 @@ func InstrumentRoundTripperCounter(counter *prometheus.CounterVec, next http.Rou
resp, err := next.RoundTrip(r)
if err == nil {
l := labels(code, method, r.Method, resp.StatusCode, rtOpts.extraMethods...)
for label, resolve := range rtOpts.extraLabelsFromCtx {
l[label] = resolve(resp.Request.Context())
for label, resolve := range rtOpts.extraLabelsFromRequest {
l[label] = resolve(resp.Request)
}
addWithExemplar(counter.With(l), 1, rtOpts.getExemplarFn(r.Context()))
addWithExemplar(counter.With(l), 1, rtOpts.getExemplarFn(r))
}
return resp, err
}
@@ -119,10 +119,10 @@ func InstrumentRoundTripperDuration(obs prometheus.ObserverVec, next http.RoundT
resp, err := next.RoundTrip(r)
if err == nil {
l := labels(code, method, r.Method, resp.StatusCode, rtOpts.extraMethods...)
for label, resolve := range rtOpts.extraLabelsFromCtx {
l[label] = resolve(resp.Request.Context())
for label, resolve := range rtOpts.extraLabelsFromRequest {
l[label] = resolve(resp.Request)
}
observeWithExemplar(obs.With(l), time.Since(start).Seconds(), rtOpts.getExemplarFn(r.Context()))
observeWithExemplar(obs.With(l), time.Since(start).Seconds(), rtOpts.getExemplarFn(r))
}
return resp, err
}

View File

@@ -28,24 +28,36 @@ import (
// magicString is used for the hacky label test in checkLabels. Remove once fixed.
const magicString = "zZgWfBxLqvG8kc8IMv3POi2Bb0tZI3vAnBx+gBaFi9FyPzB/CzKUer1yufDa"
// observeWithExemplar is a wrapper for [prometheus.ExemplarAdder.ExemplarObserver],
// which falls back to [prometheus.Observer.Observe] if no labels are provided.
// observeWithExemplar records val on obs. If labels is non-nil and obs
// implements [prometheus.ExemplarObserver], the exemplar is attached via
// ObserveWithExemplar; otherwise the exemplar is dropped and the value is
// recorded with a plain [prometheus.Observer.Observe]. This mirrors the
// safe-cast pattern in [prometheus.Timer.ObserveDurationWithExemplar] and
// ensures we never panic when callers pass an ObserverVec backed by a
// summary, which cannot carry exemplars in the Prometheus exposition format.
func observeWithExemplar(obs prometheus.Observer, val float64, labels map[string]string) {
if labels == nil {
obs.Observe(val)
return
if labels != nil {
if eo, ok := obs.(prometheus.ExemplarObserver); ok {
eo.ObserveWithExemplar(val, labels)
return
}
}
obs.(prometheus.ExemplarObserver).ObserveWithExemplar(val, labels)
obs.Observe(val)
}
// addWithExemplar is a wrapper for [prometheus.ExemplarAdder.AddWithExemplar],
// which falls back to [prometheus.Counter.Add] if no labels are provided.
func addWithExemplar(obs prometheus.Counter, val float64, labels map[string]string) {
if labels == nil {
obs.Add(val)
return
// addWithExemplar records val on c. If labels is non-nil and c implements
// [prometheus.ExemplarAdder], the exemplar is attached via AddWithExemplar;
// otherwise the exemplar is dropped and the value is recorded with a plain
// [prometheus.Counter.Add]. The safe-cast keeps the helper robust against
// custom Counter implementations that do not advertise exemplar support.
func addWithExemplar(c prometheus.Counter, val float64, labels map[string]string) {
if labels != nil {
if ea, ok := c.(prometheus.ExemplarAdder); ok {
ea.AddWithExemplar(val, labels)
return
}
}
obs.(prometheus.ExemplarAdder).AddWithExemplar(val, labels)
c.Add(val)
}
// InstrumentHandlerInFlight is a middleware that wraps the provided
@@ -97,10 +109,10 @@ func InstrumentHandlerDuration(obs prometheus.ObserverVec, next http.Handler, op
next.ServeHTTP(d, r)
l := labels(code, method, r.Method, d.Status(), hOpts.extraMethods...)
for label, resolve := range hOpts.extraLabelsFromCtx {
l[label] = resolve(r.Context())
for label, resolve := range hOpts.extraLabelsFromRequest {
l[label] = resolve(r)
}
observeWithExemplar(obs.With(l), time.Since(now).Seconds(), hOpts.getExemplarFn(r.Context()))
observeWithExemplar(obs.With(l), time.Since(now).Seconds(), hOpts.getExemplarFn(r))
}
}
@@ -108,10 +120,10 @@ func InstrumentHandlerDuration(obs prometheus.ObserverVec, next http.Handler, op
now := time.Now()
next.ServeHTTP(w, r)
l := labels(code, method, r.Method, 0, hOpts.extraMethods...)
for label, resolve := range hOpts.extraLabelsFromCtx {
l[label] = resolve(r.Context())
for label, resolve := range hOpts.extraLabelsFromRequest {
l[label] = resolve(r)
}
observeWithExemplar(obs.With(l), time.Since(now).Seconds(), hOpts.getExemplarFn(r.Context()))
observeWithExemplar(obs.With(l), time.Since(now).Seconds(), hOpts.getExemplarFn(r))
}
}
@@ -147,10 +159,10 @@ func InstrumentHandlerCounter(counter *prometheus.CounterVec, next http.Handler,
next.ServeHTTP(d, r)
l := labels(code, method, r.Method, d.Status(), hOpts.extraMethods...)
for label, resolve := range hOpts.extraLabelsFromCtx {
l[label] = resolve(r.Context())
for label, resolve := range hOpts.extraLabelsFromRequest {
l[label] = resolve(r)
}
addWithExemplar(counter.With(l), 1, hOpts.getExemplarFn(r.Context()))
addWithExemplar(counter.With(l), 1, hOpts.getExemplarFn(r))
}
}
@@ -158,10 +170,10 @@ func InstrumentHandlerCounter(counter *prometheus.CounterVec, next http.Handler,
next.ServeHTTP(w, r)
l := labels(code, method, r.Method, 0, hOpts.extraMethods...)
for label, resolve := range hOpts.extraLabelsFromCtx {
l[label] = resolve(r.Context())
for label, resolve := range hOpts.extraLabelsFromRequest {
l[label] = resolve(r)
}
addWithExemplar(counter.With(l), 1, hOpts.getExemplarFn(r.Context()))
addWithExemplar(counter.With(l), 1, hOpts.getExemplarFn(r))
}
}
@@ -200,10 +212,10 @@ func InstrumentHandlerTimeToWriteHeader(obs prometheus.ObserverVec, next http.Ha
now := time.Now()
d := newDelegator(w, func(status int) {
l := labels(code, method, r.Method, status, hOpts.extraMethods...)
for label, resolve := range hOpts.extraLabelsFromCtx {
l[label] = resolve(r.Context())
for label, resolve := range hOpts.extraLabelsFromRequest {
l[label] = resolve(r)
}
observeWithExemplar(obs.With(l), time.Since(now).Seconds(), hOpts.getExemplarFn(r.Context()))
observeWithExemplar(obs.With(l), time.Since(now).Seconds(), hOpts.getExemplarFn(r))
})
next.ServeHTTP(d, r)
}
@@ -244,10 +256,10 @@ func InstrumentHandlerRequestSize(obs prometheus.ObserverVec, next http.Handler,
size := computeApproximateRequestSize(r)
l := labels(code, method, r.Method, d.Status(), hOpts.extraMethods...)
for label, resolve := range hOpts.extraLabelsFromCtx {
l[label] = resolve(r.Context())
for label, resolve := range hOpts.extraLabelsFromRequest {
l[label] = resolve(r)
}
observeWithExemplar(obs.With(l), float64(size), hOpts.getExemplarFn(r.Context()))
observeWithExemplar(obs.With(l), float64(size), hOpts.getExemplarFn(r))
}
}
@@ -256,10 +268,10 @@ func InstrumentHandlerRequestSize(obs prometheus.ObserverVec, next http.Handler,
size := computeApproximateRequestSize(r)
l := labels(code, method, r.Method, 0, hOpts.extraMethods...)
for label, resolve := range hOpts.extraLabelsFromCtx {
l[label] = resolve(r.Context())
for label, resolve := range hOpts.extraLabelsFromRequest {
l[label] = resolve(r)
}
observeWithExemplar(obs.With(l), float64(size), hOpts.getExemplarFn(r.Context()))
observeWithExemplar(obs.With(l), float64(size), hOpts.getExemplarFn(r))
}
}
@@ -296,10 +308,10 @@ func InstrumentHandlerResponseSize(obs prometheus.ObserverVec, next http.Handler
next.ServeHTTP(d, r)
l := labels(code, method, r.Method, d.Status(), hOpts.extraMethods...)
for label, resolve := range hOpts.extraLabelsFromCtx {
l[label] = resolve(r.Context())
for label, resolve := range hOpts.extraLabelsFromRequest {
l[label] = resolve(r)
}
observeWithExemplar(obs.With(l), float64(d.Written()), hOpts.getExemplarFn(r.Context()))
observeWithExemplar(obs.With(l), float64(d.Written()), hOpts.getExemplarFn(r))
})
}
@@ -366,7 +378,7 @@ func checkLabels(c prometheus.Collector) (code, method bool) {
panic("metric partitioned with non-supported labels")
}
}
return
return code, method
}
func isLabelCurried(c prometheus.Collector, label string) bool {

View File

@@ -15,6 +15,7 @@ package promhttp
import (
"context"
"net/http"
"github.com/prometheus/client_golang/prometheus"
)
@@ -24,28 +25,31 @@ type Option interface {
apply(*options)
}
// LabelValueFromRequest is used to compute the label value from request.
type LabelValueFromRequest func(request *http.Request) string
// LabelValueFromCtx are used to compute the label value from request context.
// Context can be filled with values from request through middleware.
type LabelValueFromCtx func(ctx context.Context) string
// options store options for both a handler or round tripper.
type options struct {
extraMethods []string
getExemplarFn func(requestCtx context.Context) prometheus.Labels
extraLabelsFromCtx map[string]LabelValueFromCtx
extraMethods []string
getExemplarFn func(req *http.Request) prometheus.Labels
extraLabelsFromRequest map[string]LabelValueFromRequest
}
func defaultOptions() *options {
return &options{
getExemplarFn: func(ctx context.Context) prometheus.Labels { return nil },
extraLabelsFromCtx: map[string]LabelValueFromCtx{},
getExemplarFn: func(req *http.Request) prometheus.Labels { return nil },
extraLabelsFromRequest: map[string]LabelValueFromRequest{},
}
}
func (o *options) emptyDynamicLabels() prometheus.Labels {
labels := prometheus.Labels{}
for label := range o.extraLabelsFromCtx {
for label := range o.extraLabelsFromRequest {
labels[label] = ""
}
@@ -66,12 +70,30 @@ func WithExtraMethods(methods ...string) Option {
})
}
// WithExemplarFromContext allows to inject function that will get exemplar from context that will be put to counter and histogram metrics.
// WithExemplarFromRequest allows you to inject a function that will get exemplar from request that will be put to counter and histogram metrics.
// If the function returns nil labels or the metric does not support exemplars, no exemplar will be added (noop), but
// metric will continue to observe/increment.
func WithExemplarFromRequest(getExemplarFn func(req *http.Request) prometheus.Labels) Option {
return optionApplyFunc(func(o *options) {
o.getExemplarFn = getExemplarFn
})
}
// WithExemplarFromContext allows you to inject a function that will get exemplar from context that will be put to counter and histogram metrics.
// If the function returns nil labels or the metric does not support exemplars, no exemplar will be added (noop), but
// metric will continue to observe/increment.
func WithExemplarFromContext(getExemplarFn func(requestCtx context.Context) prometheus.Labels) Option {
return optionApplyFunc(func(o *options) {
o.getExemplarFn = getExemplarFn
o.getExemplarFn = func(req *http.Request) prometheus.Labels {
return getExemplarFn(req.Context())
}
})
}
// WithLabelFromRequest registers a label for dynamic resolution with access to the request.
func WithLabelFromRequest(name string, valueFn LabelValueFromRequest) Option {
return optionApplyFunc(func(o *options) {
o.extraLabelsFromRequest[name] = valueFn
})
}
@@ -79,6 +101,8 @@ func WithExemplarFromContext(getExemplarFn func(requestCtx context.Context) prom
// See the example for ExampleInstrumentHandlerWithLabelResolver for example usage
func WithLabelFromCtx(name string, valueFn LabelValueFromCtx) Option {
return optionApplyFunc(func(o *options) {
o.extraLabelsFromCtx[name] = valueFn
o.extraLabelsFromRequest[name] = func(req *http.Request) string {
return valueFn(req.Context())
}
})
}

View File

@@ -214,6 +214,19 @@ func (err AlreadyRegisteredError) Error() string {
// by a Gatherer to report multiple errors during MetricFamily gathering.
type MultiError []error
// SafeMultiError is a thread-safe wrapper around MultiError using a mutex.
type SafeMultiError struct {
mu sync.Mutex
errs MultiError
}
// Appends the provided error to the contained MultiError in a thread-safe way.
func (s *SafeMultiError) Append(err error) {
s.mu.Lock()
s.errs.Append(err)
s.mu.Unlock()
}
// Error formats the contained errors as a bullet point list, preceded by the
// total number of errors. Note that this results in a multi-line string.
func (errs MultiError) Error() string {
@@ -408,6 +421,16 @@ func (r *Registry) MustRegister(cs ...Collector) {
}
}
// MustGather implements Gatherer.
// Wraps around Gather and panics if Gather fails for any reason.
func (r *Registry) MustGather() []*dto.MetricFamily {
mfs, err := r.Gather()
if err != nil {
panic(err)
}
return mfs
}
// Gather implements Gatherer.
func (r *Registry) Gather() ([]*dto.MetricFamily, error) {
r.mtx.RLock()
@@ -423,7 +446,7 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) {
uncheckedMetricChan = make(chan Metric, capMetricChan)
metricHashes = map[uint64]struct{}{}
wg sync.WaitGroup
errs MultiError // The collected errors to return in the end.
safeErrs = &SafeMultiError{} // To collect errors in a threadsafe way
registeredDescIDs map[uint64]struct{} // Only used for pedantic checks
)
@@ -453,9 +476,9 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) {
for {
select {
case collector := <-checkedCollectors:
collector.Collect(checkedMetricChan)
safeErrs.Append((safeCollect(collector, checkedMetricChan)))
case collector := <-uncheckedCollectors:
collector.Collect(uncheckedMetricChan)
safeErrs.Append(safeCollect(collector, uncheckedMetricChan))
default:
return
}
@@ -499,7 +522,7 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) {
cmc = nil
break
}
errs.Append(processMetric(
safeErrs.Append(processMetric(
metric, metricFamiliesByName,
metricHashes,
registeredDescIDs,
@@ -509,7 +532,7 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) {
umc = nil
break
}
errs.Append(processMetric(
safeErrs.Append(processMetric(
metric, metricFamiliesByName,
metricHashes,
nil,
@@ -526,7 +549,7 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) {
cmc = nil
break
}
errs.Append(processMetric(
safeErrs.Append(processMetric(
metric, metricFamiliesByName,
metricHashes,
registeredDescIDs,
@@ -536,7 +559,7 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) {
umc = nil
break
}
errs.Append(processMetric(
safeErrs.Append(processMetric(
metric, metricFamiliesByName,
metricHashes,
nil,
@@ -556,7 +579,8 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) {
break
}
}
return internal.NormalizeMetricFamilies(metricFamiliesByName), errs.MaybeUnwrap()
return internal.NormalizeMetricFamilies(metricFamiliesByName), safeErrs.errs.MaybeUnwrap()
}
// Describe implements Collector.
@@ -571,6 +595,24 @@ func (r *Registry) Describe(ch chan<- *Desc) {
}
}
// Helper wrapper around Collector.Collect.
// It tries to collect from the channel, recovers on panic and
// if it has recovered from a panic, then it sends an InvalidMetric into
// the channel with an InvalidDesc, and an error that includes a stack trace.
func safeCollect(c Collector, ch chan<- Metric) (err error) {
defer func() {
if r := recover(); r != nil {
buf := make([]byte, 64<<10) // 64 KB
n := runtime.Stack(buf, false)
err = fmt.Errorf("prometheus collector panic recovered: type=%T: error=%v\nstack trace=%s", c, r, buf[:n])
ch <- NewInvalidMetric(NewInvalidDesc(err), err)
}
}()
c.Collect(ch)
return err
}
// Collect implements Collector.
func (r *Registry) Collect(ch chan<- Metric) {
r.mtx.RLock()
@@ -599,10 +641,12 @@ func WriteToTextfile(filename string, g Gatherer) error {
mfs, err := g.Gather()
if err != nil {
tmp.Close()
return err
}
for _, mf := range mfs {
if _, err := expfmt.MetricFamilyToText(tmp, mf); err != nil {
tmp.Close()
return err
}
}
@@ -685,6 +729,9 @@ func processMetric(
metricFamily = &dto.MetricFamily{}
metricFamily.Name = proto.String(desc.fqName)
metricFamily.Help = proto.String(desc.help)
if desc.unit != "" {
metricFamily.Unit = proto.String(desc.unit)
}
// TODO(beorn7): Simplify switch once Desc has type.
switch {
case dtoMetric.Gauge != nil:

View File

@@ -101,6 +101,9 @@ type SummaryOpts struct {
// string.
Help string
// Unit provides the unit of this Summary.
Unit string
// ConstLabels are used to attach fixed labels to this metric. Metrics
// with the same fully-qualified name must have the same label names in
// their ConstLabels.
@@ -181,11 +184,12 @@ type SummaryVecOpts struct {
// NewSummary creates a new Summary based on the provided SummaryOpts.
func NewSummary(opts SummaryOpts) Summary {
return newSummary(
NewDesc(
V2.NewDesc(
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help,
nil,
UnconstrainedLabels(nil),
opts.ConstLabels,
WithUnit(opts.Unit),
),
opts,
)
@@ -578,6 +582,7 @@ func (v2) NewSummaryVec(opts SummaryVecOpts) *SummaryVec {
opts.Help,
opts.VariableLabels,
opts.ConstLabels,
WithUnit(opts.Unit),
)
return &SummaryVec{
MetricVec: NewMetricVec(desc, func(lvs ...string) Metric {

View File

@@ -37,10 +37,10 @@ type Timer struct {
// or
//
// func TimeMeWithExemplar() {
// timer := NewTimer(myHistogram)
// defer timer.ObserveDurationWithExemplar(exemplar)
// // Do actual work.
// }
// timer := NewTimer(myHistogram)
// defer timer.ObserveDurationWithExemplar(exemplar)
// // Do actual work.
// }
func NewTimer(o Observer) *Timer {
return &Timer{
begin: time.Now(),
@@ -66,7 +66,7 @@ func (t *Timer) ObserveDuration() time.Duration {
// ObserveDurationWithExemplar is like ObserveDuration, but it will also
// observe exemplar with the duration unless exemplar is nil or provided Observer can't
// be casted to ExemplarObserver.
// be cast to ExemplarObserver.
func (t *Timer) ObserveDurationWithExemplar(exemplar Labels) time.Duration {
d := time.Since(t.begin)
eo, ok := t.observer.(ExemplarObserver)

View File

@@ -193,9 +193,11 @@ func (m *MetricVec) CurryWith(labels Labels) (*MetricVec, error) {
//
// Keeping the Metric for later use is possible (and should be considered if
// performance is critical), but keep in mind that Reset, DeleteLabelValues and
// Delete can be used to delete the Metric from the MetricVec. In that case, the
// Metric will still exist, but it will not be exported anymore, even if a
// Metric with the same label values is created later.
// Delete can be used to delete the Metric from the MetricVec. In that case, if
// you have previously kept a reference to that Metric, the Metric object still
// exists and can be used, but it will not be exported anymore. If a Metric with
// the same label values is created later, updates to the old Metric reference
// will not be exported.
//
// An error is returned if the number of label values is not the same as the
// number of variable labels in Desc (minus any curried labels).
@@ -657,7 +659,7 @@ func inlineLabelValues(lvs []string, curry []curriedLabelValue) []string {
}
var labelsPool = &sync.Pool{
New: func() interface{} {
New: func() any {
return make(Labels)
},
}

View File

@@ -230,6 +230,7 @@ func wrapDesc(desc *Desc, prefix string, labels Labels) *Desc {
return &Desc{
fqName: desc.fqName,
help: desc.help,
unit: desc.unit,
variableLabels: desc.variableLabels,
constLabelPairs: desc.constLabelPairs,
err: fmt.Errorf("attempted wrapping with already existing label name %q", ln),
@@ -238,8 +239,8 @@ func wrapDesc(desc *Desc, prefix string, labels Labels) *Desc {
constLabels[ln] = lv
}
// NewDesc will do remaining validations.
newDesc := V2.NewDesc(prefix+desc.fqName, desc.help, desc.variableLabels, constLabels)
// Propagate errors if there was any. This will override any errer
newDesc := V2.NewDesc(prefix+desc.fqName, desc.help, desc.variableLabels, constLabels, WithUnit(desc.unit))
// Propagate errors if there was any. This will override any error
// created by NewDesc above, i.e. earlier errors get precedence.
if desc.err != nil {
newDesc.err = desc.err

View File

@@ -122,7 +122,7 @@ func NewOpenMetricsFormat(version string) (Format, error) {
// removed.
func (f Format) WithEscapingScheme(s model.EscapingScheme) Format {
var terms []string
for _, p := range strings.Split(string(f), ";") {
for p := range strings.SplitSeq(string(f), ";") {
toks := strings.Split(p, "=")
if len(toks) != 2 {
trimmed := strings.TrimSpace(p)
@@ -194,7 +194,7 @@ func (f Format) FormatType() FormatType {
// "escaping" term exists, that will be used. Otherwise, the global default will
// be returned.
func (f Format) ToEscapingScheme() model.EscapingScheme {
for _, p := range strings.Split(string(f), ";") {
for p := range strings.SplitSeq(string(f), ";") {
toks := strings.Split(p, "=")
if len(toks) != 2 {
continue

View File

@@ -30,7 +30,6 @@ import (
type encoderOption struct {
withCreatedLines bool
withUnit bool
}
type EncoderOption func(*encoderOption)
@@ -51,17 +50,6 @@ func WithCreatedLines() EncoderOption {
}
}
// WithUnit is an EncoderOption enabling a set unit to be written to the output
// and to be added to the metric name, if it's not there already, as a suffix.
// Without opting in this way, the unit will not be added to the metric name and,
// on top of that, the unit will not be passed onto the output, even if it
// were declared in the *dto.MetricFamily struct, i.e. even if in.Unit !=nil.
func WithUnit() EncoderOption {
return func(t *encoderOption) {
t.withUnit = true
}
}
// MetricFamilyToOpenMetrics converts a MetricFamily proto message into the
// OpenMetrics text format and writes the resulting lines to 'out'. It returns
// the number of bytes written and any error encountered. The output will have
@@ -99,15 +87,6 @@ func WithUnit() EncoderOption {
// its type will be set to `unknown` in that case to avoid invalid OpenMetrics
// output.
//
// - According to the OM specs, the `# UNIT` line is optional, but if populated,
// the unit has to be present in the metric name as its suffix:
// (see https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#unit).
// However, in order to accommodate any potential scenario where such a change in the
// metric name is not desirable, the users are here given the choice of either explicitly
// opt in, in case they wish for the unit to be included in the output AND in the metric name
// as a suffix (see the description of the WithUnit function above),
// or not to opt in, in case they don't want for any of that to happen.
//
// - No support for the following (optional) features: info type,
// stateset type, gaugehistogram type.
//
@@ -151,9 +130,6 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily, options ...E
if metricType == dto.MetricType_COUNTER && strings.HasSuffix(compliantName, "_total") {
compliantName = name[:len(name)-6]
}
if toOM.withUnit && in.Unit != nil && !strings.HasSuffix(compliantName, "_"+*in.Unit) {
compliantName = compliantName + "_" + *in.Unit
}
// Comments, first HELP, then TYPE.
if in.Help != nil {
@@ -217,7 +193,7 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily, options ...E
if err != nil {
return written, err
}
if toOM.withUnit && in.Unit != nil {
if in.Unit != nil {
n, err = w.WriteString("# UNIT ")
written += n
if err != nil {

View File

@@ -42,12 +42,12 @@ const (
var (
bufPool = sync.Pool{
New: func() interface{} {
New: func() any {
return bufio.NewWriter(io.Discard)
},
}
numBufPool = sync.Pool{
New: func() interface{} {
New: func() any {
b := make([]byte, 0, initialNumBufSize)
return &b
},

View File

@@ -339,6 +339,16 @@ func (p *TextParser) startLabelName() stateFn {
return nil // Unexpected end of input.
}
if p.currentByte == '}' {
if p.currentMF == nil {
// The closing brace was reached before any metric name was read,
// e.g. for the input "{}". There is no metric to attach labels to,
// so this is a malformed exposition. This mirrors the guard in
// startLabelValue. currentMF (not currentMetric) is checked because
// reset only clears currentMF between parses.
p.parseError("invalid metric name")
p.currentLabelPairs = nil
return nil
}
p.currentMetric.Label = append(p.currentMetric.Label, p.currentLabelPairs...)
p.currentLabelPairs = nil
if p.skipBlankTab(); p.err != nil {

View File

@@ -25,7 +25,7 @@ import (
var errNaNOrInf = errors.New("value is NaN or Inf")
func ConvertToFloat(i interface{}) (float64, error) {
func ConvertToFloat(i any) (float64, error) {
switch v := i.(type) {
case float64:
return v, nil
@@ -58,7 +58,7 @@ func FloatToTime(v float64) (*time.Time, error) {
return &t, nil
}
func HumanizeDuration(i interface{}) (string, error) {
func HumanizeDuration(i any) (string, error) {
v, err := ConvertToFloat(i)
if err != nil {
return "", err
@@ -105,7 +105,7 @@ func HumanizeDuration(i interface{}) (string, error) {
return fmt.Sprintf("%.4g%ss", v, prefix), nil
}
func HumanizeTimestamp(i interface{}) (string, error) {
func HumanizeTimestamp(i any) (string, error) {
v, err := ConvertToFloat(i)
if err != nil {
return "", err

View File

@@ -124,7 +124,7 @@ func (ln LabelName) IsValidLegacy() bool {
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (ln *LabelName) UnmarshalYAML(unmarshal func(interface{}) error) error {
func (ln *LabelName) UnmarshalYAML(unmarshal func(any) error) error {
var s string
if err := unmarshal(&s); err != nil {
return err

View File

@@ -16,6 +16,7 @@ package model
import (
"encoding/json"
"fmt"
"maps"
"sort"
)
@@ -107,9 +108,7 @@ func (ls LabelSet) Before(o LabelSet) bool {
// Clone returns a copy of the label set.
func (ls LabelSet) Clone() LabelSet {
lsn := make(LabelSet, len(ls))
for ln, lv := range ls {
lsn[ln] = lv
}
maps.Copy(lsn, ls)
return lsn
}
@@ -117,13 +116,9 @@ func (ls LabelSet) Clone() LabelSet {
func (ls LabelSet) Merge(other LabelSet) LabelSet {
result := make(LabelSet, len(ls))
for k, v := range ls {
result[k] = v
}
maps.Copy(result, ls)
for k, v := range other {
result[k] = v
}
maps.Copy(result, other)
return result
}

View File

@@ -17,6 +17,7 @@ import (
"encoding/json"
"errors"
"fmt"
"maps"
"regexp"
"sort"
"strconv"
@@ -24,7 +25,6 @@ import (
"unicode/utf8"
dto "github.com/prometheus/client_model/go"
"go.yaml.in/yaml/v2"
"google.golang.org/protobuf/proto"
)
@@ -78,14 +78,6 @@ const (
UTF8Validation
)
var _ interface {
yaml.Marshaler
yaml.Unmarshaler
json.Marshaler
json.Unmarshaler
fmt.Stringer
} = new(ValidationScheme)
// String returns the string representation of s.
func (s ValidationScheme) String() string {
switch s {
@@ -267,9 +259,7 @@ func (m Metric) Before(o Metric) bool {
// Clone returns a copy of the Metric.
func (m Metric) Clone() Metric {
clone := make(Metric, len(m))
for k, v := range m {
clone[k] = v
}
maps.Copy(clone, m)
return clone
}

View File

@@ -123,44 +123,38 @@ func (t Time) MarshalJSON() ([]byte, error) {
// UnmarshalJSON implements the json.Unmarshaler interface.
func (t *Time) UnmarshalJSON(b []byte) error {
p := strings.Split(string(b), ".")
switch len(p) {
case 1:
v, err := strconv.ParseInt(p[0], 10, 64)
base, frac, found := strings.Cut(string(b), ".")
if !found {
v, err := strconv.ParseInt(base, 10, 64)
if err != nil {
return err
}
*t = Time(v * second)
case 2:
v, err := strconv.ParseInt(p[0], 10, 64)
} else {
v, err := strconv.ParseInt(base, 10, 64)
if err != nil {
return err
}
v *= second
prec := dotPrecision - len(p[1])
prec := dotPrecision - len(frac)
if prec < 0 {
p[1] = p[1][:dotPrecision]
} else if prec > 0 {
p[1] += strings.Repeat("0", prec)
frac = frac[:dotPrecision]
}
va, err := strconv.ParseInt(p[1], 10, 32)
va, err := strconv.ParseInt(frac, 10, 32)
if err != nil {
return err
}
// If the value was something like -0.1 the negative is lost in the
// parsing because of the leading zero, this ensures that we capture it.
if len(p[0]) > 0 && p[0][0] == '-' && v+va > 0 {
*t = Time(v+va) * -1
} else {
*t = Time(v + va)
switch prec {
case 1:
va *= 10
case 2:
va *= 100
}
default:
return fmt.Errorf("invalid time %q", string(b))
if len(base) > 0 && base[0] == '-' {
va = -va
}
*t = Time(v*second + va)
}
return nil
}
@@ -340,12 +334,12 @@ func (d *Duration) UnmarshalText(text []byte) error {
}
// MarshalYAML implements the yaml.Marshaler interface.
func (d Duration) MarshalYAML() (interface{}, error) {
func (d Duration) MarshalYAML() (any, error) {
return d.String(), nil
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (d *Duration) UnmarshalYAML(unmarshal func(interface{}) error) error {
func (d *Duration) UnmarshalYAML(unmarshal func(any) error) error {
var s string
if err := unmarshal(&s); err != nil {
return err

View File

@@ -259,13 +259,13 @@ func (s Scalar) String() string {
// MarshalJSON implements json.Marshaler.
func (s Scalar) MarshalJSON() ([]byte, error) {
v := strconv.FormatFloat(float64(s.Value), 'f', -1, 64)
return json.Marshal([...]interface{}{s.Timestamp, v})
return json.Marshal([...]any{s.Timestamp, v})
}
// UnmarshalJSON implements json.Unmarshaler.
func (s *Scalar) UnmarshalJSON(b []byte) error {
var f string
v := [...]interface{}{&s.Timestamp, &f}
v := [...]any{&s.Timestamp, &f}
if err := json.Unmarshal(b, &v); err != nil {
return err
@@ -291,12 +291,12 @@ func (s *String) String() string {
// MarshalJSON implements json.Marshaler.
func (s String) MarshalJSON() ([]byte, error) {
return json.Marshal([]interface{}{s.Timestamp, s.Value})
return json.Marshal([]any{s.Timestamp, s.Value})
}
// UnmarshalJSON implements json.Unmarshaler.
func (s *String) UnmarshalJSON(b []byte) error {
v := [...]interface{}{&s.Timestamp, &s.Value}
v := [...]any{&s.Timestamp, &s.Value}
return json.Unmarshal(b, &v)
}

View File

@@ -79,7 +79,7 @@ func (s SamplePair) MarshalJSON() ([]byte, error) {
if err != nil {
return nil, err
}
return []byte(fmt.Sprintf("[%s,%s]", t, v)), nil
return fmt.Appendf(nil, "[%s,%s]", t, v), nil
}
// UnmarshalJSON implements json.Unmarshaler.

View File

@@ -67,11 +67,11 @@ func (s HistogramBucket) MarshalJSON() ([]byte, error) {
if err != nil {
return nil, err
}
return []byte(fmt.Sprintf("[%s,%s,%s,%s]", b, l, u, c)), nil
return fmt.Appendf(nil, "[%s,%s,%s,%s]", b, l, u, c), nil
}
func (s *HistogramBucket) UnmarshalJSON(buf []byte) error {
tmp := []interface{}{&s.Boundaries, &s.Lower, &s.Upper, &s.Count}
tmp := []any{&s.Boundaries, &s.Lower, &s.Upper, &s.Count}
wantLen := len(tmp)
if err := json.Unmarshal(buf, &tmp); err != nil {
return err
@@ -152,11 +152,11 @@ func (s SampleHistogramPair) MarshalJSON() ([]byte, error) {
if err != nil {
return nil, err
}
return []byte(fmt.Sprintf("[%s,%s]", t, v)), nil
return fmt.Appendf(nil, "[%s,%s]", t, v), nil
}
func (s *SampleHistogramPair) UnmarshalJSON(buf []byte) error {
tmp := []interface{}{&s.Timestamp, &s.Histogram}
tmp := []any{&s.Timestamp, &s.Histogram}
wantLen := len(tmp)
if err := json.Unmarshal(buf, &tmp); err != nil {
return err

View File

@@ -61,7 +61,7 @@ func NewLevel() *Level {
}
}
func (l *Level) UnmarshalYAML(unmarshal func(interface{}) error) error {
func (l *Level) UnmarshalYAML(unmarshal func(any) error) error {
var s string
type plain string
if err := unmarshal((*plain)(&s)); err != nil {

View File

@@ -55,13 +55,13 @@ ifneq ($(shell command -v gotestsum 2> /dev/null),)
endif
endif
PROMU_VERSION ?= 0.18.0
PROMU_VERSION ?= 0.20.0
PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz
SKIP_GOLANGCI_LINT :=
GOLANGCI_LINT :=
GOLANGCI_LINT_OPTS ?=
GOLANGCI_LINT_VERSION ?= v2.10.1
GOLANGCI_LINT_VERSION ?= v2.11.4
GOLANGCI_FMT_OPTS ?=
# golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64.
# windows isn't included here because of the path separator being different.
@@ -90,8 +90,8 @@ ifdef DOCKERFILE_PATH
$(error DOCKERFILE_PATH is deprecated. Use DOCKERFILE_VARIANTS ?= $(DOCKERFILE_PATH) in the Makefile)
endif
DOCKER_ARCHS ?= amd64
DOCKERFILE_VARIANTS ?= Dockerfile $(wildcard Dockerfile.*)
DOCKER_ARCHS ?= amd64 arm64 armv7 ppc64le riscv64 s390x
DOCKERFILE_VARIANTS ?= $(wildcard Dockerfile Dockerfile.*)
# Function to extract variant from Dockerfile label.
# Returns the variant name from io.prometheus.image.variant label, or "default" if not found.
@@ -109,24 +109,6 @@ endif
# Build variant:dockerfile pairs for shell iteration.
DOCKERFILE_VARIANTS_WITH_NAMES := $(foreach df,$(DOCKERFILE_VARIANTS),$(call dockerfile_variant,$(df)):$(df))
# Shell helper to check whether a dockerfile/arch pair is excluded.
define dockerfile_arch_is_excluded
case " $(DOCKERFILE_ARCH_EXCLUSIONS) " in \
*" $$dockerfile:$(1) "*) true ;; \
*) false ;; \
esac
endef
# Shell helper to check whether a registry/arch pair is excluded.
# Extracts registry from DOCKER_REPO (e.g., quay.io/prometheus -> quay.io)
define registry_arch_is_excluded
registry=$$(echo "$(DOCKER_REPO)" | cut -d'/' -f1); \
case " $(DOCKER_REGISTRY_ARCH_EXCLUSIONS) " in \
*" $$registry:$(1) "*) true ;; \
*) false ;; \
esac
endef
BUILD_DOCKER_ARCHS = $(addprefix common-docker-,$(DOCKER_ARCHS))
PUBLISH_DOCKER_ARCHS = $(addprefix common-docker-publish-,$(DOCKER_ARCHS))
TAG_DOCKER_ARCHS = $(addprefix common-docker-tag-latest-,$(DOCKER_ARCHS))
@@ -268,10 +250,6 @@ $(BUILD_DOCKER_ARCHS): common-docker-%:
@for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \
dockerfile=$${variant#*:}; \
variant_name=$${variant%%:*}; \
if $(call dockerfile_arch_is_excluded,$*); then \
echo "Skipping $$variant_name variant for linux-$* (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \
continue; \
fi; \
distroless_arch="$*"; \
if [ "$*" = "armv7" ]; then \
distroless_arch="arm"; \
@@ -306,14 +284,6 @@ $(PUBLISH_DOCKER_ARCHS): common-docker-publish-%:
@for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \
dockerfile=$${variant#*:}; \
variant_name=$${variant%%:*}; \
if $(call dockerfile_arch_is_excluded,$*); then \
echo "Skipping push for $$variant_name variant on linux-$* (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \
continue; \
fi; \
if $(call registry_arch_is_excluded,$*); then \
echo "Skipping push for $$variant_name variant on linux-$* to $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \
continue; \
fi; \
if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \
echo "Pushing $$variant_name variant for linux-$*"; \
docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name"; \
@@ -341,14 +311,6 @@ $(TAG_DOCKER_ARCHS): common-docker-tag-latest-%:
@for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \
dockerfile=$${variant#*:}; \
variant_name=$${variant%%:*}; \
if $(call dockerfile_arch_is_excluded,$*); then \
echo "Skipping tag for $$variant_name variant on linux-$* (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \
continue; \
fi; \
if $(call registry_arch_is_excluded,$*); then \
echo "Skipping tag for $$variant_name variant on linux-$* for $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \
continue; \
fi; \
if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \
echo "Tagging $$variant_name variant for linux-$* as latest"; \
docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest-$$variant_name"; \
@@ -370,14 +332,6 @@ common-docker-manifest:
echo "Creating manifest for $$variant_name variant"; \
refs=""; \
for arch in $(DOCKER_ARCHS); do \
if $(call dockerfile_arch_is_excluded,$$arch); then \
echo " Skipping $$arch for $$variant_name (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \
continue; \
fi; \
if $(call registry_arch_is_excluded,$$arch); then \
echo " Skipping $$arch for $$variant_name on $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \
continue; \
fi; \
refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name"; \
done; \
if [ -z "$$refs" ]; then \
@@ -391,14 +345,6 @@ common-docker-manifest:
echo "Creating default variant ($$variant_name) manifest"; \
refs=""; \
for arch in $(DOCKER_ARCHS); do \
if $(call dockerfile_arch_is_excluded,$$arch); then \
echo " Skipping $$arch for default variant (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \
continue; \
fi; \
if $(call registry_arch_is_excluded,$$arch); then \
echo " Skipping $$arch for default variant on $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \
continue; \
fi; \
refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:$(SANITIZED_DOCKER_IMAGE_TAG)"; \
done; \
if [ -z "$$refs" ]; then \
@@ -413,14 +359,6 @@ common-docker-manifest:
echo "Creating manifest for $$variant_name variant version tag"; \
refs=""; \
for arch in $(DOCKER_ARCHS); do \
if $(call dockerfile_arch_is_excluded,$$arch); then \
echo " Skipping $$arch for $$variant_name version tag (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \
continue; \
fi; \
if $(call registry_arch_is_excluded,$$arch); then \
echo " Skipping $$arch for $$variant_name version tag on $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \
continue; \
fi; \
refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name"; \
done; \
if [ -z "$$refs" ]; then \
@@ -434,14 +372,6 @@ common-docker-manifest:
echo "Creating default variant version tag manifest"; \
refs=""; \
for arch in $(DOCKER_ARCHS); do \
if $(call dockerfile_arch_is_excluded,$$arch); then \
echo " Skipping $$arch for default variant version tag (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \
continue; \
fi; \
if $(call registry_arch_is_excluded,$$arch); then \
echo " Skipping $$arch for default variant version tag on $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \
continue; \
fi; \
refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:v$(DOCKER_MAJOR_VERSION_TAG)"; \
done; \
if [ -z "$$refs" ]; then \
@@ -495,9 +425,3 @@ $(1)_precheck:
exit 1; \
fi
endef
govulncheck: install-govulncheck
govulncheck ./...
install-govulncheck:
command -v govulncheck > /dev/null || go install golang.org/x/vuln/cmd/govulncheck@latest

View File

@@ -7,7 +7,7 @@ metrics from the pseudo-filesystems /proc and /sys.
backwards-incompatible ways without warnings. Use it at your own risk.
[![Go Reference](https://pkg.go.dev/badge/github.com/prometheus/procfs.svg)](https://pkg.go.dev/github.com/prometheus/procfs)
[![CircleCI](https://circleci.com/gh/prometheus/procfs/tree/master.svg?style=svg)](https://circleci.com/gh/prometheus/procfs/tree/master)
[![Build Status](https://github.com/prometheus/procfs/actions/workflows/ci.yml/badge.svg)](https://github.com/prometheus/procfs/actions/workflows/ci.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/prometheus/procfs)](https://goreportcard.com/report/github.com/prometheus/procfs)
## Usage

View File

@@ -3,4 +3,4 @@
The Prometheus security policy, including how to report vulnerabilities, can be
found here:
<https://prometheus.io/docs/operating/security/>
[https://prometheus.io/docs/operating/security/](https://prometheus.io/docs/operating/security/)

View File

@@ -48,11 +48,13 @@ type Crypto struct {
Walksize *uint64
}
var cryptoFile = "crypto"
// Crypto parses an crypto-file (/proc/crypto) and returns a slice of
// structs containing the relevant info. More information available here:
// https://kernel.readthedocs.io/en/sphinx-samples/crypto-API.html
func (fs FS) Crypto() ([]Crypto, error) {
path := fs.proc.Path("crypto")
path := fs.proc.Path(cryptoFile)
b, err := util.ReadFileNoStat(path)
if err != nil {
return nil, fmt.Errorf("%w: Cannot read file %v: %w", ErrFileRead, b, err)
@@ -82,6 +84,10 @@ func parseCrypto(r io.Reader) ([]Crypto, error) {
continue
}
if len(out) == 0 {
return nil, fmt.Errorf("%w: parsed invalid line before name parsed: %q", ErrFileParse, text)
}
kv := strings.Split(text, ":")
if len(kv) != 2 {
return nil, fmt.Errorf("%w: Cannot parse line: %q", ErrFileParse, text)

View File

@@ -17,10 +17,10 @@ import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"strconv"
"strings"
"github.com/prometheus/procfs/internal/util"
)
// A MountInfo is a type that describes the details, options
@@ -160,9 +160,19 @@ func mountOptionsParser(mountOptions string) map[string]string {
return opts
}
// readMountInfo reads a full mountinfo file (no 1 MiB cap, unlike util.ReadFileNoStat).
func readMountInfo(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return io.ReadAll(f)
}
// GetMounts retrieves mountinfo information from `/proc/self/mountinfo`.
func GetMounts() ([]*MountInfo, error) {
data, err := util.ReadFileNoStat("/proc/self/mountinfo")
data, err := readMountInfo("/proc/self/mountinfo")
if err != nil {
return nil, err
}
@@ -171,7 +181,7 @@ func GetMounts() ([]*MountInfo, error) {
// GetProcMounts retrieves mountinfo information from a processes' `/proc/<pid>/mountinfo`.
func GetProcMounts(pid int) ([]*MountInfo, error) {
data, err := util.ReadFileNoStat(fmt.Sprintf("/proc/%d/mountinfo", pid))
data, err := readMountInfo(fmt.Sprintf("/proc/%d/mountinfo", pid))
if err != nil {
return nil, err
}
@@ -180,7 +190,7 @@ func GetProcMounts(pid int) ([]*MountInfo, error) {
// GetMounts retrieves mountinfo information from `/proc/self/mountinfo`.
func (fs FS) GetMounts() ([]*MountInfo, error) {
data, err := util.ReadFileNoStat(fs.proc.Path("self/mountinfo"))
data, err := readMountInfo(fs.proc.Path("self/mountinfo"))
if err != nil {
return nil, err
}
@@ -189,7 +199,7 @@ func (fs FS) GetMounts() ([]*MountInfo, error) {
// GetProcMounts retrieves mountinfo information from a processes' `/proc/<pid>/mountinfo`.
func (fs FS) GetProcMounts(pid int) ([]*MountInfo, error) {
data, err := util.ReadFileNoStat(fs.proc.Path(fmt.Sprintf("%d/mountinfo", pid)))
data, err := readMountInfo(fs.proc.Path(fmt.Sprintf("%d/mountinfo", pid)))
if err != nil {
return nil, err
}

View File

@@ -114,47 +114,47 @@ func parseWireless(r io.Reader) ([]*Wireless, error) {
qlink, err := strconv.Atoi(strings.TrimSuffix(stats[1], "."))
if err != nil {
return nil, fmt.Errorf("%w: parse Quality:link as integer %q: %w", ErrFileParse, qlink, err)
return nil, fmt.Errorf("%w: parse Quality:link as integer %q: %w", ErrFileParse, stats[1], err)
}
qlevel, err := strconv.Atoi(strings.TrimSuffix(stats[2], "."))
if err != nil {
return nil, fmt.Errorf("%w: Quality:level as integer %q: %w", ErrFileParse, qlevel, err)
return nil, fmt.Errorf("%w: Quality:level as integer %q: %w", ErrFileParse, stats[2], err)
}
qnoise, err := strconv.Atoi(strings.TrimSuffix(stats[3], "."))
if err != nil {
return nil, fmt.Errorf("%w: Quality:noise as integer %q: %w", ErrFileParse, qnoise, err)
return nil, fmt.Errorf("%w: Quality:noise as integer %q: %w", ErrFileParse, stats[3], err)
}
dnwid, err := strconv.Atoi(stats[4])
if err != nil {
return nil, fmt.Errorf("%w: Discarded:nwid as integer %q: %w", ErrFileParse, dnwid, err)
return nil, fmt.Errorf("%w: Discarded:nwid as integer %q: %w", ErrFileParse, stats[4], err)
}
dcrypt, err := strconv.Atoi(stats[5])
if err != nil {
return nil, fmt.Errorf("%w: Discarded:crypt as integer %q: %w", ErrFileParse, dcrypt, err)
return nil, fmt.Errorf("%w: Discarded:crypt as integer %q: %w", ErrFileParse, stats[5], err)
}
dfrag, err := strconv.Atoi(stats[6])
if err != nil {
return nil, fmt.Errorf("%w: Discarded:frag as integer %q: %w", ErrFileParse, dfrag, err)
return nil, fmt.Errorf("%w: Discarded:frag as integer %q: %w", ErrFileParse, stats[6], err)
}
dretry, err := strconv.Atoi(stats[7])
if err != nil {
return nil, fmt.Errorf("%w: Discarded:retry as integer %q: %w", ErrFileParse, dretry, err)
return nil, fmt.Errorf("%w: Discarded:retry as integer %q: %w", ErrFileParse, stats[7], err)
}
dmisc, err := strconv.Atoi(stats[8])
if err != nil {
return nil, fmt.Errorf("%w: Discarded:misc as integer %q: %w", ErrFileParse, dmisc, err)
return nil, fmt.Errorf("%w: Discarded:misc as integer %q: %w", ErrFileParse, stats[8], err)
}
mbeacon, err := strconv.Atoi(stats[9])
if err != nil {
return nil, fmt.Errorf("%w: Missed:beacon as integer %q: %w", ErrFileParse, mbeacon, err)
return nil, fmt.Errorf("%w: Missed:beacon as integer %q: %w", ErrFileParse, stats[9], err)
}
w := &Wireless{

View File

@@ -60,7 +60,7 @@ func parseCgroupString(cgroupStr string) (*Cgroup, error) {
}
cgroup.HierarchyID, err = strconv.Atoi(fields[0])
if err != nil {
return nil, fmt.Errorf("%w: hierarchy ID: %q", ErrFileParse, cgroup.HierarchyID)
return nil, fmt.Errorf("%w: hierarchy ID: %q", ErrFileParse, fields[0])
}
if fields[1] != "" {
ssNames := strings.Split(fields[1], ",")

12
vendor/modules.txt vendored
View File

@@ -844,7 +844,7 @@ github.com/justinas/alice
# github.com/kevinburke/ssh_config v1.2.0
## explicit
github.com/kevinburke/ssh_config
# github.com/klauspost/compress v1.18.6
# github.com/klauspost/compress v1.19.1
## explicit; go 1.24
github.com/klauspost/compress
github.com/klauspost/compress/flate
@@ -1819,8 +1819,8 @@ github.com/prometheus/alertmanager/matcher/parse
github.com/prometheus/alertmanager/pkg/labels
github.com/prometheus/alertmanager/template
github.com/prometheus/alertmanager/types
# github.com/prometheus/client_golang v1.23.2
## explicit; go 1.23.0
# github.com/prometheus/client_golang v1.24.1
## explicit; go 1.25.0
github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil
github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil/header
github.com/prometheus/client_golang/prometheus
@@ -1831,13 +1831,13 @@ github.com/prometheus/client_golang/prometheus/promhttp/internal
# github.com/prometheus/client_model v0.6.2
## explicit; go 1.22.0
github.com/prometheus/client_model/go
# github.com/prometheus/common v0.67.5
## explicit; go 1.24.0
# github.com/prometheus/common v0.70.1
## explicit; go 1.25.0
github.com/prometheus/common/expfmt
github.com/prometheus/common/helpers/templates
github.com/prometheus/common/model
github.com/prometheus/common/promslog
# github.com/prometheus/procfs v0.20.1
# github.com/prometheus/procfs v0.21.1
## explicit; go 1.25.0
github.com/prometheus/procfs
github.com/prometheus/procfs/internal/fs