Skip to content

Commit dd3e2da

Browse files
committed
Roaring Bitmaps as a Garnet module (issue #1270)
Addresses PR review feedback from @badrishc: - Move the extension from main/GarnetServer/Extensions/RoaringBitmap to modules/RoaringBitmap so it isn't bundled by default (mirrors GarnetJSON). - Retarget the PR to dev (companion change). Implementation changes for the move: - New modules/RoaringBitmap/GarnetRoaringBitmap.csproj (mirrors GarnetJSON.csproj, signs assembly, exposes InternalsVisibleTo Garnet.test). - New RoaringBitmapModule : ModuleBase entry point that registers the factory and the four R.SETBIT/R.GETBIT/R.BITCOUNT/R.BITPOS commands. - Renamed namespace Garnet.Extensions.RoaringBitmap -> GarnetRoaringBitmap to avoid the namespace/class collision with class RoaringBitmap. - Updated CustomObjectFunctions overrides to dev-branch scoped ReadOnlySpan<byte> signatures for NeedInitialUpdate / Updater. - Updated RoaringBitmapObject to dev-branch CustomObjectBase ctor and HeapMemorySize accounting. - Wired the module into Garnet.slnx and Garnet.test.csproj. - Tests still register via server.Register.NewCommand in [SetUp] (in-process), matching the existing custom-object test pattern. - Updated StringKeyAndCustomObjectKey_AreSeparate to expect WRONGTYPE on the unified store on dev.
1 parent d0a93d7 commit dd3e2da

13 files changed

Lines changed: 2000 additions & 0 deletions

Garnet.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
</Folder>
4444
<Folder Name="/modules/">
4545
<Project Path="modules/GarnetJSON/GarnetJSON.csproj" />
46+
<Project Path="modules/RoaringBitmap/GarnetRoaringBitmap.csproj" />
4647
</Folder>
4748
<Folder Name="/playground/">
4849
<Project Path="modules/NoOpModule/NoOpModule.csproj" />
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
namespace GarnetRoaringBitmap.Containers
5+
{
6+
/// <summary>
7+
/// Sorted-array container: stores up to <see cref="ArrayThreshold"/> 16-bit values in
8+
/// ascending order. Uses ~2 bytes per element; preferred representation when the
9+
/// container's cardinality is below the threshold because membership is O(log n)
10+
/// and the memory footprint scales with cardinality. Above the threshold the parent
11+
/// promotes the container to a <see cref="BitmapContainer"/> (constant 8 KiB).
12+
///
13+
/// Serialization layout (body only — caller writes kind+cardinality):
14+
/// ushort[cardinality] values (little-endian per BinaryWriter contract)
15+
/// </summary>
16+
internal sealed class ArrayContainer : IContainer
17+
{
18+
/// <summary>
19+
/// Crossover point at which an array-encoded container should be promoted to a bitmap.
20+
/// 4096 is the canonical Roaring threshold: at 4096 elements an array uses 8 KiB
21+
/// (same as a bitmap container) but membership is still O(log n); past 4096 the
22+
/// bitmap becomes strictly cheaper per element.
23+
/// </summary>
24+
public const int ArrayThreshold = 4096;
25+
26+
// Initial capacity is intentionally small to keep tiny containers cheap;
27+
// capacity grows geometrically up to ArrayThreshold + 1 (the +1 lets us detect
28+
// overflow before promoting in Add).
29+
private const int InitialCapacity = 4;
30+
31+
private ushort[] values;
32+
private int count;
33+
34+
public ArrayContainer()
35+
{
36+
values = new ushort[InitialCapacity];
37+
count = 0;
38+
}
39+
40+
public ArrayContainer(ushort[] sortedUnique, int count)
41+
{
42+
if (sortedUnique == null) throw new ArgumentNullException(nameof(sortedUnique));
43+
if ((uint)count > (uint)sortedUnique.Length) throw new ArgumentOutOfRangeException(nameof(count));
44+
this.values = sortedUnique;
45+
this.count = count;
46+
}
47+
48+
public ContainerKind Kind => ContainerKind.Array;
49+
public int Cardinality => count;
50+
public long ByteSize => 16 + 2L * values.Length; // array header ~16B + 2B per slot
51+
52+
public bool Contains(ushort value)
53+
{
54+
return BinarySearch(value) >= 0;
55+
}
56+
57+
/// <summary>Returns the index of <paramref name="value"/>, or the bitwise complement of the insertion position.</summary>
58+
private int BinarySearch(ushort value)
59+
{
60+
int lo = 0;
61+
int hi = count - 1;
62+
while (lo <= hi)
63+
{
64+
int mid = (lo + hi) >>> 1;
65+
int cmp = values[mid] - value;
66+
if (cmp == 0) return mid;
67+
if (cmp < 0) lo = mid + 1; else hi = mid - 1;
68+
}
69+
return ~lo;
70+
}
71+
72+
public IContainer Add(ushort value, out bool added)
73+
{
74+
int idx = BinarySearch(value);
75+
if (idx >= 0) { added = false; return this; }
76+
77+
int insert = ~idx;
78+
// Promote to bitmap before exceeding the threshold to avoid one wasteful array grow.
79+
if (count >= ArrayThreshold)
80+
{
81+
var bitmap = ToBitmap();
82+
bitmap.SetUnchecked(value);
83+
added = true;
84+
return bitmap;
85+
}
86+
87+
EnsureCapacity(count + 1);
88+
if (insert < count)
89+
{
90+
Array.Copy(values, insert, values, insert + 1, count - insert);
91+
}
92+
values[insert] = value;
93+
count++;
94+
added = true;
95+
return this;
96+
}
97+
98+
public IContainer Remove(ushort value, out bool removed)
99+
{
100+
int idx = BinarySearch(value);
101+
if (idx < 0) { removed = false; return this; }
102+
103+
if (idx < count - 1)
104+
{
105+
Array.Copy(values, idx + 1, values, idx, count - idx - 1);
106+
}
107+
count--;
108+
removed = true;
109+
return count == 0 ? null : this;
110+
}
111+
112+
public ushort First()
113+
{
114+
if (count == 0) throw new InvalidOperationException("empty container");
115+
return values[0];
116+
}
117+
118+
public ushort Last()
119+
{
120+
if (count == 0) throw new InvalidOperationException("empty container");
121+
return values[count - 1];
122+
}
123+
124+
public int NextSetBit(int from)
125+
{
126+
// Find first values[i] >= from
127+
int lo = 0, hi = count;
128+
while (lo < hi)
129+
{
130+
int mid = (lo + hi) >>> 1;
131+
if (values[mid] < from) lo = mid + 1; else hi = mid;
132+
}
133+
return lo == count ? -1 : values[lo];
134+
}
135+
136+
public int NextUnsetBit(int from)
137+
{
138+
if (from < 0 || from > 65535) return -1;
139+
// Find first values[i] >= from. Then walk forward looking for a gap.
140+
int lo = 0, hi = count;
141+
while (lo < hi)
142+
{
143+
int mid = (lo + hi) >>> 1;
144+
if (values[mid] < from) lo = mid + 1; else hi = mid;
145+
}
146+
// Walk from `from`. If values[lo] != from, then `from` itself is unset.
147+
int candidate = from;
148+
while (lo < count && values[lo] == candidate)
149+
{
150+
if (candidate == 65535) return -1;
151+
candidate++;
152+
lo++;
153+
}
154+
return candidate <= 65535 ? candidate : -1;
155+
}
156+
157+
public IContainer Clone()
158+
{
159+
var copy = new ushort[count]; // tight copy — clones don't preallocate growth
160+
Array.Copy(values, copy, count);
161+
return new ArrayContainer(copy, count);
162+
}
163+
164+
public void SerializeBody(BinaryWriter writer)
165+
{
166+
for (int i = 0; i < count; i++)
167+
writer.Write(values[i]);
168+
}
169+
170+
public static ArrayContainer DeserializeBody(BinaryReader reader, int cardinality)
171+
{
172+
if (cardinality < 1 || cardinality > ArrayThreshold)
173+
throw new InvalidDataException($"ArrayContainer cardinality out of range: {cardinality}");
174+
var arr = new ushort[cardinality];
175+
ushort prev = 0;
176+
for (int i = 0; i < cardinality; i++)
177+
{
178+
ushort v = reader.ReadUInt16();
179+
if (i > 0 && v <= prev)
180+
throw new InvalidDataException("ArrayContainer values must be strictly ascending");
181+
arr[i] = v;
182+
prev = v;
183+
}
184+
return new ArrayContainer(arr, cardinality);
185+
}
186+
187+
private void EnsureCapacity(int required)
188+
{
189+
if (required <= values.Length) return;
190+
int newCap = values.Length;
191+
while (newCap < required) newCap = checked(newCap * 2);
192+
if (newCap > ArrayThreshold + 1) newCap = ArrayThreshold + 1;
193+
Array.Resize(ref values, newCap);
194+
}
195+
196+
public BitmapContainer ToBitmap()
197+
{
198+
var bitmap = new BitmapContainer();
199+
for (int i = 0; i < count; i++)
200+
bitmap.SetUnchecked(values[i]);
201+
return bitmap;
202+
}
203+
204+
// Accessor for tests and RunContainer conversion.
205+
internal ushort GetAt(int index) => values[index];
206+
}
207+
}

0 commit comments

Comments
 (0)