XCTraceTableProfileHandler.java
/*
* Copyright (c) 2024, 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 org.xml.sax.Attributes;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* Parses xctrace profiling results tables and invokes a callback on parsed samples.
* <p/>
* All supported sampling tables ({@link XCTraceTableHandler.ProfilingTableType})
* share almost identical (for our purposes) schema where only an element with sample weight differs.
* <p/>
* Here's an example with some unused elements being omitted:
* <pre>
* <row>
* <sample-time id="1" fmt="00:00.464.248">464248740</sample-time>
* ...
* <cycle-weight id="9" fmt="322.13 Kc">322133</cycle-weight>
* <backtrace id="10">
* <frame id="11" name="dyld4::PrebuiltLoader::isValid(dyld4::RuntimeState const&) const" addr="0x7ff805e6c784">
* <binary id="12" name="dyld" UUID="28FD2071-57F3-3873-87BF-E4F674A82DE6" arch="x86_64" load-addr="0x7ff805e48000" path="/usr/lib/dyld" />
* </frame>
* ...
* </backtrace>
* </row>
* </pre>
* Depending on a table type, there might be "weight" or "pmc-event" elements instead if cycle-weight.
* <p/>
* The format deduplicates identical elements by referencing them (an attribute "ref" matching
* a corresponding "id" attribute of the original element) instead of placing a copy.
*/
final class XCTraceTableProfileHandler extends XCTraceTableHandler {
private static final long UNKNOWN_ADDRESS = -1L;
private final ProfilingTableType tableType;
// Cache of previously parsed elements to use in place of ref-elements.
private final Map<Long, TraceElement> entriesCache = new HashMap<>();
// Stack of xml elements currently being parsed.
// A new value pushed on an element start and popped on an element end.
private final List<TraceElement> entriesStack = new ArrayList<>();
private final Consumer<XCTraceSample> callback;
private XCTraceSample currentSample;
/**
* Constructs the handler.
*
* @param tableType The type of the table that needs to be pared (used for validation only).
* @param onSample A callback that will be invoked on a sample once it parsed.
*/
public XCTraceTableProfileHandler(ProfilingTableType tableType, Consumer<XCTraceSample> onSample) {
this.tableType = tableType;
callback = onSample;
}
private static long parseId(Attributes attributes) {
return Long.parseLong(attributes.getValue(XCTraceTableHandler.ID));
}
private static long parseRef(Attributes attributes) {
return Long.parseLong(attributes.getValue(XCTraceTableHandler.REF));
}
private static long parseAddress(Attributes attributes) {
String val = attributes.getValue(XCTraceTableHandler.ADDRESS);
if (!val.startsWith("0x")) {
throw new IllegalStateException("Unexpected address format: " + val);
}
try {
return Long.parseUnsignedLong(val.substring(2), 16);
} catch (Exception e) {
throw new IllegalStateException("Failed to parse " + val, e);
}
}
private static String parseName(Attributes attributes) {
return attributes.getValue(XCTraceTableHandler.NAME);
}
private static boolean hasRef(Attributes attributes) {
return attributes.getValue(XCTraceTableHandler.REF) != null;
}
private <T extends TraceElement> void cache(T e) {
TraceElement old = entriesCache.put(e.getId(), e);
if (old != null) {
throw new IllegalStateException("Duplicate entry for key " + e.getId() + ". New value: "
+ e + ", old value: " + old);
}
}
private <T extends TraceElement> T get(long id) {
TraceElement value = entriesCache.get(id);
if (value == null) {
throw new IllegalStateException("Entry not found in cache for id " + id);
}
@SuppressWarnings("unchecked")
T res = (T) value;
return res;
}
private <T extends TraceElement> void pushCachedOrNew(Attributes attributes, Function<Long, T> factory) {
if (!hasRef(attributes)) {
T value = factory.apply(parseId(attributes));
cache(value);
entriesStack.add(value);
return;
}
entriesStack.add(get(parseRef(attributes)));
}
private <T extends TraceElement> T pop() {
@SuppressWarnings("unchecked")
T res = (T) entriesStack.remove(entriesStack.size() - 1);
return res;
}
private <T extends TraceElement> T peek() {
@SuppressWarnings("unchecked")
T res = (T) entriesStack.get(entriesStack.size() - 1);
return res;
}
private LongHolder popAndUpdateLongHolder() {
LongHolder value = pop();
if (isNeedToParseCharacters()) {
value.setValue(Long.parseLong(getCharacters()));
}
return value;
}
private static Frame tryParseLegacyBacktrace(Attributes attributes) {
String fmt = attributes.getValue(XCTraceTableHandler.FMT);
if (fmt == null) {
return null;
}
String nameOrAddr = fmt.split("���")[0].trim();
Frame frame = new Frame(-1 /* fake frame */, nameOrAddr,
UNKNOWN_ADDRESS /* need to parse nested text-addresses */);
// Legacy backtraces missing info about a library a symbol belongs to. But if a symbol's name is known,
// then it's definitely not JIT-compiled code. In that case [unknown] binary name is used to improve profiling
// results.
if (!nameOrAddr.startsWith("0x")) {
frame.setBinary("[unknown]");
}
return frame;
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
// check that <schema> has required table name
if (qName.equals(XCTraceTableHandler.SCHEMA)) {
String schemaName = parseName(attributes);
if (!tableType.tableName.equals(schemaName)) {
throw new IllegalStateException("Results contains schema with unexpected name: " + schemaName);
}
return;
}
switch (qName) {
case XCTraceTableHandler.SAMPLE:
currentSample = new XCTraceSample();
break;
case XCTraceTableHandler.SAMPLE_TIME:
case XCTraceTableHandler.CYCLE_WEIGHT:
case XCTraceTableHandler.WEIGHT:
case XCTraceTableHandler.PMC_EVENT:
case XCTraceTableHandler.TEXT_ADDRESSES:
pushCachedOrNew(attributes, id -> {
setNeedParseCharacters(true);
return new LongHolder(id);
});
break;
case XCTraceTableHandler.BACKTRACE:
// Starting from version ~14.3 backtraces contains all required details and saved as a sequence
// of <frame> elements.
// For older versions, there are no frames. Instead, backtraces have "fmt" attribute which contains
// the name of the symbol on the top of the call stack. Addresses are stored in a few nested
// <text-addresses> elements.
pushCachedOrNew(attributes, id -> {
ValueHolder<Frame> holder = new ValueHolder<Frame>(id);
holder.setValue(tryParseLegacyBacktrace(attributes));
return holder;
});
break;
case XCTraceTableHandler.BINARY:
pushCachedOrNew(attributes, id -> new ValueHolder<>(id, parseName(attributes)));
break;
case XCTraceTableHandler.FRAME:
// Addresses in cpu-* tables are always biased by 1, on both X86_64 and AArch64.
// At the same type, corresponding source tables contain correct addressed.
// See: https://developer.apple.com/forums/thread/748112
pushCachedOrNew(attributes, id -> new Frame(id, parseName(attributes),
parseAddress(attributes) - 1L));
break;
case XCTraceTableHandler.PMC_EVENTS:
pushCachedOrNew(attributes, id -> {
setNeedParseCharacters(true);
return new ValueHolder<long[]>(id);
});
break;
}
}
@Override
public void endElement(String uri, String localName, String qName) {
if (qName.equals(XCTraceTableHandler.NODE)) {
return;
}
switch (qName) {
case XCTraceTableHandler.SAMPLE:
callback.accept(currentSample);
currentSample = null;
break;
case XCTraceTableHandler.SAMPLE_TIME: {
LongHolder value = popAndUpdateLongHolder();
currentSample.setTime(value.getValue());
break;
}
case XCTraceTableHandler.CYCLE_WEIGHT:
case XCTraceTableHandler.WEIGHT:
case XCTraceTableHandler.PMC_EVENT:
// in practice, a sample's row will contain only one of these
LongHolder value = popAndUpdateLongHolder();
currentSample.setWeight(value.getValue());
break;
case XCTraceTableHandler.BACKTRACE:
Frame topFrame = this.<ValueHolder<Frame>>pop().getValue();
currentSample.setTopFrame(topFrame.getAddress(), topFrame.getName(), topFrame.getBinary());
break;
case XCTraceTableHandler.BINARY:
ValueHolder<String> bin = pop();
this.<Frame>peek().setBinary(bin.getValue());
break;
case XCTraceTableHandler.FRAME:
Frame frame = pop();
ValueHolder<Frame> backtrace = peek();
// we only need a top frame
if (backtrace.getValue() == null) {
backtrace.setValue(frame);
}
break;
case XCTraceTableHandler.TEXT_ADDRESSES: {
LongHolder addresses = pop();
if (isNeedToParseCharacters()) {
// peek only the first address as we're not interested in the whole backtrace
addresses.setValue(Arrays.stream(getCharacters().split(" "))
.mapToLong(Long::parseUnsignedLong).findFirst().orElse(UNKNOWN_ADDRESS));
}
ValueHolder<Frame> bt = peek();
// For legacy backtraces, the address is initially UNKNOWN_ADDRESS.
// It is then updated by the top-most address extracted from text-addresses elements.
if (bt.getValue().getAddress() == UNKNOWN_ADDRESS && addresses.getValue() != UNKNOWN_ADDRESS) {
bt.getValue().setAddress(addresses.getValue());
}
break;
}
case XCTraceTableHandler.PMC_EVENTS:
ValueHolder<long[]> events = pop();
if (isNeedToParseCharacters()) {
events.setValue(Arrays.stream(getCharacters().split(" "))
.mapToLong(Long::parseLong).toArray());
}
currentSample.setPmcValues(events.getValue());
break;
}
setNeedParseCharacters(false);
}
@Override
public void endDocument() {
entriesCache.clear();
entriesStack.clear();
}
private static abstract class TraceElement {
private final long id;
public TraceElement(long id) {
this.id = id;
}
public long getId() {
return id;
}
}
private static final class ValueHolder<T> extends TraceElement {
private T value;
ValueHolder(long id, T value) {
super(id);
this.value = value;
}
ValueHolder(long id) {
this(id, null);
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
private static final class LongHolder extends TraceElement {
private long value;
public LongHolder(long id) {
super(id);
}
public long getValue() {
return value;
}
public void setValue(long value) {
this.value = value;
}
}
private static final class Frame extends TraceElement {
private final String name;
private long address;
private String binary;
public Frame(long id, String name, long address) {
super(id);
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public long getAddress() {
return address;
}
public void setAddress(long address) {
this.address = address;
}
public String getBinary() {
return binary;
}
public void setBinary(String binary) {
this.binary = binary;
}
}
static class XCTraceSample {
public static final String TIME_SAMPLE_TRIGGER_NAME = "TIME_MICRO_SEC";
private static final long[] EMPTY_ARRAY = new long[0];
private long timeFromStartNs;
private long weight;
private String symbol;
private long address;
private String binary;
private long[] pmcValues = EMPTY_ARRAY;
public void setTopFrame(long address, String symbol, String binary) {
this.address = address;
this.symbol = symbol;
this.binary = binary;
}
public void setWeight(long weight) {
this.weight = weight;
}
public void setTime(long time) {
timeFromStartNs = time;
}
public long getTimeFromStartNs() {
return timeFromStartNs;
}
public long getWeight() {
return weight;
}
public long getAddress() {
return address;
}
public String getBinary() {
return binary;
}
public String getSymbol() {
return symbol;
}
public long[] getPmcValues() {
return pmcValues;
}
public void setPmcValues(long[] values) {
pmcValues = values;
}
}
}