8340456: Reduce overhead of proxying Object methods in ProxyGenerator
Reviewed-by: liach
This commit is contained in:
parent
5d611c0377
commit
a50440fadc
@ -114,11 +114,18 @@ final class ProxyGenerator {
|
|||||||
private static final Method OBJECT_EQUALS_METHOD;
|
private static final Method OBJECT_EQUALS_METHOD;
|
||||||
private static final Method OBJECT_TO_STRING_METHOD;
|
private static final Method OBJECT_TO_STRING_METHOD;
|
||||||
|
|
||||||
|
private static final String OBJECT_HASH_CODE_SIG;
|
||||||
|
private static final String OBJECT_EQUALS_SIG;
|
||||||
|
private static final String OBJECT_TO_STRING_SIG;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
try {
|
try {
|
||||||
OBJECT_HASH_CODE_METHOD = Object.class.getMethod("hashCode");
|
OBJECT_HASH_CODE_METHOD = Object.class.getMethod("hashCode");
|
||||||
|
OBJECT_HASH_CODE_SIG = OBJECT_HASH_CODE_METHOD.toShortSignature();
|
||||||
OBJECT_EQUALS_METHOD = Object.class.getMethod("equals", Object.class);
|
OBJECT_EQUALS_METHOD = Object.class.getMethod("equals", Object.class);
|
||||||
|
OBJECT_EQUALS_SIG = OBJECT_EQUALS_METHOD.toShortSignature();
|
||||||
OBJECT_TO_STRING_METHOD = Object.class.getMethod("toString");
|
OBJECT_TO_STRING_METHOD = Object.class.getMethod("toString");
|
||||||
|
OBJECT_TO_STRING_SIG = OBJECT_TO_STRING_METHOD.toShortSignature();
|
||||||
} catch (NoSuchMethodException e) {
|
} catch (NoSuchMethodException e) {
|
||||||
throw new NoSuchMethodError(e.getMessage());
|
throw new NoSuchMethodError(e.getMessage());
|
||||||
}
|
}
|
||||||
@ -446,9 +453,9 @@ final class ProxyGenerator {
|
|||||||
* java.lang.Object take precedence over duplicate methods in the
|
* java.lang.Object take precedence over duplicate methods in the
|
||||||
* proxy interfaces.
|
* proxy interfaces.
|
||||||
*/
|
*/
|
||||||
addProxyMethod(new ProxyMethod(OBJECT_HASH_CODE_METHOD, "m0"));
|
addProxyMethod(new ProxyMethod(OBJECT_HASH_CODE_METHOD, OBJECT_HASH_CODE_SIG, "m0"));
|
||||||
addProxyMethod(new ProxyMethod(OBJECT_EQUALS_METHOD, "m1"));
|
addProxyMethod(new ProxyMethod(OBJECT_EQUALS_METHOD, OBJECT_EQUALS_SIG, "m1"));
|
||||||
addProxyMethod(new ProxyMethod(OBJECT_TO_STRING_METHOD, "m2"));
|
addProxyMethod(new ProxyMethod(OBJECT_TO_STRING_METHOD, OBJECT_TO_STRING_SIG, "m2"));
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Accumulate all of the methods from the proxy interfaces.
|
* Accumulate all of the methods from the proxy interfaces.
|
||||||
@ -526,7 +533,7 @@ final class ProxyGenerator {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sigmethods.add(new ProxyMethod(m, sig, m.getSharedParameterTypes(), returnType,
|
sigmethods.add(new ProxyMethod(m, sig, returnType,
|
||||||
exceptionTypes, fromClass, "m" + proxyMethodCount++));
|
exceptionTypes, fromClass, "m" + proxyMethodCount++));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -617,11 +624,11 @@ final class ProxyGenerator {
|
|||||||
Label failLabel = cob.newLabel();
|
Label failLabel = cob.newLabel();
|
||||||
ClassEntry mhl = cp.classEntry(CD_MethodHandles_Lookup);
|
ClassEntry mhl = cp.classEntry(CD_MethodHandles_Lookup);
|
||||||
ClassEntry iae = cp.classEntry(CD_IllegalAccessException);
|
ClassEntry iae = cp.classEntry(CD_IllegalAccessException);
|
||||||
cob.aload(cob.parameterSlot(0))
|
cob.aload(0)
|
||||||
.invokevirtual(cp.methodRefEntry(mhl, cp.nameAndTypeEntry("lookupClass", MTD_Class)))
|
.invokevirtual(cp.methodRefEntry(mhl, cp.nameAndTypeEntry("lookupClass", MTD_Class)))
|
||||||
.ldc(proxyCE)
|
.ldc(proxyCE)
|
||||||
.if_acmpne(failLabel)
|
.if_acmpne(failLabel)
|
||||||
.aload(cob.parameterSlot(0))
|
.aload(0)
|
||||||
.invokevirtual(cp.methodRefEntry(mhl, cp.nameAndTypeEntry("hasFullPrivilegeAccess", MTD_boolean)))
|
.invokevirtual(cp.methodRefEntry(mhl, cp.nameAndTypeEntry("hasFullPrivilegeAccess", MTD_boolean)))
|
||||||
.ifeq(failLabel)
|
.ifeq(failLabel)
|
||||||
.invokestatic(CD_MethodHandles, "lookup", MTD_MethodHandles$Lookup)
|
.invokestatic(CD_MethodHandles, "lookup", MTD_MethodHandles$Lookup)
|
||||||
@ -629,7 +636,7 @@ final class ProxyGenerator {
|
|||||||
.labelBinding(failLabel)
|
.labelBinding(failLabel)
|
||||||
.new_(iae)
|
.new_(iae)
|
||||||
.dup()
|
.dup()
|
||||||
.aload(cob.parameterSlot(0))
|
.aload(0)
|
||||||
.invokevirtual(cp.methodRefEntry(mhl, cp.nameAndTypeEntry("toString", MTD_String)))
|
.invokevirtual(cp.methodRefEntry(mhl, cp.nameAndTypeEntry("toString", MTD_String)))
|
||||||
.invokespecial(cp.methodRefEntry(iae, exInit))
|
.invokespecial(cp.methodRefEntry(iae, exInit))
|
||||||
.athrow()
|
.athrow()
|
||||||
@ -650,18 +657,16 @@ final class ProxyGenerator {
|
|||||||
private final Method method;
|
private final Method method;
|
||||||
private final String shortSignature;
|
private final String shortSignature;
|
||||||
private final Class<?> fromClass;
|
private final Class<?> fromClass;
|
||||||
private final Class<?>[] parameterTypes;
|
|
||||||
private final Class<?> returnType;
|
private final Class<?> returnType;
|
||||||
private final String methodFieldName;
|
private final String methodFieldName;
|
||||||
private Class<?>[] exceptionTypes;
|
private Class<?>[] exceptionTypes;
|
||||||
private final FieldRefEntry methodField;
|
private final FieldRefEntry methodField;
|
||||||
|
|
||||||
private ProxyMethod(Method method, String sig, Class<?>[] parameterTypes,
|
private ProxyMethod(Method method, String sig,
|
||||||
Class<?> returnType, Class<?>[] exceptionTypes,
|
Class<?> returnType, Class<?>[] exceptionTypes,
|
||||||
Class<?> fromClass, String methodFieldName) {
|
Class<?> fromClass, String methodFieldName) {
|
||||||
this.method = method;
|
this.method = method;
|
||||||
this.shortSignature = sig;
|
this.shortSignature = sig;
|
||||||
this.parameterTypes = parameterTypes;
|
|
||||||
this.returnType = returnType;
|
this.returnType = returnType;
|
||||||
this.exceptionTypes = exceptionTypes;
|
this.exceptionTypes = exceptionTypes;
|
||||||
this.fromClass = fromClass;
|
this.fromClass = fromClass;
|
||||||
@ -670,14 +675,17 @@ final class ProxyGenerator {
|
|||||||
cp.nameAndTypeEntry(methodFieldName, CD_Method));
|
cp.nameAndTypeEntry(methodFieldName, CD_Method));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Class<?>[] parameterTypes() {
|
||||||
|
return method.getSharedParameterTypes();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new specific ProxyMethod with a specific field name
|
* Create a new specific ProxyMethod with a specific field name
|
||||||
*
|
*
|
||||||
* @param method The method for which to create a proxy
|
* @param method The method for which to create a proxy
|
||||||
*/
|
*/
|
||||||
private ProxyMethod(Method method, String methodFieldName) {
|
private ProxyMethod(Method method, String sig, String methodFieldName) {
|
||||||
this(method, method.toShortSignature(),
|
this(method, sig, method.getReturnType(),
|
||||||
method.getSharedParameterTypes(), method.getReturnType(),
|
|
||||||
method.getSharedExceptionTypes(), method.getDeclaringClass(), methodFieldName);
|
method.getSharedExceptionTypes(), method.getDeclaringClass(), methodFieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -685,17 +693,18 @@ final class ProxyGenerator {
|
|||||||
* Generate this method, including the code and exception table entry.
|
* Generate this method, including the code and exception table entry.
|
||||||
*/
|
*/
|
||||||
private void generateMethod(ClassBuilder clb) {
|
private void generateMethod(ClassBuilder clb) {
|
||||||
var desc = methodTypeDesc(returnType, parameterTypes);
|
var desc = methodTypeDesc(returnType, parameterTypes());
|
||||||
int accessFlags = (method.isVarArgs()) ? ACC_VARARGS | ACC_PUBLIC | ACC_FINAL
|
int accessFlags = (method.isVarArgs()) ? ACC_VARARGS | ACC_PUBLIC | ACC_FINAL
|
||||||
: ACC_PUBLIC | ACC_FINAL;
|
: ACC_PUBLIC | ACC_FINAL;
|
||||||
var catchList = computeUniqueCatchList(exceptionTypes);
|
|
||||||
clb.withMethod(method.getName(), desc, accessFlags, mb ->
|
clb.withMethod(method.getName(), desc, accessFlags, mb ->
|
||||||
mb.with(ExceptionsAttribute.of(toClassEntries(cp, List.of(exceptionTypes))))
|
mb.with(ExceptionsAttribute.of(toClassEntries(cp, List.of(exceptionTypes))))
|
||||||
.withCode(cob -> {
|
.withCode(cob -> {
|
||||||
|
var catchList = computeUniqueCatchList(exceptionTypes);
|
||||||
cob.aload(cob.receiverSlot())
|
cob.aload(cob.receiverSlot())
|
||||||
.getfield(handlerField)
|
.getfield(handlerField)
|
||||||
.aload(cob.receiverSlot())
|
.aload(cob.receiverSlot())
|
||||||
.getstatic(methodField);
|
.getstatic(methodField);
|
||||||
|
Class<?>[] parameterTypes = parameterTypes();
|
||||||
if (parameterTypes.length > 0) {
|
if (parameterTypes.length > 0) {
|
||||||
// Create an array and fill with the parameters converting primitives to wrappers
|
// Create an array and fill with the parameters converting primitives to wrappers
|
||||||
cob.loadConstant(parameterTypes.length)
|
cob.loadConstant(parameterTypes.length)
|
||||||
@ -784,6 +793,7 @@ final class ProxyGenerator {
|
|||||||
var cp = cob.constantPool();
|
var cp = cob.constantPool();
|
||||||
codeClassForName(cob, fromClass);
|
codeClassForName(cob, fromClass);
|
||||||
|
|
||||||
|
Class<?>[] parameterTypes = parameterTypes();
|
||||||
cob.ldc(method.getName())
|
cob.ldc(method.getName())
|
||||||
.loadConstant(parameterTypes.length)
|
.loadConstant(parameterTypes.length)
|
||||||
.anewarray(classCE);
|
.anewarray(classCE);
|
||||||
@ -817,10 +827,14 @@ final class ProxyGenerator {
|
|||||||
* loader is anticipated at local variable index 0.
|
* loader is anticipated at local variable index 0.
|
||||||
*/
|
*/
|
||||||
private void codeClassForName(CodeBuilder cob, Class<?> cl) {
|
private void codeClassForName(CodeBuilder cob, Class<?> cl) {
|
||||||
cob.ldc(cl.getName())
|
if (cl == Object.class) {
|
||||||
.iconst_0() // false
|
cob.ldc(objectCE);
|
||||||
.aload(0)// classLoader
|
} else {
|
||||||
.invokestatic(classForName);
|
cob.ldc(cl.getName())
|
||||||
|
.iconst_0() // false
|
||||||
|
.aload(0)// classLoader
|
||||||
|
.invokestatic(classForName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved.
|
* Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved.
|
||||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
*
|
*
|
||||||
* This code is free software; you can redistribute it and/or modify it
|
* This code is free software; you can redistribute it and/or modify it
|
||||||
@ -26,7 +26,6 @@ import java.util.List;
|
|||||||
|
|
||||||
import org.openjdk.jmh.annotations.Benchmark;
|
import org.openjdk.jmh.annotations.Benchmark;
|
||||||
import org.openjdk.jmh.annotations.BenchmarkMode;
|
import org.openjdk.jmh.annotations.BenchmarkMode;
|
||||||
import org.openjdk.jmh.annotations.CompilerControl;
|
|
||||||
import org.openjdk.jmh.annotations.Fork;
|
import org.openjdk.jmh.annotations.Fork;
|
||||||
import org.openjdk.jmh.annotations.Measurement;
|
import org.openjdk.jmh.annotations.Measurement;
|
||||||
import org.openjdk.jmh.annotations.Mode;
|
import org.openjdk.jmh.annotations.Mode;
|
||||||
@ -36,37 +35,29 @@ import org.openjdk.jmh.annotations.Setup;
|
|||||||
import org.openjdk.jmh.annotations.State;
|
import org.openjdk.jmh.annotations.State;
|
||||||
import org.openjdk.jmh.annotations.Warmup;
|
import org.openjdk.jmh.annotations.Warmup;
|
||||||
|
|
||||||
import org.openjdk.jmh.infra.Blackhole;
|
|
||||||
|
|
||||||
import java.lang.reflect.InvocationHandler;
|
import java.lang.reflect.InvocationHandler;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.Modifier;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Benchmark measuring java.lang.reflect.ProxyGenerator.generateProxyClass.
|
* Benchmark measuring java.lang.reflect.ProxyGenerator.generateProxyClass.
|
||||||
* It bypasses the cache of proxies to measure the time to construct a proxy.
|
* It bypasses the cache of proxies to measure the time to construct a proxy.
|
||||||
*/
|
*/
|
||||||
@Warmup(iterations = 5)
|
@Warmup(iterations = 5, time = 2)
|
||||||
@Measurement(iterations = 10)
|
@Measurement(iterations = 5, time = 2)
|
||||||
@Fork(value = 1)
|
@Fork(value = 1, jvmArgsPrepend = "--add-opens=java.base/java.lang.reflect=ALL-UNNAMED")
|
||||||
@BenchmarkMode(Mode.AverageTime)
|
@BenchmarkMode(Mode.AverageTime)
|
||||||
@OutputTimeUnit(TimeUnit.NANOSECONDS)
|
@OutputTimeUnit(TimeUnit.NANOSECONDS)
|
||||||
@State(Scope.Thread)
|
@State(Scope.Thread)
|
||||||
public class ProxyPerf {
|
public class ProxyGeneratorBench {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sample results from a Dell T7610.
|
* Sample results from a Dell T7610.
|
||||||
* Benchmark Mode Cnt Score Error Units
|
* Benchmark Mode Cnt Score Error Units
|
||||||
* ProxyPerf.genIntf_1 avgt 10 35325.428 +/- 780.459 ns/op
|
* ProxyPerf.genIntf_1 avgt 10 35325.428 +/- 780.459 ns/op
|
||||||
* ProxyPerf.genIntf_1_V49 avgt 10 34309.423 +/- 727.188 ns/op
|
|
||||||
* ProxyPerf.genStringsIntf_3 avgt 10 46600.366 +/- 663.812 ns/op
|
* ProxyPerf.genStringsIntf_3 avgt 10 46600.366 +/- 663.812 ns/op
|
||||||
* ProxyPerf.genStringsIntf_3_V49 avgt 10 45911.817 +/- 1598.536 ns/op
|
|
||||||
* ProxyPerf.genZeroParams avgt 10 33245.048 +/- 437.988 ns/op
|
* ProxyPerf.genZeroParams avgt 10 33245.048 +/- 437.988 ns/op
|
||||||
* ProxyPerf.genZeroParams_V49 avgt 10 32954.254 +/- 1041.932 ns/op
|
* ProxyPerf.genPrimsIntf_2 avgt 10 43987.819 +/- 837.443 ns/op
|
||||||
* ProxyPerf.getPrimsIntf_2 avgt 10 43987.819 +/- 837.443 ns/op
|
|
||||||
* ProxyPerf.getPrimsIntf_2_V49 avgt 10 42863.462 +/- 1193.480 ns/op
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public interface Intf_1 {
|
public interface Intf_1 {
|
||||||
@ -84,7 +75,6 @@ public class ProxyPerf {
|
|||||||
public String m2String(String s1, String s2);
|
public String m2String(String s1, String s2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private InvocationHandler handler;
|
|
||||||
private ClassLoader classloader;
|
private ClassLoader classloader;
|
||||||
private Method proxyGen;
|
private Method proxyGen;
|
||||||
private Method proxyGenV49;
|
private Method proxyGenV49;
|
||||||
@ -92,19 +82,11 @@ public class ProxyPerf {
|
|||||||
@Setup
|
@Setup
|
||||||
public void setup() {
|
public void setup() {
|
||||||
try {
|
try {
|
||||||
handler = (Object proxy, Method method, Object[] args) -> null;
|
|
||||||
classloader = ClassLoader.getSystemClassLoader();
|
classloader = ClassLoader.getSystemClassLoader();
|
||||||
Class<?> proxyGenClass = Class.forName("java.lang.reflect.ProxyGenerator");
|
Class<?> proxyGenClass = Class.forName("java.lang.reflect.ProxyGenerator");
|
||||||
proxyGen = proxyGenClass.getDeclaredMethod("generateProxyClass",
|
proxyGen = proxyGenClass.getDeclaredMethod("generateProxyClass",
|
||||||
ClassLoader.class, String.class, java.util.List.class, int.class);
|
ClassLoader.class, String.class, java.util.List.class, int.class);
|
||||||
proxyGen.setAccessible(true);
|
proxyGen.setAccessible(true);
|
||||||
|
|
||||||
// Init access to the old Proxy generator
|
|
||||||
Class<?> proxyGenClassV49 = Class.forName("java.lang.reflect.ProxyGenerator_v49");
|
|
||||||
proxyGenV49 = proxyGenClassV49.getDeclaredMethod("generateProxyClass",
|
|
||||||
String.class, java.util.List.class, int.class);
|
|
||||||
proxyGenV49.setAccessible(true);
|
|
||||||
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
ex.printStackTrace();
|
ex.printStackTrace();
|
||||||
throw new RuntimeException("ProxyClass setup fails", ex);
|
throw new RuntimeException("ProxyClass setup fails", ex);
|
||||||
@ -112,51 +94,35 @@ public class ProxyPerf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Benchmark
|
@Benchmark
|
||||||
public void genZeroParams(Blackhole bh) throws Exception {
|
public Object genZeroParams() throws Exception {
|
||||||
List<Class<?>> interfaces = List.of(Runnable.class);
|
List<Class<?>> interfaces = List.of(Runnable.class);
|
||||||
bh.consume(proxyGen.invoke(null, classloader, "ProxyImpl", interfaces, 1));
|
return proxyGen.invoke(null, classloader, "ProxyImpl", interfaces, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Benchmark
|
@Benchmark
|
||||||
public void genIntf_1(Blackhole bh) throws Exception {
|
public Object genIntf_1() throws Exception {
|
||||||
List<Class<?>> interfaces = List.of(Intf_1.class);
|
List<Class<?>> interfaces = List.of(Intf_1.class);
|
||||||
bh.consume(proxyGen.invoke(null, classloader, "ProxyImpl", interfaces, 1));
|
return proxyGen.invoke(null, classloader, "ProxyImpl", interfaces, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Benchmark
|
@Benchmark
|
||||||
public void getPrimsIntf_2(Blackhole bh) throws Exception {
|
public Object genPrimsIntf_2() throws Exception {
|
||||||
List<Class<?>> interfaces = List.of(Intf_2.class);
|
List<Class<?>> interfaces = List.of(Intf_2.class);
|
||||||
bh.consume(proxyGen.invoke(null, classloader, "ProxyImpl", interfaces, 1));
|
return proxyGen.invoke(null, classloader, "ProxyImpl", interfaces, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Benchmark
|
@Benchmark
|
||||||
public void genStringsIntf_3(Blackhole bh) throws Exception {
|
public Object genStringsIntf_3() throws Exception {
|
||||||
List<Class<?>> interfaces = List.of(Intf_3.class);
|
List<Class<?>> interfaces = List.of(Intf_3.class);
|
||||||
bh.consume(proxyGen.invoke(null, classloader, "ProxyImpl", interfaces, 1));
|
return proxyGen.invoke(null, classloader, "ProxyImpl", interfaces, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate using the V49inal generator for comparison
|
public static void main(String... args) throws Exception {
|
||||||
|
var benchmark = new ProxyGeneratorBench();
|
||||||
@Benchmark
|
benchmark.setup();
|
||||||
public void genZeroParams_V49(Blackhole bh) throws Exception {
|
benchmark.genZeroParams();
|
||||||
List<Class<?>> interfaces = List.of(Runnable.class);
|
benchmark.genIntf_1();
|
||||||
bh.consume(proxyGenV49.invoke(null, "ProxyImpl", interfaces, 1));
|
benchmark.genPrimsIntf_2();
|
||||||
|
benchmark.genStringsIntf_3();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Benchmark
|
|
||||||
public void genIntf_1_V49(Blackhole bh) throws Exception {
|
|
||||||
List<Class<?>> interfaces = List.of(Intf_1.class);
|
|
||||||
bh.consume(proxyGenV49.invoke(null, "ProxyImpl", interfaces, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Benchmark
|
|
||||||
public void getPrimsIntf_2_V49(Blackhole bh) throws Exception {
|
|
||||||
List<Class<?>> interfaces = List.of(Intf_2.class);
|
|
||||||
bh.consume(proxyGenV49.invoke(null, "ProxyImpl", interfaces, 1));
|
|
||||||
}
|
|
||||||
@Benchmark
|
|
||||||
public void genStringsIntf_3_V49(Blackhole bh) throws Exception {
|
|
||||||
List<Class<?>> interfaces = List.of(Intf_3.class);
|
|
||||||
bh.consume(proxyGenV49.invoke(null, "ProxyImpl", interfaces, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user