Skip to content

Commit 8dcf6bb

Browse files
gh123manclaude
andcommitted
perf(logs): collapse IPv4 at emit time instead of in a second pass
collapseHybridTokens walked the whole token list after tokenization and called ipv4At at every position. ipv4At is over the inlining budget, so that was a real call per token, and the pass rewrote both buffers even when nothing collapsed - the common case. A dotted quad can only ever be closed by its final octet, so the check belongs on that one emission. Collapsing the tail in emitToken removes the second pass entirely and leaves the per-byte scan loop untouched. emitRuns keeps working for tokenizeWithoutHybridCollapse via a skipHybridCollapse flag, so the IIS regression tests still compare against pre-IPv4 tokenization. Matching semantics unchanged: leftmost-first, non-overlapping. Verified by dumping tokens and start indices for a 250k-input corpus (51k with at least one quad) from both implementations - byte-for-byte identical. Tokenizer benchmarks, interleaved 10 rounds vs the PR parent: geomean +38.57% -> +1.89%. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 93bfd45 commit 8dcf6bb

2 files changed

Lines changed: 49 additions & 44 deletions

File tree

pkg/logs/internal/decoder/preprocessor/timestamp_detector_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,9 @@ func tokenizeWithoutHybridCollapse(tok *Tokenizer, input []byte) []Token {
164164
if tok.maxEvalBytes > 0 && tok.maxEvalBytes < maxBytes {
165165
maxBytes = tok.maxEvalBytes
166166
}
167+
tok.skipHybridCollapse = true
167168
tok.emitRuns(input[:maxBytes])
169+
tok.skipHybridCollapse = false
168170
out := make([]Token, len(tok.tsBuf))
169171
copy(out, tok.tsBuf)
170172
return out

pkg/logs/internal/decoder/preprocessor/tokenizer.go

Lines changed: 47 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import (
1616
const (
1717
maxRun = 10
1818
ipv4TokenWidth = 7 // D Period D Period D Period D
19+
// maxIPv4OctetDigits is the longest digit run that can be an octet, so a
20+
// run longer than this can never close a dotted quad.
21+
maxIPv4OctetDigits = 3
1922
)
2023

2124
// maxSpecialTokenLen and the special-token/debug-string tables are generated
@@ -73,6 +76,10 @@ type Tokenizer struct {
7376
maxEvalBytes int
7477
tsBuf []Token // Reusable token buffer
7578
idxBuf []int // Reusable index buffer
79+
// skipHybridCollapse disables hybrid-token collapsing so tests can
80+
// reconstruct the pre-IPv4-token tokenizer. Zero value collapses, so a
81+
// zero-value Tokenizer still behaves like a production one.
82+
skipHybridCollapse bool
7683
}
7784

7885
// NewTokenizer returns a new Tokenizer detection heuristic.
@@ -150,9 +157,15 @@ func (t *Tokenizer) emitToken(input []byte, token Token, start, end int) {
150157
r = maxRun - 1
151158
}
152159
t.tsBuf = append(t.tsBuf, token+Token(r))
153-
} else {
154-
t.tsBuf = append(t.tsBuf, token)
160+
// A dotted quad can only ever be closed by its final octet, so this is
161+
// the one emission that can complete the pattern. Checking here keeps
162+
// the cost off every other token and avoids a second pass entirely.
163+
if token == D1 && runLen <= maxIPv4OctetDigits && !t.skipHybridCollapse {
164+
t.collapseIPv4Tail()
165+
}
166+
return
155167
}
168+
t.tsBuf = append(t.tsBuf, token)
156169
}
157170

158171
// tokenizeIntoBuffers scans input a single time and emits tokens into the
@@ -163,13 +176,12 @@ func (t *Tokenizer) tokenizeIntoBuffers(input []byte) ([]Token, []int) {
163176
return nil, nil
164177
}
165178
t.emitRuns(input)
166-
t.collapseHybridTokens()
167179
return t.tsBuf, t.idxBuf
168180
}
169181

