blob: 52e06d77c63ecc06f761bb8aea4ea29a8b10235a [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 static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.packages.RuleClass;
import com.google.devtools.build.lib.packages.RuleFunction;
import com.google.devtools.build.lib.profiler.memory.AllocationTracker.RuleBytes;
import com.google.devtools.build.lib.syntax.Debug;
import com.google.devtools.build.lib.syntax.EvalException;
import com.google.devtools.build.lib.syntax.EvalUtils;
import com.google.devtools.build.lib.syntax.FileOptions;
import com.google.devtools.build.lib.syntax.HasBinary;
import com.google.devtools.build.lib.syntax.Module;
import com.google.devtools.build.lib.syntax.Mutability;
import com.google.devtools.build.lib.syntax.ParserInput;
import com.google.devtools.build.lib.syntax.Starlark;
import com.google.devtools.build.lib.syntax.StarlarkCallable;
import com.google.devtools.build.lib.syntax.StarlarkSemantics;
import com.google.devtools.build.lib.syntax.StarlarkThread;
import com.google.devtools.build.lib.syntax.SyntaxError;
import com.google.devtools.build.lib.syntax.TokenKind;
import com.google.perftools.profiles.ProfileProto.Function;
import com.google.perftools.profiles.ProfileProto.Profile;
import com.google.perftools.profiles.ProfileProto.Sample;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for {@link AllocationTracker}. */
@RunWith(JUnit4.class)
public final class AllocationTrackerTest {
// These tests are quite artificial as they call sampleAllocation explicitly.
// In reality, a call could occur after any 'new' operation.
private AllocationTracker tracker;
private final ArrayList<Object> live = new ArrayList<>();
// A Starlark value whose plus operator "x + 123" simulates allocation of 123 bytes.
// (We trigger allocation with an operator not a function call so as not to change the stack.)
private class SamplerValue implements HasBinary {
@Override
public Object binaryOp(TokenKind op, Object that, boolean thisLeft) throws EvalException {
if (op == TokenKind.PLUS && thisLeft && that instanceof Integer) {
int size = (Integer) that;
Object obj = new Object();
live.add(obj); // ensure that obj outlives the test assertions
tracker.sampleAllocation(1, "", obj, size);
return Starlark.NONE;
}
return null;
}
}
private static RuleClass myRuleClass() {
RuleClass myrule = mock(RuleClass.class);
when(myrule.getName()).thenReturn("myrule");
when(myrule.getKey()).thenReturn("myrule");
return myrule;
}
@Before
public void setup() {
CurrentRuleTracker.setEnabled(true);
tracker = new AllocationTracker(1, 0);
Debug.setThreadHook(tracker);
}
@After
public void tearDown() {
Debug.setThreadHook(null);
CurrentRuleTracker.setEnabled(false);
}
@Test
public void testMemoryProfileDuringExecution() throws Exception {
// The nop() calls force the frame PC location to be updated.
// It is not updated for a + operation on the assumption that
// the stack is unobservable to an implementation of the +
// operator... but the AllocationTracker sneaks a peek at it
// using thread-local storage.
// TODO(b/149023294): update this when we use a compiled representation.
exec(
"def nop(): pass",
"def g():",
" nop(); sample + 12", // sample[0]: 12 bytes
"def f():",
" g()",
" nop(); sample + 73", // sample[1]: 73 bytes
"f()");
Map<String, RuleBytes> rules = new HashMap<>();
Map<String, RuleBytes> aspects = new HashMap<>();
tracker.getRuleMemoryConsumption(rules, aspects);
assertThat(rules).isEmpty();
assertThat(aspects).isEmpty();
Profile profile = tracker.buildMemoryProfile();
assertThat(profile.getSampleList()).hasSize(2);
Set<String> lines = new HashSet<>();
for (Sample s : profile.getSampleList()) {
lines.add(sampleToCallstack(profile, s));
}
assertThat(lines).contains("a.star:f:6, a.star:<toplevel>:7");
assertThat(lines).contains("a.star:g:3, a.star:f:5, a.star:<toplevel>:7");
}
/** Formats a call stack as a comma-separated list of file:function:line elements. */
private static String sampleToCallstack(Profile profile, Sample sample) {
StringBuilder buf = new StringBuilder();
for (long locationId : sample.getLocationIdList()) {
com.google.perftools.profiles.ProfileProto.Location location =
profile.getLocation((int) locationId - 1);
assertThat(location.getLineList()).hasSize(1);
long functionId = location.getLine(0).getFunctionId();
long line = location.getLine(0).getLine();
Function function = profile.getFunction((int) functionId - 1);
long fileId = function.getFilename();
long methodId = function.getName();
String file = profile.getStringTable((int) fileId);
String method = profile.getStringTable((int) methodId);
if (buf.length() > 0) {
buf.append(", ");
}
buf.append(String.format("%s:%s:%d", file, method, line));
}
return buf.toString();
}
@Test
public void testConfiguredTargetsMemoryAllocation() throws Exception {
CurrentRuleTracker.beginConfiguredTarget(myRuleClass());
Object ruleAllocation0 = new Object();
Object ruleAllocation1 = new Object();
tracker.sampleAllocation(1, "", ruleAllocation0, 10);
tracker.sampleAllocation(1, "", ruleAllocation1, 20);
CurrentRuleTracker.endConfiguredTarget();
CurrentRuleTracker.beginConfiguredAspect(() -> "aspect");
Object aspectAllocation = new Object();
tracker.sampleAllocation(1, "", aspectAllocation, 12);
CurrentRuleTracker.endConfiguredAspect();
Map<String, RuleBytes> rules = new HashMap<>();
Map<String, RuleBytes> aspects = new HashMap<>();
tracker.getRuleMemoryConsumption(rules, aspects);
assertThat(rules).containsExactly("myrule", new RuleBytes("myrule").addBytes(30L));
assertThat(aspects).containsExactly("aspect", new RuleBytes("aspect").addBytes(12L));
Profile profile = tracker.buildMemoryProfile();
assertThat(profile.getSampleList()).isEmpty(); // no callstacks
}
@Test
public void testLoadingPhaseRuleAllocations() throws Exception {
exec(
"def g():", //
" myrule()",
"def f():",
" g()",
"f()");
Map<String, RuleBytes> rules = new HashMap<>();
Map<String, RuleBytes> aspects = new HashMap<>();
tracker.getRuleMemoryConsumption(rules, aspects);
assertThat(rules).containsExactly("myrule", new RuleBytes("myrule").addBytes(128L));
}
private void exec(String... lines)
throws SyntaxError.Exception, EvalException, InterruptedException {
ParserInput input = ParserInput.fromString(Joiner.on("\n").join(lines), "a.star");
Module module =
Module.withPredeclared(
StarlarkSemantics.DEFAULT,
ImmutableMap.of(
"sample", new SamplerValue(),
"myrule", new MyRuleFunction()));
try (Mutability mu = Mutability.create("test")) {
StarlarkThread thread = new StarlarkThread(mu, StarlarkSemantics.DEFAULT);
EvalUtils.exec(input, FileOptions.DEFAULT, module, thread);
}
}
// A fake Bazel rule. The allocation tracker reports retained memory broken down by rule class.
private class MyRuleFunction implements RuleFunction, StarlarkCallable {
@Override
public Object fastcall(StarlarkThread thread, Object[] parameters, Object[] named) {
Object obj = new Object();
live.add(obj); // ensure that obj outlives the test assertions
tracker.sampleAllocation(1, "", obj, 128);
return Starlark.NONE;
}
@Override
public String getName() {
return "myrule";
}
@Override
public RuleClass getRuleClass() {
return myRuleClass();
}
}
}