| // Copyright 2024 The Bazel Authors. All rights reserved. | |
| // | |
| // Licensed 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 com.google.devtools.build.lib.util; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableSet; | |
| import com.google.common.flogger.GoogleLogger; | |
| import com.google.devtools.build.lib.collect.ConcurrentIdentitySet; | |
| import java.lang.reflect.Array; | |
| import java.lang.reflect.Field; | |
| import java.lang.reflect.InaccessibleObjectException; | |
| import java.lang.reflect.Modifier; | |
| import java.util.ArrayDeque; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Queue; | |
| import java.util.concurrent.ConcurrentHashMap; | |
| import javax.annotation.Nullable; | |
| /** | |
| * Traverses the Java object graph. | |
| * | |
| * <p>When given a Java object, it walks the objects reachable from it. Returns each object only | |
| * once, regardless of the number of edges it is reachable through. | |
| * | |
| * <p>For each object and reference edge found, the appropriate method of {@link ObjectReceiver} is | |
| * called. | |
| * | |
| * <p>The traversal is customizable by passing in {@link DomainSpecificTraverser} instances. Each | |
| * object can choose to handle any given object instance; in that case, it should return the | |
| * user-friendly "context" the object is encountered in and the outgoing edges in the object graph | |
| * it has. | |
| * | |
| * <p>If an object is not handled by any domain-specific traverser, Java reflection is used to | |
| * discover its outgoing references. In this case, domain-specific traversers are still consulted to | |
| * learn whether any of the fields should be ignored. | |
| * | |
| * <p>The traversal stops at objects that: | |
| * | |
| * <ul> | |
| * <li>Are in the {@code seenObjects} set passed into the constructor | |
| * <li>For which at least one {@link DomainSpecificTraverser} returns false in {@link | |
| * DomainSpecificTraverser#admit(Object)} | |
| * </ul> | |
| * | |
| * <p>A traversal currently operates on a single thread. It's not an inherent limitation, it's just | |
| * that it was found to be much faster than spawning a new Executor task for each Java object. | |
| */ | |
| public class ObjectGraphTraverser { | |
| private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); | |
| /** | |
| * Cache for traversal data by object type. | |
| * | |
| * <p>Not a static field because it depends on what domain-specific traversers there are. | |
| */ | |
| public static class FieldCache { | |
| private final Map<Class<?>, List<Field>> fieldCache; | |
| private final ImmutableList<DomainSpecificTraverser> domainSpecificTraversers; | |
| public FieldCache(ImmutableList<DomainSpecificTraverser> domainSpecificTraversers) { | |
| this.fieldCache = | |
| new ConcurrentHashMap<>(128, 0.75f, Runtime.getRuntime().availableProcessors()); | |
| this.domainSpecificTraversers = domainSpecificTraversers; | |
| } | |
| } | |
| /** Domain-specific knowledge about classes to traverse. */ | |
| public interface DomainSpecificTraverser { | |
| /** | |
| * Called for each object to be traversed. | |
| * | |
| * <p>In order for the traversal of an object to be attempted, the {@link #admit(Object)} admit | |
| * method of all domain-specific traversals must return true. | |
| * | |
| * <p>If the implementation knows how to traverse this object, it should return true and call | |
| * methods on {@link Traversal} accordingly. | |
| * | |
| * <p>If not domain-specific traversal handles an object, its fields will be visited by Java | |
| * reflection. | |
| * | |
| * @return true if the object is handled. | |
| */ | |
| boolean maybeTraverse(Object o, Traversal traversal); | |
| /** | |
| * Should return true if the object is interned. | |
| * | |
| * <p>Reachable interned objects are always reported as seen objects, even if they are already | |
| * marked as seen. This makes sense because one can't assign a single owner to them so we either | |
| * assign them to everyone who references them or no one, and the latter would make us lose | |
| * track of their RAM use. | |
| */ | |
| boolean isInterned(Object o); | |
| /** | |
| * Called on each object to be traversed. | |
| * | |
| * <p>If the implementation thinks this instance should <b>not</b> be traversed, it should | |
| * return false. An implementation may well allow traversing an object and yet decline to handle | |
| * it in {@link #maybeTraverse(Object, Traversal)}; in that case, the default traversal will be | |
| * applied to the object. | |
| * | |
| * @return false if the implementation wants to prohibit the traversal of this object. | |
| */ | |
| boolean admit(Object o); | |
| /** | |
| * Compute the user-friendly context for an array item. | |
| * | |
| * <p>This is used to describe what an object is in a way that's more meaningful to the user | |
| * than its raw class. Only called of {@code collectContext} is true. If multiple | |
| * domain-specific traversals provide a context, the first one takes priority. | |
| * | |
| * <p>This method is not called for references reported by domain-specific traversers. | |
| * | |
| * @param from the array the reference originates from | |
| * @param fromContext the context of the array the reference originates from | |
| * @param to the referenced object | |
| * @return the context of {@code to}, or null, if its class is enough | |
| */ | |
| @Nullable | |
| String contextForArrayItem(Object from, String fromContext, Object to); | |
| /** | |
| * Compute the user-friendly context for a field. | |
| * | |
| * <p>This is used to describe what an object is in a way that's more meaningful to the user | |
| * than its raw class. Only called of {@code collectContext} is true. If multiple | |
| * domain-specific traversals provide a context, the first one takes priority. | |
| * | |
| * <p>This method is not called for references reported by domain-specific traversers. | |
| * | |
| * @param from the object the reference originates from | |
| * @param fromContext the context of the object the reference originates from | |
| * @param field the field the reference is through | |
| * @param to the referenced object | |
| * @return the context of {@code to}, or null, if its class is enough | |
| */ | |
| @Nullable | |
| String contextForField(Object from, String fromContext, Field field, Object to); | |
| /** | |
| * Return the set of fields of a class that should be ignored. | |
| * | |
| * @return the set of ignored fields or null if the implementation doesn't know about the given | |
| * class. | |
| */ | |
| @Nullable | |
| ImmutableSet<String> ignoredFields(Class<?> clazz); | |
| } | |
| /** | |
| * Callback through which {@link DomainSpecificTraverser} returns what objects and edges it found. | |
| */ | |
| public interface Traversal { | |
| /** | |
| * Should be called when the domain-specific traverser finds an object. It should be called for | |
| * every object for which {@link DomainSpecificTraverser#maybeTraverse(Object, Traversal)} | |
| * returns true. | |
| * | |
| * @param o the object found | |
| * @param context the context the object is in or null of its class is enough | |
| */ | |
| void objectFound(Object o, String context); | |
| /** | |
| * Should be called for each outgoing reference in an object handled by the {@link | |
| * DomainSpecificTraverser} implementation. | |
| * | |
| * <p>Objects reported through this method are subject to two kinds of filtering: each object is | |
| * only processed once and domain-specific traversers can prohibit the traversal of any object | |
| * by returning false from {@link DomainSpecificTraverser#admit(Object)}. | |
| * | |
| * @param to the object referenced | |
| * @param context the context of the referenced object or null if its class is enough | |
| */ | |
| void edgeFound(Object to, String context); | |
| } | |
| /** The type of an object graph edge. */ | |
| public enum EdgeType { | |
| /** An edge to an object discovered during this traversal. */ | |
| CURRENT_TRAVERSAL, | |
| /** An edge to an object already seen in previous traversals. */ | |
| ALREADY_SEEN | |
| }; | |
| /** A callback where {@link ObjectGraphTraverser} reports the objects and edges encountered. */ | |
| public interface ObjectReceiver { | |
| /** Reports an object in a given context. */ | |
| void objectFound(Object o, String context); | |
| /** Reports an edge in the object graph. */ | |
| void edgeFound(Object from, Object to, String toContext, EdgeType edgeType); | |
| } | |
| public static final ObjectReceiver NOOP_OBJECT_RECEIVER = | |
| new ObjectReceiver() { | |
| @Override | |
| public void objectFound(Object o, String context) {} | |
| @Override | |
| public void edgeFound(Object from, Object to, String toContext, EdgeType edgeType) {} | |
| }; | |
| /** An object to be traversed in the queue. */ | |
| private record WorkItem(Object object, String context, WorkItem parent) {} | |
| private final FieldCache fieldCache; | |
| private final boolean countInternedObjects; | |
| private final boolean reportTransientFields; | |
| private final boolean collectContext; | |
| private final Traversal traversal; | |
| private final ObjectReceiver receiver; | |
| private final Object instanceId; | |
| private WorkItem currentWorkItem; | |
| private final Queue<WorkItem> queue = new ArrayDeque<>(); | |
| private final ConcurrentIdentitySet localObjects; | |
| private final ConcurrentIdentitySet seenObjects; | |
| public ObjectGraphTraverser( | |
| FieldCache fieldCache, | |
| boolean countInternedObjects, | |
| boolean reportTransientFields, | |
| ConcurrentIdentitySet seenObjects, | |
| boolean collectContext, | |
| ObjectReceiver receiver, | |
| Object instanceId) { | |
| this( | |
| fieldCache, | |
| countInternedObjects, | |
| reportTransientFields, | |
| seenObjects, | |
| collectContext, | |
| receiver, | |
| instanceId, | |
| null); | |
| } | |
| /** | |
| * Creates a new traverser. | |
| * | |
| * @param fieldCache the cache for reflection results. | |
| * @param countInternedObjects whether to count interned objects only once or each them they are | |
| * encountered | |
| * @param reportTransientFields whether to recurse into transient fields | |
| * @param seenObjects the set of objects already seen. These are not traversed and references to | |
| * them are reported as {@link EdgeType#ALREADY_SEEN} . | |
| * @param collectContext whether to collect context for each object. Costs some CPU. | |
| * @param receiver the object to report found objects and edges to. | |
| * @param instanceId an object identifying this traversal. | |
| */ | |
| public ObjectGraphTraverser( | |
| FieldCache fieldCache, | |
| boolean countInternedObjects, | |
| boolean reportTransientFields, | |
| ConcurrentIdentitySet seenObjects, | |
| boolean collectContext, | |
| ObjectReceiver receiver, | |
| Object instanceId, | |
| String needle) { | |
| this.needle = needle; | |
| this.fieldCache = fieldCache; | |
| this.countInternedObjects = countInternedObjects; | |
| this.reportTransientFields = reportTransientFields; | |
| this.seenObjects = seenObjects; | |
| this.collectContext = collectContext; | |
| this.receiver = receiver; | |
| this.instanceId = instanceId; | |
| this.traversal = | |
| new Traversal() { | |
| @Override | |
| public void objectFound(Object o, String context) { | |
| receiver.objectFound(o, context); | |
| } | |
| @Override | |
| public void edgeFound(Object to, String context) { | |
| enqueueMaybe(to, context); | |
| } | |
| }; | |
| this.localObjects = new ConcurrentIdentitySet(64); | |
| } | |
| /** | |
| * Traverses a given object. | |
| * | |
| * <p>Can be called multiple times, but no two traversals should be active at the same time in a | |
| * given {@link ObjectGraphTraverser} instance. | |
| */ | |
| public void traverse(Object o) { | |
| for (DomainSpecificTraverser traverser : fieldCache.domainSpecificTraversers) { | |
| if (!traverser.admit(o)) { | |
| return; | |
| } | |
| } | |
| queue.offer(new WorkItem(o, null, null)); | |
| while (!queue.isEmpty()) { | |
| WorkItem workItem = queue.remove(); | |
| try { | |
| process(workItem); | |
| } catch (RuntimeException e) { | |
| logger.atSevere().withCause(e).log("While walking object graph for key %s:", instanceId); | |
| } | |
| } | |
| } | |
| private void enqueueMaybe(Object to, String toContext) { | |
| if (to == null) { | |
| return; | |
| } | |
| if (to instanceof Integer | |
| || to instanceof Long | |
| || to instanceof Short | |
| || to instanceof Byte | |
| || to instanceof Float | |
| || to instanceof Double | |
| || to instanceof Character | |
| || to instanceof Boolean) { | |
| // Boxed primitive type | |
| return; | |
| } | |
| for (DomainSpecificTraverser traverser : fieldCache.domainSpecificTraversers) { | |
| if (!traverser.admit(to)) { | |
| return; | |
| } | |
| } | |
| if (!localObjects.add(to)) { | |
| // A reference to an object visited during this traversal. | |
| receiver.edgeFound(currentWorkItem.object, to, toContext, EdgeType.CURRENT_TRAVERSAL); | |
| return; | |
| } | |
| boolean traverse; | |
| if (!seenObjects.add(to)) { | |
| // A reference to an object already seen, but not during this traversal. | |
| receiver.edgeFound(currentWorkItem.object, to, toContext, EdgeType.ALREADY_SEEN); | |
| traverse = false; | |
| if (countInternedObjects) { | |
| for (DomainSpecificTraverser traverser : fieldCache.domainSpecificTraversers) { | |
| if (traverser.isInterned(to)) { | |
| traverse = true; | |
| break; | |
| } | |
| } | |
| } | |
| } else { | |
| // A new object. | |
| receiver.edgeFound(currentWorkItem.object, to, toContext, EdgeType.CURRENT_TRAVERSAL); | |
| traverse = true; | |
| } | |
| if (traverse) { | |
| queue.offer(new WorkItem(to, toContext, currentWorkItem)); | |
| } | |
| } | |
| @Nullable | |
| private String contextOrNull(String context, String defaultContext) { | |
| if (!collectContext) { | |
| return null; | |
| } | |
| if (context != null) { | |
| return context; | |
| } | |
| return defaultContext; | |
| } | |
| private final String needle; | |
| private void dumpTrace(WorkItem workItem) { | |
| System.err.println("Needle reached by path:"); | |
| while (workItem != null) { | |
| System.err.println(" " + workItem.object.getClass().getName()); | |
| workItem = workItem.parent; | |
| } | |
| System.err.println(); | |
| } | |
| private void process(WorkItem workItem) { | |
| Object o = workItem.object; | |
| String context = workItem.context; | |
| currentWorkItem = workItem; | |
| if (needle != null && o.getClass().getName().equals(needle)) { | |
| dumpTrace(workItem); | |
| } | |
| if (o instanceof String) { | |
| traversal.objectFound(o, contextOrNull(context, "STRING")); | |
| return; | |
| } | |
| for (DomainSpecificTraverser traverser : fieldCache.domainSpecificTraversers) { | |
| if (traverser.maybeTraverse(o, traversal)) { | |
| return; | |
| } | |
| } | |
| if (o instanceof Class<?>) { | |
| traversal.objectFound(o, contextOrNull(context, "CLASS")); | |
| return; | |
| } | |
| Class<?> clazz = o.getClass(); | |
| if (clazz.isArray()) { | |
| traversal.objectFound(o, contextOrNull(context, "[] " + clazz.getComponentType().getName())); | |
| // We only care about objects | |
| if (!clazz.getComponentType().isPrimitive()) { | |
| for (int i = 0; i < Array.getLength(o); i++) { | |
| Object to = Array.get(o, i); | |
| String toContext = null; | |
| if (collectContext) { | |
| for (DomainSpecificTraverser traverser : fieldCache.domainSpecificTraversers) { | |
| String candidate = traverser.contextForArrayItem(o, context, to); | |
| if (candidate != null) { | |
| toContext = candidate; | |
| break; | |
| } | |
| } | |
| } | |
| enqueueMaybe(to, toContext); | |
| } | |
| } | |
| } else { | |
| traversal.objectFound(o, context); | |
| List<Field> fields = fieldCache.fieldCache.computeIfAbsent(clazz, this::getFields); | |
| for (Field field : fields) { | |
| try { | |
| Object to = field.get(o); | |
| String toContext = null; | |
| if (collectContext) { | |
| for (DomainSpecificTraverser traverser : fieldCache.domainSpecificTraversers) { | |
| String candidate = traverser.contextForField(o, context, field, to); | |
| if (candidate != null) { | |
| toContext = candidate; | |
| break; | |
| } | |
| } | |
| } | |
| enqueueMaybe(to, toContext); | |
| } catch (IllegalAccessException e) { | |
| throw new IllegalStateException(e); | |
| } | |
| } | |
| } | |
| } | |
| private ImmutableList<Field> getFields(Class<?> clazz) { | |
| ArrayList<Field> fields = new ArrayList<>(); | |
| for (Class<?> next = clazz; next != null; next = next.getSuperclass()) { | |
| ImmutableSet<String> ignoreSet = ImmutableSet.of(); | |
| for (DomainSpecificTraverser traverser : fieldCache.domainSpecificTraversers) { | |
| ImmutableSet<String> candidate = traverser.ignoredFields(next); | |
| if (candidate != null) { | |
| ignoreSet = candidate; | |
| break; | |
| } | |
| } | |
| for (Field field : next.getDeclaredFields()) { | |
| if (!reportTransientFields && (field.getModifiers() & Modifier.TRANSIENT) != 0) { | |
| continue; | |
| } | |
| // Skip static fields | |
| if ((field.getModifiers() & Modifier.STATIC) != 0) { | |
| continue; | |
| } | |
| if (ignoreSet.contains(field.getName())) { | |
| continue; | |
| } | |
| if (field.getType().isPrimitive()) { | |
| // We only care about the object graph | |
| continue; | |
| } | |
| if (field.getType().isEnum()) { | |
| // Enum instances are not interesting, they are always known at compile time | |
| continue; | |
| } | |
| try { | |
| field.setAccessible(true); | |
| } catch (InaccessibleObjectException e) { | |
| // Ignore this field then, dunno why this happens. | |
| continue; | |
| } | |
| fields.add(field); | |
| } | |
| } | |
| return ImmutableList.copyOf(fields); | |
| } | |
| } |