170-
// emitRuns run-length-encodes input into tsBuf/idxBuf without hybrid collapse.
171-
// Tests use this to reconstruct the pre-IPv4-token tokenizer for the IIS
172-
// false-aggregate case.
182+
// emitRuns run-length-encodes input into tsBuf/idxBuf, collapsing hybrid tokens
183+
// as they complete unless skipHybridCollapse is set. Tests set that flag to
184+
// reconstruct the pre-IPv4-token tokenizer for the IIS false-aggregate case.
173185
func (t *Tokenizer) emitRuns(input []byte) {
174186
inputLen := len(input)
175187
if inputLen == 0 {
@@ -213,49 +225,40 @@ func isIPv4OctetToken(tok Token) bool {
213225
return tok >= D1 && tok <= D3
214226
}
215227

216-
// ipv4At reports whether tokens[i:] starts with an IPv4 dotted quad.
217-
// Each octet is a 1-3 digit run and each separator is a single '.'.
218-
// A collapsed ".." / "..." Period token is rejected via the start indices.
219-
func ipv4At(tokens []Token, indices []int, i int) bool {
220-
if i+ipv4TokenWidth > len(tokens) {
221-
return false
222-
}
223-
if !isIPv4OctetToken(tokens[i]) || tokens[i+1] != Period ||
224-
!isIPv4OctetToken(tokens[i+2]) || tokens[i+3] != Period ||
225-
!isIPv4OctetToken(tokens[i+4]) || tokens[i+5] != Period ||
226-
!isIPv4OctetToken(tokens[i+6]) {
227-
return false
228-
}
229-
return indices[i+2] == indices[i+1]+1 &&
230-
indices[i+4] == indices[i+3]+1 &&
231-
indices[i+6] == indices[i+5]+1
232-
}
233-
234-
// collapseHybridTokens rewrites multi-token patterns into a single token.
235-
// IPv4 dotted quads are the first of these: as separate digit/period tokens
236-
// they look like timestamp fragments to the detector, and addresses with
237-
// different octet widths would otherwise be different sampler patterns.
238-
func (t *Tokenizer) collapseHybridTokens() {
228+
// collapseIPv4Tail rewrites a dotted quad ending at the last emitted token into
229+
// a single IPv4 token. As separate digit/period tokens a quad looks like a
230+
// timestamp fragment to the detector, and addresses with different octet widths
231+
// would otherwise be different sampler patterns.
232+
//
233+
// Called from emitToken immediately after a 1-3 digit run is appended, which is
234+
// the only emission that can close the pattern, so the tokenizer never makes a
235+
// second pass over the token list. Each octet must be a 1-3 digit run and each
236+
// separator a single '.'; a collapsed ".." / "..." Period token is rejected via
237+
// the start indices.
238+
func (t *Tokenizer) collapseIPv4Tail() {
239239
n := len(t.tsBuf)
240240
if n < ipv4TokenWidth {
241241
return
242242
}
243-
w := 0
244-
for r := 0; r < n; {
245-
if ipv4At(t.tsBuf, t.idxBuf, r) {
246-
t.tsBuf[w] = IPv4
247-
t.idxBuf[w] = t.idxBuf[r]
248-
w++
249-
r += ipv4TokenWidth
250-
continue
251-
}
252-
t.tsBuf[w] = t.tsBuf[r]
253-
t.idxBuf[w] = t.idxBuf[r]
254-
w++
255-
r++
243+
ts := t.tsBuf[n-ipv4TokenWidth:]
244+
245+
// Separators first: they reject nearly every call in a single compare, and
246+
// the trailing octet is already known from the caller.
247+
if ts[5] != Period || ts[3] != Period || ts[1] != Period {
248+
return
249+
}
250+
if !isIPv4OctetToken(ts[0]) || !isIPv4OctetToken(ts[2]) || !isIPv4OctetToken(ts[4]) {
251+
return
256252
}
257-
t.tsBuf = t.tsBuf[:w]
258-
t.idxBuf = t.idxBuf[:w]
253+
idx := t.idxBuf[n-ipv4TokenWidth:]
254+
if idx[2] != idx[1]+1 || idx[4] != idx[3]+1 || idx[6] != idx[5]+1 {
255+
return
256+
}
257+
258+
// Keep idx[0] (the address start) and drop the six tokens it absorbed.
259+
ts[0] = IPv4
260+
t.tsBuf = t.tsBuf[:n-ipv4TokenWidth+1]
261+
t.idxBuf = t.idxBuf[:n-ipv4TokenWidth+1]
259262
}
260263

261264
// tokensToString converts a list of tokens to a debug string.

0 commit comments

Comments
 (0)