提交 ca1512b8 编写于 作者: V Vlad Ilyushchenko

simple re-sampling algo prototype

上级 52eb06cb
......@@ -167,13 +167,6 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>3.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.intellij</groupId>
<artifactId>annotations</artifactId>
......
......@@ -108,8 +108,12 @@ public class AbstractDirectList implements Closeable {
}
public void clear() {
clear((byte) 0);
}
public void clear(byte b) {
pos = start;
zero((byte) 0);
zero(b);
}
public void zero(byte v) {
......
......@@ -21,8 +21,8 @@ import java.util.Arrays;
public class IntHashSet {
private static final int noEntryValue = -1;
private final double loadFactor;
private final int noEntryValue = -1;
private int[] keys;
private int free;
private int capacity;
......@@ -46,7 +46,7 @@ public class IntHashSet {
@SuppressWarnings({"unchecked"})
protected void rehash() {
int newCapacity = (int) Primes.next(keys.length << 1);
int newCapacity = Primes.next(keys.length << 1);
free = capacity = (int) (newCapacity * loadFactor);
......
......@@ -16,6 +16,8 @@
package com.nfsdb.journal.collections;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Arrays;
......@@ -49,7 +51,7 @@ public class IntObjHashMap<V> {
@SuppressWarnings({"unchecked"})
protected void rehash() {
int newCapacity = (int) Primes.next(values.length << 1);
int newCapacity = Primes.next(values.length << 1);
free = (int) (newCapacity * loadFactor);
......@@ -159,6 +161,7 @@ public class IntObjHashMap<V> {
return index < values.length;
}
@SuppressFBWarnings({"IT_NO_SUCH_ELEMENT"})
@Override
public V next() {
return values[index++];
......
/*
* Copyright (c) 2014. Vlad Ilyushchenko
* Copyright (c) 2014-2015. Vlad Ilyushchenko
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -33,7 +33,7 @@ import java.util.List;
public class MultiMap implements Closeable, Iterable<MultiMap.Record> {
private final int seed = 0xdeadbeef;
private static final int seed = 0xdeadbeef;
private final float loadFactor;
private final Key key = new Key();
private final Values values0 = new Values();
......@@ -51,16 +51,16 @@ public class MultiMap implements Closeable, Iterable<MultiMap.Record> {
private long kLimit;
private long kPos;
private int free;
private long keyCapacity;
private int keyCapacity;
private int size = 0;
private MultiMap(long capacity, long dataSize, float loadFactor, List<ColumnMetadata> valueColumns, List<ColumnMetadata> keyColumns) {
private MultiMap(int capacity, long dataSize, float loadFactor, List<ColumnMetadata> valueColumns, List<ColumnMetadata> keyColumns) {
this.loadFactor = loadFactor;
this.kAddress = Unsafe.getUnsafe().allocateMemory(dataSize + AbstractDirectList.CACHE_LINE_SIZE);
this.kStart = kPos = this.kAddress + (this.kAddress & (AbstractDirectList.CACHE_LINE_SIZE - 1));
this.kLimit = kStart + dataSize;
this.keyCapacity = Primes.next((long) (capacity / loadFactor));
this.keyCapacity = Primes.next((int) (capacity / loadFactor));
this.free = (int) (keyCapacity * loadFactor);
this.keyOffsets = new DirectLongList(keyCapacity);
this.keyOffsets.zero((byte) -1);
......@@ -97,8 +97,7 @@ public class MultiMap implements Closeable, Iterable<MultiMap.Record> {
public Values claimSlot(Key key) {
// calculate hash remembering "key" structure
// [ len | value block | key offset block | key data block ]
long h = Hash.hashXX(key.startAddr + keyBlockOffset, key.len - keyBlockOffset, seed);
long index = h % keyCapacity;
int index = Hash.hashXX(key.startAddr + keyBlockOffset, key.len - keyBlockOffset, seed) % keyCapacity;
long offset = keyOffsets.get(index);
if (offset == -1) {
......@@ -117,7 +116,7 @@ public class MultiMap implements Closeable, Iterable<MultiMap.Record> {
}
}
private Values probe0(Key key, long index) {
private Values probe0(Key key, int index) {
long offset;
while ((offset = keyOffsets.get(index = (++index % keyCapacity))) != -1) {
if (eq(key, offset)) {
......@@ -189,7 +188,7 @@ public class MultiMap implements Closeable, Iterable<MultiMap.Record> {
}
private void rehash() {
long capacity = Primes.next(keyCapacity << 1);
int capacity = Primes.next(keyCapacity << 1);
DirectLongList pointers = new DirectLongList(capacity);
pointers.zero((byte) -1);
pointers.setPos(capacity);
......@@ -235,10 +234,17 @@ public class MultiMap implements Closeable, Iterable<MultiMap.Record> {
return size;
}
public void clear() {
kPos = kStart;
free = (int) (keyCapacity * loadFactor);
size = 0;
keyOffsets.clear((byte) -1);
}
public static class Builder {
private final List<ColumnMetadata> valueColumns = new ArrayList<>();
private final List<ColumnMetadata> keyColumns = new ArrayList<>();
private long capacity = 67;
private int capacity = 67;
private long dataSize = 4096;
private float loadFactor = 0.5f;
......@@ -252,7 +258,7 @@ public class MultiMap implements Closeable, Iterable<MultiMap.Record> {
return this;
}
public Builder setCapacity(long capacity) {
public Builder setCapacity(int capacity) {
this.capacity = capacity;
return this;
}
......@@ -293,6 +299,14 @@ public class MultiMap implements Closeable, Iterable<MultiMap.Record> {
return this;
}
public Key putInt(int value) {
checkSize(4);
Unsafe.getUnsafe().putInt(appendAddr, value);
appendAddr += 4;
writeOffset();
return this;
}
public void put(long address, int len) {
checkSize(len);
Unsafe.getUnsafe().copyMemory(address, appendAddr, len);
......
......@@ -55,7 +55,7 @@ public class ObjIntHashMap<V> {
@SuppressWarnings({"unchecked"})
protected void rehash() {
int newCapacity = (int) Primes.next(values.length << 1);
int newCapacity = Primes.next(values.length << 1);
free = capacity = (int) (newCapacity * loadFactor);
int[] oldValues = values;
V[] oldKeys = keys;
......
......@@ -19,12 +19,12 @@ package com.nfsdb.journal.collections;
import java.util.Arrays;
public final class Primes {
private static final long[] primes = {3, 5, 7, 11, 17, 23, 31, 37, 43, 47, 67, 79, 89, 97, 137, 163, 179, 197, 277, 311, 331, 359, 379, 397, 433, 557, 599, 631, 673, 719, 761, 797, 877, 953, 1039, 1117, 1201, 1277, 1361, 1439, 1523, 1597, 1759, 1907, 2081, 2237, 2411, 2557, 2729, 2879, 3049, 3203, 3527, 3821, 4177, 4481, 4831, 5119, 5471, 5779, 6101, 6421, 7057, 7643, 8363, 8963, 9677, 10243, 10949, 11579, 12203, 12853, 14143, 15287, 16729, 17929, 19373, 20507, 21911, 23159, 24407, 25717, 28289, 30577, 33461, 35863, 38747, 41017, 43853, 46327, 48817, 51437, 56591, 61169, 66923, 71741, 77509, 82037, 87719, 92657, 97649, 102877, 113189, 122347, 133853, 143483, 155027, 164089, 175447, 185323, 195311, 205759, 226379, 244703, 267713, 286973, 310081, 328213, 350899, 370661, 390647, 411527, 452759, 489407, 535481, 573953, 620171, 656429, 701819, 741337, 781301, 823117, 905551, 978821, 1070981, 1147921, 1240361, 1312867, 1403641, 1482707, 1562611, 1646237, 1811107, 1957651, 2141977, 2295859, 2480729, 2625761, 2807303, 2965421, 3125257, 3292489, 3622219, 3915341, 4283963, 4591721, 4961459, 5251529, 5614657, 5930887, 6250537, 6584983, 7244441, 7830701, 8567929, 9183457, 9922933, 10503061, 11229331, 11861791, 12501169, 13169977, 14488931, 15661423, 17135863, 18366923, 19845871, 21006137, 22458671, 23723597, 25002389, 26339969, 28977863, 31322867, 34271747, 36733847, 39691759, 42012281, 44917381, 47447201, 50004791, 52679969, 57955739, 62645741, 68543509, 73467739, 79383533, 84024581, 89834777, 94894427, 100009607, 105359939, 115911563, 125291483, 137087021, 146935499, 158767069, 168049163, 179669557, 189788857, 200019221, 210719881, 231823147, 250582987, 274174111, 293871013, 317534141, 336098327, 359339171, 379577741, 400038451, 421439783, 463646329, 501165979, 548348231, 587742049, 635068283, 672196673, 718678369, 759155483, 800076929, 842879579, 927292699, 1002331963, 1096696463, 1175484103, 1270136683, 1344393353, 1437356741, 1518310967, 1600153859, 1685759167, 1854585413, 2004663929, 2147483647};
private static final int[] primes = {3, 5, 7, 11, 17, 23, 31, 37, 43, 47, 67, 79, 89, 97, 137, 163, 179, 197, 277, 311, 331, 359, 379, 397, 433, 557, 599, 631, 673, 719, 761, 797, 877, 953, 1039, 1117, 1201, 1277, 1361, 1439, 1523, 1597, 1759, 1907, 2081, 2237, 2411, 2557, 2729, 2879, 3049, 3203, 3527, 3821, 4177, 4481, 4831, 5119, 5471, 5779, 6101, 6421, 7057, 7643, 8363, 8963, 9677, 10243, 10949, 11579, 12203, 12853, 14143, 15287, 16729, 17929, 19373, 20507, 21911, 23159, 24407, 25717, 28289, 30577, 33461, 35863, 38747, 41017, 43853, 46327, 48817, 51437, 56591, 61169, 66923, 71741, 77509, 82037, 87719, 92657, 97649, 102877, 113189, 122347, 133853, 143483, 155027, 164089, 175447, 185323, 195311, 205759, 226379, 244703, 267713, 286973, 310081, 328213, 350899, 370661, 390647, 411527, 452759, 489407, 535481, 573953, 620171, 656429, 701819, 741337, 781301, 823117, 905551, 978821, 1070981, 1147921, 1240361, 1312867, 1403641, 1482707, 1562611, 1646237, 1811107, 1957651, 2141977, 2295859, 2480729, 2625761, 2807303, 2965421, 3125257, 3292489, 3622219, 3915341, 4283963, 4591721, 4961459, 5251529, 5614657, 5930887, 6250537, 6584983, 7244441, 7830701, 8567929, 9183457, 9922933, 10503061, 11229331, 11861791, 12501169, 13169977, 14488931, 15661423, 17135863, 18366923, 19845871, 21006137, 22458671, 23723597, 25002389, 26339969, 28977863, 31322867, 34271747, 36733847, 39691759, 42012281, 44917381, 47447201, 50004791, 52679969, 57955739, 62645741, 68543509, 73467739, 79383533, 84024581, 89834777, 94894427, 100009607, 105359939, 115911563, 125291483, 137087021, 146935499, 158767069, 168049163, 179669557, 189788857, 200019221, 210719881, 231823147, 250582987, 274174111, 293871013, 317534141, 336098327, 359339171, 379577741, 400038451, 421439783, 463646329, 501165979, 548348231, 587742049, 635068283, 672196673, 718678369, 759155483, 800076929, 842879579, 927292699, 1002331963, 1096696463, 1175484103, 1270136683, 1344393353, 1437356741, 1518310967, 1600153859, 1685759167, 1854585413, 2004663929, 2147483647};
private Primes() {
}
public static long next(long target) {
public static int next(int target) {
int i = Arrays.binarySearch(primes, target);
if (i < 0) {
return primes[-i - 1];
......
/*
* Copyright (c) 2014. Vlad Ilyushchenko
* Copyright (c) 2014-2015. Vlad Ilyushchenko
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -16,6 +16,7 @@
package com.nfsdb.journal.export;
import com.nfsdb.journal.exceptions.JournalRuntimeException;
import com.nfsdb.journal.lang.cst.DataRow;
import com.nfsdb.journal.utils.Dates;
import com.nfsdb.journal.utils.Numbers;
......@@ -67,6 +68,8 @@ public class JournalEntryPrinter {
case BOOLEAN:
sink.put(e.getBool(i) ? "true" : "false");
break;
default:
throw new JournalRuntimeException("Unsupported type: " + e.getColumnType(i));
}
sink.put('\t');
}
......
......@@ -16,6 +16,7 @@
package com.nfsdb.journal.export;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.jetbrains.annotations.NotNull;
public class StringSink implements CharSink, CharSequence {
......@@ -37,6 +38,8 @@ public class StringSink implements CharSink, CharSequence {
public void flush() {
}
/* Either IDEA or FireBug complain, annotation galore */
@SuppressFBWarnings({"RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE"})
@NotNull
@Override
public String toString() {
......
/*
* Copyright (c) 2014. Vlad Ilyushchenko
* Copyright (c) 2014-2015. Vlad Ilyushchenko
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -61,7 +61,7 @@ public class ServerConfig extends NetworkConfig {
InetSocketAddress address = getSocketAddress();
ServerSocketChannel channel = ServerSocketChannel.open().bind(address).setOption(StandardSocketOptions.SO_RCVBUF, getSoRcvBuf());
NetworkInterface ifn = getNetworkInterface();
LOGGER.info("Server is now listening on %s [%s]", address, ifn == null ? "all" : ifn.getName());
LOGGER.info("Server is now listening on %s [%s]", address, ifn.getName());
return channel;
} catch (IOException e) {
throw new JournalNetworkException("Cannot open server socket", e);
......
/*
* Copyright (c) 2014. Vlad Ilyushchenko
* Copyright (c) 2014-2015. Vlad Ilyushchenko
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -18,6 +18,7 @@ package com.nfsdb.journal.utils;
import com.nfsdb.journal.export.CharSink;
import com.nfsdb.journal.export.StringSink;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
final public class Dates {
......@@ -289,6 +290,7 @@ final public class Dates {
}
// YYYY-MM-DDThh:mm:ss.mmm
@SuppressFBWarnings({"ICAST_INTEGER_MULTIPLY_CAST_TO_LONG"})
public static long parseDateTime(CharSequence seq) {
int p = 0;
int year = Numbers.parseInt(seq, p, p += 4);
......@@ -393,6 +395,7 @@ final public class Dates {
return yearMillis(y, l) + monthOfYearMillis(m, l) + (d - 1) * DAY_MILLIS;
}
@SuppressFBWarnings({"ICAST_INTEGER_MULTIPLY_CAST_TO_LONG"})
public static long toMillis(int y, int m, int d, int h, int mi) {
boolean l = isLeapYear(y);
return yearMillis(y, l) + monthOfYearMillis(m, l) + (d - 1) * DAY_MILLIS + h * HOUR_MILLIS + mi * MINUTE_MILLIS;
......
/*
* Copyright (c) 2014. Vlad Ilyushchenko
* Copyright (c) 2014-2015. Vlad Ilyushchenko
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -32,32 +32,7 @@ import org.junit.Test;
public class MultiMapTest extends AbstractTest {
private CharSink sink = new StringSink();
/*
@Test
public void testMultiValue() throws Exception {
try (Multimap map = new Multimap(new ColumnType[]{ColumnType.LONG, ColumnType.STRING}, new ColumnType[]{ColumnType.DOUBLE})) {
Rnd rnd = new Rnd();
for (int i = 0; i < 10000; i++) {
double d = rnd.nextDouble();
long l = rnd.nextLong();
String s = rnd.nextString(10);
map.claimSlot(
map.claimKey().putLong(l).putStr(s).$()
).putDouble(0, d);
}
rnd = new Rnd();
for (Multimap.Record r: map) {
Assert.assertEquals(rnd.nextDouble(), r.getDouble(0), 0.0000001d);
Assert.assertEquals(rnd.nextLong(), r.getLong(1));
Assert.assertEquals(rnd.nextString(10), r.getStr(2));
}
}
}
*/
private final CharSink sink = new StringSink();
@Test
public void testCount() throws Exception {
......@@ -126,6 +101,7 @@ public class MultiMapTest extends AbstractTest {
JournalWriter<Quote> w = factory.writer(Quote.class);
TestUtils.generateQuoteData(w, 10000, 1419908881558L, 30);
w.commit();
int tsIndex = w.getMetadata().getColumnIndex("timestamp");
int symIndex = w.getMetadata().getColumnIndex("sym");
......@@ -142,7 +118,6 @@ public class MultiMapTest extends AbstractTest {
.setLoadFactor(0.5f)
.build();
for (JournalEntry e : w.rows()) {
long ts = e.getLong(tsIndex);
......@@ -156,15 +131,11 @@ public class MultiMapTest extends AbstractTest {
val.putInt(0, val.isNew() ? 1 : val.getInt(0) + 1);
}
JournalEntryPrinter out = new JournalEntryPrinter(sink, true);
out.print(map.iterator());
map.free();
Assert.assertEquals(expected, sink.toString());
// System.out.println(sink);
}
}
/*
* Copyright (c) 2014. Vlad Ilyushchenko
* Copyright (c) 2014-2015. Vlad Ilyushchenko
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -16,59 +16,56 @@
package com.nfsdb.journal.lang;
import com.nfsdb.journal.Journal;
import com.nfsdb.journal.Partition;
import com.nfsdb.journal.column.AbstractColumn;
import com.nfsdb.journal.column.FixedColumn;
import com.nfsdb.journal.factory.configuration.JournalMetadata;
import com.nfsdb.journal.JournalEntryWriter;
import com.nfsdb.journal.JournalWriter;
import com.nfsdb.journal.factory.configuration.JournalStructure;
import com.nfsdb.journal.lang.cst.Choice;
import com.nfsdb.journal.lang.cst.PartitionSlice;
import com.nfsdb.journal.lang.cst.RowAcceptor;
import com.nfsdb.journal.lang.cst.impl.fltr.DoubleGreaterThanRowFilter;
import com.nfsdb.journal.test.tools.AbstractTest;
import com.nfsdb.journal.utils.Rnd;
import org.junit.Assert;
import org.junit.Test;
import static org.easymock.EasyMock.*;
public class FuncTest {
public class FuncTest extends AbstractTest {
@Test
public void testGreaterThan() throws Exception {
DoubleGreaterThanRowFilter filter = new DoubleGreaterThanRowFilter("test", 10);
FixedColumn column = createMock(FixedColumn.class);
// let row 100 have value 5.0
// let row 101 have value 11.0
// let row 102 have value 10
expect(column.getDouble(100)).andReturn(5d);
expect(column.getDouble(101)).andReturn(11d);
expect(column.getDouble(102)).andReturn(10d);
replay(column);
// configure journal "xxx" with single double column "price"
JournalWriter w = factory.writer(new JournalStructure("xxx").$double("price"));
PartitionSlice slice = mockPartitionColumn("test", 5, column);
RowAcceptor acceptor = filter.acceptor(slice);
Assert.assertEquals(Choice.SKIP, acceptor.accept(100));
Assert.assertEquals(Choice.PICK, acceptor.accept(101));
Assert.assertEquals(Choice.SKIP, acceptor.accept(102));
}
private PartitionSlice mockPartitionColumn(String name, int index, AbstractColumn column) {
JournalMetadata metadata = createMock(JournalMetadata.class);
expect(metadata.getColumnIndex(name)).andReturn(index);
replay(metadata);
Journal journal = createMock(Journal.class);
expect(journal.getMetadata()).andReturn(metadata);
replay(journal);
Partition partition = createMock(Partition.class);
expect(partition.getJournal()).andReturn(journal);
expect(partition.getAbstractColumn(index)).andReturn(column);
replay(partition);
// populate 500 rows with pseudo-random double values
Rnd rnd = new Rnd();
for (int i = 0; i < 500; i++) {
JournalEntryWriter ew = w.entryWriter();
ew.putDouble(0, rnd.nextDouble());
ew.append();
}
w.commit();
// create test slice that covers entire journal
// our journal has only one partition
PartitionSlice slice = new PartitionSlice();
slice.partition = partition;
slice.lo = 0;
slice.partition = w.getLastPartition();
slice.hi = slice.partition.size() - 1;
// create filter and acceptor
DoubleGreaterThanRowFilter filter = new DoubleGreaterThanRowFilter("price", 10);
RowAcceptor acceptor = filter.acceptor(slice);
return slice;
for (long row = slice.lo; row <= slice.hi; row++) {
Choice choice = acceptor.accept(row);
switch (choice) {
case PICK:
Assert.assertTrue(slice.partition.getDouble(row, 0) > 10);
break;
default:
Assert.assertTrue(slice.partition.getDouble(row, 0) <= 10);
}
}
}
}
/*
* Copyright (c) 2014. Vlad Ilyushchenko
* Copyright (c) 2014-2015. Vlad Ilyushchenko
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -19,8 +19,12 @@ package com.nfsdb.journal.lang.experimental;
import com.nfsdb.journal.Journal;
import com.nfsdb.journal.JournalWriter;
import com.nfsdb.journal.Partition;
import com.nfsdb.journal.collections.MultiMap;
import com.nfsdb.journal.column.ColumnType;
import com.nfsdb.journal.column.FixedColumn;
import com.nfsdb.journal.column.SymbolTable;
import com.nfsdb.journal.factory.JournalFactory;
import com.nfsdb.journal.factory.configuration.ColumnMetadata;
import com.nfsdb.journal.factory.configuration.JournalConfigurationBuilder;
import com.nfsdb.journal.lang.cst.EntrySource;
import com.nfsdb.journal.lang.cst.JournalEntry;
......@@ -43,6 +47,7 @@ import com.nfsdb.journal.model.Quote;
import com.nfsdb.journal.test.tools.JournalTestFactory;
import com.nfsdb.journal.test.tools.TestData;
import com.nfsdb.journal.test.tools.TestUtils;
import com.nfsdb.journal.utils.Dates;
import com.nfsdb.journal.utils.Files;
import org.junit.BeforeClass;
import org.junit.ClassRule;
......@@ -222,4 +227,58 @@ public class CstTest {
System.out.println(count);
System.out.println((System.nanoTime() - t) / 20);
}
public void testResamplingPerformance() throws Exception {
JournalFactory factory = new JournalFactory("d:/data");
Journal w = factory.reader("quote");
int tsIndex = w.getMetadata().getColumnIndex("timestamp");
int symIndex = w.getMetadata().getColumnIndex("sym");
long t = 0;
for (int i = -10; i < 10; i++) {
if (i == 0) {
t = System.nanoTime();
}
MultiMap map = new MultiMap.Builder()
.keyColumn(w.getMetadata().getColumnMetadata(tsIndex))
.keyColumn(w.getMetadata().getColumnMetadata(symIndex))
.valueColumn(new ColumnMetadata() {{
name = "count";
type = ColumnType.INT;
}})
.setCapacity(50)
.setDataSize(500 * 1024)
.setLoadFactor(0.5f)
.build();
long prev = -1;
for (JournalEntry e : w.rows()) {
long ts = Dates.floorMI(e.getLong(tsIndex));
if (ts != prev) {
map.clear();
prev = ts;
}
MultiMap.Values val = map.claimSlot(
map.claimKey()
.putLong(ts)
.putInt(e.getInt(symIndex))
.$()
);
val.putInt(0, val.isNew() ? 1 : val.getInt(0) + 1);
}
System.out.println(map.size());
// JournalEntryPrinter out = new JournalEntryPrinter(sink, true);
// out.print(map.iterator());
map.free();
}
System.out.println(System.nanoTime() - t);
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册