Skip to content

NH-3037: ActionQueue Insertion sort performance degrades exponentially #67

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
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
10 changes: 10 additions & 0 deletions src/NHibernate.Test/NHSpecificTest/NH3037/Entity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;

namespace NHibernate.Test.NHSpecificTest.NH3037
{
class Entity
{
public virtual long Id { get; set; }
public virtual string Name { get; set; }
}
}
55 changes: 55 additions & 0 deletions src/NHibernate.Test/NHSpecificTest/NH3037/FixtureByCode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;
using System.Diagnostics;
using System.Linq;
using NHibernate.Cfg.MappingSchema;
using NHibernate.Linq;
using NHibernate.Mapping.ByCode;
using NUnit.Framework;

namespace NHibernate.Test.NHSpecificTest.NH3037
{
[TestFixture, Explicit("This is a performance test and may take a while.")]
public class ByCodeFixture : TestCaseMappingByCode
{
protected override HbmMapping GetMappings()
{
var mapper = new ModelMapper();
mapper.Class<Entity>(rc =>
{
rc.Id(x => x.Id, m => m.Generator(Generators.Assigned));
rc.Property(x => x.Name);
});

return mapper.CompileMappingForAllExplicitlyAddedEntities();
}

[TestCase(10)]
[TestCase(100)]
[TestCase(1000)]
[TestCase(10000)]
[TestCase(20000)]
[TestCase(30000)]
[TestCase(40000)]
public void SortInsertionActions(int iterations)
{
using (ISession session = OpenSession())
using (ITransaction transaction = session.BeginTransaction())
{
for (int i = 1; i <= iterations; i++)
{
session.Save(new Entity() { Id = i, Name = i.ToString() });
}

var impl = ((NHibernate.Impl.SessionImpl)session);

var stopwatch = Stopwatch.StartNew();

impl.ActionQueue.SortActions();

stopwatch.Stop();

System.Console.WriteLine(stopwatch.Elapsed);
}
}
}
}
2 changes: 2 additions & 0 deletions src/NHibernate.Test/NHibernate.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,8 @@
<Compile Include="NHSpecificTest\BagWithLazyExtraAndFilter\Domain.cs" />
<Compile Include="NHSpecificTest\BagWithLazyExtraAndFilter\Fixture.cs" />
<Compile Include="Component\Basic\ComponentWithUniqueConstraintTests.cs" />
<Compile Include="NHSpecificTest\NH3037\Entity.cs" />
<Compile Include="NHSpecificTest\NH3037\FixtureByCode.cs" />
<Compile Include="NHSpecificTest\NH2347\Entity.cs" />
<Compile Include="NHSpecificTest\NH2347\Fixture.cs" />
<Compile Include="NHSpecificTest\NH2664\Product.cs" />
Expand Down
203 changes: 126 additions & 77 deletions src/NHibernate/Engine/ActionQueue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -315,82 +315,7 @@ public void SortActions()
//violations
private void SortInsertActions()
{
// IMPLEMENTATION NOTES:
//
// The main data structure in this ordering algorithm is the 'positionToAction'
// map. Essentially this can be thought of as an put-ordered map (the problem with
// actually implementing it that way and doing away with the 'nameList' is that
// we'd end up having potential duplicate key values). 'positionToAction' maintains
// a mapping from a position within the 'nameList' structure to a "partial queue"
// of actions.

Dictionary<int,List<EntityInsertAction>> positionToAction =
new Dictionary<int, List<EntityInsertAction>>();
List<string> nameList = new List<string>();

while (!(insertions.Count == 0))
{
// todo-events : test behaviour
// in Java they use an implicit cast to EntityInsertAction
// but it may be not work because the insertions list may contain EntityIdentityInsertAction
// (I don't like that "goto"too)
object tempObject = insertions[0];
insertions.RemoveAt(0);
EntityInsertAction action = (EntityInsertAction)tempObject;
string thisEntityName = action.EntityName;

// see if we have already encountered this entity-name...
if (!nameList.Contains(thisEntityName))
{
// we have not, so create the proper entries in nameList and positionToAction
List<EntityInsertAction> segmentedActionQueue = new List<EntityInsertAction>();
segmentedActionQueue.Add(action);
nameList.Add(thisEntityName);
positionToAction[nameList.IndexOf(thisEntityName)] = segmentedActionQueue;
}
else
{
// we have seen it before, so we need to determine if this insert action is
// is dependent upon a previously processed action in terms of FK
// relationships (this FK checking is done against the entity's property-state
// associated with the action...)
int lastPos = nameList.LastIndexOf(thisEntityName);
object[] states = action.State;
for (int i = 0; i < states.Length; i++)
{
for (int j = 0; j < nameList.Count; j++)
{
List<EntityInsertAction> tmpList = positionToAction[j];
for (int k = 0; k < tmpList.Count; k++)
{
EntityInsertAction checkAction = tmpList[k];
if (checkAction.Instance == states[i] && j > lastPos)
{
// 'checkAction' is inserting an entity upon which 'action' depends...
// note: this is an assumption and may not be correct in the case of one-to-one
List<EntityInsertAction> segmentedActionQueue = new List<EntityInsertAction>();
segmentedActionQueue.Add(action);
nameList.Add(thisEntityName);
positionToAction[nameList.LastIndexOf(thisEntityName)] = segmentedActionQueue;
goto loopInsertion;
}
}
}
}

List<EntityInsertAction> actionQueue = positionToAction[lastPos];
actionQueue.Add(action);
}
loopInsertion: ;
}

// now iterate back through positionToAction map and move entityInsertAction back to insertion list
for (int p = 0; p < nameList.Count; p++)
{
List<EntityInsertAction> actionQueue = positionToAction[p];
foreach (EntityInsertAction action in actionQueue)
insertions.Add(action);
}
new InsertActionSorter(this).Sort();
}

