Skip to content

Commit bffb3d0

Browse files
committed
Optimizations
1 parent 2f7acde commit bffb3d0

1 file changed

Lines changed: 37 additions & 39 deletions

File tree

Microsoft.Azure.Cosmos.Encryption.Custom/src/Transformation/StreamProcessor.Decryptor.cs

Lines changed: 37 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -122,39 +122,36 @@ internal async Task<DecryptionContext> DecryptStreamAsync(
122122
// increase ArrayPool hit rate. Without bucketing we might rent many subtly different lengths
123123
// (e.g., 1372, 1419, 1510 ...) that the shared pool keeps as distinct buckets, increasing LOH
124124
// pressure and fragmentation over time when large docs with varying token sizes are processed.
125-
static void EnsureCapacity(ref byte[] scratch, int needed, ArrayPoolManager pool)
125+
// Shared bucket helper so main streaming buffer growth also reuses the same discrete sizes.
126+
static int Bucket(int value)
126127
{
127-
if (scratch != null && scratch.Length >= needed)
128+
const int minBucket = 64;
129+
if (value <= minBucket)
128130
{
129-
return; // already large enough
131+
return minBucket;
130132
}
131133

132-
// Round to next power-of-two up to MaxBufferSizeBytes to maximize reuse while capping growth.
133-
// Minimum practical bucket of 64 bytes (caller often already does Math.Max(x, 64)).
134-
static int Bucket(int value)
134+
if (value >= MaxBufferSizeBytes)
135135
{
136-
const int minBucket = 64;
137-
if (value <= minBucket)
138-
{
139-
return minBucket;
140-
}
136+
return MaxBufferSizeBytes;
137+
}
141138

142-
if (value >= MaxBufferSizeBytes)
143-
{
144-
return MaxBufferSizeBytes;
145-
}
139+
// next power of two
140+
uint v = (uint)(value - 1);
141+
v |= v >> 1;
142+
v |= v >> 2;
143+
v |= v >> 4;
144+
v |= v >> 8;
145+
v |= v >> 16;
146+
int pow2 = (int)(v + 1);
147+
return pow2 > MaxBufferSizeBytes ? MaxBufferSizeBytes : pow2;
148+
}
146149

147-
// next power of two
148-
uint v = (uint)(value - 1);
149-
v |= v >> 1;
150-
v |= v >> 2;
151-
v |= v >> 4;
152-
v |= v >> 8;
153-
v |= v >> 16;
154-
int pow2 = (int)(v + 1);
155-
156-
// safeguard (should not exceed MaxBufferSizeBytes due to earlier check)
157-
return pow2 > MaxBufferSizeBytes ? MaxBufferSizeBytes : pow2;
150+
static void EnsureCapacity(ref byte[] scratch, int needed, ArrayPoolManager pool)
151+
{
152+
if (scratch != null && scratch.Length >= needed)
153+
{
154+
return; // already large enough
158155
}
159156

160157
int bucketed = Bucket(needed);
@@ -220,12 +217,12 @@ static string PathLabel(string a, string b)
220217
}
221218
else
222219
{
223-
// Multi-segment or escaped; use stackalloc for small sizes to avoid renting
224-
const int stackThreshold = 256;
225-
if (srcLen <= stackThreshold)
220+
// Multi-segment or escaped; use stackalloc for small sizes (up to 4 KB) to avoid renting
221+
const int base64StackThreshold = 4096;
222+
if (srcLen <= base64StackThreshold)
226223
{
227-
Span<byte> local = stackalloc byte[stackThreshold];
228-
int copied = reader.CopyString(local);
224+
Span<byte> local = stackalloc byte[srcLen];
225+
int copied = reader.CopyString(local); // copied should == srcLen (escaped path may differ)
229226
OperationStatus status = Base64.DecodeFromUtf8(
230227
local[..copied],
231228
cipher,
@@ -782,19 +779,22 @@ long TransformDecryptBuffer(
782779
}
783780

784781
// Small-payload fast path: if stream is seekable and remaining length <= 2KB, read once and parse once.
782+
// Enhancement: stackalloc for <=1KB to avoid renting; for >1KB use bucketed pool sizes to reduce distinct lengths.
785783
if (inputStream.CanSeek)
786784
{
787785
long remaining = inputStream.Length - inputStream.Position;
788786
if (remaining > 0 && remaining <= 2048)
789787
{
790788
int len = (int)remaining;
791-
byte[] oneShot = arrayPoolManager.Rent(len);
789+
int rentSize = Bucket(len); // bucketed size for small one-shot payload
790+
byte[] oneShot = arrayPoolManager.Rent(rentSize);
792791
try
793792
{
794793
int total = 0;
795794
while (total < len)
796795
{
797-
int r = await inputStream.ReadAsync(oneShot.AsMemory(total, len - total), cancellationToken);
796+
int toRead = Math.Min(len - total, oneShot.Length - total);
797+
int r = await inputStream.ReadAsync(oneShot.AsMemory(total, toRead), cancellationToken);
798798
if (r == 0)
799799
{
800800
break;
@@ -803,10 +803,8 @@ long TransformDecryptBuffer(
803803
total += r;
804804
}
805805

806-
int read = total;
806+
int read = Math.Min(total, len);
807807
bytesRead += read;
808-
809-
// Process the full payload in one pass and mark final block to skip the streaming loop.
810808
_ = TransformDecryptBuffer(
811809
oneShot.AsSpan(0, read),
812810
ref state,
@@ -824,7 +822,6 @@ long TransformDecryptBuffer(
824822
ref skippingEi,
825823
ref skipEiFirstTokenPending,
826824
ref skipEiContainerDepth);
827-
828825
isFinalBlock = true;
829826
}
830827
finally
@@ -837,7 +834,7 @@ long TransformDecryptBuffer(
837834
{
838835
// Non-seekable small payload probe: optimistically read up to 2KB once; if stream ends, we finish in a single pass.
839836
const int ProbeSize = 2048;
840-
buffer = arrayPoolManager.Rent(ProbeSize); // reuse as main buffer if more data follows
837+
buffer = arrayPoolManager.Rent(Bucket(ProbeSize)); // bucketed
841838
int read = await inputStream.ReadAsync(buffer.AsMemory(0, ProbeSize), cancellationToken);
842839
bytesRead += read;
843840
if (read > 0)
@@ -906,7 +903,8 @@ long TransformDecryptBuffer(
906903
if (dataSize > 0 && leftOver == dataSize)
907904
{
908905
int target = Math.Max(buffer.Length * 2, leftOver + BufferGrowthMinIncrement);
909-
int capped = Math.Min(MaxBufferSizeBytes, target);
906+
int bucketedGrow = Bucket(target);
907+
int capped = Math.Min(MaxBufferSizeBytes, bucketedGrow);
910908
if (buffer.Length >= capped)
911909
{
912910
throw new InvalidOperationException(

0 commit comments

Comments
 (0)