提交 484563de authored 作者: Thomas Mueller's avatar Thomas Mueller

A persistent multi-version map: split based on size (memory), not number of…

A persistent multi-version map: split based on size (memory), not number of entries; allow to estimate the memory used by unsaved changes
上级 443e21c9
...@@ -164,18 +164,20 @@ public class MVRTreeMap<K, V> extends MVMap<K, V> { ...@@ -164,18 +164,20 @@ public class MVRTreeMap<K, V> extends MVMap<K, V> {
Page p = root.copyOnWrite(writeVersion); Page p = root.copyOnWrite(writeVersion);
Object result; Object result;
if (alwaysAdd || get(key) == null) { if (alwaysAdd || get(key) == null) {
if (p.getKeyCount() > store.getMaxPageSize()) { if (p.getMemory() > store.getMaxPageSize() && p.getKeyCount() > 1) {
// only possible if this is the root, else we would have split earlier // only possible if this is the root, else we would have split earlier
// (this requires maxPageSize is fixed) // (this requires maxPageSize is fixed)
long totalCount = p.getTotalCount(); long totalCount = p.getTotalCount();
Page split = split(p, writeVersion); Page split = split(p, writeVersion);
Object[] keys = { getBounds(p), getBounds(split) }; Object k1 = getBounds(p);
Object k2 = getBounds(split);
Object[] keys = { k1, k2 };
long[] children = { p.getPos(), split.getPos(), 0 }; long[] children = { p.getPos(), split.getPos(), 0 };
Page[] childrenPages = { p, split, null }; Page[] childrenPages = { p, split, null };
long[] counts = { p.getTotalCount(), split.getTotalCount(), 0 }; long[] counts = { p.getTotalCount(), split.getTotalCount(), 0 };
p = Page.create(this, writeVersion, 2, p = Page.create(this, writeVersion, 2,
keys, null, children, childrenPages, counts, keys, null, children, childrenPages, counts,
totalCount, 0); totalCount, 0, 0);
// now p is a node; continues // now p is a node; continues
} }
add(p, writeVersion, key, value); add(p, writeVersion, key, value);
...@@ -244,7 +246,7 @@ public class MVRTreeMap<K, V> extends MVMap<K, V> { ...@@ -244,7 +246,7 @@ public class MVRTreeMap<K, V> extends MVMap<K, V> {
} }
} }
Page c = p.getChildPage(index).copyOnWrite(writeVersion); Page c = p.getChildPage(index).copyOnWrite(writeVersion);
if (c.getKeyCount() >= store.getMaxPageSize()) { if (c.getMemory() > store.getMaxPageSize() && c.getKeyCount() > 1) {
// split on the way down // split on the way down
Page split = split(c, writeVersion); Page split = split(c, writeVersion);
p = p.copyOnWrite(writeVersion); p = p.copyOnWrite(writeVersion);
...@@ -365,7 +367,7 @@ public class MVRTreeMap<K, V> extends MVMap<K, V> { ...@@ -365,7 +367,7 @@ public class MVRTreeMap<K, V> extends MVMap<K, V> {
long[] c = leaf ? null : new long[1]; long[] c = leaf ? null : new long[1];
Page[] cp = leaf ? null : new Page[1]; Page[] cp = leaf ? null : new Page[1];
return Page.create(this, writeVersion, 0, return Page.create(this, writeVersion, 0,
new Object[4], values, c, cp, c, 0, 0); new Object[4], values, c, cp, c, 0, 0, 0);
} }
private static void move(Page source, Page target, int sourceIndex) { private static void move(Page source, Page target, int sourceIndex) {
......
...@@ -64,7 +64,7 @@ public class TestMVRTree extends TestMVStore { ...@@ -64,7 +64,7 @@ public class TestMVRTree extends TestMVStore {
SpatialKey k = new SpatialKey(i, x - p, x + p, y - p, y + p); SpatialKey k = new SpatialKey(i, x - p, x + p, y - p, y + p);
r.add(k, "" + i); r.add(k, "" + i);
if (i > 0 && (i % len / 10) == 0) { if (i > 0 && (i % len / 10) == 0) {
s.store(); s.save();
} }
if (i > 0 && (i % 10000) == 0) { if (i > 0 && (i % 10000) == 0) {
render(r, getBaseDir() + "/test.png"); render(r, getBaseDir() + "/test.png");
...@@ -72,7 +72,7 @@ public class TestMVRTree extends TestMVStore { ...@@ -72,7 +72,7 @@ public class TestMVRTree extends TestMVStore {
} }
// System.out.println(prof.getTop(5)); // System.out.println(prof.getTop(5));
// System.out.println("add: " + (System.currentTimeMillis() - t)); // System.out.println("add: " + (System.currentTimeMillis() - t));
s.store(); s.save();
s.close(); s.close();
s = openStore(fileName); s = openStore(fileName);
r = s.openMap("data", "r", "s2", ""); r = s.openMap("data", "r", "s2", "");
......
...@@ -185,7 +185,7 @@ public class TestMVStore extends TestBase { ...@@ -185,7 +185,7 @@ public class TestMVStore extends TestBase {
assertEquals(null, s.getSetting("hello")); assertEquals(null, s.getSetting("hello"));
s.setSetting("test", "Hello"); s.setSetting("test", "Hello");
assertEquals("Hello", s.getSetting("test")); assertEquals("Hello", s.getSetting("test"));
s.store(); s.save();
s.close(); s.close();
s = MVStore.open(fileName); s = MVStore.open(fileName);
assertEquals(1, s.getCurrentVersion()); assertEquals(1, s.getCurrentVersion());
...@@ -227,7 +227,7 @@ public class TestMVStore extends TestBase { ...@@ -227,7 +227,7 @@ public class TestMVStore extends TestBase {
map.put(1, "Hello"); map.put(1, "Hello");
map.put("2", 200); map.put("2", 200);
map.put(new Object[1], new Object[]{1, "2"}); map.put(new Object[1], new Object[]{1, "2"});
s.store(); s.save();
s.close(); s.close();
s = MVStore.open(fileName, new TestMapFactory()); s = MVStore.open(fileName, new TestMapFactory());
map = s.openMap("test"); map = s.openMap("test");
...@@ -273,7 +273,7 @@ public class TestMVStore extends TestBase { ...@@ -273,7 +273,7 @@ public class TestMVStore extends TestBase {
map.openVersion(oldVersion); map.openVersion(oldVersion);
// store the newest data to disk // store the newest data to disk
s.store(); s.save();
// print the old version (can be done // print the old version (can be done
// concurrently with further modifications) // concurrently with further modifications)
...@@ -301,7 +301,7 @@ public class TestMVStore extends TestBase { ...@@ -301,7 +301,7 @@ public class TestMVStore extends TestBase {
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
Integer x = m.get("value"); Integer x = m.get("value");
m.put("value", x == null ? 0 : x + 1); m.put("value", x == null ? 0 : x + 1);
s.store(); s.save();
} }
s.close(); s.close();
} }
...@@ -320,7 +320,7 @@ public class TestMVStore extends TestBase { ...@@ -320,7 +320,7 @@ public class TestMVStore extends TestBase {
m.put(i, "Hi"); m.put(i, "Hi");
} }
s.incrementVersion(); s.incrementVersion();
s.store(); s.save();
for (int i = 20; i < 40; i++) { for (int i = 20; i < 40; i++) {
assertEquals("Hi", m.put(i, "Hello")); assertEquals("Hi", m.put(i, "Hello"));
} }
...@@ -373,7 +373,7 @@ public class TestMVStore extends TestBase { ...@@ -373,7 +373,7 @@ public class TestMVStore extends TestBase {
assertTrue(mOld.isReadOnly()); assertTrue(mOld.isReadOnly());
s.getCurrentVersion(); s.getCurrentVersion();
s.setRetainChunk(0); s.setRetainChunk(0);
long old2 = s.store(); long old2 = s.save();
// the old version is still available // the old version is still available
assertEquals("Hello", mOld.get("1")); assertEquals("Hello", mOld.get("1"));
...@@ -381,7 +381,7 @@ public class TestMVStore extends TestBase { ...@@ -381,7 +381,7 @@ public class TestMVStore extends TestBase {
m.put("1", "Hi"); m.put("1", "Hi");
assertEquals("Welt", m.remove("2")); assertEquals("Welt", m.remove("2"));
s.store(); s.save();
s.close(); s.close();
s = openStore(fileName); s = openStore(fileName);
...@@ -404,17 +404,17 @@ public class TestMVStore extends TestBase { ...@@ -404,17 +404,17 @@ public class TestMVStore extends TestBase {
for (int i = 0; i < 1000; i++) { for (int i = 0; i < 1000; i++) {
m.put(i, "Hello World"); m.put(i, "Hello World");
} }
s.store(); s.save();
s.close(); s.close();
long len = FileUtils.size(fileName); long len = FileUtils.size(fileName);
s = openStore(fileName); s = openStore(fileName);
m = s.openMap("data", Integer.class, String.class); m = s.openMap("data", Integer.class, String.class);
m.clear(); m.clear();
s.store(); s.save();
s.compact(100); s.compact(100);
s.close(); s.close();
long len2 = FileUtils.size(fileName); long len2 = FileUtils.size(fileName);
assertTrue(len2 < len); assertTrue("len2: " + len2 + " len: " + len, len2 < len);
} }
private void testFastDelete() { private void testFastDelete() {
...@@ -423,14 +423,15 @@ public class TestMVStore extends TestBase { ...@@ -423,14 +423,15 @@ public class TestMVStore extends TestBase {
MVStore s; MVStore s;
MVMap<Integer, String> m; MVMap<Integer, String> m;
s = openStore(fileName); s = openStore(fileName);
s.setMaxPageSize(100); s.setMaxPageSize(700);
m = s.openMap("data", Integer.class, String.class); m = s.openMap("data", Integer.class, String.class);
for (int i = 0; i < 1000; i++) { for (int i = 0; i < 1000; i++) {
m.put(i, "Hello World"); m.put(i, "Hello World");
assertEquals(i + 1, m.size()); assertEquals(i + 1, m.size());
} }
assertEquals(1000, m.size()); assertEquals(1000, m.size());
s.store(); assertEquals(281, s.getUnsavedPageCount());
s.save();
assertEquals(3, s.getWriteCount()); assertEquals(3, s.getWriteCount());
s.close(); s.close();
...@@ -438,9 +439,9 @@ public class TestMVStore extends TestBase { ...@@ -438,9 +439,9 @@ public class TestMVStore extends TestBase {
m = s.openMap("data", Integer.class, String.class); m = s.openMap("data", Integer.class, String.class);
m.clear(); m.clear();
assertEquals(0, m.size()); assertEquals(0, m.size());
s.store(); s.save();
// ensure only nodes are read, but not leaves // ensure only nodes are read, but not leaves
assertEquals(4, s.getReadCount()); assertEquals(34, s.getReadCount());
assertEquals(2, s.getWriteCount()); assertEquals(2, s.getWriteCount());
s.close(); s.close();
} }
...@@ -462,7 +463,7 @@ public class TestMVStore extends TestBase { ...@@ -462,7 +463,7 @@ public class TestMVStore extends TestBase {
assertEquals(1, s.incrementVersion()); assertEquals(1, s.incrementVersion());
s.rollbackTo(1); s.rollbackTo(1);
assertEquals("Hello", m.get("1")); assertEquals("Hello", m.get("1"));
long v2 = s.store(); long v2 = s.save();
assertEquals(2, v2); assertEquals(2, v2);
assertEquals(2, s.getCurrentVersion()); assertEquals(2, s.getCurrentVersion());
assertFalse(s.hasUnsavedChanges()); assertFalse(s.hasUnsavedChanges());
...@@ -486,7 +487,7 @@ public class TestMVStore extends TestBase { ...@@ -486,7 +487,7 @@ public class TestMVStore extends TestBase {
assertNull(meta.get("map.data1")); assertNull(meta.get("map.data1"));
assertNull(m0.get("1")); assertNull(m0.get("1"));
assertEquals("Hello", m.get("1")); assertEquals("Hello", m.get("1"));
assertEquals(2, s.store()); assertEquals(2, s.save());
s.close(); s.close();
s = openStore(fileName); s = openStore(fileName);
...@@ -504,7 +505,7 @@ public class TestMVStore extends TestBase { ...@@ -504,7 +505,7 @@ public class TestMVStore extends TestBase {
m.put("1", "Hallo"); m.put("1", "Hallo");
s.incrementVersion(); s.incrementVersion();
assertEquals(3, s.getCurrentVersion()); assertEquals(3, s.getCurrentVersion());
long v4 = s.store(); long v4 = s.save();
assertEquals(4, v4); assertEquals(4, v4);
assertEquals(4, s.getCurrentVersion()); assertEquals(4, s.getCurrentVersion());
s.close(); s.close();
...@@ -513,7 +514,7 @@ public class TestMVStore extends TestBase { ...@@ -513,7 +514,7 @@ public class TestMVStore extends TestBase {
s.setRetainChunk(0); s.setRetainChunk(0);
m = s.openMap("data", String.class, String.class); m = s.openMap("data", String.class, String.class);
m.put("1", "Hello"); m.put("1", "Hello");
s.store(); s.save();
s.close(); s.close();
s = openStore(fileName); s = openStore(fileName);
...@@ -582,11 +583,11 @@ public class TestMVStore extends TestBase { ...@@ -582,11 +583,11 @@ public class TestMVStore extends TestBase {
MVMap<String, String> data = s.openMap("data", String.class, String.class); MVMap<String, String> data = s.openMap("data", String.class, String.class);
data.put("1", "Hello"); data.put("1", "Hello");
data.put("2", "World"); data.put("2", "World");
s.store(); s.save();
assertEquals("1/0///", m.get("map.data")); assertEquals("1/0///", m.get("map.data"));
assertTrue(m.containsKey("chunk.1")); assertTrue(m.containsKey("chunk.1"));
assertEquals("Hello", data.put("1", "Hallo")); assertEquals("Hello", data.put("1", "Hallo"));
s.store(); s.save();
assertEquals("1/0///", m.get("map.data")); assertEquals("1/0///", m.get("map.data"));
assertTrue(m.get("root.1").length() > 0); assertTrue(m.get("root.1").length() > 0);
assertTrue(m.containsKey("chunk.1")); assertTrue(m.containsKey("chunk.1"));
...@@ -648,10 +649,10 @@ public class TestMVStore extends TestBase { ...@@ -648,10 +649,10 @@ public class TestMVStore extends TestBase {
m.put(i, o); m.put(i, o);
i++; i++;
if (i % 10000 == 0) { if (i % 10000 == 0) {
s.store(); s.save();
} }
} }
s.store(); s.save();
s.close(); s.close();
// System.out.println(prof.getTop(5)); // System.out.println(prof.getTop(5));
// System.out.println("store time " + (System.currentTimeMillis() - t)); // System.out.println("store time " + (System.currentTimeMillis() - t));
...@@ -679,7 +680,7 @@ public class TestMVStore extends TestBase { ...@@ -679,7 +680,7 @@ public class TestMVStore extends TestBase {
// p = new Profiler(); // p = new Profiler();
//p.startCollecting(); //p.startCollecting();
// t = System.currentTimeMillis(); // t = System.currentTimeMillis();
s.store(); s.save();
// System.out.println("store: " + (System.currentTimeMillis() - t)); // System.out.println("store: " + (System.currentTimeMillis() - t));
// System.out.println(p.getTop(5)); // System.out.println(p.getTop(5));
assertEquals("hello 0", m.remove(0)); assertEquals("hello 0", m.remove(0));
...@@ -687,7 +688,7 @@ public class TestMVStore extends TestBase { ...@@ -687,7 +688,7 @@ public class TestMVStore extends TestBase {
for (int i = 1; i < count; i++) { for (int i = 1; i < count; i++) {
assertEquals("hello " + i, m.get(i)); assertEquals("hello " + i, m.get(i));
} }
s.store(); s.save();
s.close(); s.close();
s = openStore(fileName); s = openStore(fileName);
m = s.openMap("data", Integer.class, String.class); m = s.openMap("data", Integer.class, String.class);
...@@ -698,7 +699,7 @@ public class TestMVStore extends TestBase { ...@@ -698,7 +699,7 @@ public class TestMVStore extends TestBase {
for (int i = 1; i < count; i++) { for (int i = 1; i < count; i++) {
m.remove(i); m.remove(i);
} }
s.store(); s.save();
assertNull(m.get(0)); assertNull(m.get(0));
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
assertNull(m.get(i)); assertNull(m.get(i));
...@@ -713,10 +714,10 @@ public class TestMVStore extends TestBase { ...@@ -713,10 +714,10 @@ public class TestMVStore extends TestBase {
for (int j = 0; j < 20; j++) { for (int j = 0; j < 20; j++) {
MVStore s = openStore(fileName); MVStore s = openStore(fileName);
MVMap<Integer, String> m = s.openMap("data", Integer.class, String.class); MVMap<Integer, String> m = s.openMap("data", Integer.class, String.class);
for (int i = 0; i < 10; i++) { for (int i = 0; i < 100; i++) {
m.put(j + i, "Hello " + j); m.put(j + i, "Hello " + j);
} }
s.store(); s.save();
s.compact(80); s.compact(80);
s.close(); s.close();
long len = FileUtils.size(fileName); long len = FileUtils.size(fileName);
...@@ -734,7 +735,7 @@ public class TestMVStore extends TestBase { ...@@ -734,7 +735,7 @@ public class TestMVStore extends TestBase {
for (int i = 0; i < 100; i++) { for (int i = 0; i < 100; i++) {
m.remove(i); m.remove(i);
} }
s.store(); s.save();
s.compact(80); s.compact(80);
s.close(); s.close();
// len = FileUtils.size(fileName); // len = FileUtils.size(fileName);
...@@ -757,12 +758,12 @@ public class TestMVStore extends TestBase { ...@@ -757,12 +758,12 @@ public class TestMVStore extends TestBase {
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
m.put(i, "Hello"); m.put(i, "Hello");
} }
s.store(); s.save();
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
assertEquals("Hello", m.get(i)); assertEquals("Hello", m.get(i));
assertEquals("Hello", m.remove(i)); assertEquals("Hello", m.remove(i));
} }
s.store(); s.save();
s.close(); s.close();
long len = FileUtils.size(fileName); long len = FileUtils.size(fileName);
if (initialLength == 0) { if (initialLength == 0) {
...@@ -865,7 +866,7 @@ public class TestMVStore extends TestBase { ...@@ -865,7 +866,7 @@ public class TestMVStore extends TestBase {
si.put("Test", 10); si.put("Test", 10);
MVMap<String, String> ss = s.openMap("stringString", String.class, String.class); MVMap<String, String> ss = s.openMap("stringString", String.class, String.class);
ss.put("Hello", "World"); ss.put("Hello", "World");
s.store(); s.save();
s.close(); s.close();
s = openStore(fileName); s = openStore(fileName);
is = s.openMap("intString", Integer.class, String.class); is = s.openMap("intString", Integer.class, String.class);
...@@ -889,7 +890,7 @@ public class TestMVStore extends TestBase { ...@@ -889,7 +890,7 @@ public class TestMVStore extends TestBase {
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
m.put(i, "hello " + i); m.put(i, "hello " + i);
} }
s.store(); s.save();
it = m.keyIterator(null); it = m.keyIterator(null);
it.next(); it.next();
assertThrows(UnsupportedOperationException.class, it).remove(); assertThrows(UnsupportedOperationException.class, it).remove();
...@@ -920,7 +921,7 @@ public class TestMVStore extends TestBase { ...@@ -920,7 +921,7 @@ public class TestMVStore extends TestBase {
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
m.put(i, "hello " + i); m.put(i, "hello " + i);
} }
s.store(); s.save();
// closing twice should be fine // closing twice should be fine
s.close(); s.close();
s.close(); s.close();
...@@ -934,14 +935,14 @@ public class TestMVStore extends TestBase { ...@@ -934,14 +935,14 @@ public class TestMVStore extends TestBase {
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
m.put(i, "hello " + i); m.put(i, "hello " + i);
} }
s.store(); s.save();
assertEquals("hello 0", m.remove(0)); assertEquals("hello 0", m.remove(0));
assertNull(m.get(0)); assertNull(m.get(0));
for (int i = 1; i < 3; i++) { for (int i = 1; i < 3; i++) {
assertEquals("hello " + i, m.get(i)); assertEquals("hello " + i, m.get(i));
} }
s.store(); s.save();
s.close(); s.close();
s = openStore(fileName); s = openStore(fileName);
...@@ -961,7 +962,7 @@ public class TestMVStore extends TestBase { ...@@ -961,7 +962,7 @@ public class TestMVStore extends TestBase {
*/ */
protected static MVStore openStore(String fileName) { protected static MVStore openStore(String fileName) {
MVStore store = MVStore.open(fileName, new TestMapFactory()); MVStore store = MVStore.open(fileName, new TestMapFactory());
store.setMaxPageSize(10); store.setMaxPageSize(1000);
return store; return store;
} }
......
...@@ -19,6 +19,7 @@ import java.util.HashMap; ...@@ -19,6 +19,7 @@ import java.util.HashMap;
* 1 byte: 'c' * 1 byte: 'c'
* 4 bytes: length * 4 bytes: length
* 4 bytes: chunk id (an incrementing number) * 4 bytes: chunk id (an incrementing number)
* 4 bytes: pageCount
* 8 bytes: metaRootPos * 8 bytes: metaRootPos
* 8 bytes: maxLengthLive * 8 bytes: maxLengthLive
* [ Page ] * * [ Page ] *
...@@ -40,6 +41,11 @@ public class Chunk { ...@@ -40,6 +41,11 @@ public class Chunk {
*/ */
int length; int length;
/**
* The number of pages.
*/
int pageCount;
/** /**
* The sum of the max length of all pages. * The sum of the max length of all pages.
*/ */
...@@ -75,10 +81,12 @@ public class Chunk { ...@@ -75,10 +81,12 @@ public class Chunk {
} }
int length = buff.getInt(); int length = buff.getInt();
int chunkId = buff.getInt(); int chunkId = buff.getInt();
int pageCount = buff.getInt();
long metaRootPos = buff.getLong(); long metaRootPos = buff.getLong();
long maxLengthLive = buff.getLong(); long maxLengthLive = buff.getLong();
Chunk c = new Chunk(chunkId); Chunk c = new Chunk(chunkId);
c.length = length; c.length = length;
c.pageCount = pageCount;
c.start = start; c.start = start;
c.metaRootPos = metaRootPos; c.metaRootPos = metaRootPos;
c.maxLengthLive = maxLengthLive; c.maxLengthLive = maxLengthLive;
...@@ -89,6 +97,7 @@ public class Chunk { ...@@ -89,6 +97,7 @@ public class Chunk {
buff.put((byte) 'c'); buff.put((byte) 'c');
buff.putInt(length); buff.putInt(length);
buff.putInt(id); buff.putInt(id);
buff.putInt(pageCount);
buff.putLong(metaRootPos); buff.putLong(metaRootPos);
buff.putLong(maxLengthLive); buff.putLong(maxLengthLive);
} }
...@@ -105,6 +114,7 @@ public class Chunk { ...@@ -105,6 +114,7 @@ public class Chunk {
Chunk c = new Chunk(id); Chunk c = new Chunk(id);
c.start = Long.parseLong(map.get("start")); c.start = Long.parseLong(map.get("start"));
c.length = Integer.parseInt(map.get("length")); c.length = Integer.parseInt(map.get("length"));
c.pageCount = Integer.parseInt(map.get("pageCount"));
c.maxLength = Long.parseLong(map.get("maxLength")); c.maxLength = Long.parseLong(map.get("maxLength"));
c.maxLengthLive = Long.parseLong(map.get("maxLengthLive")); c.maxLengthLive = Long.parseLong(map.get("maxLengthLive"));
c.metaRootPos = Long.parseLong(map.get("metaRoot")); c.metaRootPos = Long.parseLong(map.get("metaRoot"));
...@@ -129,6 +139,7 @@ public class Chunk { ...@@ -129,6 +139,7 @@ public class Chunk {
"id:" + id + "," + "id:" + id + "," +
"start:" + start + "," + "start:" + start + "," +
"length:" + length + "," + "length:" + length + "," +
"pageCount:" + pageCount + "," +
"maxLength:" + maxLength + "," + "maxLength:" + maxLength + "," +
"maxLengthLive:" + maxLengthLive + "," + "maxLengthLive:" + maxLengthLive + "," +
"metaRoot:" + metaRootPos + "," + "metaRoot:" + metaRootPos + "," +
......
...@@ -56,6 +56,16 @@ public class DataUtils { ...@@ -56,6 +56,16 @@ public class DataUtils {
*/ */
public static final long COMPRESSED_VAR_LONG_MAX = 0x1ffffffffffffL; public static final long COMPRESSED_VAR_LONG_MAX = 0x1ffffffffffffL;
/**
* The estimated number of bytes used per page object.
*/
public static final int PAGE_MEMORY = 128;
/**
* The estimated number of bytes used per child entry.
*/
public static final int PAGE_MEMORY_CHILD = 16;
/** /**
* Get the length of the variable size int. * Get the length of the variable size int.
* *
......
...@@ -79,11 +79,13 @@ public class Dump { ...@@ -79,11 +79,13 @@ public class Dump {
} }
int chunkLength = block.getInt(); int chunkLength = block.getInt();
int chunkId = block.getInt(); int chunkId = block.getInt();
int pageCount = block.getInt();
long metaRootPos = block.getLong(); long metaRootPos = block.getLong();
long maxLengthLive = block.getLong(); long maxLengthLive = block.getLong();
writer.println(" chunk " + chunkId + writer.println(" chunk " + chunkId +
" at " + pos + " at " + pos +
" length " + chunkLength + " length " + chunkLength +
" pageCount " + pageCount +
" root " + metaRootPos + " root " + metaRootPos +
" maxLengthLive " + maxLengthLive); " maxLengthLive " + maxLengthLive);
ByteBuffer chunk = ByteBuffer.allocate(chunkLength); ByteBuffer chunk = ByteBuffer.allocate(chunkLength);
......
...@@ -55,6 +55,7 @@ public class MVMap<K, V> extends AbstractMap<K, V> ...@@ -55,6 +55,7 @@ public class MVMap<K, V> extends AbstractMap<K, V>
this.valueType = valueType; this.valueType = valueType;
this.createVersion = createVersion; this.createVersion = createVersion;
this.root = Page.createEmpty(this, createVersion - 1); this.root = Page.createEmpty(this, createVersion - 1);
store.registerUnsavedPage();
} }
/** /**
...@@ -69,7 +70,7 @@ public class MVMap<K, V> extends AbstractMap<K, V> ...@@ -69,7 +70,7 @@ public class MVMap<K, V> extends AbstractMap<K, V>
checkWrite(); checkWrite();
long writeVersion = store.getCurrentVersion(); long writeVersion = store.getCurrentVersion();
Page p = root.copyOnWrite(writeVersion); Page p = root.copyOnWrite(writeVersion);
if (p.getKeyCount() > store.getMaxPageSize()) { if (p.getMemory() > store.getMaxPageSize() && p.getKeyCount() > 1) {
int at = p.getKeyCount() / 2; int at = p.getKeyCount() / 2;
long totalCount = p.getTotalCount(); long totalCount = p.getTotalCount();
Object k = p.getKey(at); Object k = p.getKey(at);
...@@ -79,7 +80,8 @@ public class MVMap<K, V> extends AbstractMap<K, V> ...@@ -79,7 +80,8 @@ public class MVMap<K, V> extends AbstractMap<K, V>
Page[] childrenPages = { p, split }; Page[] childrenPages = { p, split };
long[] counts = { p.getTotalCount(), split.getTotalCount() }; long[] counts = { p.getTotalCount(), split.getTotalCount() };
p = Page.create(this, writeVersion, 1, p = Page.create(this, writeVersion, 1,
keys, null, children, childrenPages, counts, totalCount, 0); keys, null, children, childrenPages, counts, totalCount, 0, 0);
store.registerUnsavedPage();
// now p is a node; insert continues // now p is a node; insert continues
} }
Object result = put(p, writeVersion, key, value); Object result = put(p, writeVersion, key, value);
...@@ -114,7 +116,7 @@ public class MVMap<K, V> extends AbstractMap<K, V> ...@@ -114,7 +116,7 @@ public class MVMap<K, V> extends AbstractMap<K, V>
index++; index++;
} }
Page c = p.getChildPage(index).copyOnWrite(writeVersion); Page c = p.getChildPage(index).copyOnWrite(writeVersion);
if (c.getKeyCount() >= store.getMaxPageSize()) { if (c.getMemory() > store.getMaxPageSize() && c.getKeyCount() > 1) {
// split on the way down // split on the way down
int at = c.getKeyCount() / 2; int at = c.getKeyCount() / 2;
Object k = c.getKey(at); Object k = c.getKey(at);
...@@ -575,23 +577,6 @@ public class MVMap<K, V> extends AbstractMap<K, V> ...@@ -575,23 +577,6 @@ public class MVMap<K, V> extends AbstractMap<K, V>
} }
Page cOld = p.getChildPage(index); Page cOld = p.getChildPage(index);
Page c = cOld.copyOnWrite(writeVersion); Page c = cOld.copyOnWrite(writeVersion);
int todoMerge;
// if (c.getKeyCount() < store.getMaxPageSize() / 2) {
// if (p.getChildPageCount() == 1) {
// int todo;
// // replace this node with the child
// } else if (index > 0) {
// int indexSibling = index - 1;
// Page sOld = p.getChildPage(indexSibling);
// merge(cOld, sOld);
// p.remove(indexSibling);
// } else {
// int indexSibling = index + 1;
// Page sOld = p.getChildPage(indexSibling);
// }
// }
long oldCount = c.getTotalCount(); long oldCount = c.getTotalCount();
result = remove(c, writeVersion, key); result = remove(c, writeVersion, key);
if (oldCount == c.getTotalCount()) { if (oldCount == c.getTotalCount()) {
...@@ -611,29 +596,6 @@ public class MVMap<K, V> extends AbstractMap<K, V> ...@@ -611,29 +596,6 @@ public class MVMap<K, V> extends AbstractMap<K, V>
return result; return result;
} }
int todoMerge;
// private boolean merge(Page a, Page b, boolean left) {
// boolean leaf = a.isLeaf();
// if (leaf != b.isLeaf()) {
// return false;
// }
// if (left) {
// int moved = 0;
// while (a.getKeyCount() < b.getKeyCount() - 1) {
// if (leaf) {
// Object k = b.getKey(0);
// Object v = b.getValue(0);
// b.remove(0);
// a.insertLeaf(a.getKeyCount(), k, v);
// } else {
//
// }
// moved++;
// }
// }
// }
// }
protected void setRoot(Page newRoot) { protected void setRoot(Page newRoot) {
if (root != newRoot) { if (root != newRoot) {
removeUnusedOldVersions(); removeUnusedOldVersions();
......
...@@ -35,13 +35,12 @@ header: ...@@ -35,13 +35,12 @@ header:
H:3,blockSize=4096,... H:3,blockSize=4096,...
TODO: TODO:
- merge pages if small - support custom fields in the file header (auto-server ip address,...)
- support stores that span multiple files (chunks stored in other files) - support stores that span multiple files (chunks stored in other files)
- triggers - triggers
- r-tree: add missing features (NN search for example) - r-tree: add missing features (NN search for example)
- pluggable cache (specially for in-memory file systems) - pluggable cache (specially for in-memory file systems)
- maybe store the factory class in the file header - maybe store the factory class in the file header
- support custom fields in the header (auto-server ip address,...)
- auto-server: store port in the header - auto-server: store port in the header
- recovery: keep some old chunks; don't overwritten for 1 minute - recovery: keep some old chunks; don't overwritten for 1 minute
- pluggable caching (specially for in-memory file systems) - pluggable caching (specially for in-memory file systems)
...@@ -57,11 +56,12 @@ TODO: ...@@ -57,11 +56,12 @@ TODO:
- support background writes (concurrent modification & store) - support background writes (concurrent modification & store)
- limited support for writing to old versions (branches) - limited support for writing to old versions (branches)
- support concurrent operations (including file I/O) - support concurrent operations (including file I/O)
- maxPageSize should be size in bytes (not it is actually maxPageEntryCount)
- on insert, if the child page is already full, don't load and modify it - split directly - on insert, if the child page is already full, don't load and modify it - split directly
- performance test with encrypting file system - performance test with encrypting file system
- possibly split chunk data into immutable and mutable - possibly split chunk data into immutable and mutable
- compact: avoid processing pages using a counting bloom filter - compact: avoid processing pages using a counting bloom filter
- defragment (re-creating maps, specially those with small pages)
- write using ByteArrayOutputStream; remove DataType.getMaxLength
*/ */
...@@ -85,7 +85,7 @@ public class MVStore { ...@@ -85,7 +85,7 @@ public class MVStore {
private final int readCacheSize = 2 * 1024 * 1024; private final int readCacheSize = 2 * 1024 * 1024;
private int maxPageSize = 30; private int maxPageSize = 4 * 1024;
private FileChannel file; private FileChannel file;
private long fileSize; private long fileSize;
...@@ -123,6 +123,7 @@ public class MVStore { ...@@ -123,6 +123,7 @@ public class MVStore {
private long currentVersion; private long currentVersion;
private int readCount; private int readCount;
private int writeCount; private int writeCount;
private int unsavedPageCount;
private MVStore(String fileName, MapFactory mapFactory) { private MVStore(String fileName, MapFactory mapFactory) {
this.fileName = fileName; this.fileName = fileName;
...@@ -502,7 +503,7 @@ public class MVStore { ...@@ -502,7 +503,7 @@ public class MVStore {
* *
* @return the new version (incremented if there were changes) * @return the new version (incremented if there were changes)
*/ */
public long store() { public long save() {
if (!hasUnsavedChanges()) { if (!hasUnsavedChanges()) {
return currentVersion; return currentVersion;
} }
...@@ -560,7 +561,7 @@ public class MVStore { ...@@ -560,7 +561,7 @@ public class MVStore {
} }
Page p = m.getRoot(); Page p = m.getRoot();
if (p.getTotalCount() > 0) { if (p.getTotalCount() > 0) {
long root = p.writeTempRecursive(c, buff); long root = p.writeUnsavedRecursive(c, buff);
meta.put("root." + m.getId(), "" + root); meta.put("root." + m.getId(), "" + root);
} }
} }
...@@ -569,7 +570,7 @@ public class MVStore { ...@@ -569,7 +570,7 @@ public class MVStore {
// this will modify maxLengthLive, but // this will modify maxLengthLive, but
// the correct value is written in the chunk header // the correct value is written in the chunk header
meta.getRoot().writeTempRecursive(c, buff); meta.getRoot().writeUnsavedRecursive(c, buff);
buff.flip(); buff.flip();
int length = buff.limit(); int length = buff.limit();
...@@ -604,6 +605,7 @@ public class MVStore { ...@@ -604,6 +605,7 @@ public class MVStore {
// write the new version (after the commit) // write the new version (after the commit)
writeHeader(); writeHeader();
shrinkFileIfPossible(1); shrinkFileIfPossible(1);
unsavedPageCount = 0;
return version; return version;
} }
...@@ -799,7 +801,7 @@ public class MVStore { ...@@ -799,7 +801,7 @@ public class MVStore {
copyLive(c, old); copyLive(c, old);
} }
store(); save();
return true; return true;
} }
...@@ -885,24 +887,26 @@ public class MVStore { ...@@ -885,24 +887,26 @@ public class MVStore {
void removePage(long pos) { void removePage(long pos) {
// we need to keep temporary pages, // we need to keep temporary pages,
// to support reading old versions and rollback // to support reading old versions and rollback
if (pos > 0) { if (pos == 0) {
// this could result in a cache miss unsavedPageCount--;
// if the operation is rolled back, return;
// but we don't optimize for rollback }
cache.remove(pos); // this could result in a cache miss
Chunk c = getChunk(pos); // if the operation is rolled back,
HashMap<Integer, Chunk>freed = freedChunks.get(currentVersion); // but we don't optimize for rollback
if (freed == null) { cache.remove(pos);
freed = New.hashMap(); Chunk c = getChunk(pos);
freedChunks.put(currentVersion, freed); HashMap<Integer, Chunk>freed = freedChunks.get(currentVersion);
} if (freed == null) {
Chunk f = freed.get(c.id); freed = New.hashMap();
if (f == null) { freedChunks.put(currentVersion, freed);
f = new Chunk(c.id); }
freed.put(c.id, f); Chunk f = freed.get(c.id);
} if (f == null) {
f.maxLengthLive -= DataUtils.getPageMaxLength(pos); f = new Chunk(c.id);
freed.put(c.id, f);
} }
f.maxLengthLive -= DataUtils.getPageMaxLength(pos);
} }
/** /**
...@@ -915,14 +919,22 @@ public class MVStore { ...@@ -915,14 +919,22 @@ public class MVStore {
// System.out.println(string); // System.out.println(string);
} }
/**
* Set the maximum amount of memory a page should contain, in bytes. Larger
* pages are split. The default is 4 KB. This is not a limit in the page
* size, as pages with one entry can be larger. As a rule of thumb, pages
* should not be larger than 1 MB, for caching to work efficiently.
*
* @param maxPageSize the page size
*/
public void setMaxPageSize(int maxPageSize) { public void setMaxPageSize(int maxPageSize) {
this.maxPageSize = maxPageSize; this.maxPageSize = maxPageSize;
} }
/** /**
* The maximum number of key-value pairs in a page. * Get the maximum page size, in bytes.
* *
* @return the maximum number of entries * @return the maximum page size
*/ */
public int getMaxPageSize() { public int getMaxPageSize() {
return maxPageSize; return maxPageSize;
...@@ -1011,6 +1023,21 @@ public class MVStore { ...@@ -1011,6 +1023,21 @@ public class MVStore {
return true; return true;
} }
/**
* Get the estimated number of unsaved pages. The returned value is not
* accurate, specially after rollbacks, but can be used to estimate the
* memory usage for unsaved data.
*
* @return the number of unsaved pages
*/
public int getUnsavedPageCount() {
return unsavedPageCount;
}
void registerUnsavedPage() {
unsavedPageCount++;
}
public int getStoreVersion() { public int getStoreVersion() {
String x = getSetting("storeVersion"); String x = getSetting("storeVersion");
return x == null ? 0 : Integer.parseInt(x); return x == null ? 0 : Integer.parseInt(x);
......
...@@ -51,6 +51,11 @@ public class Page { ...@@ -51,6 +51,11 @@ public class Page {
*/ */
private int sharedFlags; private int sharedFlags;
/**
* The estimated memory used.
*/
private int memory;
private Object[] keys; private Object[] keys;
private Object[] values; private Object[] values;
private long[] children; private long[] children;
...@@ -72,7 +77,7 @@ public class Page { ...@@ -72,7 +77,7 @@ public class Page {
public static Page createEmpty(MVMap<?, ?> map, long version) { public static Page createEmpty(MVMap<?, ?> map, long version) {
return create(map, version, 0, return create(map, version, 0,
EMPTY_OBJECT_ARRAY, EMPTY_OBJECT_ARRAY, EMPTY_OBJECT_ARRAY, EMPTY_OBJECT_ARRAY,
null, null, null, 0, 0); null, null, null, 0, 0, DataUtils.PAGE_MEMORY);
} }
/** /**
...@@ -93,7 +98,7 @@ public class Page { ...@@ -93,7 +98,7 @@ public class Page {
public static Page create(MVMap<?, ?> map, long version, public static Page create(MVMap<?, ?> map, long version,
int keyCount, Object[] keys, int keyCount, Object[] keys,
Object[] values, long[] children, Page[] childrenPages, long[] counts, Object[] values, long[] children, Page[] childrenPages, long[] counts,
long totalCount, int sharedFlags) { long totalCount, int sharedFlags, int memory) {
Page p = new Page(map, version); Page p = new Page(map, version);
// the position is 0 // the position is 0
p.keys = keys; p.keys = keys;
...@@ -104,6 +109,7 @@ public class Page { ...@@ -104,6 +109,7 @@ public class Page {
p.counts = counts; p.counts = counts;
p.totalCount = totalCount; p.totalCount = totalCount;
p.sharedFlags = sharedFlags; p.sharedFlags = sharedFlags;
p.memory = memory == 0 ? p.calculateMemory() : memory;
return p; return p;
} }
...@@ -246,7 +252,9 @@ public class Page { ...@@ -246,7 +252,9 @@ public class Page {
Page newPage = create(map, writeVersion, Page newPage = create(map, writeVersion,
keyCount, keys, values, children, childrenPages, keyCount, keys, values, children, childrenPages,
counts, totalCount, counts, totalCount,
SHARED_KEYS | SHARED_VALUES | SHARED_CHILDREN | SHARED_COUNTS); SHARED_KEYS | SHARED_VALUES | SHARED_CHILDREN | SHARED_COUNTS,
memory);
map.getStore().registerUnsavedPage();
newPage.cachedCompare = cachedCompare; newPage.cachedCompare = cachedCompare;
return newPage; return newPage;
} }
...@@ -328,7 +336,10 @@ public class Page { ...@@ -328,7 +336,10 @@ public class Page {
totalCount = a; totalCount = a;
Page newPage = create(map, version, b, Page newPage = create(map, version, b,
bKeys, bValues, null, null, null, bKeys, bValues, null, null, null,
bKeys.length, 0); bKeys.length, 0, 0);
map.getStore().registerUnsavedPage();
memory = calculateMemory();
newPage.memory = newPage.calculateMemory();
return newPage; return newPage;
} }
...@@ -372,7 +383,10 @@ public class Page { ...@@ -372,7 +383,10 @@ public class Page {
} }
Page newPage = create(map, version, b - 1, Page newPage = create(map, version, b - 1,
bKeys, null, bChildren, bChildrenPages, bKeys, null, bChildren, bChildrenPages,
bCounts, t, 0); bCounts, t, 0, 0);
map.getStore().registerUnsavedPage();
memory = calculateMemory();
newPage.memory = newPage.calculateMemory();
return newPage; return newPage;
} }
...@@ -441,6 +455,11 @@ public class Page { ...@@ -441,6 +455,11 @@ public class Page {
keys = Arrays.copyOf(keys, keys.length); keys = Arrays.copyOf(keys, keys.length);
sharedFlags &= ~SHARED_KEYS; sharedFlags &= ~SHARED_KEYS;
} }
Object old = keys[index];
if (old != null) {
memory -= map.getKeyType().getMemory(old);
}
memory += map.getKeyType().getMemory(key);
keys[index] = key; keys[index] = key;
} }
...@@ -457,6 +476,8 @@ public class Page { ...@@ -457,6 +476,8 @@ public class Page {
values = Arrays.copyOf(values, values.length); values = Arrays.copyOf(values, values.length);
sharedFlags &= ~SHARED_VALUES; sharedFlags &= ~SHARED_VALUES;
} }
memory -= map.getValueType().getMemory(old);
memory += map.getValueType().getMemory(value);
values[index] = value; values[index] = value;
return old; return old;
} }
...@@ -511,6 +532,8 @@ public class Page { ...@@ -511,6 +532,8 @@ public class Page {
keyCount++; keyCount++;
sharedFlags &= ~(SHARED_KEYS | SHARED_VALUES); sharedFlags &= ~(SHARED_KEYS | SHARED_VALUES);
totalCount++; totalCount++;
memory += map.getKeyType().getMemory(key);
memory += map.getValueType().getMemory(value);
} }
/** /**
...@@ -545,6 +568,8 @@ public class Page { ...@@ -545,6 +568,8 @@ public class Page {
sharedFlags &= ~(SHARED_KEYS | SHARED_CHILDREN | SHARED_COUNTS); sharedFlags &= ~(SHARED_KEYS | SHARED_CHILDREN | SHARED_COUNTS);
totalCount += childPage.getTotalCount(); totalCount += childPage.getTotalCount();
memory += map.getKeyType().getMemory(key);
memory += DataUtils.PAGE_MEMORY_CHILD;
} }
/** /**
...@@ -554,6 +579,8 @@ public class Page { ...@@ -554,6 +579,8 @@ public class Page {
*/ */
public void remove(int index) { public void remove(int index) {
int keyIndex = index >= keyCount ? index - 1 : index; int keyIndex = index >= keyCount ? index - 1 : index;
Object old = keys[keyIndex];
memory -= map.getKeyType().getMemory(old);
if ((sharedFlags & SHARED_KEYS) == 0 && keys.length > keyCount - 4) { if ((sharedFlags & SHARED_KEYS) == 0 && keys.length > keyCount - 4) {
if (keyIndex < keyCount - 1) { if (keyIndex < keyCount - 1) {
System.arraycopy(keys, keyIndex + 1, keys, keyIndex, keyCount - keyIndex - 1); System.arraycopy(keys, keyIndex + 1, keys, keyIndex, keyCount - keyIndex - 1);
...@@ -567,6 +594,8 @@ public class Page { ...@@ -567,6 +594,8 @@ public class Page {
} }
if (values != null) { if (values != null) {
old = values[index];
memory -= map.getValueType().getMemory(old);
if ((sharedFlags & SHARED_VALUES) == 0 && values.length > keyCount - 4) { if ((sharedFlags & SHARED_VALUES) == 0 && values.length > keyCount - 4) {
if (index < keyCount - 1) { if (index < keyCount - 1) {
System.arraycopy(values, index + 1, values, index, keyCount - index - 1); System.arraycopy(values, index + 1, values, index, keyCount - index - 1);
...@@ -582,6 +611,7 @@ public class Page { ...@@ -582,6 +611,7 @@ public class Page {
} }
keyCount--; keyCount--;
if (children != null) { if (children != null) {
memory -= DataUtils.PAGE_MEMORY_CHILD;
long countOffset = counts[index]; long countOffset = counts[index];
long[] newChildren = new long[children.length - 1]; long[] newChildren = new long[children.length - 1];
...@@ -638,8 +668,10 @@ public class Page { ...@@ -638,8 +668,10 @@ public class Page {
compressor.expand(comp, 0, compLen, exp, 0, exp.length); compressor.expand(comp, 0, compLen, exp, 0, exp.length);
buff = ByteBuffer.wrap(exp); buff = ByteBuffer.wrap(exp);
} }
DataType keyType = map.getKeyType();
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
keys[i] = map.getKeyType().read(buff); Object k = keyType.read(buff);
keys[i] = k;
} }
if (node) { if (node) {
children = new long[len + 1]; children = new long[len + 1];
...@@ -657,11 +689,14 @@ public class Page { ...@@ -657,11 +689,14 @@ public class Page {
totalCount = total; totalCount = total;
} else { } else {
values = new Object[len]; values = new Object[len];
DataType valueType = map.getValueType();
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
values[i] = map.getValueType().read(buff); Object v = valueType.read(buff);
values[i] = v;
} }
totalCount = len; totalCount = len;
} }
memory = calculateMemory();
} }
/** /**
...@@ -722,6 +757,7 @@ public class Page { ...@@ -722,6 +757,7 @@ public class Page {
long max = DataUtils.getPageMaxLength(pos); long max = DataUtils.getPageMaxLength(pos);
chunk.maxLength += max; chunk.maxLength += max;
chunk.maxLengthLive += max; chunk.maxLengthLive += max;
chunk.pageCount++;
} }
/** /**
...@@ -769,13 +805,13 @@ public class Page { ...@@ -769,13 +805,13 @@ public class Page {
* @param buff the target buffer * @param buff the target buffer
* @return the page id * @return the page id
*/ */
long writeTempRecursive(Chunk chunk, ByteBuffer buff) { long writeUnsavedRecursive(Chunk chunk, ByteBuffer buff) {
if (!isLeaf()) { if (!isLeaf()) {
int len = children.length; int len = children.length;
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
Page p = childrenPages[i]; Page p = childrenPages[i];
if (p != null) { if (p != null) {
children[i] = p.writeTempRecursive(chunk, buff); children[i] = p.writeUnsavedRecursive(chunk, buff);
childrenPages[i] = null; childrenPages[i] = null;
} }
} }
...@@ -809,4 +845,28 @@ public class Page { ...@@ -809,4 +845,28 @@ public class Page {
return pos != 0 ? (int) (pos | (pos >>> 32)) : super.hashCode(); return pos != 0 ? (int) (pos | (pos >>> 32)) : super.hashCode();
} }
public int getMemory() {
if (MVStore.ASSERT) {
if (memory != calculateMemory()) {
throw new RuntimeException("Memory calculation error");
}
}
return memory;
}
private int calculateMemory() {
int mem = DataUtils.PAGE_MEMORY;
for (int i = 0; i < keyCount; i++) {
mem += map.getKeyType().getMemory(keys[i]);
}
if (this.isLeaf()) {
for (int i = 0; i < keyCount; i++) {
mem += map.getValueType().getMemory(values[i]);
}
} else {
mem += this.getChildPageCount() * DataUtils.PAGE_MEMORY_CHILD;
}
return mem;
}
} }
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论