Merge pull request #2978 from ebean-orm/feature/stackWalker

Use StackWalker for ProfileLocation and CallOrigin
This commit is contained in:
Rob Bygrave
2023-02-21 14:35:58 +13:00
committed by GitHub
9 changed files with 107 additions and 84 deletions
@@ -2,6 +2,7 @@ package io.ebean.bean;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
import static io.ebean.util.EncodeB64.enc;
@@ -27,19 +28,26 @@ public final class CallStack implements Serializable, CallOrigin {
private final String zeroHash;
private final String pathHash;
private final StackTraceElement[] callStack;
private final Object[] callStack;
private final int hc;
public CallStack(StackTraceElement[] callStack, int zeroHash, int pathHash) {
public CallStack(Object[] callStack, int zeroHash, int pathHash) {
this.callStack = callStack;
this.hc = computeHashCode();
this.zeroHash = enc(zeroHash);
this.pathHash = enc(pathHash);
}
public CallStack(List<StackWalker.StackFrame> frames) {
this.callStack = frames.toArray(new Object[0]);
this.hc = computeHashCode();
this.zeroHash = enc(callStack[0].hashCode());
this.pathHash = enc(hc);
}
private int computeHashCode() {
int hc = 0;
for (StackTraceElement element : callStack) {
for (Object element : callStack) {
hc = 92821 * hc + element.hashCode();
}
return hc;
@@ -0,0 +1,28 @@
package io.ebean.util;
import java.util.function.Predicate;
/**
* Provides a stack filter that excludes ebean and jdk code.
*/
public final class StackWalkFilter {
private static final Filter FILTER = new Filter();
/**
* Return a stack filter that excludes ebean and jdk code.
*/
public static Predicate<StackWalker.StackFrame> filter() {
return FILTER;
}
private static class Filter implements Predicate<StackWalker.StackFrame> {
@Override
public boolean test(StackWalker.StackFrame stackFrame) {
return !stackFrame.getClassName().startsWith("io.ebean")
&& !stackFrame.getClassName().startsWith("jdk.")
&& !stackFrame.getMethodName().startsWith("_ebean_");
}
}
}
@@ -2,17 +2,17 @@ package io.ebeaninternal.server.core;
import io.ebean.bean.CallOrigin;
import io.ebean.bean.CallStack;
import io.ebean.util.StackWalkFilter;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Default CallStackFactory where the Hash function for StackTraceElement includes the line number.
*/
public final class DefaultCallOriginFactory implements CallOriginFactory {
private static final int IGNORE_LEADING_ELEMENTS = 5;
private static final String IO_EBEAN = "io.ebean";
final class DefaultCallOriginFactory implements CallOriginFactory {
private final int maxCallStack;
@@ -22,57 +22,18 @@ public final class DefaultCallOriginFactory implements CallOriginFactory {
@Override
public CallOrigin createCallOrigin() {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
// ignore the first 6 as they are always avaje stack elements
int startIndex = IGNORE_LEADING_ELEMENTS;
// find the first non-avaje stackElement
for (; startIndex < stackTrace.length; startIndex++) {
if (!ignore(stackTrace[startIndex])) {
break;
}
}
int stackLength = stackTrace.length - startIndex;
if (stackLength > maxCallStack) {
// maximum of maxCallStack stackTrace elements
stackLength = maxCallStack;
}
// create the 'interesting' part of the stackTrace
StackTraceElement[] finalTrace = new StackTraceElement[stackLength];
System.arraycopy(stackTrace, startIndex, finalTrace, 0, stackLength);
if (stackLength < 1) {
final var frames = StackWalker.getInstance().walk(this::filter);
if (frames.isEmpty()) {
// this should not really happen
throw new RuntimeException("StackTraceElement size 0? stack: " + Arrays.toString(stackTrace));
throw new RuntimeException("stackFrames filtered to empty for stack: " + Arrays.toString(Thread.currentThread().getStackTrace()));
}
return createCallStack(finalTrace);
return new CallStack(frames);
}
private boolean ignore(StackTraceElement element) {
if (element.getClassName().startsWith(IO_EBEAN)) {
return true;
}
return element.getMethodName().startsWith("_ebean_");
}
private CallOrigin createCallStack(StackTraceElement[] finalTrace) {
return new CallStack(finalTrace, finalTrace[0].hashCode(), pathHash(finalTrace));
}
/**
* Return the hash code for the path excluding the first element.
*/
private int pathHash(StackTraceElement[] callStack) {
int hc = 0;
for (int i = 1; i < callStack.length; i++) {
hc = 92821 * hc + callStack[i].hashCode();
}
return hc;
private <T> List<StackWalker.StackFrame> filter(Stream<StackWalker.StackFrame> frames) {
return frames.filter(StackWalkFilter.filter())
.limit(maxCallStack)
.collect(Collectors.toList());
}
}
@@ -10,7 +10,7 @@ final class NoopCallOriginFactory implements CallOriginFactory {
private static final StackTraceElement E0 = new StackTraceElement("none", "none", "none", 0);
private final CallOrigin COMMON = new CallStack(new StackTraceElement[]{E0}, 0, 0);
private final CallOrigin COMMON = new CallStack(new Object[]{E0}, 0, 0);
@Override
public CallOrigin createCallOrigin() {
@@ -1,13 +1,15 @@
package io.ebeaninternal.server.profile;
import io.ebean.ProfileLocation;
import io.ebean.util.StackWalkFilter;
import java.util.stream.Stream;
/**
* Default profile location that uses stack trace.
*/
class DProfileLocation implements ProfileLocation {
private static final String IO_EBEAN = "io.ebean";
private static final String UNKNOWN = "unknown";
private String fullLocation;
@@ -88,14 +90,14 @@ class DProfileLocation implements ProfileLocation {
}
private String create() {
// relatively expensive but we only do it once per profile location
StackTraceElement[] trace = Thread.currentThread().getStackTrace();
for (int i = 3; i < trace.length; i++) {
if (!trace[i].getClassName().startsWith(IO_EBEAN)) {
return withLineNumber(trace[i].toString());
}
}
return UNKNOWN;
return StackWalker.getInstance().walk(this::filter);
}
private String filter(Stream<StackWalker.StackFrame> frames) {
return frames.filter(StackWalkFilter.filter())
.findFirst()
.map(line -> withLineNumber(line.toString()))
.orElse(UNKNOWN);
}
private String withLineNumber(String traceLine) {
@@ -0,0 +1,11 @@
package io.ebeaninternal.server.core;
/**
* Make DefaultCallOriginFactory accessible to tests.
*/
public class HelpDefaultCallOriginFactory {
public static CallOriginFactory create(int maxStack) {
return new DefaultCallOriginFactory(maxStack);
}
}
@@ -54,21 +54,10 @@ class BasicProfileLocationTest {
void obtain() {
DProfileLocation loc = new DTimedProfileLocation(12, "foo", MetricFactory.get().createTimedMetric("junk"));
String javaVersion = System.getProperty("java.version");
assertThat(loc.obtain()).isTrue();
if (javaVersion.startsWith("1.8")) {
assertThat(loc.fullLocation()).endsWith("invoke0(Native Method:12)");
assertThat(loc.location()).isEqualTo("sun.reflect.NativeMethodAccessorImpl.invoke0");
assertThat(loc.label()).isEqualTo("NativeMethodAccessorImpl.invoke0");
} else if (javaVersion.startsWith("18") || javaVersion.startsWith("19") || javaVersion.startsWith("20")){
assertThat(loc.fullLocation()).endsWith("jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)");
assertThat(loc.location()).isEqualTo("java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke");
assertThat(loc.label()).isEqualTo("DirectMethodHandleAccessor.invoke");
} else if (javaVersion.startsWith("11") || javaVersion.startsWith("17")) {
assertThat(loc.fullLocation()).endsWith("jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method:12)");
assertThat(loc.location()).isEqualTo("java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0");
assertThat(loc.label()).isEqualTo("NativeMethodAccessorImpl.invoke0");
}
assertThat(loc.fullLocation()).endsWith("org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725)");
assertThat(loc.location()).isEqualTo("org.junit.platform.commons.util.ReflectionUtils.invokeMethod");
assertThat(loc.label()).isEqualTo("ReflectionUtils.invokeMethod");
}
@Test
@@ -0,0 +1,25 @@
package org.tests.server;
import io.ebean.bean.CallOrigin;
import io.ebeaninternal.server.core.CallOriginFactory;
import io.ebeaninternal.server.core.HelpDefaultCallOriginFactory;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class TestDefaultCallOriginFactory {
private final CallOriginFactory factory = HelpDefaultCallOriginFactory.create(2);
@Test
void createCallOrigin() {
CallOrigin callOrigin = inner();
String topElement = callOrigin.getTopElement();
assertThat(topElement).contains("org.tests.server.TestDefaultCallOriginFactory.inner(TestDefaultCallOriginFactory.java:23)");
assertThat(callOrigin.getFullDescription()).contains("TestDefaultCallOriginFactory.java:16");
}
private CallOrigin inner() {
return factory.createCallOrigin();
}
}
@@ -5,7 +5,7 @@ import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class ProfileLocationTest {
class ProfileLocationTest {
private static final ProfileLocation loc = ProfileLocation.create(12, "foo");
private static final ProfileLocation locB = ProfileLocation.create();
@@ -17,7 +17,7 @@ public class ProfileLocationTest {
}
@Test
public void test_obtain() {
void test_obtain() {
assertThat(doIt()).isTrue();
assertThat(loc.fullLocation()).isEqualTo("org.tests.profile.ProfileLocationTest.doIt(ProfileLocationTest.java:16)");
assertThat(loc.location()).isEqualTo("org.tests.profile.ProfileLocationTest.doIt");
@@ -30,13 +30,12 @@ public class ProfileLocationTest {
}
@Test
public void test_add() {
void test_add() {
loc.add(100);
}
@Test
public void test_constructor() {
void test_constructor() {
Other other = new Other();
other.hashCode();