Repository navigation
Expand file tree
/
Copy pathShardingCodec.cs
More file actions
208 lines (179 loc) · 7.85 KB
/
Copy pathShardingCodec.cs
File metadata and controls
208 lines (179 loc) · 7.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
using System.Buffers.Binary;
using ZarrNET;
using ZarrNET.Core.Zarr.Store;
namespace ZarrNET;
/// <summary>
/// Configuration for a sharding_indexed codec parsed from zarr.json.
///
/// Sharding packs multiple "inner chunks" (sub-chunks) into a single shard
/// file. The shard file contains the compressed inner chunks plus a binary
/// index that maps each inner chunk's grid position to its (offset, length)
/// within the shard.
///
/// zarr.json example:
/// <code>
/// "codecs": [{
/// "name": "sharding_indexed",
/// "configuration": {
/// "chunk_shape": [64, 64],
/// "codecs": [{"name": "bytes"}, {"name": "blosc", ...}],
/// "index_codecs": [{"name": "bytes", "configuration": {"endian": "little"}}],
/// "index_location": "end"
/// }
/// }]
/// </code>
///
/// The outer chunk_grid.chunk_shape in zarr.json defines the shard shape.
/// The inner chunk_shape here defines the sub-chunk shape within each shard.
/// </summary>
public sealed class ShardingConfig
{
/// <summary>Inner chunk (sub-chunk) shape within each shard.</summary>
public int[] InnerChunkShape { get; init; } = Array.Empty<int>();
/// <summary>Codec pipeline applied to each inner chunk.</summary>
public CodecPipeline InnerPipeline { get; init; } = null!;
/// <summary>Codec pipeline applied to the binary shard index.</summary>
public CodecPipeline IndexPipeline { get; init; } = null!;
/// <summary>Where the index sits within the shard file: "end" or "start".</summary>
public ShardIndexLocation IndexLocation { get; init; } = ShardIndexLocation.End;
/// <summary>
/// Number of inner chunks per shard along each axis.
/// Computed from shard shape / inner chunk shape.
/// </summary>
public int[] InnerChunksPerShard { get; init; } = Array.Empty<int>();
/// <summary>Total number of inner chunks per shard (product of InnerChunksPerShard).</summary>
public int TotalInnerChunks { get; init; }
/// <summary>
/// Total byte size of the encoded shard index as stored on disk.
/// This is the raw index data (TotalInnerChunks × 16 bytes) plus any
/// overhead added by the index codecs (e.g. crc32c adds 4 bytes).
///
/// Used by ShardReader to know how many bytes to slice from the shard
/// file before feeding them into the index codec pipeline for decoding.
/// </summary>
public int IndexEncodedSize { get; init; }
}
public enum ShardIndexLocation
{
End,
Start
}
/// <summary>
/// Reads individual inner chunks from a shard file.
///
/// Shard binary layout (index_location = "end"):
/// [inner chunk 0 bytes] [inner chunk 1 bytes] ... [index]
///
/// Index layout:
/// For each inner chunk in C-order grid position:
/// uint64 offset (byte offset from start of shard, or 2^64-1 if empty)
/// uint64 nbytes (byte count of the compressed inner chunk)
///
/// The index is decoded by the index_codecs pipeline (typically just "bytes"
/// with little-endian). Empty chunks are indicated by offset = 0xFFFFFFFFFFFFFFFF
/// and nbytes = 0xFFFFFFFFFFFFFFFF.
/// </summary>
public static class ShardReader
{
private const ulong EmptyMarker = 0xFFFF_FFFF_FFFF_FFFF;
// -------------------------------------------------------------------------
// Public API
// -------------------------------------------------------------------------
/// <summary>
/// Reads a single inner chunk from a shard file.
///
/// <paramref name="shardBytes"/> is the raw shard file content.
/// <paramref name="innerChunkCoord"/> is the inner chunk's grid position
/// within the shard (not the global chunk coordinate).
/// </summary>
public static async Task<byte[]?> ReadInnerChunkAsync(
byte[] shardBytes,
long[] innerChunkCoord,
ShardingConfig config,
CancellationToken ct = default)
{
// Parse the shard index
var index = await ParseShardIndexAsync(shardBytes, config, ct).ConfigureAwait(false);
// Find this inner chunk in the index
var flatIndex = ComputeFlatInnerIndex(innerChunkCoord, config.InnerChunksPerShard);
var indexEntry = index[flatIndex];
// Empty chunk — caller should use fill value
if (indexEntry.Offset == EmptyMarker && indexEntry.NBytes == EmptyMarker)
return null;
// Extract and decode the inner chunk bytes
var offset = (int)indexEntry.Offset;
var nbytes = (int)indexEntry.NBytes;
var compressed = new byte[nbytes];
Array.Copy(shardBytes, offset, compressed, 0, nbytes);
var decoded = await config.InnerPipeline.DecodeAsync(compressed, ct).ConfigureAwait(false);
return decoded;
}
// -------------------------------------------------------------------------
// Index parsing
// -------------------------------------------------------------------------
/// <summary>
/// Extracts and decodes the shard index from the shard file bytes.
/// The index is an array of (offset, nbytes) pairs, one per inner chunk.
/// </summary>
private static async Task<ShardIndexEntry[]> ParseShardIndexAsync(
byte[] shardBytes,
ShardingConfig config,
CancellationToken ct)
{
var entryCount = config.TotalInnerChunks;
var encodedIndexSize = config.IndexEncodedSize;
// Extract the encoded index bytes from the shard.
// The encoded size includes any overhead from index codecs (e.g. crc32c
// appends 4 bytes). We must slice exactly this many bytes so the codec
// pipeline receives the complete encoded payload.
byte[] encodedIndexBytes;
if (config.IndexLocation == ShardIndexLocation.End)
{
var indexStart = shardBytes.Length - encodedIndexSize;
encodedIndexBytes = new byte[encodedIndexSize];
Array.Copy(shardBytes, indexStart, encodedIndexBytes, 0, encodedIndexSize);
}
else // Start
{
encodedIndexBytes = new byte[encodedIndexSize];
Array.Copy(shardBytes, 0, encodedIndexBytes, 0, encodedIndexSize);
}
// Decode through the index codec pipeline (handles crc32c + endianness)
var decodedIndex = await config.IndexPipeline.DecodeAsync(encodedIndexBytes, ct)
.ConfigureAwait(false);
// Parse the (offset, nbytes) pairs
var entries = new ShardIndexEntry[entryCount];
for (int i = 0; i < entryCount; i++)
{
var byteOffset = i * 16;
var offset = BinaryPrimitives.ReadUInt64LittleEndian(
decodedIndex.AsSpan(byteOffset, 8));
var nbytes = BinaryPrimitives.ReadUInt64LittleEndian(
decodedIndex.AsSpan(byteOffset + 8, 8));
entries[i] = new ShardIndexEntry(offset, nbytes);
}
return entries;
}
// -------------------------------------------------------------------------
// Coordinate mapping
// -------------------------------------------------------------------------
/// <summary>
/// Converts an N-dimensional inner chunk coordinate to a flat index
/// in C-order (row-major) within the shard's inner chunk grid.
/// </summary>
private static int ComputeFlatInnerIndex(long[] innerCoord, int[] innerChunksPerShard)
{
int index = 0;
int stride = 1;
for (int d = innerCoord.Length - 1; d >= 0; d--)
{
index += (int)innerCoord[d] * stride;
stride *= innerChunksPerShard[d];
}
return index;
}
// -------------------------------------------------------------------------
// Types
// -------------------------------------------------------------------------
private readonly record struct ShardIndexEntry(ulong Offset, ulong NBytes);
}