Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 43 additions & 2 deletions LiteDB.Tests/Document/ObjectId_Tests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using FluentAssertions;
using System;
using FluentAssertions;
using LiteDB;
using Xunit;

namespace LiteDB.Tests.Document
Expand Down Expand Up @@ -41,5 +43,44 @@ public void ObjectId_Equals_Null_Does_Not_Throw()
oid1.Equals(null).Should().BeFalse();
oid1.Equals(oid0).Should().BeFalse();
}

[Fact]
public void ObjectId_ToString_Minimizes_Allocations()
{
var objectId = ObjectId.NewObjectId();

objectId.ToString();

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

var before = GC.GetAllocatedBytesForCurrentThread();
var hex = objectId.ToString();
var allocated = GC.GetAllocatedBytesForCurrentThread() - before;

hex.Should().HaveLength(24);
allocated.Should().BeLessThan(220);
}

[Fact]
public void ObjectId_FromHex_Minimizes_Allocations()
{
var original = ObjectId.NewObjectId();
var hex = original.ToString();

_ = new ObjectId(hex);

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

var before = GC.GetAllocatedBytesForCurrentThread();
var parsed = new ObjectId(hex);
var allocated = GC.GetAllocatedBytesForCurrentThread() - before;

parsed.Should().Be(original);
allocated.Should().BeLessThan(220);
}
}
}
}
142 changes: 120 additions & 22 deletions LiteDB/Document/ObjectId.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Security;
Expand Down Expand Up @@ -120,22 +121,30 @@ public ObjectId(byte[] bytes, int startIndex = 0)
bytes[startIndex + 11];
}

private const int ObjectIdByteLength = 12;
private const int ObjectIdStringLength = ObjectIdByteLength * 2;

/// <summary>
/// Convert hex value string in byte array
/// </summary>
private static byte[] FromHex(string value)
{
if (string.IsNullOrEmpty(value)) throw new ArgumentNullException(nameof(value));
if (value.Length != 24) throw new ArgumentException(string.Format("ObjectId strings should be 24 hex characters, got {0} : \"{1}\"", value.Length, value));
if (value.Length != ObjectIdStringLength) throw new ArgumentException(string.Format("ObjectId strings should be 24 hex characters, got {0} : \"{1}\"", value.Length, value));

var bytes = new byte[12];
var hex = value.AsSpan();

for (var i = 0; i < 24; i += 2)
{
bytes[i / 2] = Convert.ToByte(value.Substring(i, 2), 16);
}
#if NET8_0_OR_GREATER
return Convert.FromHexString(hex);
#else
Span<byte> buffer = stackalloc byte[ObjectIdByteLength];
WriteBytesFromHex(hex, buffer);

return bytes;
var result = new byte[ObjectIdByteLength];
buffer.CopyTo(result);

return result;
#endif
}

#endregion
Expand Down Expand Up @@ -199,34 +208,123 @@ public int CompareTo(ObjectId other)
/// </summary>
public void ToByteArray(byte[] bytes, int startIndex)
{
bytes[startIndex + 0] = (byte)(this.Timestamp >> 24);
bytes[startIndex + 1] = (byte)(this.Timestamp >> 16);
bytes[startIndex + 2] = (byte)(this.Timestamp >> 8);
bytes[startIndex + 3] = (byte)(this.Timestamp);
bytes[startIndex + 4] = (byte)(this.Machine >> 16);
bytes[startIndex + 5] = (byte)(this.Machine >> 8);
bytes[startIndex + 6] = (byte)(this.Machine);
bytes[startIndex + 7] = (byte)(this.Pid >> 8);
bytes[startIndex + 8] = (byte)(this.Pid);
bytes[startIndex + 9] = (byte)(this.Increment >> 16);
bytes[startIndex + 10] = (byte)(this.Increment >> 8);
bytes[startIndex + 11] = (byte)(this.Increment);
this.WriteBytes(bytes.AsSpan(startIndex, ObjectIdByteLength));
}

public byte[] ToByteArray()
{
var bytes = new byte[12];
var bytes = new byte[ObjectIdByteLength];

this.ToByteArray(bytes, 0);
this.WriteBytes(bytes);

return bytes;
}

public override string ToString()
{
return BitConverter.ToString(this.ToByteArray()).Replace("-", "").ToLower();
#if NET8_0_OR_GREATER
Span<byte> buffer = stackalloc byte[ObjectIdByteLength];

this.WriteBytes(buffer);

return Convert.ToHexString(buffer).ToLowerInvariant();
#else
Span<byte> buffer = stackalloc byte[ObjectIdByteLength];

this.WriteBytes(buffer);

var rented = ArrayPool<char>.Shared.Rent(ObjectIdStringLength);

try
{
var chars = rented.AsSpan(0, ObjectIdStringLength);
WriteHexLower(buffer, chars);

return new string(rented, 0, ObjectIdStringLength);
}
finally
{
ArrayPool<char>.Shared.Return(rented);
}
#endif
}

private void WriteBytes(Span<byte> destination)
{
if (destination.Length < ObjectIdByteLength)
{
throw new ArgumentException("Destination span is too short.", nameof(destination));
}

destination[0] = (byte)(this.Timestamp >> 24);
destination[1] = (byte)(this.Timestamp >> 16);
destination[2] = (byte)(this.Timestamp >> 8);
destination[3] = (byte)(this.Timestamp);
destination[4] = (byte)(this.Machine >> 16);
destination[5] = (byte)(this.Machine >> 8);
destination[6] = (byte)(this.Machine);
destination[7] = (byte)(this.Pid >> 8);
destination[8] = (byte)(this.Pid);
destination[9] = (byte)(this.Increment >> 16);
destination[10] = (byte)(this.Increment >> 8);
destination[11] = (byte)(this.Increment);
}

#if !NET8_0_OR_GREATER
private static void WriteHexLower(ReadOnlySpan<byte> source, Span<char> destination)
{
if (destination.Length < ObjectIdStringLength)
{
throw new ArgumentException("Destination span is too short.", nameof(destination));
}

for (var i = 0; i < source.Length; i++)
{
var value = source[i];
destination[i * 2] = GetHexCharacter(value >> 4);
destination[i * 2 + 1] = GetHexCharacter(value & 0x0F);
}
}

private static char GetHexCharacter(int value)
{
return (char)(value < 10 ? '0' + value : 'a' + (value - 10));
}

private static void WriteBytesFromHex(ReadOnlySpan<char> hex, Span<byte> destination)
{
if (destination.Length < ObjectIdByteLength)
{
throw new ArgumentException("Destination span is too short.", nameof(destination));
}

for (var i = 0; i < destination.Length; i++)
{
var high = ParseHexDigit(hex[i * 2]);
var low = ParseHexDigit(hex[i * 2 + 1]);

destination[i] = (byte)((high << 4) | low);
}
}

private static int ParseHexDigit(char c)
{
if ((uint)(c - '0') <= 9)
{
return c - '0';
}

var lowered = (char)(c | 0x20);

if ((uint)(lowered - 'a') <= 5)
{
return lowered - 'a' + 10;
}

throw new FormatException(string.Format("Invalid hex character '{0}' in ObjectId.", c));
}
#endif

#endregion

#region Operators
Expand Down
Loading