blob: ddc67d5aa7f07e9d8a0b010674667d5c863a1bb3 [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.callcounts;
import com.google.common.collect.Interner;
import com.google.common.collect.Interners;
import com.google.perftools.profiles.ProfileProto.Function;
import com.google.perftools.profiles.ProfileProto.Line;
import com.google.perftools.profiles.ProfileProto.Location;
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.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.zip.GZIPOutputStream;
/**
* Developer utility. Add calls to {@link Callcounts#registerCall} in places that you want to count
* call occurrences with call stacks. At the end of the command, a pprof-compatible file will be
* dumped to the path specified by --call_count_output_path (see also {@link CallcountsModule}).
*/
public class Callcounts {
private static int maxCallstackDepth;
// Every Nth call is actually logged
private static int samplePeriod;
/**
* We use some variance to avoid patterns where the same calls get logged over and over.
*
* <p>Without a little variance, we could end up having call patterns that line up with the sample
* period, leading to the same calls getting sampled over and over again. For instance, consider
* 90 calls to method A followed by 10 calls to method B, over and over. If our sample period was
* 100 then we could end up in a situation where either only method A or (worse) only method B
* gets sampled.
*/
private static int sampleVariance;
private static final Random random = new Random();
private static final AtomicLong currentSampleCount = new AtomicLong();
private static final AtomicLong nextSampleCount = new AtomicLong();
private static final Interner<Callstack> callstacks =
Interners.newBuilder().weak().concurrencyLevel(8).build();
private static final Map<Callstack, Long> callstackCounts = new ConcurrentHashMap<>();
static class Callstack {
final StackTraceElement[] stackTraceElements;
Callstack(StackTraceElement[] stackTraceElements) {
this.stackTraceElements = stackTraceElements;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Callstack entry = (Callstack) o;
return Arrays.equals(stackTraceElements, entry.stackTraceElements);
}
@Override
public int hashCode() {
return Arrays.hashCode(stackTraceElements);
}
}
/** Call to register a call trace at the current call location */
public static void registerCall() {
// All of this is totally thread-unsafe for speed
// The worst that can happen is we occasionally over-sample
if (currentSampleCount.incrementAndGet() < nextSampleCount.get()) {
return;
}
long count = 0;
synchronized (Callcounts.class) {
// Check again if somebody else already got here first
// This greatly improves performance compared to eager locking
if (currentSampleCount.get() < nextSampleCount.get()) {
return;
}
count = currentSampleCount.get();
resetSampleTarget();
}
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
int skip = 2; // Otherwise we show up ourselves
int len = Math.min(stackTrace.length - skip, maxCallstackDepth);
if (len <= 0) {
return;
}
StackTraceElement[] entries = new StackTraceElement[len];
for (int i = 0; i < len; ++i) {
entries[i] = stackTrace[i + skip];
}
Callstack callstack = new Callstack(entries);
callstack = callstacks.intern(callstack);
callstackCounts.put(callstack, callstackCounts.getOrDefault(callstack, 0L) + count);
}
static void resetSampleTarget() {
currentSampleCount.set(0);
nextSampleCount.set(
samplePeriod
+ (sampleVariance > 0 ? random.nextInt(sampleVariance * 2) - sampleVariance : 0));
}
static void init(int samplePeriod, int sampleVariance, int maxCallstackDepth) {
Callcounts.samplePeriod = samplePeriod;
Callcounts.sampleVariance = sampleVariance;
Callcounts.maxCallstackDepth = maxCallstackDepth;
resetSampleTarget();
}
static void reset() {
callstackCounts.clear();
}
static void dump(String path) throws IOException {
Profile profile = createProfile();
try (GZIPOutputStream gzipOutputStream = new GZIPOutputStream(new FileOutputStream(path))) {
profile.writeTo(gzipOutputStream);
}
}
static Profile createProfile() {
Profile.Builder profile = Profile.newBuilder();
StringTable stringTable = new StringTable(profile);
profile.addSampleType(
ValueType.newBuilder()
.setType(stringTable.get("calls"))
.setUnit(stringTable.get("count"))
.build());
FunctionTable functionTable = new FunctionTable(profile, stringTable);
LocationTable locationTable = new LocationTable(profile, functionTable);
for (Map.Entry<Callstack, Long> entry : callstackCounts.entrySet()) {
Sample.Builder sample = Sample.newBuilder();
sample.addValue(entry.getValue());
for (StackTraceElement stackTraceElement : entry.getKey().stackTraceElements) {
String name = stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName();
int line = stackTraceElement.getLineNumber();
sample.addLocationId(locationTable.get(name, line));
}
profile.addSample(sample);
}
Instant instant = Instant.now();
profile.setTimeNanos(instant.getEpochSecond() * 1_000_000_000);
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 function) {
return table.computeIfAbsent(
function,
key -> {
Function fn =
Function.newBuilder().setId(index).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 function, long line) {
return table.computeIfAbsent(
function + "#" + line,
key -> {
Location location =
Location.newBuilder()
.setId(index)
.addLine(
Line.newBuilder()
.setFunctionId(functionTable.get(function))
.setLine(line)
.build())
.build();
profile.addLocation(location);
return index++;
});
}
}
}