Skip to content

Commit 1fb26c4

Browse files
mike-spencerhazzik
authored andcommitted
Action Queue Insertion sort performance improvements (NH-3037, port of HHH-2957)
1 parent 4c7cc80 commit 1fb26c4

File tree

4 files changed

+192
-76
lines changed

4 files changed

+192
-76
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System;
2+
3+
namespace NHibernate.Test.NHSpecificTest.NH3037
4+
{
5+
class Entity
6+
{
7+
public virtual long Id { get; set; }
8+
public virtual string Name { get; set; }
9+
}
10+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.Linq;
4+
using NHibernate.Cfg.MappingSchema;
5+
using NHibernate.Linq;
6+
using NHibernate.Mapping.ByCode;
7+
using NUnit.Framework;
8+
9+
namespace NHibernate.Test.NHSpecificTest.NH3037
10+
{
11+
[TestFixture, Explicit("This is a performance test and may take a while.")]
12+
public class ByCodeFixture : TestCaseMappingByCode
13+
{
14+
protected override HbmMapping GetMappings()
15+
{
16+
var mapper = new ModelMapper();
17+
mapper.Class<Entity>(rc =>
18+
{
19+
rc.Id(x => x.Id, m => m.Generator(Generators.Assigned));
20+
rc.Property(x => x.Name);
21+
});
22+
23+
return mapper.CompileMappingForAllExplicitlyAddedEntities();
24+
}
25+
26+
[TestCase(10)]
27+
[TestCase(100)]
28+
[TestCase(1000)]
29+
[TestCase(10000)]
30+
[TestCase(20000)]
31+
[TestCase(30000)]
32+
[TestCase(40000)]
33+
public void SortInsertionActions(int iterations)
34+
{
35+
using (ISession session = OpenSession())
36+
using (ITransaction transaction = session.BeginTransaction())
37+
{
38+
for (int i = 1; i <= iterations; i++)
39+
{
40+
session.Save(new Entity() { Id = i, Name = i.ToString() });
41+
}
42+
43+
var impl = ((NHibernate.Impl.SessionImpl)session);
44+
45+
var stopwatch = Stopwatch.StartNew();
46+
47+
impl.ActionQueue.SortActions();
48+
49+
stopwatch.Stop();
50+
51+
System.Console.WriteLine(stopwatch.Elapsed);
52+
}
53+
}
54+
}
55+
}

src/NHibernate.Test/NHibernate.Test.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,8 @@
697697
<Compile Include="NHSpecificTest\NH3160\DialectNotSupportingUniqueKeyword.cs" />
698698
<Compile Include="NHSpecificTest\NH3160\Entity.cs" />
699699
<Compile Include="NHSpecificTest\NH3160\FixtureByCode.cs" />
700+
<Compile Include="NHSpecificTest\NH3037\Entity.cs" />
701+
<Compile Include="NHSpecificTest\NH3037\FixtureByCode.cs" />
700702
<Compile Include="NHSpecificTest\NH2347\Entity.cs" />
701703
<Compile Include="NHSpecificTest\NH2347\Fixture.cs" />
702704
<Compile Include="NHSpecificTest\NH2664\Product.cs" />

src/NHibernate/Engine/ActionQueue.cs

Lines changed: 125 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -315,83 +315,8 @@ public void SortActions()
315315
//violations
316316
private void SortInsertActions()
317317
{
318-
// IMPLEMENTATION NOTES:
319-
//
320-
// The main data structure in this ordering algorithm is the 'positionToAction'
321-
// map. Essentially this can be thought of as an put-ordered map (the problem with
322-
// actually implementing it that way and doing away with the 'nameList' is that
323-
// we'd end up having potential duplicate key values). 'positionToAction' maintains
324-
// a mapping from a position within the 'nameList' structure to a "partial queue"
325-
// of actions.
326-
327-
Dictionary<int,List<EntityInsertAction>> positionToAction =
328-
new Dictionary<int, List<EntityInsertAction>>();
329-
List<string> nameList = new List<string>();
330-
331-
while (!(insertions.Count == 0))
332-
{
333-
// todo-events : test behaviour
334-
// in Java they use an implicit cast to EntityInsertAction
335-
// but it may be not work because the insertions list may contain EntityIdentityInsertAction
336-
// (I don't like that "goto"too)
337-
object tempObject = insertions[0];
338-
insertions.RemoveAt(0);
339-
EntityInsertAction action = (EntityInsertAction)tempObject;
340-
string thisEntityName = action.EntityName;
341-
342-
// see if we have already encountered this entity-name...
343-
if (!nameList.Contains(thisEntityName))
344-
{
345-
// we have not, so create the proper entries in nameList and positionToAction
346-
List<EntityInsertAction> segmentedActionQueue = new List<EntityInsertAction>();
347-
segmentedActionQueue.Add(action);
348-
nameList.Add(thisEntityName);
349-
positionToAction[nameList.IndexOf(thisEntityName)] = segmentedActionQueue;
318+
new InsertActionSorter(this).Sort();
350319
}
351-
else
352-
{
353-
// we have seen it before, so we need to determine if this insert action is
354-
// is dependent upon a previously processed action in terms of FK
355-
// relationships (this FK checking is done against the entity's property-state
356-
// associated with the action...)
357-
int lastPos = nameList.LastIndexOf(thisEntityName);
358-
object[] states = action.State;
359-
for (int i = 0; i < states.Length; i++)
360-
{
361-
for (int j = 0; j < nameList.Count; j++)
362-
{
363-
List<EntityInsertAction> tmpList = positionToAction[j];
364-
for (int k = 0; k < tmpList.Count; k++)
365-
{
366-
EntityInsertAction checkAction = tmpList[k];
367-
if (checkAction.Instance == states[i] && j > lastPos)
368-
{
369-
// 'checkAction' is inserting an entity upon which 'action' depends...
370-
// note: this is an assumption and may not be correct in the case of one-to-one
371-
List<EntityInsertAction> segmentedActionQueue = new List<EntityInsertAction>();
372-
segmentedActionQueue.Add(action);
373-
nameList.Add(thisEntityName);
374-
positionToAction[nameList.LastIndexOf(thisEntityName)] = segmentedActionQueue;
375-
goto loopInsertion;
376-
}
377-
}
378-
}
379-
}
380-
381-
List<EntityInsertAction> actionQueue = positionToAction[lastPos];
382-
actionQueue.Add(action);
383-
}
384-
loopInsertion: ;
385-
}
386-
387-
// now iterate back through positionToAction map and move entityInsertAction back to insertion list
388-
for (int p = 0; p < nameList.Count; p++)
389-
{
390-
List<EntityInsertAction> actionQueue = positionToAction[p];
391-
foreach (EntityInsertAction action in actionQueue)
392-
insertions.Add(action);
393-
}
394-
}
395320

396321
public IList<EntityDeleteAction> CloneDeletions()
397322
{
@@ -575,5 +500,129 @@ public void AfterTransactionCompletion(bool success)
575500
querySpacesToInvalidate.Clear();
576501
}
577502
}
503+
504+
[Serializable]
505+
private class InsertActionSorter
506+
{
507+
private readonly ActionQueue _actionQueue;
508+
509+
// the mapping of entity names to their latest batch numbers.
510+
private readonly Dictionary<string, int> _latestBatches = new Dictionary<string, int>();
511+
private readonly Dictionary<object, int> _entityBatchNumber;
512+
513+
// the map of batch numbers to EntityInsertAction lists
514+
private readonly Dictionary<int, List<EntityInsertAction>> _actionBatches = new Dictionary<int, List<EntityInsertAction>>();
515+
516+
public InsertActionSorter(ActionQueue actionQueue)
517+
{
518+
_actionQueue = actionQueue;
519+
520+
//optimize the hash size to eliminate a rehash.
521+
_entityBatchNumber = new Dictionary<object, int>(actionQueue.insertions.Count + 1);
522+
}
523+
524+
public void Sort()
525+
{
526+
// the list of entity names that indicate the batch number
527+
foreach (EntityInsertAction action in _actionQueue.insertions)
528+
{
529+
// remove the current element from insertions. It will be added back later.
530+
var entityName = action.EntityName;
531+
532+
// the entity associated with the current action.
533+
var currentEntity = action.Instance;
534+
535+
int batchNumber;
536+
if (_latestBatches.ContainsKey(entityName))
537+
{
538+
// There is already an existing batch for this type of entity.
539+
// Check to see if the latest batch is acceptable.
540+
batchNumber = FindBatchNumber(action, entityName);
541+
}
542+
else
543+
{
544+
// add an entry for this type of entity.
545+
// we can be assured that all referenced entities have already
546+
// been processed,
547+
// so specify that this entity is with the latest batch.
548+
// doing the batch number before adding the name to the list is
549+
// a faster way to get an accurate number.
550+
551+
batchNumber = _actionBatches.Count;
552+
_latestBatches[entityName] = batchNumber;
553+
}
554+
_entityBatchNumber[currentEntity] = batchNumber;
555+
AddToBatch(batchNumber, action);
556+
}
557+
_actionQueue.insertions.Clear();
558+
559+
// now rebuild the insertions list. There is a batch for each entry in the name list.
560+
for (var i = 0; i < _actionBatches.Count; i++)
561+
{
562+
var batch = _actionBatches[i];
563+
foreach (var action in batch)
564+
{
565+
_actionQueue.insertions.Add(action);
566+
}
567+
}
568+
}
569+
570+
/// <summary>
571+
/// Finds an acceptable batch for this entity to be a member as part of the <see cref="InsertActionSorter" />
572+
/// </summary>
573+
/// <param name="action">The action being sorted</param>
574+
/// <param name="entityName">The name of the entity affected by the action</param>
575+
/// <returns>An appropriate batch number; todo document this process better</returns>
576+
private int FindBatchNumber(EntityInsertAction action, string entityName)
577+
{
578+
// loop through all the associated entities and make sure they have been
579+
// processed before the latest
580+
// batch associated with this entity type.
581+
582+
// the current batch number is the latest batch for this entity type.
583+
var latestBatchNumberForType = _latestBatches[entityName];
584+
585+
// loop through all the associations of the current entity and make sure that they are processed
586+
// before the current batch number
587+
var propertyValues = action.State;
588+
var propertyTypes = action.Persister.ClassMetadata.PropertyTypes;
589+
590+
for (var i = 0; i < propertyValues.Length; i++)
591+
{
592+
var value = propertyValues[i];
593+
var type = propertyTypes[i];
594+
595+
if (type.IsEntityType && value != null)
596+
{
597+
// find the batch number associated with the current association, if any.
598+
int associationBatchNumber;
599+
if (_entityBatchNumber.TryGetValue(value, out associationBatchNumber) && associationBatchNumber > latestBatchNumberForType)
600+
{
601+
// create a new batch for this type. The batch number is the number of current batches.
602+
latestBatchNumberForType = _actionBatches.Count;
603+
_latestBatches[entityName] = latestBatchNumberForType;
604+
// since this entity will now be processed in the latest possible batch,
605+
// we can be assured that it will come after all other associations,
606+
// there's not need to continue checking.
607+
break;
608+
}
609+
}
610+
}
611+
return latestBatchNumberForType;
612+
}
613+
614+
private void AddToBatch(int batchNumber, EntityInsertAction action)
615+
{
616+
List<EntityInsertAction> actions;
617+
618+
if (!_actionBatches.TryGetValue(batchNumber, out actions))
619+
{
620+
actions = new List<EntityInsertAction>();
621+
_actionBatches[batchNumber] = actions;
622+
}
623+
624+
actions.Add(action);
625+
}
626+
}
578627
}
579628
}

0 commit comments

Comments
 (0)