mirror of
https://github.com/nosqlbench/nosqlbench.git
synced 2026-10-12 09:24:52 -05:00
feat(vectordata): chunk-range residency and range prebuffer on storage
RangeFill reports what fetching a byte range would cost at chunk granularity — resident chunks are free, and alignment overfetch is visible before the fetch. prebufferRange drives only the covering chunks; CacheStats carries the transfer-chunk size so consumers can map byte offsets to chunk ordinals; rangeCapable distinguishes sources that cannot service partial fetches.
This commit is contained in:
1 parent
2af9dec9cc
commit
b30d1d074e
6 files changed
+131
-4
No files matched your search
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2026 The NoSQLBench Authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.nosqlbench.vectordata;
|
||||
|
||||
/// A half-open byte range `[start, end)` within a facet's payload.
|
||||
public record ByteRange(long start, long end) {
|
||||
/// Bytes spanned by this range.
|
||||
public long length() { return Math.max(0, end - start); }
|
||||
@Override public String toString() { return "[" + start + ".." + end + ")"; }
|
||||
}
|
||||
@@ -15,6 +15,9 @@
|
||||
*/
|
||||
package io.nosqlbench.vectordata;
|
||||
|
||||
/** Immutable observation of a reader's backing cache. */
|
||||
public record CacheStats(AccessMode accessMode, long totalBytes, long cachedBytes,
|
||||
/// Immutable observation of a reader's backing cache. `chunkSize` is
|
||||
/// the transfer-chunk size in bytes — `0` for local storage, which has
|
||||
/// no chunks — so consumers can map byte offsets to chunk ordinals
|
||||
/// (e.g. for chunk-aligned sampling or prefetch).
|
||||
public record CacheStats(AccessMode accessMode, long totalBytes, long cachedBytes, long chunkSize,
|
||||
long hits, long misses, boolean complete) { }
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2026 The NoSQLBench Authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.nosqlbench.vectordata;
|
||||
|
||||
/// What fetching a byte range would actually cost, at chunk
|
||||
/// granularity. The unit of fetch is a chunk, not a byte, so a range's
|
||||
/// real cost is rarely the range's length: a 4 KiB window against 8 MiB
|
||||
/// chunks is 8 MiB, and a window whose chunks are already resident is
|
||||
/// free. Both facts have to be visible *before* the fetch, or a caller
|
||||
/// cannot tell an incremental warm-up from a full download.
|
||||
///
|
||||
/// `firstChunk` and `lastChunk` are inclusive; `alignedStart` and
|
||||
/// `alignedEnd` are the byte range the fetch actually spans once
|
||||
/// widened to chunk boundaries — always a superset of what was asked
|
||||
/// for.
|
||||
public record RangeFill(int firstChunk, int lastChunk, long chunkSize, int chunks, int chunksResident,
|
||||
long alignedStart, long alignedEnd) {
|
||||
|
||||
/// Chunks that still have to be fetched.
|
||||
public int chunksToFetch() { return Math.max(0, chunks - chunksResident); }
|
||||
|
||||
/// Bytes that will cross the network, at chunk granularity.
|
||||
public long bytesToFetch() { return chunksToFetch() * chunkSize; }
|
||||
|
||||
/// Bytes fetched beyond the requested range because chunks are the
|
||||
/// granularity. This is the number that tells a caller its
|
||||
/// scattered single-record prefetches are really a full download.
|
||||
public long overfetchBytes(long requestedStart, long requestedEnd) {
|
||||
long requested = Math.max(0, requestedEnd - requestedStart);
|
||||
long spanned = Math.max(0, alignedEnd - alignedStart);
|
||||
return Math.max(0, spanned - requested);
|
||||
}
|
||||
|
||||
/// Whether every chunk covering the range is already resident.
|
||||
public boolean isResident() { return chunksResident >= chunks; }
|
||||
}
|
||||
@@ -17,6 +17,7 @@ package io.nosqlbench.vectordata.internal;
|
||||
|
||||
import io.nosqlbench.vectordata.CacheStats;
|
||||
import io.nosqlbench.vectordata.PrebufferProgress;
|
||||
import io.nosqlbench.vectordata.RangeFill;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
@@ -25,6 +26,20 @@ public interface ByteStorage extends AutoCloseable {
|
||||
long size();
|
||||
ByteBuffer read(long offset, int length);
|
||||
void prebuffer(PrebufferProgress progress);
|
||||
/// Drives only the chunks covering `[byteStart, byteEnd)` to
|
||||
/// resident state, reporting bytes resident within the covered
|
||||
/// span against the span's total. Local storage reports the span
|
||||
/// as immediately complete.
|
||||
void prebufferRange(long byteStart, long byteEnd, PrebufferProgress progress);
|
||||
/// Chunk-level residency for a byte range, or `null` when the
|
||||
/// storage has no chunks — local storage is always fully resident
|
||||
/// and a range question about it has no meaning beyond "yes".
|
||||
/// Callers should treat `null` as "free".
|
||||
RangeFill rangeFill(long byteStart, long byteEnd);
|
||||
/// Whether a sub-range of this storage can be fetched without
|
||||
/// fetching the rest. Local storage is trivially range-capable; a
|
||||
/// remote source that cannot service byte ranges is not.
|
||||
boolean rangeCapable();
|
||||
boolean isComplete();
|
||||
CacheStats stats();
|
||||
@Override default void close() { }
|
||||
|
||||
@@ -18,6 +18,7 @@ package io.nosqlbench.vectordata.internal;
|
||||
import io.nosqlbench.vectordata.AccessMode;
|
||||
import io.nosqlbench.vectordata.CacheStats;
|
||||
import io.nosqlbench.vectordata.PrebufferProgress;
|
||||
import io.nosqlbench.vectordata.RangeFill;
|
||||
import io.nosqlbench.vectordata.VectorDataException;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -54,7 +55,13 @@ public final class MappedStorage implements ByteStorage {
|
||||
throw new VectorDataException("Byte range outside source: offset=" + offset + ", length=" + length + ", size=" + size);
|
||||
}
|
||||
@Override public void prebuffer(PrebufferProgress progress) { progress.onProgress(size, size); }
|
||||
@Override public void prebufferRange(long byteStart, long byteEnd, PrebufferProgress progress) {
|
||||
long length = Math.max(0, Math.min(byteEnd, size) - byteStart);
|
||||
progress.onProgress(length, length);
|
||||
}
|
||||
@Override public RangeFill rangeFill(long byteStart, long byteEnd) { return null; }
|
||||
@Override public boolean rangeCapable() { return true; }
|
||||
@Override public boolean isComplete() { return true; }
|
||||
@Override public CacheStats stats() { return new CacheStats(AccessMode.LOCAL, size, size, 0, 0, true); }
|
||||
@Override public CacheStats stats() { return new CacheStats(AccessMode.LOCAL, size, size, 0, 0, 0, true); }
|
||||
@Override public void close() { try { channel.close(); } catch (IOException ignored) { } }
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import io.nosqlbench.vectordata.AccessMode;
|
||||
import io.nosqlbench.vectordata.CacheStats;
|
||||
import io.nosqlbench.vectordata.IntegrityException;
|
||||
import io.nosqlbench.vectordata.PrebufferProgress;
|
||||
import io.nosqlbench.vectordata.RangeFill;
|
||||
import io.nosqlbench.vectordata.VectorDataException;
|
||||
import io.nosqlbench.vectordata.VectorDataSettings;
|
||||
|
||||
@@ -149,11 +150,40 @@ public final class RemoteStorage implements ByteStorage {
|
||||
@Override public void prebuffer(PrebufferProgress progress) {
|
||||
for (int i = 0; i < chunks(); i++) { ensure(i, i); progress.onProgress(Math.min(size, (long) present.cardinality() * chunkSize), size); }
|
||||
}
|
||||
@Override public void prebufferRange(long byteStart, long byteEnd, PrebufferProgress progress) {
|
||||
long end = Math.min(byteEnd, size);
|
||||
if (byteStart < 0 || byteStart >= end) { progress.onProgress(0, 0); return; }
|
||||
int first = Math.toIntExact(byteStart / chunkSize);
|
||||
int last = Math.toIntExact((end - 1) / chunkSize);
|
||||
long rangeBytes = Math.min(size, (last + 1L) * chunkSize) - (long) first * chunkSize;
|
||||
synchronized (lock) {
|
||||
long done = 0;
|
||||
for (int chunk = first; chunk <= last; chunk++) {
|
||||
if (present.get(chunk)) hits.incrementAndGet(); else fetch(chunk);
|
||||
done += chunkLength(chunk);
|
||||
progress.onProgress(done, rangeBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@Override public RangeFill rangeFill(long byteStart, long byteEnd) {
|
||||
if (size == 0 || byteEnd <= byteStart || byteStart < 0 || byteStart >= size) return null;
|
||||
long end = Math.min(byteEnd, size);
|
||||
int first = Math.toIntExact(byteStart / chunkSize);
|
||||
int last = Math.toIntExact(Math.min((end - 1) / chunkSize, chunks() - 1L));
|
||||
if (first > last) return null;
|
||||
int resident = 0;
|
||||
synchronized (lock) { for (int chunk = first; chunk <= last; chunk++) if (present.get(chunk)) resident++; }
|
||||
long alignedStart = (long) first * chunkSize;
|
||||
long alignedEnd = Math.min((last + 1L) * chunkSize, size);
|
||||
return new RangeFill(first, last, chunkSize, last - first + 1, resident, alignedStart, alignedEnd);
|
||||
}
|
||||
@Override public boolean rangeCapable() { return ranges; }
|
||||
private long chunkLength(int chunk) { return Math.min(chunkSize, size - (long) chunk * chunkSize); }
|
||||
@Override public boolean isComplete() { return complete; }
|
||||
@Override public CacheStats stats() {
|
||||
long cached = Math.min(size, (long) present.cardinality() * chunkSize);
|
||||
AccessMode mode = reference == null ? (ranges ? AccessMode.MERKLE_CHUNKED : AccessMode.FULL_TRANSFER) : AccessMode.MERKLE_HASHED;
|
||||
return new CacheStats(mode, size, cached, hits.get(), misses.get(), complete);
|
||||
return new CacheStats(mode, size, cached, chunkSize, hits.get(), misses.get(), complete);
|
||||
}
|
||||
private int chunks() { return size == 0 ? 0 : Math.toIntExact((size + chunkSize - 1) / chunkSize); }
|
||||
private void checkBounds(long offset, int length) {
|
||||
|
||||
Reference in new issue
Block a user