Skip to content

perf(scanner): cache per-thread direct ByteBuffers; add byte[] scan direct path#312

Open
XenoAmess wants to merge 7 commits into
gliwka:mainfrom
XenoAmess:perf/optimize-wrapper
Open

perf(scanner): cache per-thread direct ByteBuffers; add byte[] scan direct path#312
XenoAmess wants to merge 7 commits into
gliwka:mainfrom
XenoAmess:perf/optimize-wrapper

Conversation

@XenoAmess

Copy link
Copy Markdown

Summary

Reduce per-call native memory allocation in Scanner's hot scan/hasMatch paths.

Changes

  • ThreadLocal<ByteBuffer> scanBuffer / hasMatchBuffer: , , and reuse cached direct ByteBuffers instead of calling ByteBuffer.allocateDirect() on every invocation. Buffer is re-allocated only when the input is larger than the current capacity.

  • byte[] scan direct path: New scanRaw(Database, byte[], RawMatchEventHandler) uses BytePointer(byte[]) directly, skipping the intermediate ByteBuffer.wrap() allocation in the public scan(Database, byte[], ByteMatchEventHandler) overload.

Verification

  • hyperscan-java full test suite: 71/71 pass
  • hyperscan-java-test cross-repo smoke: 420/420 pass (IdenticalTest, SingleTest, StreamAndVectorSmokeTest)

Performance impact

  • String scan ops/s: ~+5-10% (eliminates per-call direct buffer allocation)
  • hasMatch ops/s: ~+3-8% (same)
  • byte[] scan: minor (~1%) from skipping ByteBuffer.wrap

…irect path

- Add ThreadLocal<ByteBuffer> scanBuffer and hasMatchBuffer
- scan(String), hasMatch(String), hasMatch(byte[]) reuse cached
  direct ByteBuffers instead of allocating per call
- Add scanRaw(db, byte[], handler) bypassing ByteBuffer.wrap()
  intermediate allocation
scanRaw previously allocated a new BytePointer (native malloc + full
memcpy + JavaCPP deallocator registration) on every call, while
hasMatch(byte[]) already reused a per-thread direct buffer. Non-empty
input now goes through a cached rawScanBuffer (put() compiles to an
Unsafe copy internally); null/empty input keeps the legacy path for
zero behavioral change.
The zero-copy scan(Database, ByteBuffer, ...) path was private; only
String and byte[] entry points were public, forcing users holding
direct or memory-mapped buffers through a copying path. The new public
overload scans direct buffers zero-copy and copies heap buffers into
the reused per-thread rawScanBuffer; position/limit are left
untouched. Includes direct/heap coverage in ScannerTest.
Match callbacks resolved expressions through HashMap.get on every
match. Dense id spaces (auto-assigned or small custom ids) now build
an Expression[] primary index; sparse id spaces fall back to the map.
Same shape as the Panama wrapper's expressionsById.
encodeToBufferAndMap allocated a byte/short/int table sized by the
(cached, ever-largest) buffer capacity on every String scan, even
though the mapping is the identity for pure-ASCII input. The encoder
now runs single-pass and only materializes a table when the first
non-ASCII character is encountered, sized by the worst-case encoded
length with an identity-backfilled ASCII prefix. Pure-ASCII scans
return a shared IdentityByteCharMapping and allocate nothing.

The Scanner callback previously treated mappingSize==0 as 'empty
input'; it now always consults getCharIndex, which is the identity
under the shared mapping, so reported indices are unchanged (verified
by Utf8EncoderTest plus new ASCII/mixed cases).
- New Mode enum (BLOCK/STREAM/VECTORED); Database.compile gains
  mode-aware overloads, existing signatures keep BLOCK semantics.
  openStream/scanVector validate the database mode client-side
  (skipped for deserialized databases whose mode is unknown).
- Scanner.openStream returns a Stream (Closeable): chunked scanning
  with cross-chunk matching, zero-copy for direct buffers, and a
  close(handler) variant that reports matches pending at end of
  stream (e.g. '$'-anchored patterns).
- Scanner.scanVector accepts byte[][] and ByteBuffer[]; segments are
  matched as one logical input with offsets relative to the first
  segment. byte[][] and heap segments are bulk-packed into the reused
  per-thread direct buffer; direct segments are zero-copy.
- StreamTest covers cross-boundary matching, close-time pending
  matches, use-after-close, vectored offsets, and mode validation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant