001/*
002 *  Copyright 2020 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.core.util;
017
018import java.lang.annotation.ElementType;
019import java.lang.annotation.Retention;
020import java.lang.annotation.RetentionPolicy;
021import java.lang.annotation.Target;
022import java.lang.reflect.Field;
023import java.util.Arrays;
024import java.util.Collections;
025import java.util.IdentityHashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.Set;
029import java.util.concurrent.ConcurrentHashMap;
030import java.util.stream.Collectors;
031
032import org.apache.commons.lang3.tuple.Pair;
033import org.openjdk.jol.info.ClassData;
034import org.openjdk.jol.info.FieldData;
035import org.openjdk.jol.layouters.CurrentLayouter;
036import org.openjdk.jol.util.ObjectUtils;
037import org.slf4j.LoggerFactory;
038
039/**
040 * Helper for manipulating objects heap size.
041 */
042public final class SizeUtils
043{
044    private static Map<Class<?>, Pair<Long, List<Field>>> __cache = new ConcurrentHashMap<>();
045    
046    static
047    {
048        // add opens to JOL for all java.base packages, to avoid illegal access warnings
049        Object.class.getModule().getPackages().forEach(p -> 
050        {
051            if (Object.class.getModule().isOpen(p, ObjectUtils.class.getModule()))
052            {
053                Object.class.getModule().addOpens(p, ObjectUtils.class.getModule());
054            }
055        });
056    }
057    
058    private SizeUtils()
059    {
060        // empty
061    }
062    
063    /**
064     * Calculates the heap size of the given object.
065     * @param object the object
066     * @return the size of the object.
067     */
068    public static long sizeOf(Object object)
069    {
070        if (object == null)
071        {
072            return 0;
073        }
074        
075        try
076        {
077            return _sizeOf(object, Collections.newSetFromMap(new IdentityHashMap<Object, Boolean>()));
078        }
079        catch (Error e)
080        {
081            // Special case for intercepting eg. StackOverflowError and log the concerned object
082            LoggerFactory.getLogger(SizeUtils.class.getName()).error("Error computing size of {}", object, e);
083            throw e;
084        }
085    }
086    
087    private static long _sizeOf(Object object, Set<Object> visited)
088    {
089        if (!visited.add(object))
090        {
091            return 0;
092        }
093        
094        Class<?> klass = object.getClass();
095        
096        if (klass.isArray())
097        {
098            long size = new CurrentLayouter().layout(ClassData.parseInstance(object)).instanceSize();
099
100            if (klass.getComponentType().isPrimitive()) 
101            {
102                return size;
103            }
104            
105            Object[] array = (Object[]) object;
106            
107            return Arrays.stream(array)
108                    .filter(SizeUtils::_isComputable)
109                    .map(value -> _sizeOf(value, visited))
110                    .reduce(size, Long::sum);
111        }
112        
113        // we cache the shallow class size and the list of class' fields
114        Pair<Long, List<Field>> data = __cache.computeIfAbsent(klass, c -> 
115        {
116            ClassData classData = ClassData.parseClass(c);
117
118            List<Field> fields = classData.fields().stream()
119                                                   .map(FieldData::refField)
120                                                   .filter(field -> !field.getType().isPrimitive())
121                                                   .filter(field -> !field.isAnnotationPresent(ExcludeFromSizeCalculation.class))
122                                                   .collect(Collectors.toList());
123
124            return Pair.of(new CurrentLayouter().layout(classData).instanceSize(), fields);
125        });
126        
127        // the actual size of a non-array object is the sum of its shallow size and the size of each of its fields
128        long size = data.getLeft();
129        
130        for (Field field : data.getRight())
131        {
132            Object value = ObjectUtils.value(object, field);
133            
134            if (_isComputable(value))
135            {
136                size += _sizeOf(value, visited);
137            }
138        }
139        
140        return size;
141    }
142
143    private static boolean _isComputable(Object object)
144    {
145        return object != null && !(object instanceof Class);
146    }
147    
148    /**
149     * Fields annotated with {@link ExcludeFromSizeCalculation} are excluded for size calculation
150     */
151    @Retention(RetentionPolicy.RUNTIME)
152    @Target(ElementType.FIELD)
153    public static @interface ExcludeFromSizeCalculation
154    {
155        // marker annotation
156    }
157}