Skip to content
66 changes: 61 additions & 5 deletions MaxMind.Db.Benchmark/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,23 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Text;

BenchmarkRunner.Run<CityBenchmark>();

[MemoryDiagnoser]
public class CityBenchmark
{
// A random IP that has city info.
private Reader _reader = null!;
private Reader _memMapReader = null!;

private Reader _arrayBufferCachedReader = null!;
private Reader _memMapCachedReader = null!;
private Reader _arrayBufferReader = null!;

private IPAddress[] _ipAddresses = [];

[GlobalSetup]
Expand All @@ -22,7 +29,10 @@ public void GlobalSetup()
const string dbPathVarName = "MAXMIND_BENCHMARK_DB";
string dbPath = Environment.GetEnvironmentVariable(dbPathVarName) ??
throw new InvalidOperationException($"{dbPathVarName} was not set");
_reader = new Reader(dbPath);
_memMapReader = new Reader(dbPath, FileAccessMode.MemoryMapped);
_memMapCachedReader = new Reader(dbPath, FileAccessMode.MemoryMapped, 4_096);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may make sense to have a separate PR for improvements to the benchmark tool, if they are unrelated to the PR. I realize in a prior version the changes were more relevant as you were exercising new parameters to the reader.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There will still need to a be change to exercise the new parameters, but it will make the pr easier to review. Thank you

_arrayBufferReader = new Reader(dbPath, FileAccessMode.Memory);
_arrayBufferCachedReader = new Reader(dbPath, FileAccessMode.Memory, 4_096);

const string ipAddressesVarName = "MAXMIND_BENCHMARK_IP_ADDRESSES";
string ipAddressesStr = Environment.GetEnvironmentVariable(ipAddressesVarName) ?? "";
Expand All @@ -46,23 +56,69 @@ public void GlobalSetup()
[GlobalCleanup]
public void GlobalCleanup()
{
_reader.Dispose();
_memMapReader.Dispose();
}

[Benchmark]
public int CityMemoryMappedLookup()
{
int x = 0;
foreach (var ipAddress in _ipAddresses)
{
if (_memMapReader.Find<CityResponse>(ipAddress) != null)
{
x += 1;
}
}

return x;
}

[Benchmark]
public int CityMemoryMappedCachedLookup()
{
int x = 0;
foreach (var ipAddress in _ipAddresses)
{
if (_memMapCachedReader.Find<CityResponse>(ipAddress) != null)
{
x += 1;
}
}

return x;
}

[Benchmark]
public int CityMemoryLookup()
{
int x = 0;
foreach (var ipAddress in _ipAddresses)
{
if (_arrayBufferReader.Find<CityResponse>(ipAddress) != null)
{
x += 1;
}
}

return x;
}

[Benchmark]
public int City()
public int CityMemoryCachedLookup()
{
int x = 0;
foreach (var ipAddress in _ipAddresses)
{
if (_reader.Find<CityResponse>(ipAddress) != null)
if (_arrayBufferCachedReader?.Find<CityResponse>(ipAddress) != null)
{
x += 1;
}
}

return x;
}

}

public abstract class AbstractCountryResponse
Expand Down
16 changes: 16 additions & 0 deletions MaxMind.Db/AllocatorDelegates.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;

namespace MaxMind.Db;

/// <summary>
/// Delegate methods for allocations
/// </summary>
public static class AllocatorDelegates
Comment thread
jashook marked this conversation as resolved.
{
/// <summary>
/// Allocate a string based on a sequence of bytes
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public delegate string GetString(ReadOnlyMemory<byte> bytes);
}
114 changes: 110 additions & 4 deletions MaxMind.Db/Decoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#endif
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Numerics;

#endregion
Expand Down Expand Up @@ -47,21 +48,24 @@

private readonly DictionaryActivatorCreator _dictionaryActivatorCreator;
private readonly ListActivatorCreator _listActivatorCreator;
private readonly SNECache? _cache;

/// <summary>
/// Initializes a new instance of the <see cref="Decoder" /> class.
/// </summary>
/// <param name="database">The database.</param>
/// <param name="pointerBase">The base address in the stream.</param>
/// <param name="followPointers">Whether to follow pointers. For testing.</param>
internal Decoder(MemoryMapBuffer database, long pointerBase, bool followPointers = true)
/// <param name="defaultCacheSize">Whether to use a cache or not.</param>
internal Decoder(Buffer database, long pointerBase, bool followPointers = true, int? defaultCacheSize = null)

