Skip to content

Commit

Permalink
Use StackWalker for TTEXT serialization format when on Java 9+ (#1686) (
Browse files Browse the repository at this point in the history
#2055)

Motivation:

In `StructContext`, we need to acquire the class name from the stack trace using `Thread.current().getStackTrace()`:

- https://github.com/line/armeria/blob/armeria-0.82.0/thrift/src/main/java/com/linecorp/armeria/common/thrift/text/StructContext.java#L104-L164

However, `getStackTrace()` is a high-overhead operation since it has to walk the call stack up to its root. We could leverage Java 9's stack-walking API so we can stop climbing the call stack when we found what we want.

Modification:

Use StackWalker for getting a full stack trace in Thrift TEXT serialization format for Java 9+

Result:

- Better TTEXT performance on Java 9+
- Closes #1686
  • Loading branch information
moonchanyong authored and trustin committed Sep 20, 2019
1 parent 1e839a8 commit df3fde1
Show file tree
Hide file tree
Showing 5 changed files with 414 additions and 58 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright 2019 LINE Corporation
*
* LINE Corporation licenses this file to you 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:
*
* https://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.
*/
// =================================================================================================
// Copyright 2011 Twitter, Inc.
// -------------------------------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this work except in compliance with the License.
// You may obtain a copy of the License in the LICENSE file, or at:
//
// https://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.linecorp.armeria.common.thrift.text;

import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.function.Supplier;

import javax.annotation.Nullable;

import org.apache.thrift.TApplicationException;
import org.apache.thrift.TBase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.linecorp.armeria.internal.thrift.TApplicationExceptions;

abstract class AbstractThriftMessageClassFinder implements Supplier<Class<?>> {
private static final Logger logger = LoggerFactory.getLogger(AbstractThriftMessageClassFinder.class);

@Nullable
static Class<?> getClassByName(String className) {
try {
return Class.forName(className);
} catch (ClassNotFoundException ex) {
logger.warn("Can't find a class: {}", className, ex);
}
return null;
}

@Nullable
static Class<?> getMatchedClass(@Nullable Class<?> clazz) {
if (clazz == null) {
return null;
}
// Note, we need to check
// if the class is abstract, because abstract class does not have metaDataMap
// if the class has no-arg constructor, because FieldMetaData.getStructMetaDataMap
// calls clazz.newInstance
if (isTBase(clazz) && !isAbstract(clazz) && hasNoArgConstructor(clazz)) {
return clazz;
}

if (isTApplicationException(clazz)) {
return clazz;
}

if (isTApplicationExceptions(clazz)) {
return TApplicationException.class;
}

return null;
}

static boolean isTBase(Class<?> clazz) {
return TBase.class.isAssignableFrom(clazz);
}

private static boolean isTApplicationExceptions(Class<?> clazz) {
return clazz == TApplicationExceptions.class;
}

private static boolean isTApplicationException(Class<?> clazz) {
return TApplicationException.class.isAssignableFrom(clazz);
}

private static boolean isAbstract(Class<?> clazz) {
return Modifier.isAbstract(clazz.getModifiers());
}

private static boolean hasNoArgConstructor(Class<?> clazz) {
final Constructor<?>[] allConstructors = clazz.getConstructors();
for (Constructor<?> ctor : allConstructors) {
final Class<?>[] pType = ctor.getParameterTypes();
if (pType.length == 0) {
return true;
}
}

return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2019 LINE Corporation
*
* LINE Corporation licenses this file to you 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:
*
* https://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.
*/
// =================================================================================================
// Copyright 2011 Twitter, Inc.
// -------------------------------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this work except in compliance with the License.
// You may obtain a copy of the License in the LICENSE file, or at:
//
// https://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.linecorp.armeria.common.thrift.text;

import javax.annotation.Nullable;

final class DefaultThriftMessageClassFinder extends AbstractThriftMessageClassFinder {

@Nullable
@Override
public Class<?> get() {
final StackTraceElement[] frames =
Thread.currentThread().getStackTrace();

for (StackTraceElement f : frames) {
final String className = f.getClassName();
final Class<?> matchedClazz = getMatchedClass(getClassByName(className));

if (matchedClazz != null) {
return matchedClazz;
}
}

return null;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* Copyright 2019 LINE Corporation
*
* LINE Corporation licenses this file to you 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:
*
* https://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.
*/
// =================================================================================================
// Copyright 2011 Twitter, Inc.
// -------------------------------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this work except in compliance with the License.
// You may obtain a copy of the License in the LICENSE file, or at:
//
// https://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.linecorp.armeria.common.thrift.text;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.MethodType;
import java.util.Arrays;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Stream;

import javax.annotation.Nullable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

final class StackWalkingThriftMessageClassFinder extends AbstractThriftMessageClassFinder {

private static final Logger logger =
LoggerFactory.getLogger(StackWalkingThriftMessageClassFinder.class);

private static final String INVOKING_FAIL_MSG =
"Failed to invoke StackWalker.StackFrame.getDeclaringClass():";

@Nullable
private final Function<Stream<Object>, Class<?>> walkHandler;
private final MethodHandle walkMH;
private final Object stackWalker;

StackWalkingThriftMessageClassFinder() throws Throwable {
final ClassLoader classLoader = ClassLoader.getSystemClassLoader();
final Lookup lookup = MethodHandles.lookup();

final Class<?> stackWalkerClass = classLoader.loadClass("java.lang.StackWalker");
walkMH = lookup.findVirtual(stackWalkerClass,
"walk",
MethodType.methodType(Object.class, Function.class));

final Class<?> stackFrameClass = classLoader.loadClass("java.lang.StackWalker$StackFrame");
Function<Object, Class<?>> getClassByStackFrameTemp;
Object instance;

try {
// StackWalker instantiate with RETAIN_CLASS_REFERENCE option
final Class<?> Option = classLoader.loadClass("java.lang.StackWalker$Option");
final MethodHandle getInstanceMH =
lookup.findStatic(stackWalkerClass,
"getInstance",
MethodType.methodType(stackWalkerClass, Option));
final Enum<?> RETAIN_CLASS_REFERENCE =
Arrays.stream((Enum<?>[]) Option.getEnumConstants())
.filter(op -> "RETAIN_CLASS_REFERENCE".equals(op.name()))
.findFirst().orElseGet(null);

if (RETAIN_CLASS_REFERENCE == null) {
throw new IllegalStateException("Failed to get RETAIN_CLASS_REFERENCE option");
}
instance = getInstanceMH.invoke(RETAIN_CLASS_REFERENCE);
final MethodHandle getDeclaringClassMH = lookup.findVirtual(stackFrameClass,
"getDeclaringClass",
MethodType.methodType(Class.class));

getClassByStackFrameTemp = stackFrame -> {
try {
return getMatchedClass((Class<?>) getDeclaringClassMH.invoke(stackFrame));
} catch (Throwable t) {
logger.warn(INVOKING_FAIL_MSG, t);
}
return null;
};
} catch (Throwable throwable) {
// StackWalker instantiate without option
logger.warn("Falling back to StackWalker without option:", throwable);
final MethodHandle getInstanceMH =
lookup.findStatic(stackWalkerClass,
"getInstance",
MethodType.methodType(stackWalkerClass));
final MethodHandle getClassNameMH =
lookup.findVirtual(stackFrameClass, "getClassName", MethodType.methodType(String.class));

instance = getInstanceMH.invoke();
getClassByStackFrameTemp = stackFrame -> {
try {
return getMatchedClass(getClassByName(getClassNameMH.invoke(stackFrame).toString()));
} catch (Throwable t) {
logger.warn(INVOKING_FAIL_MSG, t);
}
return null;
};
}

stackWalker = instance;

final Function<Object, Class<?>> getClassByStackFrame = getClassByStackFrameTemp;
walkHandler = stackFrameStream ->
stackFrameStream.map(getClassByStackFrame)
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}

@Nullable
@Override
public Class<?> get() {
try {
return (Class<?>) walkMH.invoke(stackWalker, walkHandler);
} catch (Throwable t) {
throw new IllegalStateException("Failed to invoke StackWalker.walk():", t);
}
}
}

Loading

0 comments on commit df3fde1

Please sign in to comment.