public IList<EntityDeleteAction> CloneDeletions()
Expand Down Expand Up @@ -575,5 +500,129 @@ public void AfterTransactionCompletion(bool success)
querySpacesToInvalidate.Clear();
}
}
}

[Serializable]
private class InsertActionSorter
{
private ActionQueue actionQueue;

// the mapping of entity names to their latest batch numbers.
private Dictionary<string, int> latestBatches = new Dictionary<string, int>();
private Dictionary<object, int> entityBatchNumber;

// the map of batch numbers to EntityInsertAction lists
private Dictionary<int, List<EntityInsertAction>> actionBatches = new Dictionary<int, List<EntityInsertAction>>();

public InsertActionSorter(ActionQueue actionQueue)
{
this.actionQueue = actionQueue;

//optimize the hash size to eliminate a rehash.
this.entityBatchNumber = new Dictionary<object, int>(actionQueue.insertions.Count + 1);
}

public void Sort()
{
// the list of entity names that indicate the batch number
foreach (EntityInsertAction action in actionQueue.insertions)
{
// remove the current element from insertions. It will be added back later.
string entityName = action.EntityName;

// the entity associated with the current action.
object currentEntity = action.Instance;

int batchNumber;
if (latestBatches.ContainsKey(entityName))
{
// There is already an existing batch for this type of entity.
// Check to see if the latest batch is acceptable.
batchNumber = FindBatchNumber(action, entityName);
}
else
{
// add an entry for this type of entity.
// we can be assured that all referenced entities have already
// been processed,
// so specify that this entity is with the latest batch.
// doing the batch number before adding the name to the list is
// a faster way to get an accurate number.

batchNumber = actionBatches.Count;
latestBatches[entityName] = batchNumber;
}
entityBatchNumber[currentEntity] = batchNumber;
AddToBatch(batchNumber, action);
}
actionQueue.insertions.Clear();

// now rebuild the insertions list. There is a batch for each entry in the name list.
for (int i = 0; i < actionBatches.Count; i++)
{
List<EntityInsertAction> batch = actionBatches[i];
foreach (EntityInsertAction action in batch)
{
actionQueue.insertions.Add(action);
}
}
}

// <summary>
// Finds an acceptable batch for this entity to be a member as part of the <see cref="InsertActionSorter" />
// </summary>
// <param name="action">The action being sorted</param>
// <param name="event">The name of the entity affected by the action</param>
// <returns>An appropriate batch number; todo document this process better</returns>
private int FindBatchNumber(EntityInsertAction action, string entityName)
{
// loop through all the associated entities and make sure they have been
// processed before the latest
// batch associated with this entity type.

// the current batch number is the latest batch for this entity type.
int latestBatchNumberForType = (int)latestBatches[entityName];

// loop through all the associations of the current entity and make sure that they are processed
// before the current batch number
object[] propertyValues = action.State;
Type.IType[] propertyTypes = action.Persister.ClassMetadata.PropertyTypes;

for (int i = 0; i < propertyValues.Length; i++)
{
object value = propertyValues[i];
Type.IType type = propertyTypes[i];

if (type.IsEntityType && value != null)
{
// find the batch number associated with the current association, if any.
int associationBatchNumber;
if (entityBatchNumber.TryGetValue(value, out associationBatchNumber) && associationBatchNumber > latestBatchNumberForType)
{
// create a new batch for this type. The batch number is the number of current batches.
latestBatchNumberForType = actionBatches.Count;
latestBatches[entityName] = latestBatchNumberForType;
// since this entity will now be processed in the latest possible batch,
// we can be assured that it will come after all other associations,
// there's not need to continue checking.
break;
}
}
}
return latestBatchNumberForType;
}

private void AddToBatch(int batchNumber, EntityInsertAction action)
{
List<EntityInsertAction> actions;

if (!actionBatches.TryGetValue(batchNumber, out actions))
{
actions = new List<EntityInsertAction>();
actionBatches[batchNumber] = actions;
}

actions.Add(action);
}
}
}
}