|  | // Copyright 2020 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.packages; | 
|  |  | 
|  | import com.google.common.base.Preconditions; | 
|  | import com.google.common.collect.ImmutableList; | 
|  | import com.google.common.collect.Interner; | 
|  | import com.google.devtools.build.lib.concurrent.BlazeInterners; | 
|  | import com.google.devtools.build.lib.util.StringCanonicalizer; | 
|  | import com.google.protobuf.CodedInputStream; | 
|  | import com.google.protobuf.CodedOutputStream; | 
|  | import java.io.IOException; | 
|  | import java.util.ArrayList; | 
|  | import java.util.HashMap; | 
|  | import java.util.IdentityHashMap; | 
|  | import java.util.List; | 
|  | import java.util.Map; | 
|  | import java.util.Objects; | 
|  | import javax.annotation.Nullable; | 
|  | import net.starlark.java.eval.StarlarkThread; | 
|  | import net.starlark.java.syntax.Location; | 
|  |  | 
|  | /** | 
|  | * Optimized representation of a Starlark call stack. | 
|  | * | 
|  | * <p>Implementation is optimized for minimizing the overhead at a package level. All {@link | 
|  | * CallStack}s created from the same {@link Factory} share internal state, so all {@link CallStack}s | 
|  | * in a package are expected to be created from the same {@link Factory}. | 
|  | */ | 
|  | public final class CallStack { | 
|  |  | 
|  | /** Null instance, for use in testing or where contents doesn't actually matter. */ | 
|  | public static final CallStack EMPTY = new CallStack(/* size = */ 0, /* head = */ null); | 
|  |  | 
|  | /** Number of frames in this stack. */ | 
|  | private final int size; | 
|  | /** Top (innermost call) of the call stack. */ | 
|  | @Nullable final Node head; | 
|  |  | 
|  | private CallStack(int size, @Nullable Node head) { | 
|  | this.size = size; | 
|  | this.head = head; | 
|  | } | 
|  |  | 
|  | /** Returns the call stack as a list of frames, outermost call first. */ | 
|  | public ImmutableList<StarlarkThread.CallStackEntry> toList() { | 
|  | StarlarkThread.CallStackEntry[] array = new StarlarkThread.CallStackEntry[size]; | 
|  | int i = 0; | 
|  | for (Node n = head; n != null; n = n.child) { | 
|  | array[i++] = nodeFrame(n); | 
|  | } | 
|  | return ImmutableList.copyOf(array); | 
|  | } | 
|  |  | 
|  | /** Returns a single frame, like {@code toList().get(i)} but more efficient. */ | 
|  | public StarlarkThread.CallStackEntry getFrame(int i) { | 
|  | for (Node n = head; n != null; n = n.child, i--) { | 
|  | if (i == 0) { | 
|  | return nodeFrame(n); | 
|  | } | 
|  | } | 
|  | throw new IndexOutOfBoundsException(); // !(0 <= i < size) | 
|  | } | 
|  |  | 
|  | /** Returns the number of frames in the call stack. */ | 
|  | public int size() { | 
|  | return size; | 
|  | } | 
|  |  | 
|  | private StarlarkThread.CallStackEntry nodeFrame(Node n) { | 
|  | Location loc = Location.fromFileLineColumn(n.file, n.line, n.col); | 
|  | return new StarlarkThread.CallStackEntry(n.name, loc); | 
|  | } | 
|  |  | 
|  | /** Compact representation of a call stack entry. */ | 
|  | static class Node { | 
|  | /** Function name. */ | 
|  | private final String name; | 
|  | /** File name. */ | 
|  | private final String file; | 
|  |  | 
|  | private final int line; | 
|  | private final int col; | 
|  | @Nullable final Node child; | 
|  |  | 
|  | Node(String name, String file, int line, int col, Node child) { | 
|  | this.name = name; | 
|  | this.file = file; | 
|  | this.line = line; | 
|  | this.col = col; | 
|  | this.child = child; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean equals(Object o) { | 
|  | if (this == o) { | 
|  | return true; | 
|  | } | 
|  | if (!(o instanceof Node)) { | 
|  | return false; | 
|  | } | 
|  | Node node = (Node) o; | 
|  | return line == node.line | 
|  | && col == node.col | 
|  | && name.equals(node.name) | 
|  | && file.equals(node.file) | 
|  | && Objects.equals(child, node.child); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public int hashCode() { | 
|  | return Objects.hash(name, file, line, col, child); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Preferred instantiation method. All {@link CallStack} instances produced from a {@link Factory} | 
|  | * will share some amount of internal state. | 
|  | */ | 
|  | static final class Factory { | 
|  |  | 
|  | private final Interner<Node> nodeCache = BlazeInterners.newWeakInterner(); | 
|  |  | 
|  | /** | 
|  | * Returns a {@link CallStack}. | 
|  | */ | 
|  | CallStack createFrom(List<StarlarkThread.CallStackEntry> stack) { | 
|  | Node child = null; | 
|  | int n = stack.size(); | 
|  | for (int i = n - 1; i >= 0; i--) { | 
|  | StarlarkThread.CallStackEntry entry = stack.get(i); | 
|  |  | 
|  | String name = entry.name; | 
|  | String file = entry.location.file(); | 
|  | int line = entry.location.line(); | 
|  | int column = entry.location.column(); | 
|  |  | 
|  | child = new Node(name, file, line, column, child); | 
|  | if (i != 0) { | 
|  | // We don't intern top node, because it hardly ever repeats. | 
|  | child = nodeCache.intern(child); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Use the same node for all empty stacks, to avoid allocations. | 
|  | return child != null ? new CallStack(n, child) : EMPTY; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Efficient serializer for {@link CallStack}s. All {@link CallStack}s instances passed to a | 
|  | * {@link Serializer} <b>MUST</b> have originated from the same {@link Factory} instance. Before | 
|  | * callstacks are serialized in a package {@link #prepareCallStack(CallStack)} method must be | 
|  | * called on them (to prepare a table of strings). | 
|  | */ | 
|  | static class Serializer { | 
|  | private static final int NULL_NODE_ID = 0; | 
|  |  | 
|  | private final IdentityHashMap<Node, Integer> nodeTable = new IdentityHashMap<>(); | 
|  | private final List<String> stringTable = new ArrayList<>(); | 
|  | private boolean stringTableSerialized = false; | 
|  |  | 
|  | private final Map<String, Integer> stringTableIndex = new HashMap<>(); | 
|  |  | 
|  | private int indexOf(String s) { | 
|  | int i = stringTableIndex.size(); | 
|  | Integer prev = stringTableIndex.putIfAbsent(s, i); | 
|  | if (prev != null) { | 
|  | i = prev; | 
|  | } else { | 
|  | Preconditions.checkArgument( | 
|  | !stringTableSerialized, "Can only serialize CallStacks that were prepared before."); | 
|  | stringTable.add(s); | 
|  | } | 
|  | return i; | 
|  | } | 
|  |  | 
|  | Serializer() { | 
|  | nodeTable.put(null, NULL_NODE_ID); | 
|  | } | 
|  |  | 
|  | void serializeCallStack(CallStack callStack, CodedOutputStream codedOut) throws IOException { | 
|  | if (!stringTableSerialized) { | 
|  | codedOut.writeInt32NoTag(stringTable.size()); | 
|  | for (String string : stringTable) { | 
|  | codedOut.writeStringNoTag(string); | 
|  | } | 
|  | stringTableSerialized = true; | 
|  | } | 
|  |  | 
|  | codedOut.writeInt32NoTag(callStack.size); | 
|  | emitNode(callStack.head, codedOut); | 
|  | } | 
|  |  | 
|  | private void emitNode(Node node, CodedOutputStream codedOut) throws IOException { | 
|  | Integer index = nodeTable.get(node); | 
|  | if (index != null) { | 
|  | codedOut.writeInt32NoTag(index); | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (node == null) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | int newIndex = nodeTable.size(); | 
|  | codedOut.writeInt32NoTag(newIndex); | 
|  | nodeTable.put(node, newIndex); | 
|  | codedOut.writeInt32NoTag(indexOf(node.name)); | 
|  | codedOut.writeInt32NoTag(indexOf(node.file)); | 
|  | codedOut.writeInt32NoTag(node.line); | 
|  | codedOut.writeInt32NoTag(node.col); | 
|  | emitNode(node.child, codedOut); | 
|  | } | 
|  |  | 
|  | public void prepareCallStack(CallStack callStack) { | 
|  | for (Node n = callStack.head; n != null; n = n.child) { | 
|  | indexOf(n.name); | 
|  | indexOf(n.file); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Deserializes {@link CallStack}s as serialized by a {@link Serializer}. Deserialized instances | 
|  | * are optimized as if they had been created from the same {@link Factory}. | 
|  | */ | 
|  | static class Deserializer { | 
|  | private static final Node DUMMY_NODE = new Node("", "", -1, -1, null); | 
|  |  | 
|  | private final List<Node> nodeTable = new ArrayList<>(); | 
|  | @Nullable private List<String> stringTable; | 
|  |  | 
|  | Deserializer() { | 
|  | // By convention index 0 = null. | 
|  | nodeTable.add(null); | 
|  | } | 
|  |  | 
|  | CallStack deserializeCallStack(CodedInputStream codedIn) throws IOException { | 
|  | if (stringTable == null) { | 
|  | int length = codedIn.readInt32(); | 
|  | stringTable = new ArrayList<>(length); | 
|  | for (int i = 0; i < length; i++) { | 
|  | // Avoid having a new set of strings per deserialized string table. Use common | 
|  | // canonicalizer based on assertion that most strings (function names, locations) were | 
|  | // already some degree of shared across packages. | 
|  | stringTable.add(StringCanonicalizer.intern(codedIn.readString())); | 
|  | } | 
|  | } | 
|  |  | 
|  | int size = codedIn.readInt32(); | 
|  | return new CallStack(size, readNode(codedIn)); | 
|  | } | 
|  |  | 
|  | @Nullable | 
|  | private Node readNode(CodedInputStream codedIn) throws IOException { | 
|  | int index = codedIn.readInt32(); | 
|  | if (index < nodeTable.size()) { | 
|  | Node result = nodeTable.get(index); | 
|  | Preconditions.checkState(result != DUMMY_NODE, "Loop detected at index %s", index); | 
|  | return result; | 
|  | } | 
|  |  | 
|  | Preconditions.checkState( | 
|  | index == nodeTable.size(), | 
|  | "Unexpected next value index - read %s, expected %s", | 
|  | index, | 
|  | nodeTable.size()); | 
|  |  | 
|  | // Add dummy node to grow the table and save our spot in the table until we're done. | 
|  | nodeTable.add(DUMMY_NODE); | 
|  | int name = codedIn.readInt32(); | 
|  | int file = codedIn.readInt32(); | 
|  | int line = codedIn.readInt32(); | 
|  | int col = codedIn.readInt32(); | 
|  | Node child = readNode(codedIn); | 
|  |  | 
|  | Node result = new Node(stringTable.get(name), stringTable.get(file), line, col, child); | 
|  | nodeTable.set(index, result); | 
|  | return result; | 
|  | } | 
|  | } | 
|  | } |