From d7d2bc6402f7e000d280d76a7432ef037a050b2b Mon Sep 17 00:00:00 2001 From: Vasileios Zois <96085550+vazois@users.noreply.github.com> Date: Thu, 13 Feb 2025 16:26:51 -0800 Subject: [PATCH] Refactor BITPOS Implementation (#1016) * wip * cleanup bitpos tests * Fix bitpos bit search * simplify bitpos byte search * cleanup unit tests for bitpos * fix masked invalidPayload matching * add more tests * fix boundary conditions for bit search and add tests * change to portable intrinsics * add more tests for bitpos --- .../server/Resp/Bitmap/BitmapManagerBitPos.cs | 267 ++++++++--------- .../Functions/MainStore/PrivateMethods.cs | 10 +- test/Garnet.test/GarnetBitmapTests.cs | 275 +++++++++++++----- 3 files changed, 338 insertions(+), 214 deletions(-) diff --git a/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs b/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs index aba15d649a..0cde3dcdd6 100644 --- a/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs +++ b/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs @@ -1,185 +1,174 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using System.Diagnostics; -using System.Runtime.Intrinsics.X86; +using System.Buffers.Binary; +using System.Numerics; namespace Garnet.server { public unsafe partial class BitmapManager { /// - /// Find pos of bit set/clear for given bit offsets within a single byte. + /// Main driver for BITPOS command /// - /// Byte value to search within. - /// Bit value to search for (0|1). - /// Start most significant bit offset in byte value. - /// End most significant bit offset in bitmap. - /// - private static long BitPosIndexBitSingleByteSearch(byte value, byte bSetVal, int startBitOffset = 0, int endBitOffset = 8) - { - Debug.Assert(startBitOffset >= 0 && startBitOffset <= 8); - Debug.Assert(endBitOffset >= 0 && endBitOffset <= 8); - bool bflag = (bSetVal == 0); - long mask = bflag ? -1 : 0; - - int leftBitIndex = 1 << (8 - startBitOffset); - int rightBitIndex = 1 << (8 - endBitOffset); - - // Create extraction mask - long extract = leftBitIndex - rightBitIndex; - - long payload = (long)(value & extract) << 56; - // Trim leading bits - payload = payload << startBitOffset; - - // Transform to count leading zeros - payload = bflag ? ~payload : payload; - - // Return not found - if (payload == mask) return -1; - - return (long)Lzcnt.X64.LeadingZeroCount((ulong)payload); - } - - /// - /// Find pos of bit set/clear for given bit offset. - /// - /// Pointer to start of bitmap. - /// Bit value to search for (0|1). - /// Bit offset in bitmap. - /// - private static long BitPosIndexBitSearch(byte* value, byte bSetVal, long offset = 0) - { - bool bflag = (bSetVal == 0); - long mask = bflag ? -1 : 0; - long startByteOffset = (offset / 8); - int bitOffset = (int)(offset & 7); - - long payload = (long)value[startByteOffset] << 56; - // Trim leading bits - payload = payload << bitOffset; - - // Transform to count leading zeros - payload = bflag ? ~payload : payload; - - // Return not found - if (payload == mask) - return -1; - - return (long)Lzcnt.X64.LeadingZeroCount((ulong)payload); - } - - /// - /// Main driver for bit position command. - /// - /// + /// + /// /// /// + /// /// - /// Pointer to start of bitmap. - /// Length of bitmap. /// - public static long BitPosDriver(byte setVal, long startOffset, long endOffset, byte offsetType, byte* value, int valLen) + public static long BitPosDriver(byte* input, int inputLen, long startOffset, long endOffset, byte searchFor, byte offsetType) { if (offsetType == 0x0) { - startOffset = startOffset < 0 ? ProcessNegativeOffset(startOffset, valLen) : startOffset; - endOffset = endOffset < 0 ? ProcessNegativeOffset(endOffset, valLen) : endOffset; + startOffset = startOffset < 0 ? ProcessNegativeOffset(startOffset, inputLen) : startOffset; + endOffset = endOffset < 0 ? ProcessNegativeOffset(endOffset, inputLen) : endOffset; - if (startOffset >= valLen) // If startOffset greater that valLen always bitpos -1 + if (startOffset >= inputLen) // If startOffset greater that valLen always bitpos -1 return -1; if (startOffset > endOffset) // If start offset beyond endOffset return 0 return -1; - endOffset = endOffset >= valLen ? valLen : endOffset; - long pos = BitPosByte(value, setVal, startOffset, endOffset); - // check if position is exceeding the last byte in acceptable range - return pos >= ((endOffset + 1) * 8) ? -1 : pos; + endOffset = endOffset >= inputLen ? inputLen : endOffset; + // BYTE search + return BitPosByteSearch(input, inputLen, startOffset, endOffset, searchFor); } - - startOffset = startOffset < 0 ? ProcessNegativeOffset(startOffset, valLen * 8) : startOffset; - endOffset = endOffset < 0 ? ProcessNegativeOffset(endOffset, valLen * 8) : endOffset; - - var startByte = (startOffset / 8); - var endByte = (endOffset / 8); - if (startByte == endByte) + else { - // Search only inside single byte for pos - var leftBitIndex = (int)(startOffset & 7); - var rightBitIndex = (int)((endOffset + 1) & 7); - var _ipos = BitPosIndexBitSingleByteSearch(value[startByte], setVal, leftBitIndex, rightBitIndex); - return _ipos == -1 ? _ipos : startOffset + _ipos; - } + startOffset = startOffset < 0 ? ProcessNegativeOffset(startOffset, inputLen * 8) : startOffset; + endOffset = endOffset < 0 ? ProcessNegativeOffset(endOffset, inputLen * 8) : endOffset; + + var startByteIndex = startOffset >> 3; + var endByteIndex = endOffset >> 3; - // Search prefix and terminate if found position of bit - var _ppos = BitPosIndexBitSearch(value, setVal, startOffset); - if (_ppos != -1) return startOffset + _ppos; + if (startByteIndex >= inputLen) // If startOffset greater that valLen always bitpos -1 + return -1; - // Adjust offsets to skip first and last byte - var _startOffset = (startOffset / 8) + 1; - var _endOffset = (endOffset / 8) - 1; - var _bpos = BitPosByte(value, setVal, _startOffset, _endOffset); + if (startByteIndex > endByteIndex) // If start offset beyond endOffset return 0 + return -1; - if (_bpos != -1 && _bpos < (_endOffset + 1) * 8) return _bpos; + endOffset = endByteIndex >= inputLen ? inputLen << 3 : endOffset; - // Search suffix - var _spos = BitPosIndexBitSearch(value, setVal, endOffset); - return _spos; + // BIT search + return BitPosBitSearch(input, inputLen, startOffset, endOffset, searchFor); + } } /// - /// Find pos of set/clear bit in a sequence of bytes. + /// Search for position of bit set in byte array using bit offset for start and end range /// - /// Pointer to start of bitmap. - /// The bit value to search for (0 for cleared bit or 1 for set bit). - /// Starting offset into bitmap. - /// End offset into bitmap. + /// + /// + /// + /// + /// /// - private static long BitPosByte(byte* value, byte bSetVal, long startOffset, long endOffset) + private static long BitPosBitSearch(byte* input, long inputLen, long startBitOffset, long endBitOffset, byte searchFor) { - // Mask set to look for 0 or 1 depending on clear/set flag - bool bflag = (bSetVal == 0); - long mask = bflag ? -1 : 0; - long len = (endOffset - startOffset) + 1; - long remainder = len & 7; - byte* curr = value + startOffset; - byte* end = curr + (len - remainder); - - // Search for first word not matching mask. - while (curr < end) + var searchBit = searchFor == 1; + var invalidPayload = (byte)(searchBit ? 0x00 : 0xff); + var currentBitOffset = (int)startBitOffset; + while (currentBitOffset <= endBitOffset) { - long v = *(long*)(curr); - if (v != mask) break; - curr += 8; - } + var byteIndex = currentBitOffset >> 3; + var leftBitOffset = currentBitOffset & 7; + var boundary = 8 - leftBitOffset; + var rightBitOffset = currentBitOffset + boundary <= endBitOffset ? leftBitOffset + boundary : (int)(endBitOffset & 7) + 1; + + // Trim byte to start and end bit index + var mask = (0xff >> leftBitOffset) ^ (0xff >> rightBitOffset); + var payload = (long)(input[byteIndex] & mask); - long pos = (((long)(curr - value)) << 3); + // Invalid only if equals the masked payload + var invalidMask = invalidPayload & mask; - long payload = 0; - // Adjust end so we can retrieve word - end = end + remainder; + // If transformed payload is invalid skip to next byte + if (payload != invalidMask) + { + payload <<= (56 + leftBitOffset); + payload = searchBit ? payload : ~payload; - // Build payload at least one byte to examine - if (curr < end) payload |= (long)curr[0] << 56; - if (curr + 1 < end) payload |= (long)curr[1] << 48; - if (curr + 2 < end) payload |= (long)curr[2] << 40; - if (curr + 3 < end) payload |= (long)curr[3] << 32; - if (curr + 4 < end) payload |= (long)curr[4] << 24; - if (curr + 5 < end) payload |= (long)curr[5] << 16; - if (curr + 6 < end) payload |= (long)curr[6] << 8; - if (curr + 7 < end) payload |= (long)curr[7]; + var lzcnt = (long)BitOperations.LeadingZeroCount((ulong)payload); + return currentBitOffset + lzcnt; + } - // Transform to count leading zeros - payload = (bSetVal == 0) ? ~payload : payload; + currentBitOffset += boundary; + } - if (payload == mask) - return pos + 0; + return -1; + } - pos += (long)Lzcnt.X64.LeadingZeroCount((ulong)payload); + /// + /// Search for position of bit set in byte array using byte offset for start and end range + /// + /// + /// + /// + /// + /// + /// + private static long BitPosByteSearch(byte* input, long inputLen, long startOffset, long endOffset, byte searchFor) + { + // Initialize variables + var searchBit = searchFor == 1; + var invalidMask8 = searchBit ? 0x00 : 0xff; + var invalidMask32 = searchBit ? 0 : -1; + var invalidMask64 = searchBit ? 0L : -1L; + var currentStartOffset = startOffset; + + while (currentStartOffset <= endOffset) + { + var remainder = endOffset - currentStartOffset + 1; + if (remainder >= 8) + { + var payload = *(long*)(input + currentStartOffset); + payload = BinaryPrimitives.ReverseEndianness(payload); + + // Process only if payload is valid (i.e. not all bits are set or clear based on searchFor parameter) + if (payload != invalidMask64) + { + // Transform to count leading zeros + payload = searchBit ? payload : ~payload; + var lzcnt = (long)BitOperations.LeadingZeroCount((ulong)payload); + return (currentStartOffset << 3) + lzcnt; + } + currentStartOffset += 8; + } + else if (remainder >= 4) + { + var payload = *(int*)(input + currentStartOffset); + payload = BinaryPrimitives.ReverseEndianness(payload); + + // Process only if payload is valid (i.e. not all bits are set or clear based on searchFor parameter) + if (payload != invalidMask32) + { + // Transform to count leading zeros + payload = searchBit ? payload : ~payload; + var lzcnt = (long)BitOperations.LeadingZeroCount((uint)payload); + return (currentStartOffset << 3) + lzcnt; + } + currentStartOffset += 4; + } + else + { + // Process only if payload is valid (i.e. not all bits are set or clear based on searchFor parameter) + if (input[currentStartOffset] != invalidMask8) + { + // Create a payload with the current byte shifted to the most significant byte position + var payload = (long)input[currentStartOffset] << 56; + // Transform to count leading zeros + payload = searchBit ? payload : ~payload; + var lzcnt = (long)BitOperations.LeadingZeroCount((ulong)payload); + return (currentStartOffset << 3) + lzcnt; + } + currentStartOffset++; + } + } - return pos; + // Return -1 if no matching bit is found + return -1; } } } \ No newline at end of file diff --git a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs index b3fcf728ee..625bff7bf8 100644 --- a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs +++ b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs @@ -184,8 +184,14 @@ void CopyRespToWithInput(ref RawStringInput input, ref SpanByte value, ref SpanB } } - var pos = BitmapManager.BitPosDriver(bpSetVal, bpStartOffset, bpEndOffset, bpOffsetType, - value.ToPointer() + functionsState.etagState.etagSkippedStart, value.Length - functionsState.etagState.etagSkippedStart); + var pos = BitmapManager.BitPosDriver( + input: value.ToPointer() + functionsState.etagState.etagSkippedStart, + inputLen: value.Length - functionsState.etagState.etagSkippedStart, + startOffset: bpStartOffset, + endOffset: bpEndOffset, + searchFor: bpSetVal, + offsetType: bpOffsetType + ); *(long*)dst.SpanByte.ToPointer() = pos; CopyRespNumber(pos, ref dst); break; diff --git a/test/Garnet.test/GarnetBitmapTests.cs b/test/Garnet.test/GarnetBitmapTests.cs index 627a4984d4..9ce31136fb 100644 --- a/test/Garnet.test/GarnetBitmapTests.cs +++ b/test/Garnet.test/GarnetBitmapTests.cs @@ -501,8 +501,8 @@ public unsafe void BitmapSimpleBITCOUNT_PCT(int bytesPerSend) private static unsafe long Bitpos(byte[] bitmap, int startOffset = 0, int endOffset = -1, bool set = true) { long pos = 0; - int start = startOffset < 0 ? (startOffset % bitmap.Length) + bitmap.Length : startOffset; - int end = endOffset < 0 ? (endOffset % bitmap.Length) + bitmap.Length : endOffset; + var start = startOffset < 0 ? (startOffset % bitmap.Length) + bitmap.Length : startOffset; + var end = endOffset < 0 ? (endOffset % bitmap.Length) + bitmap.Length : endOffset; if (start >= bitmap.Length) // If startOffset greater that valLen alway bitcount zero return -1; @@ -510,24 +510,27 @@ private static unsafe long Bitpos(byte[] bitmap, int startOffset = 0, int endOff if (start > end) // If start offset beyond endOffset return 0 return -1; - byte mask = (byte)(!set ? 0xFF : 0x00); + var mask = (byte)(!set ? 0xFF : 0x00); + var setbit = set ? 1 : 0; fixed (byte* b = bitmap) { - byte* curr = b + start; - byte* vend = b + end + 1; + var curr = b + start; + var vend = b + end + 1; while (curr < vend) { if (*curr != mask) break; curr++; } + + if (curr > vend) return -1; + pos = (curr - b) << 3; - byte byteVal = *curr; - byte bitv = (byte)(!set ? 0x0 : 0x1); - int bit = 7; - while (((byteVal >> bit) & 0x1) != bitv && bit > 0) + var value = *curr; + for (var i = 7; i >= 0; i--) { - bit--; + if (((value & (1 << i)) >> i) == setbit) + return pos; pos++; } } @@ -542,47 +545,49 @@ public void BitmapSimpleBitPosTests() using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); - string key = "SimpleBitPosTests"; + var key = "SimpleBitPosTests"; byte[] buf; - int maxBitmapLen = 1 << 10; - int iter = 256; + var maxBitmapLen = 1 << 10; + var iter = 256; long maxOffset = 0; - for (int i = 0; i < iter; i++) + for (var i = 0; i < iter; i++) { long offset = r.Next(1, maxBitmapLen); - db.StringSetBit(key, offset, true); + _ = db.StringSetBit(key, offset, true); + buf = db.StringGet(key); - long offsetPos = db.StringBitPosition(key, true); - ClassicAssert.AreEqual(offset, offsetPos); + var offsetPos = db.StringBitPosition(key, true); + ClassicAssert.AreEqual(offset, offsetPos, $"iter:{i}"); buf = db.StringGet(key); - long expectedPos = Bitpos(buf, set: true); - ClassicAssert.AreEqual(expectedPos, offsetPos); + var expectedPos = Bitpos(buf, set: true); + ClassicAssert.AreEqual(expectedPos, offsetPos, $"iter:{i}"); - db.StringSetBit(key, offset, false); + _ = db.StringSetBit(key, offset, false); maxOffset = Math.Max(maxOffset, offset); } - for (int i = 0; i < maxOffset; i++) - db.StringSetBit(key, i, true); + for (var i = 0; i < maxOffset; i++) + _ = db.StringSetBit(key, i, true); - long count = db.StringBitCount(key); + var count = db.StringBitCount(key); ClassicAssert.AreEqual(count, maxOffset); - for (int i = 0; i < iter; i++) + for (var i = 0; i < iter; i++) { long offset = r.Next(1, (int)maxOffset); - db.StringSetBit(key, offset, false); + _ = db.StringSetBit(key, offset, false); - long offsetPos = db.StringBitPosition(key, false); - ClassicAssert.AreEqual(offset, offsetPos); + buf = db.StringGet(key); + var offsetPos = db.StringBitPosition(key, false); + ClassicAssert.AreEqual(offset, offsetPos, $"iter:{i}"); buf = db.StringGet(key); - long expectedPos = Bitpos(buf, set: false); - ClassicAssert.AreEqual(expectedPos, offsetPos); + var expectedPos = Bitpos(buf, set: false); + ClassicAssert.AreEqual(expectedPos, offsetPos, $"iter:{i}"); - db.StringSetBit(key, offset, true); + _ = db.StringSetBit(key, offset, true); } } @@ -593,43 +598,53 @@ public void BitmapBitPosOffsetsTest() using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); - string key = "BitmapBitPosNegativeOffsets"; + var key = "BitmapBitPosNegativeOffsets"; - int maxBitmapLen = 1 << 12; - int maxByteLen = maxBitmapLen >> 3; - int iter = 1 << 5; - byte[] buf = new byte[maxByteLen]; + var maxBitmapLen = 1 << 12; + var maxByteLen = maxBitmapLen >> 3; + var iter = 1 << 5; + var buf = new byte[maxByteLen]; long expectedPos; long pos; - for (int j = 0; j < iter; j++) + for (var j = 0; j < iter; j++) { r.NextBytes(buf); - db.StringSet(key, buf); + _ = db.StringSet(key, buf); - int startOffset = r.Next(0, maxByteLen); - int endOffset = r.Next(startOffset, maxByteLen); + var startOffset = r.Next(0, maxByteLen); + var endOffset = r.Next(startOffset, maxByteLen); - bool set = r.Next(0, 1) == 0 ? false : true; + var set = r.Next(0, 1) == 0 ? false : true; expectedPos = Bitpos(buf, startOffset, endOffset, set); pos = db.StringBitPosition(key, set, startOffset, endOffset); - ClassicAssert.AreEqual(expectedPos, pos, $"{set} {startOffset} {endOffset}"); + ClassicAssert.AreEqual(expectedPos, pos, $"{j} {set} {startOffset} {endOffset}"); + + var startBitOffset = startOffset << 3; + var endBitOffset = endOffset << 3; + pos = db.StringBitPosition(key, set, startBitOffset, endBitOffset, StringIndexType.Bit); + ClassicAssert.AreEqual(expectedPos, pos, $"{j} {set} {startBitOffset} {endBitOffset} bit"); } - //check negative offsets in range - for (int j = 0; j < iter; j++) + // check negative offsets in range + for (var j = 0; j < iter; j++) { r.NextBytes(buf); - db.StringSet(key, buf); + _ = db.StringSet(key, buf); - int startOffset = j == 0 ? -10 : r.Next(-maxByteLen, 0); - int endOffset = j == 0 ? -1 : r.Next(startOffset, 0); + var startOffset = j == 0 ? -10 : r.Next(-maxByteLen, 0); + var endOffset = j == 0 ? -1 : r.Next(startOffset, 0); + + var set = r.Next(0, 1) != 0; + expectedPos = Bitpos(buf, startOffset, endOffset, set); + pos = db.StringBitPosition(key, set, startOffset, endOffset); + ClassicAssert.AreEqual(expectedPos, pos, $"{j} {set} {startOffset} {endOffset}"); - bool set = r.Next(0, 1) == 0 ? false : true; - pos = Bitpos(buf, startOffset, endOffset, set); - expectedPos = db.StringBitPosition(key, set, startOffset, endOffset); - ClassicAssert.AreEqual(pos, expectedPos); + var startBitOffset = startOffset << 3; + var endBitOffset = endOffset << 3; + pos = db.StringBitPosition(key, set, startBitOffset, endBitOffset, StringIndexType.Bit); + ClassicAssert.AreEqual(expectedPos, pos, $"{j} {set} {startBitOffset} {endBitOffset} bit"); } } @@ -637,7 +652,7 @@ public void BitmapBitPosOffsetsTest() [Category("BITPOS")] public void BitmapBitPosTest_LTM() { - int bitmapBytes = 512; + var bitmapBytes = 512; server.Dispose(); server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, lowMemory: true, @@ -647,26 +662,26 @@ public void BitmapBitPosTest_LTM() using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); - int keyCount = 64; - byte[] bitmap = new byte[bitmapBytes]; + var keyCount = 64; + var bitmap = new byte[bitmapBytes]; List bitmapList = []; - for (int i = 0; i < keyCount; i++) + for (var i = 0; i < keyCount; i++) { - string sKey = i.ToString(); + var sKey = i.ToString(); r.NextBytes(bitmap); bitmapList.Add(Bitpos(bitmap, set: true)); - db.StringSet(sKey, bitmap); + _ = db.StringSet(sKey, bitmap); } - int iter = 128; - for (int i = 0; i < iter; i++) + var iter = 128; + for (var i = 0; i < iter; i++) { - int key = r.Next(0, keyCount); - string sKey = key.ToString(); - long pos = db.StringBitPosition(sKey, true); - long expectedPos = bitmapList[key]; + var key = r.Next(0, keyCount); + var sKey = key.ToString(); + var pos = db.StringBitPosition(sKey, true); + var expectedPos = bitmapList[key]; ClassicAssert.AreEqual(expectedPos, pos); } } @@ -704,15 +719,15 @@ public unsafe void BitmapSimpleBITPOS_PCT(int bytesPerSend) using var lightClientRequest = TestUtils.CreateRequest(); var db = redis.GetDatabase(0); - string key = "mykey"; - int maxBitmapLen = 1 << 12; - byte[] buf = new byte[maxBitmapLen >> 3]; + var key = "mykey"; + var maxBitmapLen = 1 << 12; + var buf = new byte[maxBitmapLen >> 3]; r.NextBytes(buf); db.StringSet(key, buf); - long expectedPos = Bitpos(buf); + var expectedPos = Bitpos(buf); long pos = 0; - byte[] response = lightClientRequest.SendCommandChunks("BITPOS mykey 1", bytesPerSend); + var response = lightClientRequest.SendCommandChunks("BITPOS mykey 1", bytesPerSend); pos = ResponseToLong(response, 1); ClassicAssert.AreEqual(expectedPos, pos); } @@ -2267,24 +2282,27 @@ public void BitmapBitPosFixedTests() using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); - string key = "mykey"; - byte[] value = [0x0, 0xff, 0xf0]; + var key = "mykey"; + byte[] value = [0x00, 0xff, 0xf0]; db.StringSet(key, value); - long pos = db.StringBitPosition(key, true, 0); + var pos = db.StringBitPosition(key, true, 0); ClassicAssert.AreEqual(8, pos); pos = db.StringBitPosition(key, true, 2, -1, StringIndexType.Byte); ClassicAssert.AreEqual(16, pos); pos = db.StringBitPosition(key, true, 0, 0, StringIndexType.Byte); - ClassicAssert.AreEqual(0, pos); + ClassicAssert.AreEqual(-1, pos); pos = db.StringBitPosition(key, false, 0, 0, StringIndexType.Byte); ClassicAssert.AreEqual(0, pos); + pos = db.StringBitPosition(key, true, 7, 15, StringIndexType.Bit); + ClassicAssert.AreEqual(8, pos); + value = [0xf8, 0x6f, 0xf0]; - db.StringSet(key, value); + _ = db.StringSet(key, value); pos = db.StringBitPosition(key, true, 5, 17, StringIndexType.Bit); ClassicAssert.AreEqual(9, pos); @@ -2295,12 +2313,17 @@ public void BitmapBitPosFixedTests() ClassicAssert.AreEqual(-1, pos); key = "mykey2"; - db.StringSetBit(key, 63, false); + _ = db.StringSetBit(key, 63, false); pos = db.StringBitPosition(key, false, 1); ClassicAssert.AreEqual(8, pos); pos = db.StringBitPosition(key, false, 0); ClassicAssert.AreEqual(0, pos); + + value = [0xff, 0x7f, 0xf0]; + _ = db.StringSet(key, value); + pos = db.StringBitPosition(key, false, 7, 15, StringIndexType.Bit); + ClassicAssert.AreEqual(8, pos); } [Test, Order(35)] @@ -2357,5 +2380,111 @@ public void BitmapOperationTooManyKeys() ClassicAssert.AreEqual("ERR Bitop source key limit (64) exceeded", ex.Message); } } + + [Test, Order(38)] + [Category("BITPOS")] + public void BitmapBitPosBitOffsetTests([Values] bool searchFor) + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var key = "mykey"; + byte[] value = searchFor ? + [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] : + [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]; + _ = db.StringSet(key, value); + + var bitLength = value.Length * 8; + var expectedPosOffset = 5; + + for (var i = 0; i < 10; i++) + { + // Set or clear bit + _ = db.StringSetBit(key, offset: expectedPosOffset, bit: searchFor); + + // Find pos of bit set/clear + var pos = db.StringBitPosition(key, bit: searchFor, 0, 19, StringIndexType.Bit); + ClassicAssert.AreEqual(expectedPosOffset, pos); + + // Toggle bit back to initial value + _ = db.StringSetBit(key, offset: expectedPosOffset, bit: !searchFor); + + expectedPosOffset++; + } + } + + [Test, Order(38)] + [Category("BITPOS")] + public void BitmapBitPosBitInvalidMaskTests() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var key = "mykey"; + // 0x3e = 00111110 + byte[] value = [0x3e]; + _ = db.StringSet(key, value); + + // 0x3e = 00111110 + var pos = db.StringBitPosition(key, bit: false, start: 0, end: 5, StringIndexType.Bit); + ClassicAssert.AreEqual(0, pos); + + pos = db.StringBitPosition(key, bit: false, start: 1, end: 5, StringIndexType.Bit); + ClassicAssert.AreEqual(1, pos); + + pos = db.StringBitPosition(key, bit: false, start: 2, end: 5, StringIndexType.Bit); + ClassicAssert.AreEqual(-1, pos); + + pos = db.StringBitPosition(key, bit: false, start: 2, end: 6, StringIndexType.Bit); + ClassicAssert.AreEqual(-1, pos); + + pos = db.StringBitPosition(key, bit: false, start: 2, end: 7, StringIndexType.Bit); + ClassicAssert.AreEqual(7, pos); + + // 0x7e02 = 0111111000000010 + value = [0x7e, 0x02]; + _ = db.StringSet(key, value); + pos = db.StringBitPosition(key, bit: true, start: 7, end: 13, StringIndexType.Bit); + ClassicAssert.AreEqual(-1, pos); + + pos = db.StringBitPosition(key, bit: true, start: 7, end: 14, StringIndexType.Bit); + ClassicAssert.AreEqual(14, pos); + } + + [Test, Order(39)] + [Category("BITPOS")] + public void BitmapBitPosBitSearchSingleBitRangeTests() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var key = "mykey"; + var valueLen = 1 << 12; + var value = new byte[valueLen]; + for (var i = 0; i < valueLen; i++) + value[i] = 0xAA; + + _ = db.StringSet(key, value); + + var iter = 1 << 12; + var valueLenBits = valueLen << 3; + for (var i = 0; i < iter; i++) + { + var offset = r.NextInt64(0, valueLenBits); + BitSearch(offset, searchFor: true); + BitSearch(offset, searchFor: false); + } + + void BitSearch(long offset, bool searchFor) + { + var pos = db.StringBitPosition(key, bit: searchFor, start: offset, end: offset, StringIndexType.Bit); + var equalsSearchFor = (offset & 0x1) == (searchFor ? 0 : 1); + + if (equalsSearchFor) + ClassicAssert.AreEqual(offset, pos); + else + ClassicAssert.AreEqual(-1, pos); + } + } } } \ No newline at end of file