blob: 459887b152888ee903f306afdeea31605ff78201 [file] [log] [blame]
// Copyright 2023 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.skyframe.serialization.testutils;
import static com.google.common.collect.Iterables.isEmpty;
import static com.google.devtools.build.lib.unsafe.UnsafeProvider.unsafe;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* A utility for creating high fidelity string dumps of arbitrary objects.
*
* <p>Uses reflection to perform depth-first traversal of arbitrary objects and formats them as an
* indented, multiline string.
*
* <p>This class exists mainly to help test and debug serialization. Consequently, it skips {@code
* transient} fields. It also performs reference-based memoization to handle cyclic structures or
* structures that would have an exponential path structure, for example, {@code NestedSets}.
*/
public final class Dumper {
/**
* Stores an identifier for every object traversed.
*
* <p>When an object is encountered again, it is represented with just its type and previous
* identifier instead of being fully expanded.
*/
private final IdentityHashMap<Object, Integer> referenceIds = new IdentityHashMap<>();
/** The current indentation level. */
private int indent = 0;
private final StringBuilder out = new StringBuilder();
private Dumper() {}
/**
* Formats an arbitrary object into a string.
*
* <p>The format is verbose and suitable for tests and debugging.
*
* @return a multiline String representation of {@code obj} without a trailing newline.
*/
public static String dumpStructure(Object obj) {
var deep = new Dumper();
deep.outputObject(obj);
return deep.out.toString();
}
/** Formats an arbitrary object into {@link #out}. */
private void outputObject(Object obj) {
if (obj == null) {
out.append("null");
return;
}
var type = obj.getClass();
if (shouldInline(type)) {
out.append(obj);
return;
}
Integer id = referenceIds.get(obj);
if (id != null) {
// This instance has been observed previously. Outputs only a backreference.
outputIdentifier(type, id);
return;
}
referenceIds.put(obj, id = referenceIds.size());
// All non-inlined, non-backreference objects are represented like
// "<type name>(<id>) [<contents>]".
//
// <contents> depends on the type, but is generally a sequence of recursively formatted
// objects. For arrays and iterables, this is the sequence of elements, for maps,
// it is an alternating sequence of keys and values and for any other type of object, it is
// a sequence of its fields, like "<field name>=<object>".
outputIdentifier(type, id);
out.append(" [");
indent++;
boolean addedLine; // True if the <content> includes a newline.
if (type.isArray()) {
addedLine = outputArrayElements(obj);
} else if (obj instanceof Map) {
addedLine = outputMapEntries((Map<?, ?>) obj);
} else if (obj instanceof Iterable) {
addedLine = outputIterableElements((Iterable<?>) obj);
} else {
addedLine = outputObjectFields(obj);
}
indent--;
if (addedLine) {
// The <content> sequence typically emits a newline per-sequence element, like
// \n<indent><e1>\n<indent><e2>\n<indent><e3>, which would look like the following.
//
// <type name>(id) [
// <e1>
// <e2>
// <e3>▊
//
// The code below emits a newline and indents to the parent's indentation level before
// emitting the closing bracket.
//
// <type name>(id) [
// <e1>
// <e2>
// <e3>
// ]▊
//
// When the <content> sequence is empty, or one of the special cased arrays that do not
// emit newlines, no trailing newline is needed before the closing bracket, as in the
// following examples.
//
// <type name>(id) []▊
// or
// byte[](1234) [DEADBEEF]▊
//
// Note that the output always leaves the cursor at the end of the last written line. The
// caller should add a trailing newline if needed.
addNewlineAndIndent();
}
out.append(']');
}
/** Emits an object identifier like {@code "<type name>(<id>)"}. */
private void outputIdentifier(Class<?> type, int id) {
String name = type.getCanonicalName();
if (name == null) {
// According to the documentation for `Class.getCanonicalName`, not all classes have one.
// Falls back on the name in such cases. (It's unclear if this code is reachable because
// synthetic types are inlined).
name = type.getName();
}
out.append(name).append('(').append(id).append(')');
}
private boolean outputArrayElements(Object arr) {
var componentType = arr.getClass().getComponentType();
if (componentType.equals(byte.class)) {
// It's a byte array. Outputs as hex.
for (byte b : (byte[]) arr) {
out.append(String.format("%02X", b));
}
return false;
}
if (shouldInline(componentType)) {
// It's a type that should be inlined. Outputs elements delimited by commas.
boolean isFirst = true;
for (int i = 0; i < Array.getLength(arr); i++) {
if (isFirst) {
isFirst = false;
} else {
out.append(", ");
}
out.append(Array.get(arr, i));
}
return false;
}
for (int i = 0; i < Array.getLength(arr); i++) {
addNewlineAndIndent();
outputObject(Array.get(arr, i));
}
return Array.getLength(arr) > 0;
}
private boolean outputMapEntries(Map<?, ?> map) {
for (Map.Entry<?, ?> entry : map.entrySet()) {
addNewlineAndIndent();
out.append("key=");
outputObject(entry.getKey());
addNewlineAndIndent();
out.append("value=");
outputObject(entry.getValue());
}
return !map.isEmpty();
}
private boolean outputIterableElements(Iterable<?> iterable) {
for (var next : iterable) {
addNewlineAndIndent();
outputObject(next);
}
return !isEmpty(iterable);
}
private boolean outputObjectFields(Object obj) {
ImmutableList<FieldInfo> fieldInfo = getFieldInfo(obj.getClass());
for (FieldInfo info : fieldInfo) {
addNewlineAndIndent();
outputField(obj, info);
}
return !fieldInfo.isEmpty();
}
private void outputField(Object parent, FieldInfo info) {
out.append(info.name).append('=');
Class<?> type = info.type;
if (!type.isPrimitive()) {
outputObject(unsafe().getObject(parent, info.offset));
return;
}
if (type.equals(boolean.class)) {
out.append(unsafe().getBoolean(parent, info.offset));
} else if (type.equals(byte.class)) {
out.append(unsafe().getByte(parent, info.offset));
} else if (type.equals(short.class)) {
out.append(unsafe().getShort(parent, info.offset));
} else if (type.equals(char.class)) {
out.append(unsafe().getChar(parent, info.offset));
} else if (type.equals(int.class)) {
out.append(unsafe().getInt(parent, info.offset));
} else if (type.equals(long.class)) {
out.append(unsafe().getLong(parent, info.offset));
} else if (type.equals(float.class)) {
out.append(unsafe().getFloat(parent, info.offset));
} else if (type.equals(double.class)) {
out.append(unsafe().getDouble(parent, info.offset));
} else {
throw new UnsupportedOperationException("Unexpected primitive type: " + type);
}
}
private void addNewlineAndIndent() {
out.append('\n');
for (int i = 0; i < indent; i++) {
out.append(" "); // Indentation is 2 spaces.
}
}
private static final ConcurrentHashMap<Class<?>, ImmutableList<FieldInfo>> fieldInfoCache =
new ConcurrentHashMap<>();
private static ImmutableList<FieldInfo> getFieldInfo(Class<?> type) {
return fieldInfoCache.computeIfAbsent(type, Dumper::getFieldInfoUncached);
}
private static class FieldInfo {
private final String name;
private final Class<?> type;
private final long offset;
private FieldInfo(Field field) {
this.name = field.getName();
this.type = field.getType();
this.offset = unsafe().objectFieldOffset(field);
}
}
private static ImmutableList<FieldInfo> getFieldInfoUncached(Class<?> type) {
var fieldInfo = ImmutableList.<FieldInfo>builder();
for (Class<?> next = type; next != null; next = next.getSuperclass()) {
Field[] declaredFields = next.getDeclaredFields();
var classFields = new ArrayList<Field>(declaredFields.length);
for (Field field : next.getDeclaredFields()) {
if ((field.getModifiers() & (Modifier.STATIC | Modifier.TRANSIENT)) != 0) {
continue; // Skips any static or transient fields.
}
classFields.add(field);
}
classFields.stream()
// Sorts by name for determinism. Shadowed fields always have separate entries because
// they occur at different levels in the inheritance hierarchy.
//
// Reverses the order here, then reverses it again below. This makes superclass fields
// appear before subclass fields.
.sorted(Comparator.comparing(Field::getName).reversed())
.map(FieldInfo::new)
.forEach(fieldInfo::add);
}
return fieldInfo.build().reverse();
}
private static boolean shouldInline(Class<?> type) {
return type.isPrimitive() || DIRECT_INLINE_TYPES.contains(type) || type.isSynthetic();
}
private static final ImmutableSet<Class<?>> WRAPPER_TYPES =
ImmutableSet.of(
Byte.class,
Short.class,
Integer.class,
Long.class,
Float.class,
Double.class,
Boolean.class,
Character.class);
private static final ImmutableSet<Class<?>> DIRECT_INLINE_TYPES =
ImmutableSet.<Class<?>>builder()
.addAll(WRAPPER_TYPES)
// Treats Strings as values for readability of the output. It might be good to make this
// configurable later on.
.add(String.class)
// The string representation of a Class is sufficient to identify it.
.add(Class.class)
.build();
}