Skip to content

Commit

Permalink
Lucene.Net.Search.ReferenceContext: Converted from a sealed class to …
Browse files Browse the repository at this point in the history
…a ref struct to eliminate the heap allocation. Converted TestControlledRealTimeReopenThread.TestStraightForwardDemonstration() to test using ReferenceContext<T> to verify functionality. See #920.
  • Loading branch information
NightOwl888 committed Mar 10, 2024
1 parent e08a342 commit 4356997
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 30 deletions.
88 changes: 66 additions & 22 deletions src/Lucene.Net.Tests/Search/TestControlledRealTimeReopenThread.cs
Original file line number Diff line number Diff line change
Expand Up @@ -770,15 +770,36 @@ public void TestStraightForwardDemonstration()
controlledRealTimeReopenThread.Start();

//An indexSearcher only sees Doc1
IndexSearcher indexSearcher = searcherManager.Acquire();
try

// In Java, to obtain a threadsafe IndexSearcher reference, the following pattern could
// be used. This also works in .NET.
//IndexSearcher indexSearcher = searcherManager.Acquire();
//try
//{
// TopDocs topDocs = indexSearcher.Search(new MatchAllDocsQuery(), 1);
// assertEquals(1, topDocs.TotalHits); //There is only one doc
//}
//finally
//{
// searcherManager.Release(indexSearcher);
//}

// However, in .NET it can be done like this with less code. We get an instance of
// ReferenceContext<IndexSearcher> in a using block so the call to searcherManager.Release()
// happens implicitly. ReferenceContext<IndexSearcher> is a ref struct so it doesn't allocate
// on the heap and will be deallocated at the end of this block automatically.
using (var context = searcherManager.GetContext())
{
IndexSearcher indexSearcher = context.Reference;
TopDocs topDocs = indexSearcher.Search(new MatchAllDocsQuery(), 1);
assertEquals(1, topDocs.TotalHits); //There is only one doc
}
finally

using (var context = searcherManager.GetContext())
{
searcherManager.Release(indexSearcher);
IndexSearcher indexSearcher = context.Reference;
TopDocs topDocs = indexSearcher.Search(new MatchAllDocsQuery(), 1);
assertEquals(1, topDocs.TotalHits); //There is only one doc
}

//Add a 2nd document
Expand All @@ -789,32 +810,42 @@ public void TestStraightForwardDemonstration()

//Demonstrate that we can only see the first doc because we haven't
//waited 1 sec or called WaitForGeneration
indexSearcher = searcherManager.Acquire();
try

// In Java, to obtain a threadsafe IndexSearcher reference, the following pattern could
// be used. This also works in .NET.
//indexSearcher = searcherManager.Acquire();
//try
//{
// TopDocs topDocs = indexSearcher.Search(new MatchAllDocsQuery(), 1);
// assertEquals(1, topDocs.TotalHits); //Can see both docs due to auto refresh after 1.1 secs
//}
//finally
//{
// searcherManager.Release(indexSearcher);
//}

// However, in .NET it can be done like this with less code. We get an instance of
// ReferenceContext<IndexSearcher> in a using block so the call to searcherManager.Release()
// happens implicitly. ReferenceContext<IndexSearcher> is a ref struct so it doesn't allocate
// on the heap and will be deallocated at the end of this block automatically.
using (var context = searcherManager.GetContext())
{
IndexSearcher indexSearcher = context.Reference;
TopDocs topDocs = indexSearcher.Search(new MatchAllDocsQuery(), 1);
assertEquals(1, topDocs.TotalHits); //Can see both docs due to auto refresh after 1.1 secs
}
finally
{
searcherManager.Release(indexSearcher);
}


//Demonstrate that we can see both docs after we wait a little more
//then 1 sec so that controlledRealTimeReopenThread max interval is exceeded
//and it calls MaybeRefresh
Thread.Sleep(1100); //wait 1.1 secs as ms
indexSearcher = searcherManager.Acquire();
try
using (var context = searcherManager.GetContext())
{
IndexSearcher indexSearcher = context.Reference;
TopDocs topDocs = indexSearcher.Search(new MatchAllDocsQuery(), 1);
assertEquals(2, topDocs.TotalHits); //Can see both docs due to auto refresh after 1.1 secs
}
finally
{
searcherManager.Release(indexSearcher);
}


