HHH-6848 : Performance Optimization of in memory merge algorithm (Wim Ockerman)

This commit is contained in:
Gail Badner 2012-04-12 21:50:49 -07:00
parent 71a0698226
commit 62192827cc
2 changed files with 440 additions and 129 deletions

View File

@ -20,47 +20,12 @@
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*
NOTICE: File changed by Wim Ockerman @ CISCO on 2011/10/20
* Reasons of change:
* 1. The EventCache is used during the merging of object graphs
* to persisted objects. During this process the EventCache
* builds up a map of entities (persistable objects) to
* copies (original detached objects or loaded entities).
* At certain places in the merge algorithm the inverse relation
* is needed, see
* ...def.AbstractSaveEventListener.performSaveOrReplicate
* -> calling persister.getPropertyValuesToInsert(.., getMergeMap(),..)
* The getMergeMap() call calls through this this class' invertMap method.
*
* In the original implementation of invertMap() a map was created on the spot
* and all the elements in the entityToCopyMap where put in, now as copy
* to entity pair.
* Because this action can happen a lot in a big detached graph wile merging, the size
* of the entityToCopyMap can become substantial and thus also the creation of the
* inverted map.
* Tests with large graphs thus showed quadratic loss of performance. From a certain
* graph size this became substantial.
*
*
* As solution an inverseEntitiyToCopyMap (so a copyToEntityMap) is maintained now
* along with the changes to the entityToCopyMap.
*
* This made the merge action more linear in terms of performance.
*
* The changes are covered by a new UnitTest.
*
*
* Changes in the code have preceding comments of format
* CHANGE Wim Ockerman <date> [:<comment>]
* And end with:
* END OF CHANGE
*/
package org.hibernate.event.internal;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
@ -86,15 +51,14 @@ import org.hibernate.AssertionFailure;
* @author Gail Badner
*/
class EventCache implements Map {
private Map entityToCopyMap = new IdentityHashMap(10);
private Map<Object,Object> entityToCopyMap = new IdentityHashMap<Object,Object>(10);
// key is an entity involved with the operation performed by the listener;
// value can be either a copy of the entity or the entity itself
// CHANGE Wim Ockerman 2011/10/20: maintains the inverse of the entityToCopyMap for performance reasons.
private Map copyToEntityMap = new IdentityHashMap( 10 );
// END OF CHANGE
private Map<Object,Object> copyToEntityMap = new IdentityHashMap<Object,Object>( 10 );
// maintains the inverse of the entityToCopyMap for performance reasons.
private Map entityToOperatedOnFlagMap = new IdentityHashMap( 10 );
private Map<Object,Boolean> entityToOperatedOnFlagMap = new IdentityHashMap<Object,Boolean>( 10 );
// key is an entity involved with the operation performed by the listener;
// value is a flag indicating if the listener explicitly operates on the entity
@ -103,11 +67,7 @@ class EventCache implements Map {
*/
public void clear() {
entityToCopyMap.clear();
// CHANGE Wim Ockerman 2011/10/20
copyToEntityMap.clear();
// END OF CHANGE
entityToOperatedOnFlagMap.clear();
}
@ -134,15 +94,17 @@ class EventCache implements Map {
if ( copy == null ) {
throw new NullPointerException( "null copies are not supported by " + getClass().getName() );
}
return entityToCopyMap.containsValue( copy );
return copyToEntityMap.containsKey( copy );
}
/**
* Returns a set view of the entity-to-copy mappings contained in this EventCache.
* @return set view of the entity-to-copy mappings contained in this EventCache
* Returns an unmodifiable set view of the entity-to-copy mappings contained in this EventCache.
* @return an unmodifiable set view of the entity-to-copy mappings contained in this EventCache
*
* @see {@link Collections#unmodifiableSet(java.util.Set)}
*/
public Set entrySet() {
return entityToCopyMap.entrySet();
return Collections.unmodifiableSet( entityToCopyMap.entrySet() );
}
/**
@ -167,11 +129,13 @@ class EventCache implements Map {
}
/**
* Returns a set view of the entities contained in this EventCache
* @return a set view of the entities contained in this EventCache
* Returns an unmodifiable set view of the entities contained in this EventCache
* @return an unmodifiable set view of the entities contained in this EventCache
*
* @see {@link Collections#unmodifiableSet(java.util.Set)}
*/
public Set keySet() {
return entityToCopyMap.keySet();
return Collections.unmodifiableSet( entityToCopyMap.keySet() );
}
/**
@ -183,14 +147,7 @@ class EventCache implements Map {
* @throws NullPointerException if entity or copy is null
*/
public Object put(Object entity, Object copy) {
if ( entity == null || copy == null ) {
throw new NullPointerException( "null entities and copies are not supported by " + getClass().getName() );
}
entityToOperatedOnFlagMap.put( entity, Boolean.FALSE );
// CHANGE Wim Ockerman 2011/10/20
copyToEntityMap.put(copy, entity);
// END OF CHANGE
return entityToCopyMap.put( entity, copy );
return put( entity, copy, Boolean.FALSE );
}
/**
@ -207,11 +164,35 @@ class EventCache implements Map {
if ( entity == null || copy == null ) {
throw new NullPointerException( "null entities and copies are not supported by " + getClass().getName() );
}
entityToOperatedOnFlagMap.put( entity, Boolean.valueOf( isOperatedOn ) );
// CHANGE Wim Ockerman 2011/10/20
copyToEntityMap.put(copy, entity);
// END OF CHANGE
return entityToCopyMap.put( entity, copy );
Object oldCopy = entityToCopyMap.put( entity, copy );
Boolean oldOperatedOn = entityToOperatedOnFlagMap.put( entity, isOperatedOn );
Object oldEntity = copyToEntityMap.put( copy, entity );
if ( oldCopy == null ) {
if ( oldEntity != null ) {
throw new IllegalStateException( "An entity copy is already assigned to a different entity." );
}
if ( oldOperatedOn != null ) {
throw new IllegalStateException( "entityToOperatedOnFlagMap contains an entity, but entityToCopyMap does not." );
}
}
else {
if ( oldEntity == null ) {
throw new IllegalStateException( "An entity already had a copy in entityToCopyMap, but the specified copy was not in copyToEntityMap." );
}
if ( oldOperatedOn == null ) {
throw new IllegalStateException( "entityToCopyMap contained an entity, but entityToOperatedOnFlagMap did not." );
}
if ( oldEntity != entity ) {
throw new IllegalStateException( "An entity copy was associated with a different entity than provided." );
}
if ( oldCopy != copy ) {
throw new IllegalStateException( "An entity was already associated with a copy that is different from the copy provided." );
}
}
return oldCopy;
}
/**
@ -220,16 +201,9 @@ class EventCache implements Map {
* @throws NullPointerException if any map keys or values are null
*/
public void putAll(Map map) {
for ( Iterator it=map.entrySet().iterator(); it.hasNext(); ) {
Map.Entry entry = ( Map.Entry ) it.next();
if ( entry.getKey() == null || entry.getValue() == null ) {
throw new NullPointerException( "null entities and copies are not supported by " + getClass().getName() );
}
entityToCopyMap.put( entry.getKey(), entry.getValue() );
// CHANGE Wim Ockerman 2011/10/20
copyToEntityMap.put(entry.getValue(), entry.getKey());
// END OF CHANGE
entityToOperatedOnFlagMap.put( entry.getKey(), Boolean.FALSE );
for ( Object o : map.entrySet() ) {
Entry entry = (Entry) o;
put( entry.getKey(), entry.getValue() );
}
}
@ -243,12 +217,28 @@ class EventCache implements Map {
if ( entity == null ) {
throw new NullPointerException( "null entities are not supported by " + getClass().getName() );
}
entityToOperatedOnFlagMap.remove( entity );
// CHANGE Wim Ockerman 2011/10/20
Object result = entityToCopyMap.remove( entity );
copyToEntityMap.remove(result);
// END OF CHANGE
return result;
Boolean oldOperatedOn = entityToOperatedOnFlagMap.remove( entity );
Object oldCopy = entityToCopyMap.remove( entity );
Object oldEntity = oldCopy != null ? copyToEntityMap.remove( oldCopy ) : null;
if ( oldCopy == null ) {
if ( oldOperatedOn != null ) {
throw new IllegalStateException( "Removed entity from entityToOperatedOnFlagMap, but entityToCopyMap did not contain the entity." );
}
}
else {
if ( oldEntity == null ) {
throw new IllegalStateException( "Removed entity from entityToCopyMap, but copyToEntityMap did not contain the entity." );
}
if ( oldOperatedOn == null ) {
throw new IllegalStateException( "entityToCopyMap contained an entity, but entityToOperatedOnFlagMap did not." );
}
if ( oldEntity != entity ) {
throw new IllegalStateException( "An entity copy was associated with a different entity than provided." );
}
}
return oldCopy;
}
/**
@ -260,11 +250,13 @@ class EventCache implements Map {
}
/**
* Returns a collection view of the entity copies contained in this EventCache.
* @return a collection view of the entity copies contained in this EventCache
* Returns an unmodifiable set view of the entity copies contained in this EventCache.
* @return an unmodifiable set view of the entity copies contained in this EventCache
*
* @see {@link Collections#unmodifiableSet(java.util.Set)}
*/
public Collection values() {
return entityToCopyMap.values();
return Collections.unmodifiableCollection( entityToCopyMap.values() );
}
/**
@ -277,13 +269,12 @@ class EventCache implements Map {
if ( entity == null ) {
throw new NullPointerException( "null entities are not supported by " + getClass().getName() );
}
return ( ( Boolean ) entityToOperatedOnFlagMap.get( entity ) ).booleanValue();
return entityToOperatedOnFlagMap.get( entity );
}
/**
* Set flag to indicate if the listener is performing the operation on the specified entity.
* @param entity must be non-null and this EventCache must contain a mapping for this entity
* @return true if the listener is performing the operation on the specified entity
* @throws NullPointerException if entity is null
* @throws AssertionFailure if this EventCache does not contain a mapping for the specified entity
*/
@ -295,16 +286,16 @@ class EventCache implements Map {
! entityToCopyMap.containsKey( entity ) ) {
throw new AssertionFailure( "called EventCache.setOperatedOn() for entity not found in EventCache" );
}
entityToOperatedOnFlagMap.put( entity, Boolean.valueOf( isOperatedOn ) );
entityToOperatedOnFlagMap.put( entity, isOperatedOn );
}
/**
* Returns the copy-entity mappings
* @return the copy-entity mappings
* Returns an unmodifiable map view of the copy-entity mappings
* @return an unmodifiable map view of the copy-entity mappings
*
* @see {@link Collections#unmodifiableMap(java.util.Map)}
*/
public Map invertMap() {
// CHANGE Wim Ockerman 2011/10/20
return copyToEntityMap;
// END OF CHANGE
return Collections.unmodifiableMap( copyToEntityMap );
}
}

View File

@ -24,8 +24,11 @@
*/
package org.hibernate.event.internal;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import junit.framework.TestCase;
@ -40,50 +43,67 @@ public class EventCacheTest extends TestCase {
public void testEntityToCopyFillFollowedByCopyToEntityMapping() {
EventCache cache = new EventCache();
Object entity = new Integer(1);
Object copy = new Integer(2);
Object entity = new Simple( 1 );
Object copy = new Simple( 2 );
cache.put(entity, copy);
assertTrue(cache.containsKey(entity));
assertFalse(cache.containsKey(copy));
assertTrue(cache.invertMap().containsKey(copy));
assertFalse(cache.invertMap().containsKey(entity));
cache.clear();
assertFalse(cache.containsKey(entity));
assertFalse(cache.invertMap().containsKey(copy));
}
checkCacheConsistency( cache, 1 );
assertTrue( cache.containsKey( entity ) );
assertFalse( cache.containsKey( copy ) );
assertTrue( cache.containsValue( copy ) );
assertTrue( cache.invertMap().containsKey( copy ) );
assertFalse( cache.invertMap().containsKey( entity ) );
assertTrue( cache.invertMap().containsValue( entity ) );
cache.clear();
checkCacheConsistency( cache, 0 );
assertFalse(cache.containsKey(entity));
assertFalse(cache.invertMap().containsKey(copy));
}
public void testEntityToCopyFillFollowedByCopyToEntityMappingOnRemove() {
EventCache cache = new EventCache();
Object entity = new Integer(1);
Object copy = new Integer(2);
Object entity = new Simple( 1 );
Object copy = new Simple( 2 );
cache.put(entity, copy);
assertTrue(cache.containsKey(entity));
assertFalse(cache.containsKey(copy));
checkCacheConsistency( cache, 1 );
assertTrue(cache.containsKey(entity));
assertFalse( cache.containsKey( copy ) );
assertTrue(cache.invertMap().containsKey(copy));
assertFalse(cache.invertMap().containsKey(entity));
assertTrue( cache.invertMap().containsKey( copy ) );
assertFalse( cache.invertMap().containsKey( entity ) );
cache.remove(entity);
cache.remove( entity );
checkCacheConsistency( cache, 0 );
assertFalse(cache.containsKey(entity));
assertFalse(cache.invertMap().containsKey(copy));
}
public void testEntityToCopyFillFollowedByCopyToEntityUsingPutAll() {
EventCache cache = new EventCache();
Map input = new HashMap();
Object entity1 = new Integer(1);
Object copy1 = new Integer(2);
Map<Object,Object> input = new HashMap<Object,Object>();
Object entity1 = new Simple( 1 );
//
Object copy1 = new Integer( 2 );
input.put(entity1, copy1);
Object entity2 = new Integer(3);
Object copy2 = new Integer(2);
Object entity2 = new Simple( 3 );
Object copy2 = new Integer( 2 );
input.put(entity2, copy2);
cache.putAll(input);
assertTrue(cache.containsKey(entity1));
checkCacheConsistency( cache, 2 );
assertTrue(cache.containsKey(entity1));
assertFalse(cache.containsKey(copy1));
assertTrue(cache.containsKey(entity2));
assertFalse(cache.containsKey(copy2));
@ -97,21 +117,321 @@ public class EventCacheTest extends TestCase {
public void testEntityToCopyFillFollowedByCopyToEntityMappingUsingPutWithSetOperatedOnArg() {
EventCache cache = new EventCache();
Object entity = new Integer(1);
Object copy = new Integer(2);
Object entity = new Simple( 1 );
Object copy = new Simple( 2 );
cache.put(entity, copy, true);
assertTrue(cache.containsKey(entity));
assertFalse(cache.containsKey(copy));
checkCacheConsistency( cache, 1 );
assertTrue(cache.containsKey(entity));
assertFalse( cache.containsKey( copy ) );
assertTrue( cache.invertMap().containsKey( copy ) );
assertFalse( cache.invertMap().containsKey( entity ) );
assertTrue(cache.invertMap().containsKey(copy));
assertFalse(cache.invertMap().containsKey(entity));
cache.clear();
cache.put(entity, copy, false);
assertTrue(cache.containsKey(entity));
cache.clear();
checkCacheConsistency( cache, 0 );
cache.put(entity, copy, false);
checkCacheConsistency( cache, 1 );
assertTrue(cache.containsKey(entity));
assertFalse(cache.containsKey(copy));
}
public void testEntityToCopyFillFollowedByIterateEntrySet() {
EventCache cache = new EventCache();
Object entity = new Simple( 1 );
Object copy = new Simple( 2 );
cache.put( entity, copy, true );
checkCacheConsistency( cache, 1 );
Iterator it = cache.entrySet().iterator();
assertTrue( it.hasNext() );
Map.Entry entry = ( Map.Entry ) it.next();
assertSame( entity, entry.getKey() );
assertSame( copy, entry.getValue() );
assertFalse( it.hasNext() );
}
public void testEntityToCopyFillFollowedByModifyEntrySet() {
EventCache cache = new EventCache();
Object entity = new Simple( 1 );
Object copy = new Simple( 2 );
cache.put( entity, copy, true );
Iterator it = cache.entrySet().iterator();
try {
it.remove();
fail( "should have thrown UnsupportedOperationException" );
}
catch ( UnsupportedOperationException ex ) {
// expected
}
Map.Entry entry = (Map.Entry) cache.entrySet().iterator().next();
try {
cache.entrySet().remove( entry );
fail( "should have thrown UnsupportedOperationException" );
}
catch ( UnsupportedOperationException ex ) {
// expected
}
Map.Entry anotherEntry = new Map.Entry() {
private Object key = new Simple( 3 );
private Object value = 4;
@Override
public Object getKey() {
return key;
}
@Override
public Object getValue() {
return value;
}
@Override
public Object setValue(Object value) {
Object oldValue = this.value;
this.value = value;
return oldValue;
}
};
try {
cache.entrySet().add( anotherEntry );
fail( "should have thrown UnsupportedOperationException" );
}
catch ( UnsupportedOperationException ex ) {
// expected
}
}
public void testEntityToCopyFillFollowedByModifyKeys() {
EventCache cache = new EventCache();
Object entity = new Simple( 1 );
Object copy = new Simple( 2 );
cache.put( entity, copy, true );
Iterator it = cache.keySet().iterator();
try {
it.remove();
fail( "should have thrown UnsupportedOperationException" );
}
catch ( UnsupportedOperationException ex ) {
// expected
}
try {
cache.keySet().remove( entity );
fail( "should have thrown UnsupportedOperationException" );
}
catch ( UnsupportedOperationException ex ) {
// expected
}
Object newCopy = new Simple( 3 );
try {
cache.keySet().add( newCopy );
fail( "should have thrown UnsupportedOperationException" );
}
catch ( UnsupportedOperationException ex ) {
// expected
}
}
public void testEntityToCopyFillFollowedByModifyValues() {
EventCache cache = new EventCache();
Object entity = new Simple( 1 );
Object copy = new Simple( 2 );
cache.put( entity, copy, true );
Iterator it = cache.values().iterator();
try {
it.remove();
fail( "should have thrown UnsupportedOperationException" );
}
catch ( UnsupportedOperationException ex ) {
// expected
}
try {
cache.values().remove( copy );
fail( "should have thrown UnsupportedOperationException" );
}
catch ( UnsupportedOperationException ex ) {
// expected
}
Object newCopy = new Simple( 3 );
try {
cache.values().add( newCopy );
fail( "should have thrown UnsupportedOperationException" );
}
catch ( UnsupportedOperationException ex ) {
// expected
}
}
public void testEntityToCopyFillFollowedByModifyKeyOfEntrySetElement() {
EventCache cache = new EventCache();
Simple entity = new Simple( 1 );
Simple copy = new Simple( 0 );
cache.put(entity, copy, true);
Map.Entry entry = (Map.Entry) cache.entrySet().iterator().next();
( ( Simple ) entry.getKey() ).setValue( 2 );
assertEquals( 2, entity.getValue() );
checkCacheConsistency( cache, 1 );
entry = (Map.Entry) cache.entrySet().iterator().next();
assertSame( entity, entry.getKey() );
assertSame( copy, entry.getValue() );
}
public void testEntityToCopyFillFollowedByModifyValueOfEntrySetElement() {
EventCache cache = new EventCache();
Simple entity = new Simple( 1 );
Simple copy = new Simple( 0 );
cache.put(entity, copy, true);
Map.Entry entry = (Map.Entry) cache.entrySet().iterator().next();
( ( Simple ) entry.getValue() ).setValue( 2 );
assertEquals( 2, copy.getValue() );
checkCacheConsistency( cache, 1 );
entry = (Map.Entry) cache.entrySet().iterator().next();
assertSame( entity, entry.getKey() );
assertSame( copy, entry.getValue() );
}
public void testReplaceCopyForEntity() {
EventCache cache = new EventCache();
Simple entity = new Simple( 1 );
Simple copy = new Simple( 0 );
cache.put(entity, copy);
try {
cache.put( entity, new Simple( 0 ) );
fail( "should have thrown IllegalStateException");
}
catch( IllegalStateException ex ) {
// expected
}
}
public void testReplaceEntityForCopy() {
EventCache cache = new EventCache();
Simple entity = new Simple( 1 );
Simple copy = new Simple( 0 );
cache.put(entity, copy);
try {
cache.put( new Simple( 1 ), copy );
fail( "should have thrown IllegalStateException");
}
catch( IllegalStateException ex ) {
// expected
}
}
public void testReplaceEntityForExistingCopy() {
EventCache cache = new EventCache();
Simple entity1 = new Simple( 1 );
Simple copy1 = new Simple( 0 );
cache.put(entity1, copy1);
Simple entity2 = new Simple( 2 );
Simple copy2 = new Simple( 0 );
cache.put( entity2, copy2 );
try {
cache.put( entity1, copy2 );
fail( "should have thrown IllegalStateException");
}
catch( IllegalStateException ex ) {
// expected
}
}
public void testReplaceCopyForExistingEntity() {
EventCache cache = new EventCache();
Simple entity1 = new Simple( 1 );
Simple copy1 = new Simple( 0 );
cache.put(entity1, copy1);
Simple entity2 = new Simple( 2 );
Simple copy2 = new Simple( 0 );
cache.put( entity2, copy2 );
try {
cache.put( entity1, copy2 );
fail( "should have thrown IllegalStateException");
}
catch( IllegalStateException ex ) {
// expected
}
}
public void testRemoveNonExistingEntity() {
EventCache cache = new EventCache();
assertNull( cache.remove( new Simple( 1 ) ) );
}
private void checkCacheConsistency(EventCache cache, int expectedSize) {
Set entrySet = cache.entrySet();
Set cacheKeys = cache.keySet();
Collection cacheValues = cache.values();
Map invertedMap = cache.invertMap();
assertEquals( expectedSize, entrySet.size() );
assertEquals( expectedSize, cache.size() );
assertEquals( expectedSize, cacheKeys.size() );
assertEquals( expectedSize, cacheValues.size() );
assertEquals( expectedSize, invertedMap.size() );
for ( Object entry : cache.entrySet() ) {
Map.Entry mapEntry = ( Map.Entry ) entry;
assertSame( cache.get( mapEntry.getKey() ), mapEntry.getValue() );
assertTrue( cacheKeys.contains( mapEntry.getKey() ) );
assertTrue( cacheValues.contains( mapEntry.getValue() ) );
assertSame( mapEntry.getKey(), invertedMap.get( mapEntry.getValue() ) );
}
}
private static class Simple {
private int value;
public Simple(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
}