aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/profiler/memory/AllocationTracker.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/profiler/memory/AllocationTracker.java')
-rw-r--r--src/main/java/com/google/devtools/build/lib/profiler/memory/AllocationTracker.java370
1 files changed, 370 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/memory/AllocationTracker.java b/src/main/java/com/google/devtools/build/lib/profiler/memory/AllocationTracker.java
new file mode 100644
index 0000000000..9631279654
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/profiler/memory/AllocationTracker.java
@@ -0,0 +1,370 @@
+// Copyright 2017 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.profiler.memory;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.MapMaker;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ConditionallyThreadCompatible;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.packages.AspectClass;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleFunction;
+import com.google.devtools.build.lib.syntax.ASTNode;
+import com.google.devtools.build.lib.syntax.BaseFunction;
+import com.google.devtools.build.lib.syntax.Callstack;
+import com.google.monitoring.runtime.instrumentation.Sampler;
+import com.google.perftools.profiles.ProfileProto.Function;
+import com.google.perftools.profiles.ProfileProto.Line;
+import com.google.perftools.profiles.ProfileProto.Profile;
+import com.google.perftools.profiles.ProfileProto.Profile.Builder;
+import com.google.perftools.profiles.ProfileProto.Sample;
+import com.google.perftools.profiles.ProfileProto.ValueType;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.zip.GZIPOutputStream;
+import javax.annotation.Nullable;
+
+/** Tracks allocations for memory reporting. */
+@ConditionallyThreadCompatible
+public class AllocationTracker implements Sampler {
+
+ private static class AllocationSample {
+ @Nullable final RuleClass ruleClass; // Current rule being analysed, if any
+ @Nullable final AspectClass aspectClass; // Current aspect being analysed, if any
+ final List<Object> callstack; // Skylark callstack, if any
+ final long bytes;
+
+ AllocationSample(
+ @Nullable RuleClass ruleClass,
+ @Nullable AspectClass aspectClass,
+ List<Object> callstack,
+ long bytes) {
+ this.ruleClass = ruleClass;
+ this.aspectClass = aspectClass;
+ this.callstack = callstack;
+ this.bytes = bytes;
+ }
+ }
+
+ private final Map<Object, AllocationSample> allocations =
+ new MapMaker().weakKeys().concurrencyLevel(1).makeMap();
+ private final int samplePeriod;
+ private final int sampleVariance;
+ private boolean enabled = true;
+
+ /**
+ * Cheap wrapper class for a long. Avoids having to do two thread-local lookups per allocation.
+ */
+ private static final class LongValue {
+ long value;
+ }
+
+ private final ThreadLocal<LongValue> currentSampleBytes = ThreadLocal.withInitial(LongValue::new);
+ private final ThreadLocal<Long> nextSampleBytes = ThreadLocal.withInitial(this::getNextSample);
+ private final Random random = new Random();
+
+ AllocationTracker(int samplePeriod, int variance) {
+ this.samplePeriod = samplePeriod;
+ this.sampleVariance = variance;
+ }
+
+ @Override
+ @ThreadSafe
+ public void sampleAllocation(int count, String desc, Object newObj, long size) {
+ if (!enabled) {
+ return;
+ }
+ List<Object> callstack = Callstack.get();
+ RuleClass ruleClass = CurrentRuleTracker.getRule();
+ AspectClass aspectClass = CurrentRuleTracker.getAspect();
+ // Should we bother sampling?
+ if (callstack.isEmpty() && ruleClass == null && aspectClass == null) {
+ return;
+ }
+ // If we start getting stack overflows here, it's because the memory sampling
+ // implementation has changed to call back into the sampling method immediately on
+ // every allocation. Since thread locals can allocate, this can in this case lead
+ // to infinite recursion. This method will then need to be rewritten to not
+ // allocate, or at least not allocate to obtain its sample counters.
+ LongValue bytesValue = currentSampleBytes.get();
+ long bytes = bytesValue.value + size;
+ if (bytes < nextSampleBytes.get()) {
+ bytesValue.value = bytes;
+ return;
+ }
+ bytesValue.value = 0;
+ nextSampleBytes.set(getNextSample());
+ allocations.put(
+ newObj,
+ new AllocationSample(ruleClass, aspectClass, ImmutableList.copyOf(callstack), bytes));
+ }
+
+ private long getNextSample() {
+ return (long) samplePeriod
+ + (sampleVariance > 0 ? (random.nextInt(sampleVariance * 2) - sampleVariance) : 0);
+ }
+
+ /** A pair of rule/aspect name and the bytes it consumes. */
+ public static class RuleBytes {
+ private final String name;
+ private long bytes;
+
+ public RuleBytes(String name) {
+ this.name = name;
+ }
+
+ /** The name of the rule or aspect. */
+ public String getName() {
+ return name;
+ }
+
+ /** The number of bytes total occupied by this rule or aspect class. */
+ public long getBytes() {
+ return bytes;
+ }
+
+ public RuleBytes addBytes(long bytes) {
+ this.bytes += bytes;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("RuleBytes(%s, %d)", name, bytes);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ RuleBytes ruleBytes = (RuleBytes) o;
+ return bytes == ruleBytes.bytes && Objects.equal(name, ruleBytes.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(name, bytes);
+ }
+ }
+
+ @Nullable
+ private static RuleFunction getRuleCreationCall(AllocationSample allocationSample) {
+ Object topOfCallstack = Iterables.getLast(allocationSample.callstack, null);
+ if (topOfCallstack instanceof RuleFunction) {
+ return (RuleFunction) topOfCallstack;
+ }
+ return null;
+ }
+
+ /**
+ * Returns the total memory consumption for rules and aspects, keyed by {@link RuleClass#getKey}
+ * or {@link AspectClass#getKey}.
+ */
+ public void getRuleMemoryConsumption(
+ Map<String, RuleBytes> rules, Map<String, RuleBytes> aspects) {
+ // Make sure we don't track our own allocations
+ enabled = false;
+ System.gc();
+
+ // Get loading phase memory for rules.
+ for (AllocationSample allocationSample : allocations.values()) {
+ RuleFunction ruleCreationCall = getRuleCreationCall(allocationSample);
+ if (ruleCreationCall != null) {
+ RuleClass ruleClass = ruleCreationCall.getRuleClass();
+ String key = ruleClass.getKey();
+ RuleBytes ruleBytes = rules.computeIfAbsent(key, k -> new RuleBytes(ruleClass.getName()));
+ rules.put(key, ruleBytes.addBytes(allocationSample.bytes));
+ }
+ }
+ // Get analysis phase memory for rules and aspects
+ for (AllocationSample allocationSample : allocations.values()) {
+ if (allocationSample.ruleClass != null) {
+ String key = allocationSample.ruleClass.getKey();
+ RuleBytes ruleBytes =
+ rules.computeIfAbsent(key, k -> new RuleBytes(allocationSample.ruleClass.getName()));
+ rules.put(key, ruleBytes.addBytes(allocationSample.bytes));
+ }
+ if (allocationSample.aspectClass != null) {
+ String key = allocationSample.aspectClass.getKey();
+ RuleBytes ruleBytes =
+ aspects.computeIfAbsent(
+ key, k -> new RuleBytes(allocationSample.aspectClass.getName()));
+ aspects.put(key, ruleBytes.addBytes(allocationSample.bytes));
+ }
+ }
+
+ enabled = true;
+ }
+
+ /** Dumps all skylark analysis time allocations to a pprof-compatible file. */
+ public void dumpSkylarkAllocations(String path) throws IOException {
+ // Make sure we don't track our own allocations
+ enabled = false;
+ System.gc();
+ Profile profile = buildMemoryProfile();
+ try (GZIPOutputStream outputStream = new GZIPOutputStream(new FileOutputStream(path))) {
+ profile.writeTo(outputStream);
+ outputStream.finish();
+ }
+ enabled = true;
+ }
+
+ Profile buildMemoryProfile() {
+ Profile.Builder profile = Profile.newBuilder();
+ StringTable stringTable = new StringTable(profile);
+ FunctionTable functionTable = new FunctionTable(profile, stringTable);
+ LocationTable locationTable = new LocationTable(profile, functionTable);
+ profile.addSampleType(
+ ValueType.newBuilder()
+ .setType(stringTable.get("memory"))
+ .setUnit(stringTable.get("bytes"))
+ .build());
+ for (AllocationSample allocationSample : allocations.values()) {
+ // Skip empty callstacks
+ if (allocationSample.callstack.isEmpty()) {
+ continue;
+ }
+ Sample.Builder sample = Sample.newBuilder().addValue(allocationSample.bytes);
+ int line = -1;
+ String file = null;
+ for (int i = allocationSample.callstack.size() - 1; i >= 0; --i) {
+ Object object = allocationSample.callstack.get(i);
+ if (line == -1) {
+ final Location location;
+ if (object instanceof ASTNode) {
+ location = ((ASTNode) object).getLocation();
+ } else if (object instanceof BaseFunction) {
+ location = ((BaseFunction) object).getLocation();
+ } else {
+ throw new IllegalStateException(
+ "Unknown node type: " + object.getClass().getSimpleName());
+ }
+ if (location != null) {
+ file = location.getPath() != null ? location.getPath().getPathString() : "<unknown>";
+ line = location.getStartLine() != null ? location.getStartLine() : -1;
+ } else {
+ file = "<native>";
+ }
+ }
+ String function = null;
+ if (object instanceof BaseFunction) {
+ BaseFunction baseFunction = (BaseFunction) object;
+ function = baseFunction.getName();
+ }
+ if (function != null) {
+ sample.addLocationId(
+ locationTable.get(Strings.nullToEmpty(file), Strings.nullToEmpty(function), line));
+ line = -1;
+ file = null;
+ }
+ }
+ profile.addSample(sample.build());
+ }
+ profile.setTimeNanos(Instant.now().getEpochSecond() * 1000000000);
+ return profile.build();
+ }
+
+ private static class StringTable {
+ final Profile.Builder profile;
+ final Map<String, Long> table = new HashMap<>();
+ long index = 0;
+
+ StringTable(Profile.Builder profile) {
+ this.profile = profile;
+ get(""); // 0 is reserved for the empty string
+ }
+
+ long get(String str) {
+ return table.computeIfAbsent(
+ str,
+ key -> {
+ profile.addStringTable(key);
+ return index++;
+ });
+ }
+ }
+
+ private static class FunctionTable {
+ final Profile.Builder profile;
+ final StringTable stringTable;
+ final Map<String, Long> table = new HashMap<>();
+ long index = 1; // 0 is reserved
+
+ FunctionTable(Profile.Builder profile, StringTable stringTable) {
+ this.profile = profile;
+ this.stringTable = stringTable;
+ }
+
+ long get(String file, String function) {
+ return table.computeIfAbsent(
+ file + "#" + function,
+ key -> {
+ Function fn =
+ Function.newBuilder()
+ .setId(index)
+ .setFilename(stringTable.get(file))
+ .setName(stringTable.get(function))
+ .build();
+ profile.addFunction(fn);
+ table.put(key, index);
+ return index++;
+ });
+ }
+ }
+
+ private static class LocationTable {
+ final Profile.Builder profile;
+ final FunctionTable functionTable;
+ final Map<String, Long> table = new HashMap<>();
+ long index = 1; // 0 is reserved
+
+ LocationTable(Builder profile, FunctionTable functionTable) {
+ this.profile = profile;
+ this.functionTable = functionTable;
+ }
+
+ long get(String file, String function, long line) {
+ return table.computeIfAbsent(
+ file + "#" + function + "#" + line,
+ key -> {
+ com.google.perftools.profiles.ProfileProto.Location location =
+ com.google.perftools.profiles.ProfileProto.Location.newBuilder()
+ .setId(index)
+ .addLine(
+ Line.newBuilder()
+ .setFunctionId(functionTable.get(file, function))
+ .setLine(line)
+ .build())
+ .build();
+ profile.addLocation(location);
+ table.put(key, index);
+ return index++;
+ });
+ }
+ }
+}