//Add a 3rd document
Expand All @@ -831,16 +862,29 @@ public void TestStraightForwardDemonstration()
stopwatch.Stop();
assertTrue(stopwatch.Elapsed.TotalMilliseconds <= 200 + 30); //30ms is fudged factor to account for call overhead.

indexSearcher = searcherManager.Acquire();
try
// In Java, to obtain a threadsafe IndexSearcher reference, the following pattern could
// be used. This also works in .NET.
//indexSearcher = searcherManager.Acquire();
//try
//{
// TopDocs topDocs = indexSearcher.Search(new MatchAllDocsQuery(), 1);
// assertEquals(3, topDocs.TotalHits); //Can see both docs due to auto refresh after 1.1 secs
//}
//finally
//{
// searcherManager.Release(indexSearcher);
//}

// However, in .NET it can be done like this with less code. We get an instance of
// ReferenceContext<IndexSearcher> in a using block so the call to searcherManager.Release()
// happens implicitly. ReferenceContext<IndexSearcher> is a ref struct so it doesn't allocate
// on the heap and will be deallocated at the end of this block automatically.
using (var context = searcherManager.GetContext())
{
IndexSearcher indexSearcher = context.Reference;
TopDocs topDocs = indexSearcher.Search(new MatchAllDocsQuery(), 1);
assertEquals(3, topDocs.TotalHits); //Can see both docs due to auto refresh after 1.1 secs
}
finally
{
searcherManager.Release(indexSearcher);
}

controlledRealTimeReopenThread.Dispose();
searcherManager.Dispose();
Expand Down
22 changes: 14 additions & 8 deletions src/Lucene.Net/Support/Search/ReferenceContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Diagnostics.CodeAnalysis;
#nullable enable

namespace Lucene.Net.Search
{
Expand All @@ -22,28 +24,32 @@ namespace Lucene.Net.Search
/// <summary>
/// <see cref="ReferenceContext{T}"/> holds a reference instance and
/// ensures it is properly de-referenced from its corresponding <see cref="ReferenceManager{G}"/>
/// when <see cref="Dispose()"/> is called. This class is primarily intended
/// to be used with a using block.
/// when <see cref="Dispose()"/> is called. This struct is intended
/// to be used with a using block to simplify releasing a reference
/// such as a <see cref="SearcherManager"/> instance.
/// <para/>
/// LUCENENET specific
/// </summary>
/// <typeparam name="T">The reference type</typeparam>
public sealed class ReferenceContext<T> : IDisposable
/// <typeparam name="T">The reference type.</typeparam>
public ref struct ReferenceContext<T>
where T : class
{
private readonly ReferenceManager<T> referenceManager;
private T reference;
[SuppressMessage("CodeQuality", "IDE0079:Remove unnecessary suppression", Justification = "This is a SonarCloud issue")]
[SuppressMessage("Major Code Smell", "S2933:Fields that are only assigned in the constructor should be \"readonly\"", Justification = "Structs are known to have performance issues with readonly fields")]
[SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "Structs are known to have performance issues with readonly fields")]
private ReferenceManager<T> referenceManager;
private T? reference;

internal ReferenceContext(ReferenceManager<T> referenceManager)
{
this.referenceManager = referenceManager;
this.referenceManager = referenceManager ?? throw new ArgumentNullException(nameof(referenceManager));
this.reference = referenceManager.Acquire();
}

/// <summary>
/// The reference acquired from the <see cref="ReferenceManager{G}"/>.
/// </summary>
public T Reference => reference;
public readonly T Reference => reference ?? throw new ObjectDisposedException(nameof(ReferenceContext<T>));

/// <summary>
/// Ensures the reference is properly de-referenced from its <see cref="ReferenceManager{G}"/>.
Expand Down

0 comments on commit 4356997

Please sign in to comment.