提交 429c3764 编写于 作者: V vlivanov

8050057: Improve caching of MethodHandle reinvokers

Reviewed-by: vlivanov, psandoz
Contributed-by: john.r.rose@oracle.com
上级 adb9b34e
...@@ -117,6 +117,31 @@ import jdk.internal.org.objectweb.asm.Type; ...@@ -117,6 +117,31 @@ import jdk.internal.org.objectweb.asm.Type;
return copyWithExtendD(type, form, value); return copyWithExtendD(type, form, value);
} }
@Override
BoundMethodHandle rebind() {
if (!tooComplex()) {
return this;
}
return makeReinvoker(this);
}
private boolean tooComplex() {
return (fieldCount() > FIELD_COUNT_THRESHOLD ||
form.expressionCount() > FORM_EXPRESSION_THRESHOLD);
}
private static final int FIELD_COUNT_THRESHOLD = 12; // largest convenient BMH field count
private static final int FORM_EXPRESSION_THRESHOLD = 24; // largest convenient BMH expression count
/**
* A reinvoker MH has this form:
* {@code lambda (bmh, arg*) { thismh = bmh[0]; invokeBasic(thismh, arg*) }}
*/
static BoundMethodHandle makeReinvoker(MethodHandle target) {
LambdaForm form = DelegatingMethodHandle.makeReinvokerForm(
target, MethodTypeForm.LF_REBIND, Species_L.SPECIES_DATA.getterFunction(0) );
return Species_L.make(target.type(), form, target);
}
/** /**
* Return the {@link SpeciesData} instance representing this BMH species. All subclasses must provide a * Return the {@link SpeciesData} instance representing this BMH species. All subclasses must provide a
* static field containing this value, and they must accordingly implement this method. * static field containing this value, and they must accordingly implement this method.
...@@ -168,15 +193,6 @@ import jdk.internal.org.objectweb.asm.Type; ...@@ -168,15 +193,6 @@ import jdk.internal.org.objectweb.asm.Type;
/*non-public*/ abstract BoundMethodHandle copyWithExtendF(MethodType mt, LambdaForm lf, float narg); /*non-public*/ abstract BoundMethodHandle copyWithExtendF(MethodType mt, LambdaForm lf, float narg);
/*non-public*/ abstract BoundMethodHandle copyWithExtendD(MethodType mt, LambdaForm lf, double narg); /*non-public*/ abstract BoundMethodHandle copyWithExtendD(MethodType mt, LambdaForm lf, double narg);
// The following is a grossly irregular hack:
@Override MethodHandle reinvokerTarget() {
try {
return (MethodHandle) arg(0);
} catch (Throwable ex) {
throw newInternalError(ex);
}
}
// //
// concrete BMH classes required to close bootstrap loops // concrete BMH classes required to close bootstrap loops
// //
...@@ -188,8 +204,6 @@ import jdk.internal.org.objectweb.asm.Type; ...@@ -188,8 +204,6 @@ import jdk.internal.org.objectweb.asm.Type;
super(mt, lf); super(mt, lf);
this.argL0 = argL0; this.argL0 = argL0;
} }
// The following is a grossly irregular hack:
@Override MethodHandle reinvokerTarget() { return (MethodHandle) argL0; }
@Override @Override
/*non-public*/ SpeciesData speciesData() { /*non-public*/ SpeciesData speciesData() {
return SPECIES_DATA; return SPECIES_DATA;
...@@ -585,16 +599,6 @@ import jdk.internal.org.objectweb.asm.Type; ...@@ -585,16 +599,6 @@ import jdk.internal.org.objectweb.asm.Type;
mv.visitMaxs(0, 0); mv.visitMaxs(0, 0);
mv.visitEnd(); mv.visitEnd();
// emit implementation of reinvokerTarget()
mv = cw.visitMethod(NOT_ACC_PUBLIC + ACC_FINAL, "reinvokerTarget", "()" + MH_SIG, null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, className, "argL0", JLO_SIG);
mv.visitTypeInsn(CHECKCAST, MH);
mv.visitInsn(ARETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
// emit implementation of speciesData() // emit implementation of speciesData()
mv = cw.visitMethod(NOT_ACC_PUBLIC + ACC_FINAL, "speciesData", MYSPECIES_DATA_SIG, null, null); mv = cw.visitMethod(NOT_ACC_PUBLIC + ACC_FINAL, "speciesData", MYSPECIES_DATA_SIG, null, null);
mv.visitCode(); mv.visitCode();
......
/*
* Copyright (c) 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 java.lang.invoke;
import java.util.Arrays;
import static java.lang.invoke.LambdaForm.*;
import static java.lang.invoke.MethodHandleStatics.*;
/**
* A method handle whose invocation behavior is determined by a target.
* The delegating MH itself can hold extra "intentions" beyond the simple behavior.
* @author jrose
*/
/*non-public*/
abstract class DelegatingMethodHandle extends MethodHandle {
protected DelegatingMethodHandle(MethodHandle target) {
this(target.type(), target);
}
protected DelegatingMethodHandle(MethodType type, MethodHandle target) {
super(type, chooseDelegatingForm(target));
}
/** Define this to extract the delegated target which supplies the invocation behavior. */
abstract protected MethodHandle getTarget();
@Override
abstract MethodHandle asTypeUncached(MethodType newType);
@Override
MemberName internalMemberName() {
return getTarget().internalMemberName();
}
@Override
boolean isInvokeSpecial() {
return getTarget().isInvokeSpecial();
}
@Override
Class<?> internalCallerClass() {
return getTarget().internalCallerClass();
}
@Override
MethodHandle copyWith(MethodType mt, LambdaForm lf) {
// FIXME: rethink 'copyWith' protocol; it is too low-level for use on all MHs
throw newIllegalArgumentException("do not use this");
}
@Override
String internalProperties() {
return "\n& Class="+getClass().getSimpleName()+
"\n& Target="+getTarget().debugString();
}
@Override
BoundMethodHandle rebind() {
return getTarget().rebind();
}
private static LambdaForm chooseDelegatingForm(MethodHandle target) {
if (target instanceof SimpleMethodHandle)
return target.internalForm(); // no need for an indirection
return makeReinvokerForm(target, MethodTypeForm.LF_DELEGATE, NF_getTarget);
}
/** Create a LF which simply reinvokes a target of the given basic type. */
static LambdaForm makeReinvokerForm(MethodHandle target,
int whichCache,
NamedFunction getTargetFn) {
MethodType mtype = target.type().basicType();
boolean customized = (whichCache < 0 ||
mtype.parameterSlotCount() > MethodType.MAX_MH_INVOKER_ARITY);
LambdaForm form;
if (!customized) {
form = mtype.form().cachedLambdaForm(whichCache);
if (form != null) return form;
}
final int THIS_DMH = 0;
final int ARG_BASE = 1;
final int ARG_LIMIT = ARG_BASE + mtype.parameterCount();
int nameCursor = ARG_LIMIT;
final int NEXT_MH = customized ? -1 : nameCursor++;
final int REINVOKE = nameCursor++;
LambdaForm.Name[] names = LambdaForm.arguments(nameCursor - ARG_LIMIT, mtype.invokerType());
assert(names.length == nameCursor);
Object[] targetArgs;
if (customized) {
targetArgs = Arrays.copyOfRange(names, ARG_BASE, ARG_LIMIT, Object[].class);
names[REINVOKE] = new LambdaForm.Name(target, targetArgs); // the invoker is the target itself
} else {
names[NEXT_MH] = new LambdaForm.Name(getTargetFn, names[THIS_DMH]);
targetArgs = Arrays.copyOfRange(names, THIS_DMH, ARG_LIMIT, Object[].class);
targetArgs[0] = names[NEXT_MH]; // overwrite this MH with next MH
names[REINVOKE] = new LambdaForm.Name(mtype, targetArgs);
}
String debugString;
switch(whichCache) {
case MethodTypeForm.LF_REBIND: debugString = "BMH.reinvoke"; break;
case MethodTypeForm.LF_DELEGATE: debugString = "MH.delegate"; break;
default: debugString = "MH.reinvoke"; break;
}
form = new LambdaForm(debugString, ARG_LIMIT, names);
if (!customized) {
form = mtype.form().setCachedLambdaForm(whichCache, form);
}
return form;
}
private static final NamedFunction NF_getTarget;
static {
try {
NF_getTarget = new NamedFunction(DelegatingMethodHandle.class
.getDeclaredMethod("getTarget"));
} catch (ReflectiveOperationException ex) {
throw newInternalError(ex);
}
}
}
...@@ -126,6 +126,11 @@ class DirectMethodHandle extends MethodHandle { ...@@ -126,6 +126,11 @@ class DirectMethodHandle extends MethodHandle {
return new Constructor(mtype, lform, ctor, init, instanceClass); return new Constructor(mtype, lform, ctor, init, instanceClass);
} }
@Override
BoundMethodHandle rebind() {
return BoundMethodHandle.makeReinvoker(this);
}
@Override @Override
MethodHandle copyWith(MethodType mt, LambdaForm lf) { MethodHandle copyWith(MethodType mt, LambdaForm lf) {
assert(this.getClass() == DirectMethodHandle.class); // must override in subclasses assert(this.getClass() == DirectMethodHandle.class); // must override in subclasses
......
...@@ -466,6 +466,11 @@ class LambdaForm { ...@@ -466,6 +466,11 @@ class LambdaForm {
return arity; return arity;
} }
/** Report the number of expressions (non-parameter names). */
int expressionCount() {
return names.length - arity;
}
/** Return the method type corresponding to my basic type signature. */ /** Return the method type corresponding to my basic type signature. */
MethodType methodType() { MethodType methodType() {
return signatureType(basicTypeSignature()); return signatureType(basicTypeSignature());
......
...@@ -1393,66 +1393,11 @@ assertEquals("[three, thee, tee]", asListFix.invoke((Object)argv).toString()); ...@@ -1393,66 +1393,11 @@ assertEquals("[three, thee, tee]", asListFix.invoke((Object)argv).toString());
/*non-public*/ /*non-public*/
abstract MethodHandle copyWith(MethodType mt, LambdaForm lf); abstract MethodHandle copyWith(MethodType mt, LambdaForm lf);
/*non-public*/ /** Require this method handle to be a BMH, or else replace it with a "wrapper" BMH.
BoundMethodHandle rebind() { * Many transforms are implemented only for BMHs.
// Bind 'this' into a new invoker, of the known class BMH. * @return a behaviorally equivalent BMH
MethodType type2 = type();
LambdaForm form2 = reinvokerForm(this);
// form2 = lambda (bmh, arg*) { thismh = bmh[0]; invokeBasic(thismh, arg*) }
return BoundMethodHandle.bindSingle(type2, form2, this);
}
/*non-public*/
MethodHandle reinvokerTarget() {
throw new InternalError("not a reinvoker MH: "+this.getClass().getName()+": "+this);
}
/** Create a LF which simply reinvokes a target of the given basic type.
* The target MH must override {@link #reinvokerTarget} to provide the target.
*/ */
static LambdaForm reinvokerForm(MethodHandle target) { abstract BoundMethodHandle rebind();
MethodType mtype = target.type().basicType();
LambdaForm reinvoker = mtype.form().cachedLambdaForm(MethodTypeForm.LF_REINVOKE);
if (reinvoker != null) return reinvoker;
if (mtype.parameterSlotCount() >= MethodType.MAX_MH_ARITY)
return makeReinvokerForm(target.type(), target); // cannot cache this
reinvoker = makeReinvokerForm(mtype, null);
return mtype.form().setCachedLambdaForm(MethodTypeForm.LF_REINVOKE, reinvoker);
}
private static LambdaForm makeReinvokerForm(MethodType mtype, MethodHandle customTargetOrNull) {
boolean customized = (customTargetOrNull != null);
MethodHandle MH_invokeBasic = customized ? null : MethodHandles.basicInvoker(mtype);
final int THIS_BMH = 0;
final int ARG_BASE = 1;
final int ARG_LIMIT = ARG_BASE + mtype.parameterCount();
int nameCursor = ARG_LIMIT;
final int NEXT_MH = customized ? -1 : nameCursor++;
final int REINVOKE = nameCursor++;
LambdaForm.Name[] names = LambdaForm.arguments(nameCursor - ARG_LIMIT, mtype.invokerType());
Object[] targetArgs;
MethodHandle targetMH;
if (customized) {
targetArgs = Arrays.copyOfRange(names, ARG_BASE, ARG_LIMIT, Object[].class);
targetMH = customTargetOrNull;
} else {
names[NEXT_MH] = new LambdaForm.Name(NF_reinvokerTarget, names[THIS_BMH]);
targetArgs = Arrays.copyOfRange(names, THIS_BMH, ARG_LIMIT, Object[].class);
targetArgs[0] = names[NEXT_MH]; // overwrite this MH with next MH
targetMH = MethodHandles.basicInvoker(mtype);
}
names[REINVOKE] = new LambdaForm.Name(targetMH, targetArgs);
return new LambdaForm("BMH.reinvoke", ARG_LIMIT, names);
}
private static final LambdaForm.NamedFunction NF_reinvokerTarget;
static {
try {
NF_reinvokerTarget = new LambdaForm.NamedFunction(MethodHandle.class
.getDeclaredMethod("reinvokerTarget"));
} catch (ReflectiveOperationException ex) {
throw newInternalError(ex);
}
}
/** /**
* Replace the old lambda form of this method handle with a new one. * Replace the old lambda form of this method handle with a new one.
......
...@@ -351,33 +351,45 @@ import static java.lang.invoke.MethodHandles.Lookup.IMPL_LOOKUP; ...@@ -351,33 +351,45 @@ import static java.lang.invoke.MethodHandles.Lookup.IMPL_LOOKUP;
if (type.parameterType(last) != arrayType) if (type.parameterType(last) != arrayType)
target = target.asType(type.changeParameterType(last, arrayType)); target = target.asType(type.changeParameterType(last, arrayType));
target = target.asFixedArity(); // make sure this attribute is turned off target = target.asFixedArity(); // make sure this attribute is turned off
return new AsVarargsCollector(target, target.type(), arrayType); return new AsVarargsCollector(target, arrayType);
} }
static class AsVarargsCollector extends MethodHandle { private static final class AsVarargsCollector extends DelegatingMethodHandle {
private final MethodHandle target; private final MethodHandle target;
private final Class<?> arrayType; private final Class<?> arrayType;
private @Stable MethodHandle asCollectorCache; private @Stable MethodHandle asCollectorCache;
AsVarargsCollector(MethodHandle target, MethodType type, Class<?> arrayType) { AsVarargsCollector(MethodHandle target, Class<?> arrayType) {
super(type, reinvokerForm(target)); this(target.type(), target, arrayType);
}
AsVarargsCollector(MethodType type, MethodHandle target, Class<?> arrayType) {
super(type, target);
this.target = target; this.target = target;
this.arrayType = arrayType; this.arrayType = arrayType;
this.asCollectorCache = target.asCollector(arrayType, 0); this.asCollectorCache = target.asCollector(arrayType, 0);
} }
@Override MethodHandle reinvokerTarget() { return target; }
@Override @Override
public boolean isVarargsCollector() { public boolean isVarargsCollector() {
return true; return true;
} }
@Override
protected MethodHandle getTarget() {
return target;
}
@Override @Override
public MethodHandle asFixedArity() { public MethodHandle asFixedArity() {
return target; return target;
} }
@Override
MethodHandle setVarargs(MemberName member) {
if (member.isVarargs()) return this;
return asFixedArity();
}
@Override @Override
public MethodHandle asTypeUncached(MethodType newType) { public MethodHandle asTypeUncached(MethodType newType) {
MethodType type = this.type(); MethodType type = this.type();
...@@ -416,32 +428,6 @@ import static java.lang.invoke.MethodHandles.Lookup.IMPL_LOOKUP; ...@@ -416,32 +428,6 @@ import static java.lang.invoke.MethodHandles.Lookup.IMPL_LOOKUP;
: Arrays.asList(this, newType); : Arrays.asList(this, newType);
return true; return true;
} }
@Override
MethodHandle setVarargs(MemberName member) {
if (member.isVarargs()) return this;
return asFixedArity();
}
@Override
MemberName internalMemberName() {
return asFixedArity().internalMemberName();
}
@Override
Class<?> internalCallerClass() {
return asFixedArity().internalCallerClass();
}
/*non-public*/
@Override
boolean isInvokeSpecial() {
return asFixedArity().isInvokeSpecial();
}
@Override
MethodHandle copyWith(MethodType mt, LambdaForm lf) {
throw newIllegalArgumentException("do not use this");
}
} }
/** Factory method: Spread selected argument. */ /** Factory method: Spread selected argument. */
...@@ -972,7 +958,7 @@ import static java.lang.invoke.MethodHandles.Lookup.IMPL_LOOKUP; ...@@ -972,7 +958,7 @@ import static java.lang.invoke.MethodHandles.Lookup.IMPL_LOOKUP;
/** This subclass allows a wrapped method handle to be re-associated with an arbitrary member name. */ /** This subclass allows a wrapped method handle to be re-associated with an arbitrary member name. */
static class WrappedMember extends MethodHandle { private static final class WrappedMember extends DelegatingMethodHandle {
private final MethodHandle target; private final MethodHandle target;
private final MemberName member; private final MemberName member;
private final Class<?> callerClass; private final Class<?> callerClass;
...@@ -981,23 +967,13 @@ import static java.lang.invoke.MethodHandles.Lookup.IMPL_LOOKUP; ...@@ -981,23 +967,13 @@ import static java.lang.invoke.MethodHandles.Lookup.IMPL_LOOKUP;
private WrappedMember(MethodHandle target, MethodType type, private WrappedMember(MethodHandle target, MethodType type,
MemberName member, boolean isInvokeSpecial, MemberName member, boolean isInvokeSpecial,
Class<?> callerClass) { Class<?> callerClass) {
super(type, reinvokerForm(target)); super(type, target);
this.target = target; this.target = target;
this.member = member; this.member = member;
this.callerClass = callerClass; this.callerClass = callerClass;
this.isInvokeSpecial = isInvokeSpecial; this.isInvokeSpecial = isInvokeSpecial;
} }
@Override
MethodHandle reinvokerTarget() {
return target;
}
@Override
public MethodHandle asTypeUncached(MethodType newType) {
// This MH is an alias for target, except for the MemberName
// Drop the MemberName if there is any conversion.
return asTypeCache = target.asType(newType);
}
@Override @Override
MemberName internalMemberName() { MemberName internalMemberName() {
return member; return member;
...@@ -1010,10 +986,15 @@ import static java.lang.invoke.MethodHandles.Lookup.IMPL_LOOKUP; ...@@ -1010,10 +986,15 @@ import static java.lang.invoke.MethodHandles.Lookup.IMPL_LOOKUP;
boolean isInvokeSpecial() { boolean isInvokeSpecial() {
return isInvokeSpecial; return isInvokeSpecial;
} }
@Override @Override
MethodHandle copyWith(MethodType mt, LambdaForm lf) { protected MethodHandle getTarget() {
throw newIllegalArgumentException("do not use this"); return target;
}
@Override
public MethodHandle asTypeUncached(MethodType newType) {
// This MH is an alias for target, except for the MemberName
// Drop the MemberName if there is any conversion.
return asTypeCache = target.asType(newType);
} }
} }
......
...@@ -70,8 +70,8 @@ final class MethodTypeForm { ...@@ -70,8 +70,8 @@ final class MethodTypeForm {
LF_INVINTERFACE = 4, LF_INVINTERFACE = 4,
LF_INVSTATIC_INIT = 5, // DMH invokeStatic with <clinit> barrier LF_INVSTATIC_INIT = 5, // DMH invokeStatic with <clinit> barrier
LF_INTERPRET = 6, // LF interpreter LF_INTERPRET = 6, // LF interpreter
LF_COUNTER = 7, // CMH wrapper LF_REBIND = 7, // BoundMethodHandle
LF_REINVOKE = 8, // other wrapper LF_DELEGATE = 8, // DelegatingMethodHandle
LF_EX_LINKER = 9, // invokeExact_MT (for invokehandle) LF_EX_LINKER = 9, // invokeExact_MT (for invokehandle)
LF_EX_INVOKER = 10, // MHs.invokeExact LF_EX_INVOKER = 10, // MHs.invokeExact
LF_GEN_LINKER = 11, // generic invoke_MT (for invokehandle) LF_GEN_LINKER = 11, // generic invoke_MT (for invokehandle)
......
...@@ -25,21 +25,77 @@ ...@@ -25,21 +25,77 @@
package java.lang.invoke; package java.lang.invoke;
import static java.lang.invoke.LambdaForm.BasicType.*;
import static java.lang.invoke.MethodHandleStatics.*;
/** /**
* A method handle whose behavior is determined only by its LambdaForm. * A method handle whose behavior is determined only by its LambdaForm.
* @author jrose * @author jrose
*/ */
final class SimpleMethodHandle extends MethodHandle { final class SimpleMethodHandle extends BoundMethodHandle {
private SimpleMethodHandle(MethodType type, LambdaForm form) { private SimpleMethodHandle(MethodType type, LambdaForm form) {
super(type, form); super(type, form);
} }
/*non-public*/ static SimpleMethodHandle make(MethodType type, LambdaForm form) { /*non-public*/ static BoundMethodHandle make(MethodType type, LambdaForm form) {
return new SimpleMethodHandle(type, form); return new SimpleMethodHandle(type, form);
} }
/*non-public*/ static final SpeciesData SPECIES_DATA = SpeciesData.EMPTY;
/*non-public*/ public SpeciesData speciesData() {
return SPECIES_DATA;
}
@Override @Override
/*non-public*/ SimpleMethodHandle copyWith(MethodType mt, LambdaForm lf) { /*non-public*/ BoundMethodHandle copyWith(MethodType mt, LambdaForm lf) {
return make(mt, lf); return make(mt, lf);
} }
@Override
String internalProperties() {
return "\n& Class="+getClass().getSimpleName();
}
@Override
/*non-public*/ public int fieldCount() {
return 0;
}
@Override
/*non-public*/ final BoundMethodHandle copyWithExtendL(MethodType mt, LambdaForm lf, Object narg) {
return BoundMethodHandle.bindSingle(mt, lf, narg); // Use known fast path.
}
@Override
/*non-public*/ final BoundMethodHandle copyWithExtendI(MethodType mt, LambdaForm lf, int narg) {
try {
return (BoundMethodHandle) SPECIES_DATA.extendWith(I_TYPE).constructor().invokeBasic(mt, lf, narg);
} catch (Throwable ex) {
throw uncaughtException(ex);
}
}
@Override
/*non-public*/ final BoundMethodHandle copyWithExtendJ(MethodType mt, LambdaForm lf, long narg) {
try {
return (BoundMethodHandle) SPECIES_DATA.extendWith(J_TYPE).constructor().invokeBasic(mt, lf, narg);
} catch (Throwable ex) {
throw uncaughtException(ex);
}
}
@Override
/*non-public*/ final BoundMethodHandle copyWithExtendF(MethodType mt, LambdaForm lf, float narg) {
try {
return (BoundMethodHandle) SPECIES_DATA.extendWith(F_TYPE).constructor().invokeBasic(mt, lf, narg);
} catch (Throwable ex) {
throw uncaughtException(ex);
}
}
@Override
/*non-public*/ final BoundMethodHandle copyWithExtendD(MethodType mt, LambdaForm lf, double narg) {
try {
return (BoundMethodHandle) SPECIES_DATA.extendWith(D_TYPE).constructor().invokeBasic(mt, lf, narg);
} catch (Throwable ex) {
throw uncaughtException(ex);
}
}
} }
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册