LinuxPerfNormProfiler.java
/*
* Copyright (c) 2014, 2014, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.jmh.profile;
import joptsimple.OptionException;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.openjdk.jmh.infra.BenchmarkParams;
import org.openjdk.jmh.results.*;
import org.openjdk.jmh.util.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.*;
public class LinuxPerfNormProfiler implements ExternalProfiler {
/** This is a non-exhaustive list of events we care about. */
private static final String[] interestingEvents = new String[]{
"cycles", "instructions",
"branches", "branch-misses",
"L1-dcache-loads", "L1-dcache-load-misses",
"L1-dcache-stores", "L1-dcache-store-misses",
"L1-icache-loads", "L1-icache-load-misses",
"LLC-loads", "LLC-load-misses",
"LLC-stores", "LLC-store-misses",
"dTLB-loads", "dTLB-load-misses",
"dTLB-stores", "dTLB-store-misses",
"iTLB-loads", "iTLB-load-misses",
"stalled-cycles-frontend", "stalled-cycles-backend",
};
private final int delayMs;
private final int lengthMs;
private final boolean useDefaultStats;
private final int incrementInterval;
private final boolean doFilter;
private final Collection<String> supportedEvents = new ArrayList<>();
public LinuxPerfNormProfiler(String initLine) throws ProfilerException {
OptionParser parser = new OptionParser();
parser.formatHelpWith(new ProfilerOptionFormatter("perfnorm"));
OptionSpec<String> optEvents = parser.accepts("events",
"Events to gather.")
.withRequiredArg().ofType(String.class).withValuesSeparatedBy(",").describedAs("event+");
OptionSpec<Integer> optDelay = parser.accepts("delay",
"Delay collection for a given time, in milliseconds; -1 to detect automatically.")
.withRequiredArg().ofType(Integer.class).describedAs("ms").defaultsTo(-1);
OptionSpec<Integer> optLength = parser.accepts("length",
"Do the collection for a given time, in milliseconds; -1 to detect automatically.")
.withRequiredArg().ofType(Integer.class).describedAs("ms").defaultsTo(-1);
OptionSpec<Integer> optIncrementInterval = parser.accepts("interval",
"The interval between incremental updates from a concurrently running perf. " +
"Lower values may improve accuracy, while increasing the profiling overhead.")
.withRequiredArg().ofType(Integer.class).describedAs("ms").defaultsTo(100);
OptionSpec<Boolean> optFilter = parser.accepts("filter",
"Filter problematic samples from infrastructure and perf itself.")
.withRequiredArg().ofType(Boolean.class).describedAs("bool").defaultsTo(true);
OptionSpec<Boolean> optDefaultStat = parser.accepts("useDefaultStat",
"Use \"perf stat -d -d -d\" instead of explicit counter list.")
.withRequiredArg().ofType(Boolean.class).describedAs("bool").defaultsTo(false);
OptionSet set = ProfilerUtils.parseInitLine(initLine, parser);
Collection<String> userEvents;
try {
delayMs = set.valueOf(optDelay);
lengthMs = set.valueOf(optLength);
incrementInterval = set.valueOf(optIncrementInterval);
doFilter = set.valueOf(optFilter);
useDefaultStats = set.valueOf(optDefaultStat);
userEvents = set.valuesOf(optEvents);
} catch (OptionException e) {
throw new ProfilerException(e.getMessage());
}
Collection<String> msgs = Utils.tryWith(PerfSupport.PERF_EXEC, "stat", "--log-fd", "2", "--field-separator", ",", "echo", "1");
if (!msgs.isEmpty()) {
throw new ProfilerException(msgs.toString());
}
Collection<String> incremental = Utils.tryWith(PerfSupport.PERF_EXEC, "stat", "--log-fd", "2", "--field-separator", ",", "--interval-print", String.valueOf(incrementInterval), "echo", "1");
if (!incremental.isEmpty()) {
throw new ProfilerException("\\\"perf\\\" is too old, needs incremental mode (-I).");
}
Collection<String> candidateEvents = new ArrayList<>();
if (userEvents != null) {
for (String ev : userEvents) {
if (ev.trim().isEmpty()) continue;
candidateEvents.add(ev);
}
}
if (candidateEvents.isEmpty()) {
candidateEvents.addAll(Arrays.asList(interestingEvents));
}
for (String ev : candidateEvents) {
String[] senseCmd = { PerfSupport.PERF_EXEC, "stat", "--log-fd", "2", "--field-separator", ",", "--event", ev, "echo", "1" };
Collection<String> res = Utils.tryWith(senseCmd);
if (res.isEmpty()) {
Collection<String> out = Utils.runWith(senseCmd);
if (!PerfSupport.containsUnsupported(out, ev)) {
supportedEvents.add(ev);
}
}
}
if (!useDefaultStats && supportedEvents.isEmpty()) {
throw new ProfilerException("No supported events.");
}
}
@Override
public Collection<String> addJVMInvokeOptions(BenchmarkParams params) {
List<String> cmd = new ArrayList<>();
if (useDefaultStats) {
cmd.addAll(Arrays.asList(PerfSupport.PERF_EXEC, "stat", "--log-fd", "2", "--field-separator", ",", "--detailed", "--detailed", "--detailed"));
} else {
cmd.addAll(Arrays.asList(PerfSupport.PERF_EXEC, "stat", "--log-fd", "2", "--field-separator", ",", "--event", Utils.join(supportedEvents, ",")));
}
cmd.addAll(Arrays.asList("-I", String.valueOf(incrementInterval)));
return cmd;
}
@Override
public Collection<String> addJVMOptions(BenchmarkParams params) {
return Collections.emptyList();
}
@Override
public void beforeTrial(BenchmarkParams params) {
// do nothing
}
@Override
public Collection<? extends Result> afterTrial(BenchmarkResult br, long pid, File stdOut, File stdErr) {
return process(br, stdOut, stdErr);
}
@Override
public boolean allowPrintOut() {
return true;
}
@Override
public boolean allowPrintErr() {
return false;
}
@Override
public String getDescription() {
return "Linux perf statistics, normalized by operation count";
}
private static class EventRecord {
final double time;
final double value;
public EventRecord(double time, double value) {
this.time = time;
this.value = value;
}
}
private Collection<? extends Result> process(BenchmarkResult br, File stdOut, File stdErr) {
Multimap<String, EventRecord> eventRecords = new HashMultimap<>();
try (FileReader fr = new FileReader(stdErr);
BufferedReader reader = new BufferedReader(fr)) {
long skipMs;
if (delayMs == -1) { // not set
skipMs = ProfilerUtils.measurementDelayMs(br);
} else {
skipMs = delayMs;
}
double lenMs;
if (lengthMs == -1) { // not set
lenMs = ProfilerUtils.measuredTimeMs(br);
} else {
lenMs = lengthMs;
}
double readFrom = skipMs / 1000D;
double readTo = (skipMs + lenMs + incrementInterval) / 1000D;
NumberFormat nf = NumberFormat.getInstance();
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("#")) continue;
String[] split = line.split(",");
String time;
String count;
String event;
if (split.length == 3) {
// perf 3.13: time,count,event
time = split[0].trim();
count = split[1].trim();
event = split[2].trim();
} else if (split.length >= 4) {
// perf >3.13: time,count,<other>,event,<others>
time = split[0].trim();
count = split[1].trim();
event = split[3].trim();
} else {
// Malformed line, ignore
continue;
}
double timeSec = 0D;
try {
timeSec = nf.parse(time).doubleValue();
} catch (ParseException e) {
continue;
}
if (timeSec < readFrom || timeSec > readTo) {
// Outside measurement window, ignore
continue;
}
long lValue = 0L;
try {
lValue = nf.parse(count).longValue();
} catch (ParseException e) {
continue;
}
eventRecords.put(event, new EventRecord(timeSec, lValue));
}
Map<String, Double> finalThroughputs = new HashMap<>();
for (String key : eventRecords.keys()) {
List<EventRecord> countedEvents = new ArrayList<>(eventRecords.get(key));
// If needed and possible, filter out a few head and tail iterations.
// Head iteration potentially contains the infrastructure startup.
// That iteration would only be handled for getting time, not the counter itself.
// Trailing iterations come with both infrastructure rampdown, and the final
// profiler output on shutdown. Sometimes these manifest as two separate iterations.
final int filteredCount = countedEvents.size() - 2;
if (doFilter && filteredCount > 0) {
countedEvents = countedEvents.subList(0, filteredCount);
}
double s = 0;
double minTime = Double.MAX_VALUE;
double maxTime = Double.MIN_VALUE;
for (int i = 0; i < countedEvents.size(); i++) {
EventRecord v = countedEvents.get(i);
if (i != 0) {
// Do not count the first event in the series, since time interval
// does not actually include it.
s += v.value;
}
minTime = Math.min(minTime, v.time);
maxTime = Math.max(maxTime, v.time);
}
double thr = s / (maxTime - minTime);
finalThroughputs.put(key, thr);
}
BenchmarkResultMetaData md = br.getMetadata();
if (md == null) {
return emptyResults();
}
long timeMs = md.getStopTime() - md.getMeasurementTime();
if (timeMs == 0) {
return emptyResults();
}
double opsThroughput = 1000D * md.getMeasurementOps() / timeMs;
if (opsThroughput == 0) {
return emptyResults();
}
Collection<Result> results = new ArrayList<>();
for (String key : finalThroughputs.keySet()) {
results.add(new PerfResult(key, "#/op", finalThroughputs.get(key) / opsThroughput));
}
// Also figure out IPC/CPI, if enough counters available:
{
Double c1 = finalThroughputs.get("cycles");
Double c2 = finalThroughputs.get("cycles:u");
Double i1 = finalThroughputs.get("instructions");
Double i2 = finalThroughputs.get("instructions:u");
Double cycles = (c1 != null) ? c1 : c2;
Double instructions = (i1 != null) ? i1 : i2;
if (cycles != null && instructions != null &&
cycles != 0 && instructions != 0) {
results.add(new PerfResult("CPI", "clks/insn", cycles / instructions));
results.add(new PerfResult("IPC", "insns/clk", instructions / cycles));
}
}
return results;
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
private static Set<PerfResult> emptyResults() {
return Collections.singleton(new PerfResult("N/A", "", Double.NaN));
}
static class PerfResult extends ScalarResult {
private static final long serialVersionUID = -1262685915873231436L;
public PerfResult(String key, String unit, double value) {
super(key, value, unit, AggregationPolicy.AVG);
}
@Override
public String extendedInfo() {
// omit printing in extended info
return "";
}
}
}