Check failure on line 60 in MaxMind.Db/Decoder.cs

View workflow job for this annotation

GitHub Actions / Dotnet on ubuntu-latest

'Buffer': static types cannot be used as parameters

Check failure on line 60 in MaxMind.Db/Decoder.cs

View workflow job for this annotation

GitHub Actions / Dotnet on ubuntu-latest

'Buffer': static types cannot be used as parameters

Check failure on line 60 in MaxMind.Db/Decoder.cs

View workflow job for this annotation

GitHub Actions / Dotnet on ubuntu-latest

'Buffer': static types cannot be used as parameters

Check failure on line 60 in MaxMind.Db/Decoder.cs

View workflow job for this annotation

GitHub Actions / build

'Buffer': static types cannot be used as parameters

Check failure on line 60 in MaxMind.Db/Decoder.cs

View workflow job for this annotation

GitHub Actions / build

'Buffer': static types cannot be used as parameters

Check failure on line 60 in MaxMind.Db/Decoder.cs

View workflow job for this annotation

GitHub Actions / build

'Buffer': static types cannot be used as parameters

Check failure on line 60 in MaxMind.Db/Decoder.cs

View workflow job for this annotation

GitHub Actions / CodeQL-Build

'Buffer': static types cannot be used as parameters

Check failure on line 60 in MaxMind.Db/Decoder.cs

View workflow job for this annotation

GitHub Actions / CodeQL-Build

'Buffer': static types cannot be used as parameters

Check failure on line 60 in MaxMind.Db/Decoder.cs

View workflow job for this annotation

GitHub Actions / CodeQL-Build

'Buffer': static types cannot be used as parameters
{
_pointerBase = pointerBase;
_database = database;
_followPointers = followPointers;
_listActivatorCreator = new ListActivatorCreator();
_dictionaryActivatorCreator = new DictionaryActivatorCreator();
_typeActivatorCreator = new TypeActivatorCreator();
_cache = defaultCacheSize is null ? null : new (defaultCacheSize.Value);
}

/// <summary>
Expand All @@ -81,10 +85,19 @@
return decoded;
}

/// <summary>
/// Get cache size
/// </summary>
/// <returns></returns>
internal int CacheSize()
{
return _cache is null ? 0 : _cache.Size();
}

private object Decode(Type expectedType, long offset, out long outOffset, InjectableValues? injectables = null, Network? network = null)
{
var type = CtrlData(offset, out var size, out offset);
return DecodeByType(expectedType, type, offset, size, out outOffset, injectables, network);
return DecodeByTypeFromCacheOrCreate(expectedType, type, offset, size, out outOffset, injectables, network);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should also have a test looking up different IPs that resolve to the same offset where we use Inject. It looks like we may cache the first IP for example if we inject IP, which doesn't seem desirable. Same issue with Network.

}

private ObjectType CtrlData(long offset, out int size, out long outOffset)
Expand Down Expand Up @@ -145,6 +158,38 @@
/// <param name="network"></param>
/// <returns></returns>
/// <exception cref="Exception">Unable to handle type!</exception>
private object DecodeByTypeFromCacheOrCreate(
Type expectedType,
ObjectType type,
long offset,
int size,
out long outOffset,
InjectableValues? injectables,
Network? network
)
{
ValueTuple<object, long> returnValue = DecodeFromCacheOrCreate(offset, size, expectedType, type, injectables, network, static (Buffer database, long offset, int size, Type type, ObjectType objectType, Decoder decoder, InjectableValues? injectableValues, Network? network) =>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it would be simpler if we only cached by data section offset / pointer. You can see we actually have caching by that already for when iterating the tree in FindAll():

if (!dataCache.TryGetValue(node.Pointer, out var data))

We cache by that in MaxMind-DB-Reader-java as well.

It may make sense to extend that caching too so we don't have two kinds of caches. I didn't realize we had that cache when you first made your PR.

{
long returnOffset = 0;
return (decoder.DecodeByType(type, objectType, offset, size, out returnOffset, injectableValues, network), returnOffset);
});

outOffset = returnValue.Item2;
return returnValue.Item1;
}

