|
17 | 17 | import java.util.List; |
18 | 18 |
|
19 | 19 | /// Write-only encoder for `vortex.zstd`. |
| 20 | +/// |
| 21 | +/// By default the whole array compresses into a single zstd frame. Construct with a positive |
| 22 | +/// `valuesPerFrame` to split the payload into independently compressed frames of that many values |
| 23 | +/// each (the last frame holds the remainder), emitting one `ZstdFrameMetadata` per frame. Multiple |
| 24 | +/// frames let a slice scan decompress only the frames overlapping its row range. |
20 | 25 | public final class ZstdEncodingEncoder implements EncodingEncoder { |
21 | 26 |
|
22 | | - /// Public no-arg constructor required by [java.util.ServiceLoader]. |
| 27 | + /// Values per zstd frame; `0` (or any non-positive value) means a single frame for the whole array. |
| 28 | + private final long valuesPerFrame; |
| 29 | + |
| 30 | + /// Public no-arg constructor required by [java.util.ServiceLoader]; compresses each array into |
| 31 | + /// a single frame. |
23 | 32 | public ZstdEncodingEncoder() { |
| 33 | + this(0); |
| 34 | + } |
| 35 | + |
| 36 | + /// Creates an encoder that splits the payload into frames of `valuesPerFrame` values each. |
| 37 | + /// |
| 38 | + /// @param valuesPerFrame the number of values per zstd frame; non-positive means a single frame |
| 39 | + /// for the whole array |
| 40 | + public ZstdEncodingEncoder(long valuesPerFrame) { |
| 41 | + this.valuesPerFrame = valuesPerFrame; |
24 | 42 | } |
25 | 43 |
|
26 | 44 | @Override |
@@ -66,78 +84,142 @@ public EncodeResult encode(DType dtype, Object data, EncodeContext ctx) { |
66 | 84 | throw new VortexException(EncodingId.VORTEX_ZSTD, "unsupported dtype: " + dtype); |
67 | 85 | } |
68 | 86 |
|
69 | | - private static EncodeResult encodePrimitive(DType.Primitive dt, Object data, Arena arena) { |
| 87 | + private EncodeResult encodePrimitive(DType.Primitive dt, Object data, Arena arena) { |
| 88 | + int byteWidth = dt.ptype().byteSize(); |
70 | 89 | MemorySegment raw = primitiveToLeBytes(dt.ptype(), data, arena); |
71 | 90 | long n = primitiveLength(dt.ptype(), data); |
72 | | - return buildResult(raw, n, arena); |
| 91 | + return buildResult(raw, uniformLayout(n, byteWidth), arena); |
73 | 92 | } |
74 | 93 |
|
75 | | - private static EncodeResult encodeVarBin(String[] strings, Arena arena) { |
| 94 | + private EncodeResult encodeVarBin(String[] strings, Arena arena) { |
76 | 95 | MemorySegment raw = buildLengthPrefixed(strings, arena); |
77 | | - return buildResult(raw, strings.length, arena); |
| 96 | + return buildResult(raw, varBinLayout(raw, strings.length), arena); |
78 | 97 | } |
79 | 98 |
|
80 | | - private static EncodeResult buildResult(MemorySegment raw, long n, Arena arena) { |
81 | | - // Zero-copy: compress the arena-native raw segment straight into another arena segment, |
82 | | - // no heap byte[] bounce on either side. The compressed slice is owned by the caller arena. |
83 | | - MemorySegment compressed; |
84 | | - try (ZstdCompressCtx cctx = new ZstdCompressCtx()) { |
85 | | - compressed = cctx.compress(arena, raw); |
86 | | - } |
87 | | - byte[] meta = new ProtoZstdMetadata( |
88 | | - 0, |
89 | | - List.of(new ProtoZstdFrameMetadata(raw.byteSize(), n)) |
90 | | - ).encode(); |
91 | | - EncodeNode root = new EncodeNode(EncodingId.VORTEX_ZSTD, MemorySegment.ofArray(meta), |
92 | | - new EncodeNode[0], new int[]{0}); |
93 | | - return new EncodeResult(root, List.of(compressed), null, null); |
| 99 | + private EncodeResult buildResult(MemorySegment raw, FrameLayout layout, Arena arena) { |
| 100 | + // Zero-copy: each frame is an arena-native slice of raw, compressed straight into another |
| 101 | + // arena segment. A single-value-per-array config yields one frame (the prior behaviour). |
| 102 | + Frames frames = compressFrames(raw, layout, arena); |
| 103 | + EncodeNode root = new EncodeNode(EncodingId.VORTEX_ZSTD, MemorySegment.ofArray(frames.metadata()), |
| 104 | + new EncodeNode[0], frameBufferIndices(frames.compressed().size(), 0)); |
| 105 | + return new EncodeResult(root, List.copyOf(frames.compressed()), null, null); |
94 | 106 | } |
95 | 107 |
|
96 | | - private static EncodeResult encodeNullablePrimitive(DType.Primitive dt, NullableData nd, EncodeContext ctx) { |
| 108 | + private EncodeResult encodeNullablePrimitive(DType.Primitive dt, NullableData nd, EncodeContext ctx) { |
97 | 109 | Arena arena = ctx.arena(); |
98 | 110 | int byteWidth = dt.ptype().byteSize(); |
99 | 111 | boolean[] validity = nd.validity(); |
100 | 112 | // Strip null positions: only valid values reach the compressed payload (mirrors the Rust |
101 | 113 | // reference). The decoder scatters them back over the validity mask carried by child[0]. |
102 | 114 | MemorySegment full = primitiveToLeBytes(dt.ptype(), nd.values(), arena); |
103 | 115 | MemorySegment packed = packValidBytes(full, validity, byteWidth, arena); |
104 | | - return buildNullableResult(packed, countValid(validity), validity, ctx); |
| 116 | + return buildNullableResult(packed, uniformLayout(countValid(validity), byteWidth), validity, ctx); |
105 | 117 | } |
106 | 118 |
|
107 | | - private static EncodeResult encodeNullableVarBin(NullableData nd, EncodeContext ctx) { |
| 119 | + private EncodeResult encodeNullableVarBin(NullableData nd, EncodeContext ctx) { |
108 | 120 | // Strip null positions: only valid strings reach the compressed payload (mirrors the Rust |
109 | 121 | // reference). The decoder scatters them back over the validity mask carried by child[0]. |
110 | 122 | String[] valid = stripNulls((String[]) nd.values()); |
111 | 123 | MemorySegment packed = buildLengthPrefixed(valid, ctx.arena()); |
112 | | - return buildNullableResult(packed, valid.length, nd.validity(), ctx); |
| 124 | + return buildNullableResult(packed, varBinLayout(packed, valid.length), nd.validity(), ctx); |
113 | 125 | } |
114 | 126 |
|
115 | | - private static EncodeResult buildNullableResult( |
116 | | - MemorySegment raw, long nValues, boolean[] validity, EncodeContext ctx) { |
117 | | - // Zero-copy: compress the arena-native packed segment into another arena segment. |
118 | | - MemorySegment compressed; |
119 | | - try (ZstdCompressCtx cctx = new ZstdCompressCtx()) { |
120 | | - compressed = cctx.compress(ctx.arena(), raw); |
121 | | - } |
122 | | - byte[] meta = new ProtoZstdMetadata( |
123 | | - 0, |
124 | | - List.of(new ProtoZstdFrameMetadata(raw.byteSize(), nValues)) |
125 | | - ).encode(); |
| 127 | + private EncodeResult buildNullableResult( |
| 128 | + MemorySegment raw, FrameLayout layout, boolean[] validity, EncodeContext ctx) { |
| 129 | + Frames frames = compressFrames(raw, layout, ctx.arena()); |
| 130 | + int frameCount = frames.compressed().size(); |
126 | 131 |
|
127 | 132 | EncodeResult validityResult = new BoolEncodingEncoder().encode(DType.BOOL, validity, ctx); |
128 | | - // The frame payload owns buffer[0]; the validity child's buffers follow, so shift its |
129 | | - // buffer indices by one. |
130 | | - EncodeNode validityNode = EncodeNode.remapBufferIndices(validityResult.rootNode(), 1); |
| 133 | + // The frame payloads own buffer[0..frameCount-1]; the validity child's buffers follow, so |
| 134 | + // shift its buffer indices past them. |
| 135 | + EncodeNode validityNode = EncodeNode.remapBufferIndices(validityResult.rootNode(), frameCount); |
131 | 136 |
|
132 | | - List<MemorySegment> buffers = new ArrayList<>(1 + validityResult.buffers().size()); |
133 | | - buffers.add(compressed); |
| 137 | + List<MemorySegment> buffers = new ArrayList<>(frameCount + validityResult.buffers().size()); |
| 138 | + buffers.addAll(frames.compressed()); |
134 | 139 | buffers.addAll(validityResult.buffers()); |
135 | 140 |
|
136 | | - EncodeNode root = new EncodeNode(EncodingId.VORTEX_ZSTD, MemorySegment.ofArray(meta), |
137 | | - new EncodeNode[]{validityNode}, new int[]{0}); |
| 141 | + EncodeNode root = new EncodeNode(EncodingId.VORTEX_ZSTD, MemorySegment.ofArray(frames.metadata()), |
| 142 | + new EncodeNode[]{validityNode}, frameBufferIndices(frameCount, 0)); |
138 | 143 | return new EncodeResult(root, buffers, null, null); |
139 | 144 | } |
140 | 145 |
|
| 146 | + /// Byte spans and value counts of each frame; spans sum to the payload size. |
| 147 | + private record FrameLayout(long[] byteLengths, long[] valueCounts) { |
| 148 | + } |
| 149 | + |
| 150 | + /// Compressed frame payloads paired with the encoded `ZstdMetadata` describing them. |
| 151 | + private record Frames(List<MemorySegment> compressed, byte[] metadata) { |
| 152 | + } |
| 153 | + |
| 154 | + private static int[] frameBufferIndices(int frameCount, int base) { |
| 155 | + int[] indices = new int[frameCount]; |
| 156 | + for (int i = 0; i < frameCount; i++) { |
| 157 | + indices[i] = base + i; |
| 158 | + } |
| 159 | + return indices; |
| 160 | + } |
| 161 | + |
| 162 | + /// Frame layout for `n` fixed-width values (`byteWidth` bytes each): `valuesPerFrame` values |
| 163 | + /// per frame, the last frame holding the remainder. One frame when framing is disabled. |
| 164 | + private FrameLayout uniformLayout(long n, int byteWidth) { |
| 165 | + if (valuesPerFrame <= 0 || n <= valuesPerFrame) { |
| 166 | + return new FrameLayout(new long[]{n * byteWidth}, new long[]{n}); |
| 167 | + } |
| 168 | + int frameCount = (int) ((n + valuesPerFrame - 1) / valuesPerFrame); |
| 169 | + long[] byteLengths = new long[frameCount]; |
| 170 | + long[] valueCounts = new long[frameCount]; |
| 171 | + long remaining = n; |
| 172 | + for (int f = 0; f < frameCount; f++) { |
| 173 | + long count = Math.min(valuesPerFrame, remaining); |
| 174 | + valueCounts[f] = count; |
| 175 | + byteLengths[f] = count * byteWidth; |
| 176 | + remaining -= count; |
| 177 | + } |
| 178 | + return new FrameLayout(byteLengths, valueCounts); |
| 179 | + } |
| 180 | + |
| 181 | + /// Frame layout for a length-prefixed varbin payload: `valuesPerFrame` values per frame, with |
| 182 | + /// each frame's byte span found by walking the 4-byte length prefixes to a value boundary. |
| 183 | + private FrameLayout varBinLayout(MemorySegment raw, long nValues) { |
| 184 | + if (valuesPerFrame <= 0 || nValues <= valuesPerFrame) { |
| 185 | + return new FrameLayout(new long[]{raw.byteSize()}, new long[]{nValues}); |
| 186 | + } |
| 187 | + int frameCount = (int) ((nValues + valuesPerFrame - 1) / valuesPerFrame); |
| 188 | + long[] byteLengths = new long[frameCount]; |
| 189 | + long[] valueCounts = new long[frameCount]; |
| 190 | + long pos = 0; |
| 191 | + long valueIdx = 0; |
| 192 | + for (int f = 0; f < frameCount; f++) { |
| 193 | + long count = Math.min(valuesPerFrame, nValues - valueIdx); |
| 194 | + long start = pos; |
| 195 | + for (long k = 0; k < count; k++) { |
| 196 | + int len = raw.get(PTypeIO.LE_INT, pos); |
| 197 | + pos += 4L + len; |
| 198 | + } |
| 199 | + byteLengths[f] = pos - start; |
| 200 | + valueCounts[f] = count; |
| 201 | + valueIdx += count; |
| 202 | + } |
| 203 | + return new FrameLayout(byteLengths, valueCounts); |
| 204 | + } |
| 205 | + |
| 206 | + private static Frames compressFrames(MemorySegment raw, FrameLayout layout, Arena arena) { |
| 207 | + int frameCount = layout.byteLengths().length; |
| 208 | + List<MemorySegment> compressed = new ArrayList<>(frameCount); |
| 209 | + List<ProtoZstdFrameMetadata> metas = new ArrayList<>(frameCount); |
| 210 | + long offset = 0; |
| 211 | + try (ZstdCompressCtx cctx = new ZstdCompressCtx()) { |
| 212 | + for (int f = 0; f < frameCount; f++) { |
| 213 | + long len = layout.byteLengths()[f]; |
| 214 | + compressed.add(cctx.compress(arena, raw.asSlice(offset, len))); |
| 215 | + metas.add(new ProtoZstdFrameMetadata(len, layout.valueCounts()[f])); |
| 216 | + offset += len; |
| 217 | + } |
| 218 | + } |
| 219 | + byte[] metadata = new ProtoZstdMetadata(0, List.copyOf(metas)).encode(); |
| 220 | + return new Frames(compressed, metadata); |
| 221 | + } |
| 222 | + |
141 | 223 | private static MemorySegment packValidBytes( |
142 | 224 | MemorySegment full, boolean[] validity, int byteWidth, Arena arena) { |
143 | 225 | long validBytes = (long) countValid(validity) * byteWidth; |
|
0 commit comments