blob: 970cac4a5a4b5e392f1118e84a7bb496b6f86997 [file] [log] [blame]
// 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.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().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);
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(Profile.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);
return index++;
});
}
}
}