/// <summary>
/// Decodes the type of the by.
/// </summary>
/// <param name="expectedType"></param>
/// <param name="type">The type.</param>
/// <param name="offset">The offset.</param>
/// <param name="size">The size.</param>
/// <param name="outOffset">The out offset</param>
/// <param name="injectables"></param>
/// <param name="network"></param>
/// <returns></returns>
/// <exception cref="Exception">Unable to handle type!</exception>
private object DecodeByType(
Type expectedType,
ObjectType type,
Expand Down Expand Up @@ -282,14 +327,20 @@
{
ReflectionUtil.CheckType(expectedType, typeof(string));

return _database.ReadString(offset, size);
return (string)DecodeFromCacheOrCreate(offset, size, expectedType, static (Buffer database, long offset, int size) =>
{
return (database.ReadString(offset, size), offset + size);
}).Item1;
}

private byte[] DecodeBytes(Type expectedType, long offset, int size)
{
ReflectionUtil.CheckType(expectedType, typeof(byte[]));

return _database.Read(offset, size);
return (byte[])DecodeFromCacheOrCreate(offset, size, expectedType, static (Buffer database, long offset, int size) =>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These lower level DecodeFromCacheOrCreate calls seem to cause us to bump size a second time. Once here, and once in the call to DecodeByType.

{
return (database.Read(offset, size), offset + size);
}).Item1;
}

/// <summary>
Expand Down Expand Up @@ -662,5 +713,60 @@

return _database.ReadVarInt(offset, size);
}

private (object, long) DecodeFromCacheOrCreate(long offset, int size, Type type, Func<Buffer, long, int, (object, long)> factory)

Check failure on line 717 in MaxMind.Db/Decoder.cs

View workflow job for this annotation

GitHub Actions / Dotnet on ubuntu-latest

'Buffer': static types cannot be used as type arguments

Check failure on line 717 in MaxMind.Db/Decoder.cs

View workflow job for this annotation

GitHub Actions / Dotnet on ubuntu-latest

'Buffer': static types cannot be used as type arguments

Check failure on line 717 in MaxMind.Db/Decoder.cs

View workflow job for this annotation

GitHub Actions / Dotnet on ubuntu-latest

'Buffer': static types cannot be used as type arguments

Check failure on line 717 in MaxMind.Db/Decoder.cs

View workflow job for this annotation

GitHub Actions / build

'Buffer': static types cannot be used as type arguments

Check failure on line 717 in MaxMind.Db/Decoder.cs

View workflow job for this annotation

GitHub Actions / build

'Buffer': static types cannot be used as type arguments

Check failure on line 717 in MaxMind.Db/Decoder.cs

View workflow job for this annotation

GitHub Actions / CodeQL-Build

'Buffer': static types cannot be used as type arguments

Check failure on line 717 in MaxMind.Db/Decoder.cs

View workflow job for this annotation

GitHub Actions / CodeQL-Build

'Buffer': static types cannot be used as type arguments
{
if (_cache is not null)
{
ValueTuple<object, long> item;
bool found = _cache.TryGet(offset, size, type, out item);
if (found)
{
// Not null if found.
Debug.Assert(item.Item1 is not null);
return item;
}
else
{
item = factory(_database, offset, size);
bool added = _cache.TryAdd(offset, size, type, item);
}

return item;
}

return factory(_database, offset, size);
}

private (object, long) DecodeFromCacheOrCreate(
long offset,
int size,
Type type,
ObjectType objectType,
InjectableValues? injectableValues,
Network? network,
Func<Buffer, long, int, Type, ObjectType, Decoder, InjectableValues?, Network?, (object, long)> factory)

Check failure on line 748 in MaxMind.Db/Decoder.cs

View workflow job for this annotation

GitHub Actions / Dotnet on ubuntu-latest

'Buffer': static types cannot be used as type arguments

Check failure on line 748 in MaxMind.Db/Decoder.cs

View workflow job for this annotation

GitHub Actions / Dotnet on ubuntu-latest

'Buffer': static types cannot be used as type arguments

Check failure on line 748 in MaxMind.Db/Decoder.cs

View workflow job for this annotation

GitHub Actions / build

'Buffer': static types cannot be used as type arguments

Check failure on line 748 in MaxMind.Db/Decoder.cs

View workflow job for this annotation

GitHub Actions / build

'Buffer': static types cannot be used as type arguments

Check failure on line 748 in MaxMind.Db/Decoder.cs

View workflow job for this annotation

GitHub Actions / CodeQL-Build

'Buffer': static types cannot be used as type arguments

Check failure on line 748 in MaxMind.Db/Decoder.cs

View workflow job for this annotation

GitHub Actions / CodeQL-Build

