/** * Copyright (c) 2002-2012 "Neo Technology," * Network Engine for Objects in Lund AB [http://neotechnology.com] * * This file is part of Neo4j. * * Neo4j is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.neo4j.kernel.impl.cache; import static org.junit.Assert.assertEquals; import static org.neo4j.helpers.collection.IteratorUtil.count; import static org.neo4j.helpers.collection.MapUtil.map; import static org.neo4j.kernel.impl.cache.SizeOfs.REFERENCE_SIZE; import static org.neo4j.kernel.impl.cache.SizeOfs.sizeOf; import static org.neo4j.kernel.impl.cache.SizeOfs.sizeOfArray; import static org.neo4j.kernel.impl.cache.SizeOfs.withArrayOverhead; import static org.neo4j.kernel.impl.cache.SizeOfs.withArrayOverheadIncludingReferences; import static org.neo4j.kernel.impl.cache.SizeOfs.withObjectOverhead; import static org.neo4j.kernel.impl.cache.SizeOfs.withReference; import java.lang.reflect.Array; import java.util.Map; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.neo4j.graphdb.DynamicRelationshipType; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.PropertyContainer; import org.neo4j.graphdb.Relationship; import org.neo4j.graphdb.RelationshipType; import org.neo4j.graphdb.Transaction; import org.neo4j.graphdb.factory.GraphDatabaseSettings; import org.neo4j.helpers.collection.IteratorUtil; import org.neo4j.kernel.GraphDatabaseAPI; import org.neo4j.kernel.impl.MyRelTypes; import org.neo4j.kernel.impl.core.NodeImpl; import org.neo4j.test.TestGraphDatabaseFactory; public class TestSizeOf { private static GraphDatabaseAPI db; public static final int _8_BYTES_FOR_ID = 8; public static final int _4_BYTES_FOR_INDEX = 4; public static final int _4_BYTES_FOR_VALUE = 4; @BeforeClass public static void setupDB() { db = (GraphDatabaseAPI) new TestGraphDatabaseFactory(). newImpermanentDatabaseBuilder(). setConfig( GraphDatabaseSettings.cache_type, GCResistantCacheProvider.NAME ). newGraphDatabase(); } @AfterClass public static void shutdown() throws Exception { db.shutdown(); } @Before public void clearCache() { db.getNodeManager().clearCache(); } @SuppressWarnings( "unchecked" ) private Cache<NodeImpl> getNodeCache() { // This is a bit fragile because we depend on the order of caches() returns its caches. return (Cache<NodeImpl>) IteratorUtil.first( db.getNodeManager().caches() ); } private Node createNodeAndLoadFresh( Map<String, Object> properties, int nrOfRelationships, int nrOfTypes ) { return createNodeAndLoadFresh( properties, nrOfRelationships, nrOfTypes, 0 ); } private Node createNodeAndLoadFresh( Map<String, Object> properties, int nrOfRelationships, int nrOfTypes, int directionStride ) { Node node = null; Transaction tx = db.beginTx(); try { node = db.createNode(); setProperties( properties, node ); for ( int t = 0; t < nrOfTypes; t++ ) { RelationshipType type = DynamicRelationshipType.withName( relTypeName( t ) ); for ( int i = 0, dir = 0; i < nrOfRelationships; i++, dir = (dir+directionStride)%3 ) { switch ( dir ) { case 0: node.createRelationshipTo( db.createNode(), type ); break; case 1: db.createNode().createRelationshipTo( node, type ); break; case 2: node.createRelationshipTo( node, type ); break; default: throw new IllegalArgumentException( "Invalid direction " + dir ); } } } tx.success(); } finally { tx.finish(); } clearCache(); if ( !properties.isEmpty() ) loadProperties( node ); if ( nrOfRelationships*nrOfTypes > 0 ) count( node.getRelationships() ); return node; } private void loadProperties( PropertyContainer entity ) { for ( String key : entity.getPropertyKeys() ) entity.getProperty( key ); } private void setProperties( Map<String, Object> properties, PropertyContainer entity ) { for ( Map.Entry<String, Object> property : properties.entrySet() ) entity.setProperty( property.getKey(), property.getValue() ); } private Relationship createRelationshipAndLoadFresh( Map<String, Object> properties ) { Relationship relationship = null; Transaction tx = db.beginTx(); try { relationship = db.createNode().createRelationshipTo( db.createNode(), MyRelTypes.TEST ); setProperties( properties, relationship ); tx.success(); } finally { tx.finish(); } clearCache(); if ( !properties.isEmpty() ) loadProperties( relationship ); return relationship; } private String relTypeName( int t ) { return "mytype" + t; } @Test public void cacheSizeCorrelatesWithNodeSizeAfterFullyLoadingRelationships() throws Exception { // Create node with a couple of relationships Node node = createNodeAndLoadFresh( map(), 10, 1 ); Cache<NodeImpl> nodeCache = getNodeCache(); // Just an initial sanity assertion, we start off with a clean cache clearCache(); assertEquals( 0, nodeCache.size() ); // Fully cache the node and its relationships count( node.getRelationships() ); // Now the node cache size should be the same as doing node.size() assertEquals( db.getNodeManager().getNodeForProxy( node.getId(), null ).size(), nodeCache.size() ); } private int sizeOfNode( Node node ) { return db.getNodeManager().getNodeForProxy( node.getId(), null ).size(); } private int sizeOfRelationship( Relationship relationship ) { return db.getNodeManager().getRelationshipForProxy( relationship.getId(), null ).size(); } private int withNodeOverhead( int size ) { return withObjectOverhead( REFERENCE_SIZE/*properties reference*/+ 8/*registeredSize w/ padding*/+ REFERENCE_SIZE/*relationships reference*/+ 8/*relChainPosition*/+ 8/*id*/+ size ); } private int withRelationshipOverhead( int size ) { return withObjectOverhead( REFERENCE_SIZE/*properties reference*/+ 8/*registeredSize w/ padding*/+ 8/*idAndMore*/+ 8/*startNodeId and endNodeId*/+ size ); } public static RelationshipArraySize array( String type ) { return new RelationshipArraySize( type ); } private static class RelationshipArraySize { private final String type; private int nrOut; private int nrIn; private int nrLoop; RelationshipArraySize( String type ) { this.type = type; } RelationshipArraySize withOutRelationships( int nrOut ) { this.nrOut = nrOut; return this; } RelationshipArraySize withInRelationships( int nrIn ) { this.nrIn = nrIn; return this; } RelationshipArraySize withLoopRelationships( int nrLoop ) { this.nrLoop = nrLoop; return this; } RelationshipArraySize withRelationshipInAllDirections( int nr ) { this.nrOut = this.nrIn = this.nrLoop = nr; return this; } int size() { int size = REFERENCE_SIZE + sizeOf( type ) + // for the type String reference and size itself in RelIdArray REFERENCE_SIZE * 2; // for the IdBlock references in RelIdArray for ( int rels : new int[] { nrOut, nrIn, nrLoop } ) { if ( rels > 0 ) size += withObjectOverhead( withReference( withArrayOverhead( 4 * (rels + 1) ) ) ); } if ( nrLoop > 0 ) size += REFERENCE_SIZE; // RelIdArrayWithLoops is used for for those with loops in return withObjectOverhead( size ); } } static RelationshipsSize relationships() { return new RelationshipsSize(); } private static class RelationshipsSize { private int length; private int size; RelationshipsSize add( RelationshipArraySize array ) { this.size += array.size(); this.length++; return this; } int size() { return withArrayOverheadIncludingReferences( size, length ); } } static PropertiesSize properties() { return new PropertiesSize(); } private static class PropertiesSize { private int length; private int size; /* * For each PropertyData, this will include the object overhead of the PropertyData object itself. */ private PropertiesSize add( int size ) { length++; this.size += withObjectOverhead( size ); return this; } PropertiesSize withSmallPrimitiveProperty() { return add( _8_BYTES_FOR_ID + _4_BYTES_FOR_INDEX + _4_BYTES_FOR_VALUE ); } PropertiesSize withStringProperty( String value ) { return add( _8_BYTES_FOR_ID + _4_BYTES_FOR_INDEX + withReference( sizeOf( value ) ) ); } PropertiesSize withArrayProperty( Object array ) { return add( _8_BYTES_FOR_ID + _4_BYTES_FOR_INDEX + REFERENCE_SIZE + sizeOfArray( array ) ); } public int size() { return withArrayOverheadIncludingReferences( size, length ); } } private void assertArraySize( Object array, int sizePerItem ) { assertEquals( withArrayOverhead( Array.getLength( array )*sizePerItem ), sizeOfArray( array ) ); } @Test public void sizeOfEmptyNode() throws Exception { Node node = createNodeAndLoadFresh( map(), 0, 0 ); assertEquals( withNodeOverhead( 0 ), sizeOfNode( node ) ); } @Test public void sizeOfNodeWithOneProperty() throws Exception { Node node = createNodeAndLoadFresh( map( "age", 5 ), 0, 0 ); assertEquals( withNodeOverhead( properties().withSmallPrimitiveProperty().size() ), sizeOfNode( node ) ); } @Test public void sizeOfNodeWithSomeProperties() throws Exception { String name = "Mattias"; Node node = createNodeAndLoadFresh( map( "age", 5, "name", name ), 0, 0 ); assertEquals( withNodeOverhead( properties().withSmallPrimitiveProperty().withStringProperty( name ).size() ), sizeOfNode( node ) ); } @Test public void sizeOfNodeWithOneRelationship() throws Exception { Node node = createNodeAndLoadFresh( map(), 1, 1 ); assertEquals( withNodeOverhead( relationships().add( array( relTypeName( 0 ) ).withOutRelationships( 1 ) ).size() ), sizeOfNode( node ) ); } @Test public void sizeOfNodeWithSomeRelationshipsOfSameType() throws Exception { Node node = createNodeAndLoadFresh( map(), 10, 1 ); assertEquals( withNodeOverhead( relationships().add( array( relTypeName( 0 ) ).withOutRelationships( 10 ) ).size() ), sizeOfNode( node ) ); } @Test public void sizeOfNodeWithSomeRelationshipOfDifferentTypes() throws Exception { Node node = createNodeAndLoadFresh( map(), 3, 3 ); assertEquals( withNodeOverhead( relationships().add( array( relTypeName( 0 ) ).withOutRelationships( 3 ) ).add( array( relTypeName( 1 ) ).withOutRelationships( 3 ) ).add( array( relTypeName( 2 ) ).withOutRelationships( 3 ) ).size() ), sizeOfNode( node ) ); } @Test public void sizeOfNodeWithSomeRelationshipOfDifferentTypesAndDirections() throws Exception { Node node = createNodeAndLoadFresh( map(), 9, 3, 1 ); assertEquals( withNodeOverhead( relationships().add( array( relTypeName( 0 ) ).withRelationshipInAllDirections( 3 ) ).add( array( relTypeName( 1 ) ).withRelationshipInAllDirections( 3 ) ).add( array( relTypeName( 2 ) ).withRelationshipInAllDirections( 3 ) ).size() ), sizeOfNode( node ) ); } @Test public void sizeOfNodeWithRelationshipsAndProperties() throws Exception { int[] array = new int[]{10, 11, 12, 13}; Node node = createNodeAndLoadFresh( map( "age", 10, "array", array ), 9, 3, 1 ); assertEquals( withNodeOverhead( relationships().add( array( relTypeName( 0 ) ).withRelationshipInAllDirections( 3 ) ).add( array( relTypeName( 1 ) ).withRelationshipInAllDirections( 3 ) ).add( array( relTypeName( 2 ) ).withRelationshipInAllDirections( 3 ) ).size() + properties().withSmallPrimitiveProperty().withArrayProperty( array ).size() ), sizeOfNode( node ) ); } @Test public void sizeOfEmptyRelationship() throws Exception { Relationship relationship = createRelationshipAndLoadFresh( map() ); assertEquals( withRelationshipOverhead( 0 ), sizeOfRelationship( relationship ) ); } @Test public void sizeOfRelationshipWithOneProperty() throws Exception { Relationship relationship = createRelationshipAndLoadFresh( map( "age", 5 ) ); assertEquals( withRelationshipOverhead( properties().withSmallPrimitiveProperty().size() ), sizeOfRelationship( relationship ) ); } @Test public void sizeOfRelationshipWithSomeProperties() throws Exception { String name = "Mattias"; Relationship relationship = createRelationshipAndLoadFresh( map( "age", 5, "name", name ) ); assertEquals( withRelationshipOverhead( properties().withSmallPrimitiveProperty().withStringProperty( name ).size() ), sizeOfRelationship( relationship ) ); } @Test public void assumeWorstCaseJavaObjectOverhead() throws Exception { int size = 88; assertEquals( size+16, withObjectOverhead( size ) ); } @Test public void assumeWorstCaseJavaArrayObjectOverhead() throws Exception { int size = 88; assertEquals( size+24, withArrayOverhead( size ) ); } @Test public void assumeWorstCaseJavaArrayObjectOverheadIncludingReferences() throws Exception { int size = 24; int length = 5; assertEquals( withArrayOverhead( size+REFERENCE_SIZE*length ), withArrayOverheadIncludingReferences( size, length ) ); } @Test public void assumeWorstCaseReferenceToJavaObject() throws Exception { int size = 24; assertEquals( size + REFERENCE_SIZE, withReference( size ) ); } @Test public void sizeOfPrimitiveArrayIsCorrectlyCalculated() throws Exception { assertArraySize( new boolean[] { true, false }, 1 ); assertArraySize( new byte[] { 1, 2, 3 }, 1 ); assertArraySize( new short[] { 23, 21, 1, 45 }, 2 ); assertArraySize( new int[] { 100, 121, 1, 3, 45 }, 4 ); assertArraySize( new long[] { 403L, 3849329L, 23829L }, 8 ); assertArraySize( new float[] { 342.0F, 21F, 43.4567F }, 4 ); assertArraySize( new double[] { 45748.98D, 38493D }, 8 ); // 32 includes the reference to the Integer item (8), object overhead (16) and int value w/ padding (8) assertArraySize( new Integer[] { 123, 456, 789 }, 32 ); } }