| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.jackrabbit.oak.segment; |
| |
| import static java.lang.System.getProperty; |
| import static org.apache.jackrabbit.oak.segment.Segment.GC_FULL_GENERATION_OFFSET; |
| import static org.apache.jackrabbit.oak.segment.SegmentCache.newSegmentCache; |
| import static org.apache.jackrabbit.oak.segment.SegmentVersion.LATEST_VERSION; |
| import static org.junit.Assume.assumeTrue; |
| |
| import java.io.IOException; |
| import java.nio.ByteBuffer; |
| import java.util.AbstractMap.SimpleImmutableEntry; |
| import java.util.Map.Entry; |
| import java.util.UUID; |
| import java.util.function.Supplier; |
| |
| import org.apache.commons.lang.RandomStringUtils; |
| import org.apache.jackrabbit.oak.cache.AbstractCacheStats; |
| import org.apache.jackrabbit.oak.commons.StringUtils; |
| import org.apache.jackrabbit.oak.segment.CacheWeights.StringCacheWeigher; |
| import org.apache.jackrabbit.oak.segment.file.PriorityCache; |
| import org.apache.jackrabbit.oak.segment.memory.MemoryStore; |
| import org.junit.Before; |
| import org.junit.Test; |
| |
| /** |
| * Test/Utility class to measure size in memory for common segment-tar objects. |
| * <p> |
| * The test is <b>disabled</b> by default, to run it you need to set the |
| * {@code CacheWeightsTest} system property:<br> |
| * {@code mvn clean test -Dtest=CacheWeightEstimator -Dtest.opts.memory=-Xmx2G} |
| * </p> |
| * <p> |
| * To collect the results check the |
| * {@code org.apache.jackrabbit.oak.segment.CacheWeightsTest-output.txt} file:<br> |
| * {@code cat target/surefire-reports/org.apache.jackrabbit.oak.segment.CacheWeightEstimator-output.txt} |
| * </p> |
| */ |
| public class CacheWeightEstimator { |
| |
| // http://www.javaworld.com/article/2077496/testing-debugging/java-tip-130--do-you-know-your-data-size-.html |
| // http://www.javaspecialists.eu/archive/Issue029.html |
| // http://www.slideshare.net/cnbailey/memory-efficient-java |
| |
| /*- |
| * Open JDK's JOL report on various segment classes: |
| |
| org.apache.jackrabbit.oak.segment.RecordId object internals: |
| OFFSET SIZE TYPE DESCRIPTION VALUE |
| 0 12 (object header) N/A |
| 12 4 int RecordId.offset N/A |
| 16 4 SegmentId RecordId.segmentId N/A |
| 20 4 (loss due to the next object alignment) |
| Instance size: 24 bytes |
| Space losses: 0 bytes internal + 4 bytes external = 4 bytes total |
| |
| org.apache.jackrabbit.oak.segment.SegmentId object internals: |
| OFFSET SIZE TYPE DESCRIPTION VALUE |
| 0 12 (object header) N/A |
| 12 4 int SegmentId.gcGeneration N/A |
| 16 8 long SegmentId.msb N/A |
| 24 8 long SegmentId.lsb N/A |
| 32 8 long SegmentId.creationTime N/A |
| 40 4 SegmentStore SegmentId.store N/A |
| 44 4 String SegmentId.gcInfo N/A |
| 48 4 Segment SegmentId.segment N/A |
| 52 4 (loss due to the next object alignment) |
| Instance size: 56 bytes |
| Space losses: 0 bytes internal + 4 bytes external = 4 bytes total |
| |
| org.apache.jackrabbit.oak.segment.Segment object internals: |
| OFFSET SIZE TYPE DESCRIPTION VALUE |
| 0 12 (object header) N/A |
| 12 4 SegmentStore Segment.store N/A |
| 16 4 SegmentReader Segment.reader N/A |
| 20 4 SegmentId Segment.id N/A |
| 24 4 ByteBuffer Segment.data N/A |
| 28 4 SegmentVersion Segment.version N/A |
| 32 4 RecordNumbers Segment.recordNumbers N/A |
| 36 4 SegmentReferences Segment.segmentReferences N/A |
| 40 4 String Segment.info N/A |
| 44 4 (loss due to the next object alignment) |
| Instance size: 48 bytes |
| Space losses: 0 bytes internal + 4 bytes external = 4 bytes total |
| |
| org.apache.jackrabbit.oak.segment.Template object internals: |
| OFFSET SIZE TYPE DESCRIPTION VALUE |
| 0 12 (object header) N/A |
| 12 4 SegmentReader Template.reader N/A |
| 16 4 PropertyState Template.primaryType N/A |
| 20 4 PropertyState Template.mixinTypes N/A |
| 24 4 PropertyTemplate[] Template.properties N/A |
| 28 4 String Template.childName N/A |
| Instance size: 32 bytes |
| Space losses: 0 bytes internal + 0 bytes external = 0 bytes total |
| |
| */ |
| /** Only run if explicitly asked to via -Dtest=SegmentCompactionIT */ |
| private static final boolean ENABLED = |
| CacheWeightEstimator.class.getSimpleName().equals(getProperty("test")); |
| |
| private final MemoryStore store; |
| |
| public CacheWeightEstimator() throws IOException { |
| store = new MemoryStore(); |
| } |
| |
| public static void main(String... args) throws Exception { |
| CacheWeightEstimator cwe = new CacheWeightEstimator(); |
| cwe.testObjects(); |
| cwe.testSegmentIds(); |
| cwe.testSegmentIdsWGc(); |
| cwe.testRecordIds(); |
| cwe.testRecordIdsWGc(); |
| cwe.testStringCache(); |
| cwe.testNodeCache(); |
| cwe.testSegments(); |
| cwe.testSegmentCache(); |
| cwe.testStrings(); |
| } |
| |
| @Before |
| public void setup() { |
| assumeTrue(ENABLED); |
| } |
| |
| @Test |
| public void testObjects() { |
| final int count = 1000000; |
| Supplier<Entry<Object, Long[]>> factory = () -> { |
| Object[] objects = new Object[count]; |
| for (int i = 0; i < count; ++i) { |
| Object o = new Object(); |
| objects[i] = o; |
| } |
| long weight = CacheWeights.OBJECT_HEADER_SIZE * count; |
| return new SimpleImmutableEntry<>(objects, new Long[] {(long) count, weight}); |
| }; |
| runTest(factory, "Object[x" + count + "]"); |
| } |
| |
| private void testSegmentIds() { |
| runSegmentIds(1000000, false); |
| } |
| |
| @Test |
| public void testSegmentIdsWGc() { |
| runSegmentIds(1000000, true); |
| } |
| |
| private void runSegmentIds(final int count, final boolean gcInfo) { |
| Supplier<Entry<Object, Long[]>> factory = () -> { |
| long weight = 0; |
| Object[] objects = new Object[count]; |
| for (int i = 0; i < count; ++i) { |
| SegmentId o = randomSegmentId(gcInfo); |
| weight += o.estimateMemoryUsage(); |
| objects[i] = o; |
| } |
| return new SimpleImmutableEntry<>(objects, new Long[] {(long) count, weight}); |
| }; |
| String name = "SegmentId"; |
| if (gcInfo) { |
| name += "[x" + count + "|GCInfo]"; |
| } else { |
| name += "[x" + count + "]"; |
| } |
| runTest(factory, name); |
| } |
| |
| @Test |
| public void testRecordIds() { |
| runRecordIds(1000000, false); |
| } |
| |
| @Test |
| public void testRecordIdsWGc() { |
| runRecordIds(1000000, true); |
| } |
| |
| private void runRecordIds(final int count, final boolean gcInfo) { |
| Supplier<Entry<Object, Long[]>> factory = () -> { |
| long weight = 0; |
| Object[] objects = new Object[count]; |
| for (int i = 0; i < count; ++i) { |
| RecordId o = randomRecordId(gcInfo); |
| weight += o.estimateMemoryUsage(); |
| objects[i] = o; |
| } |
| return new SimpleImmutableEntry<>(objects, new Long[] {(long) count, weight}); |
| }; |
| String name = "RecordId"; |
| if (gcInfo) { |
| name += "[x" + count + "|GCInfo]"; |
| } else { |
| name += "[x" + count + "]"; |
| } |
| runTest(factory, name); |
| } |
| |
| @Test |
| public void testStringCache() { |
| final int count = 1000000; |
| final int keySize = 96; |
| final boolean gcInfo = true; |
| Supplier<Entry<Object, Long[]>> factory = () -> { |
| RecordCache<String> cache = RecordCache.factory(count, new StringCacheWeigher()).get(); |
| for (int i = 0; i < count; ++i) { |
| String k = randomString(keySize); |
| RecordId v = randomRecordId(gcInfo); |
| cache.put(k, v); |
| } |
| long weight = cache.estimateCurrentWeight(); |
| return new SimpleImmutableEntry<>(cache, new Long[] {(long) count, weight}); |
| }; |
| runTest(factory, "StringCache[x" + count |
| + "|RecordCache<String, RecordId>]"); |
| } |
| |
| @Test |
| public void testNodeCache() { |
| final int count = 1000000; |
| // key usually is a stableid, see SegmentNodeState#getStableId |
| // 2fdd370e-423c-43d6-aad7-6e336c551a38:xxxxxx |
| final int keySize = 43; |
| final boolean gcInfo = true; |
| Supplier<Entry<Object, Long[]>> factory = () -> { |
| int size = (int) PriorityCache.nextPowerOfTwo(count); |
| PriorityCache<String, RecordId> cache = PriorityCache.factory(size, new CacheWeights.NodeCacheWeigher()).get(); |
| for (int i = 0; i < count; ++i) { |
| String k = randomString(keySize); |
| RecordId v = randomRecordId(gcInfo); |
| cache.put(k, v, 0, (byte) 0); |
| } |
| long weight = cache.estimateCurrentWeight(); |
| return new SimpleImmutableEntry<>(cache, new Long[] {(long) count, weight}); |
| }; |
| runTest(factory, "NodeCache[x" + count |
| + "|PriorityCache<String, RecordId>]"); |
| } |
| |
| @Test |
| public void testSegments() { |
| final int count = 10000; |
| final int bufferSize = 5 * 1024; |
| Supplier<Entry<Object, Long[]>> factory = () -> { |
| long weight = 0; |
| Object[] objects = new Object[count]; |
| for (int i = 0; i < count; ++i) { |
| Segment o = randomSegment(bufferSize); |
| weight += o.estimateMemoryUsage(); |
| objects[i] = o; |
| } |
| return new SimpleImmutableEntry<>(objects, new Long[] {(long) count, weight}); |
| }; |
| runTest(factory, "Segment[x" + count + "|" + bufferSize + "]"); |
| } |
| |
| @Test |
| public void testSegmentCache() { |
| final int count = 10000; |
| final int cacheSizeMB = 100; |
| final int bufferSize = 5 * 1024; |
| Supplier<Entry<Object, Long[]>> factory = () -> { |
| SegmentCache cache = newSegmentCache(cacheSizeMB); |
| for (int i = 0; i < count; ++i) { |
| Segment segment = randomSegment(bufferSize); |
| cache.putSegment(segment); |
| } |
| AbstractCacheStats stats = cache.getCacheStats(); |
| long elements = stats.getElementCount(); |
| long weight = stats.estimateCurrentWeight(); |
| return new SimpleImmutableEntry<>(cache, new Long[] {elements, weight}); |
| }; |
| runTest(factory, "SegmentCache[x" + cacheSizeMB + "MB|" + bufferSize + "|Cache<SegmentId, Segment>]"); |
| } |
| |
| @Test |
| public void testStrings() { |
| final int count = 10000; |
| final int length = 256; |
| Supplier<Entry<Object, Long[]>> factory = () -> { |
| long weight = 0; |
| Object[] objects = new Object[count]; |
| for (int i = 0; i < count; ++i) { |
| String s = randomString(length); |
| weight += StringUtils.estimateMemoryUsage(s); |
| objects[i] = s; |
| } |
| return new SimpleImmutableEntry<>(objects, new Long[] {(long) count, weight}); |
| }; |
| runTest(factory, "String[x" + count + "|" + length + "]"); |
| } |
| |
| private SegmentId randomSegmentId(boolean withGc) { |
| UUID u = UUID.randomUUID(); |
| SegmentId id = new SegmentId(store, u.getMostSignificantBits(), u.getLeastSignificantBits()); |
| if (withGc) { |
| id.reclaimed(randomString(80)); |
| } |
| return id; |
| } |
| |
| private RecordId randomRecordId(boolean withGc) { |
| return new RecordId(randomSegmentId(withGc), 128); |
| } |
| |
| private Segment randomSegment(int bufferSize) { |
| byte[] buffer = new byte[bufferSize]; |
| buffer[0] = '0'; |
| buffer[1] = 'a'; |
| buffer[2] = 'K'; |
| buffer[3] = SegmentVersion.asByte(LATEST_VERSION); |
| buffer[4] = 0; // reserved |
| buffer[5] = 0; // refcount |
| |
| int generation = 0; |
| buffer[GC_FULL_GENERATION_OFFSET] = (byte) (generation >> 24); |
| buffer[GC_FULL_GENERATION_OFFSET + 1] = (byte) (generation >> 16); |
| buffer[GC_FULL_GENERATION_OFFSET + 2] = (byte) (generation >> 8); |
| buffer[GC_FULL_GENERATION_OFFSET + 3] = (byte) generation; |
| |
| ByteBuffer data = ByteBuffer.wrap(buffer); |
| SegmentId id = randomSegmentId(false); |
| Segment segment = new Segment(store.getSegmentIdProvider(), store.getReader(), id, data); |
| |
| // |
| // TODO check impact of MutableRecordNumbers overhead of 65k bytes |
| // |
| // MutableRecordNumbers recordNumbers = new MutableRecordNumbers(); |
| // MutableSegmentReferences segmentReferences = new |
| // MutableSegmentReferences(); |
| // String metaInfo = "{\"wid\":\"" + wid + '"' + ",\"sno\":" + 0 |
| // + ",\"t\":" + currentTimeMillis() + "}"; |
| // segment = new Segment(store, store.getReader(), buffer, |
| // recordNumbers, segmentReferences, metaInfo); |
| |
| return segment; |
| } |
| |
| private static String randomString(int length) { |
| return RandomStringUtils.randomAlphanumeric(length); |
| } |
| |
| private static void runTest(Supplier<Entry<Object, Long[]>> factory, String name) { |
| long heapBefore = memory(); |
| Entry<Object, Long[]> e = factory.get(); |
| Object object = e.getKey(); // prevent gc |
| long count = e.getValue()[0]; |
| long heapEstimate = e.getValue()[1]; |
| long heapAfter = memory(); |
| |
| long heapDelta = heapAfter - heapBefore; |
| long perItemHeap = heapDelta / count; |
| long perItemHeapEstimate = heapEstimate / count; |
| |
| System.out.printf(":: %s Test\n", name); |
| System.out.printf("measured heap usage: %d bytes. %d bytes per item\n", heapDelta, perItemHeap); |
| System.out.printf("estimated heap usage: %d bytes. %d bytes per item\n", heapEstimate, perItemHeapEstimate); |
| double percentageOff = 100 * ((double) heapEstimate / (double) heapDelta - 1); |
| if (percentageOff < 0) { |
| System.out.printf("estimated heap usage is %.2f%% to low\n", -percentageOff); |
| } else { |
| System.out.printf("estimated heap usage is %.2f%% to high\n", percentageOff); |
| } |
| } |
| |
| private static long memory() { |
| gc(); |
| return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); |
| } |
| |
| private static void gc() { |
| for (int i = 0; i < 10; i++) { |
| System.gc(); |
| try { |
| Thread.sleep(100); |
| } catch (InterruptedException e) { |
| Thread.currentThread().interrupt(); |
| } |
| } |
| try { |
| Thread.sleep(5000); |
| } catch (InterruptedException e) { |
| Thread.currentThread().interrupt(); |
| } |
| } |
| } |