'Buffer': static types cannot be used as type arguments
{
if (_cache is not null)
{
ValueTuple<object, long> item;
bool found = _cache.TryGet(offset, size, type, out item);
if (found)
{
// Not null if found.
Debug.Assert(item.Item1 is not null);
return item;
}
else
{
item = factory(_database, offset, size, type, objectType, this, injectableValues, network);
bool added = _cache.TryAdd(offset, size, type, item);
}

return item;
}

return factory(_database, offset, size, type, objectType, this, injectableValues, network);
}
}
}
3 changes: 3 additions & 0 deletions MaxMind.Db/MaxMind.Db.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,7 @@
</None>
</ItemGroup>

<ItemGroup>
<PackageReference Include="System.Memory" Version="4.5.5" />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this?

</ItemGroup>
</Project>
30 changes: 26 additions & 4 deletions MaxMind.Db/Reader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@
/// Initializes a new instance of the <see cref="Reader" /> class.
/// </summary>
/// <param name="file">The file.</param>
public Reader(string file) : this(file, FileAccessMode.MemoryMapped)
public Reader(string file)
: this(file, FileAccessMode.MemoryMapped)
{
}

Expand All @@ -119,7 +120,19 @@
/// </summary>
/// <param name="file">The MaxMind DB file.</param>
/// <param name="mode">The mode by which to access the DB file.</param>
public Reader(string file, FileAccessMode mode) : this(BufferForMode(file, mode), file)
public Reader(string file, FileAccessMode mode)
: this(BufferForMode(file, mode), file)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="Reader" /> class.
/// </summary>
/// <param name="file">The MaxMind DB file.</param>
/// <param name="mode">The mode by which to access the DB file.</param>
/// <param name="cacheSize">Cache size for optional internal cache</param>
public Reader(string file, FileAccessMode mode, int cacheSize)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like an improvement in terms of not breaking the API. However I wonder if we would want to provide some way to swap out the cache implementation. I'm not sure and not sure what it would best look like if so.

: this(BufferForMode(file, mode), file, cacheSize)
{
}

Expand All @@ -136,7 +149,7 @@
{
}

private Reader(MemoryMapBuffer buffer, string? file)
private Reader(Buffer buffer, string? file, int? cacheSize = null)

Check failure on line 152 in MaxMind.Db/Reader.cs

View workflow job for this annotation

GitHub Actions / Dotnet on ubuntu-latest

'Buffer': static types cannot be used as parameters

Check failure on line 152 in MaxMind.Db/Reader.cs

View workflow job for this annotation

GitHub Actions / Dotnet on ubuntu-latest

'Buffer': static types cannot be used as parameters

Check failure on line 152 in MaxMind.Db/Reader.cs

View workflow job for this annotation

GitHub Actions / build

'Buffer': static types cannot be used as parameters

Check failure on line 152 in MaxMind.Db/Reader.cs

View workflow job for this annotation

GitHub Actions / build

'Buffer': static types cannot be used as parameters

Check failure on line 152 in MaxMind.Db/Reader.cs

View workflow job for this annotation

GitHub Actions / build

'Buffer': static types cannot be used as parameters

Check failure on line 152 in MaxMind.Db/Reader.cs

View workflow job for this annotation

GitHub Actions / CodeQL-Build

'Buffer': static types cannot be used as parameters

Check failure on line 152 in MaxMind.Db/Reader.cs

View workflow job for this annotation

GitHub Actions / CodeQL-Build

'Buffer': static types cannot be used as parameters

Check failure on line 152 in MaxMind.Db/Reader.cs

View workflow job for this annotation

GitHub Actions / CodeQL-Build

'Buffer': static types cannot be used as parameters
{
_fileName = file;
_database = buffer;
Expand All @@ -148,7 +161,7 @@
_nodeByteSize = Metadata.NodeByteSize;
_nodeCount = Metadata.NodeCount;
_recordSize = Metadata.RecordSize;
Decoder = new Decoder(_database, Metadata.SearchTreeSize + DataSectionSeparatorSize);
Decoder = new Decoder(_database, Metadata.SearchTreeSize + DataSectionSeparatorSize, defaultCacheSize: cacheSize);

if (_dbIPVersion == 6)
{
Expand Down Expand Up @@ -227,6 +240,15 @@
_disposed = true;
}

/// <summary>
/// Cache size
/// </summary>
/// <returns></returns>
public int CacheSize()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could leave this out unless it's necessary, to reduce adding to the public API for now.

{
return Decoder.CacheSize();
}

/// <summary>
/// Finds the data related to the specified address.
/// </summary>
Expand Down
Loading
Loading