Skip to content

Commit fe28c23

Browse files
Table of Persistance.Reference at the end of the stream (#9972)
Fixes #9361 by delaying storing of `Persistance.Reference` instances and creating their table at the end of the stream.
1 parent c5a91a6 commit fe28c23

File tree

8 files changed

+279
-71
lines changed

8 files changed

+279
-71
lines changed

build.sbt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1251,6 +1251,8 @@ lazy val `ydoc-server` = project
12511251
lazy val `persistance` = (project in file("lib/java/persistance"))
12521252
.settings(
12531253
version := "0.1",
1254+
Test / fork := true,
1255+
commands += WithDebugCommand.withDebug,
12541256
frgaalJavaCompilerSetting,
12551257
Compile / javacOptions := ((Compile / javacOptions).value),
12561258
libraryDependencies ++= Seq(

engine/runtime-parser/src/test/java/org/enso/compiler/core/IrPersistanceTest.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public void resetDebris() {
3333
@Test
3434
public void locationTest() throws Exception {
3535
var l = new Location(12, 33);
36-
var n = serde(Location.class, l, 8);
36+
var n = serde(Location.class, l, 16);
3737

3838
assertEquals(12, n.start());
3939
assertEquals(33, n.end());
@@ -43,14 +43,14 @@ public void locationTest() throws Exception {
4343
@Test
4444
public void identifiedLocation() throws Exception {
4545
var il = new IdentifiedLocation(new Location(5, 19), null);
46-
var in = serde(IdentifiedLocation.class, il, 12);
46+
var in = serde(IdentifiedLocation.class, il, 20);
4747
assertEquals(il, in);
4848
}
4949

5050
@Test
5151
public void identifiedLocationWithUUID() throws Exception {
5252
var il = new IdentifiedLocation(new Location(5, 19), UUID.randomUUID());
53-
var in = serde(IdentifiedLocation.class, il, 33);
53+
var in = serde(IdentifiedLocation.class, il, 41);
5454
assertEquals("UUIDs are serialized at the moment", il, in);
5555
}
5656

@@ -63,7 +63,7 @@ public void identifiedLocationNoUUID() throws Exception {
6363
case UUID any -> null;
6464
default -> obj;
6565
};
66-
var in = serde(IdentifiedLocation.class, il, 12, fn);
66+
var in = serde(IdentifiedLocation.class, il, 20, fn);
6767
var withoutUUID = new IdentifiedLocation(il.location());
6868
assertEquals("UUIDs are no longer serialized", withoutUUID, in);
6969
}
@@ -107,7 +107,7 @@ public void refHolderNoUUID() throws Exception {
107107
case UUID any -> null;
108108
default -> obj;
109109
};
110-
var in = serde(IdHolder.class, il, 1, fn);
110+
var in = serde(IdHolder.class, il, 9, fn);
111111
var withoutUUID = new IdHolder(null);
112112
assertEquals("UUIDs are no longer serialized", withoutUUID, in);
113113
}
@@ -118,7 +118,7 @@ public void scalaMap() throws Exception {
118118
var idLoc1 = new IdentifiedLocation(new Location(1, 5));
119119
var in = scala.collection.immutable.Map$.MODULE$.empty().$plus(new Tuple2("Hi", idLoc1));
120120

121-
var out = serde(scala.collection.immutable.Map.class, in, 36);
121+
var out = serde(scala.collection.immutable.Map.class, in, 44);
122122

123123
assertEquals("One element", 1, out.size());
124124
assertEquals(in, out);
@@ -137,7 +137,7 @@ public void scalaImmutableMapIsLazy() throws Exception {
137137
.$plus(new Tuple2("World", s2));
138138

139139
LazySeq.forbidden = true;
140-
var out = (scala.collection.immutable.Map) serde(scala.collection.immutable.Map.class, in, 64);
140+
var out = (scala.collection.immutable.Map) serde(scala.collection.immutable.Map.class, in, 72);
141141

142142
assertEquals("Two pairs element", 2, out.size());
143143
assertEquals("Two keys", 2, out.keySet().size());
@@ -159,7 +159,7 @@ public void scalaHashMap() throws Exception {
159159
(scala.collection.mutable.HashMap)
160160
scala.collection.mutable.HashMap$.MODULE$.apply(immutable);
161161

162-
var out = serde(scala.collection.mutable.Map.class, in, 36);
162+
var out = serde(scala.collection.mutable.Map.class, in, 44);
163163

164164
assertEquals("One element", 1, out.size());
165165
assertEquals(in, out);
@@ -171,7 +171,7 @@ public void scalaSet() throws Exception {
171171
var idLoc1 = new IdentifiedLocation(new Location(1, 5));
172172
var in = scala.collection.immutable.Set$.MODULE$.empty().$plus(idLoc1);
173173

174-
var out = serde(scala.collection.immutable.Set.class, in, 24);
174+
var out = serde(scala.collection.immutable.Set.class, in, 32);
175175

176176
assertEquals("One element", 1, out.size());
177177
assertEquals(in, out);
@@ -183,7 +183,7 @@ public void scalaList() throws Exception {
183183
var idLoc2 = new IdentifiedLocation(new Location(2, 4), UUID.randomUUID());
184184
var in = join(idLoc2, join(idLoc1, nil()));
185185

186-
var out = serde(List.class, in, 65);
186+
var out = serde(List.class, in, 73);
187187

188188
assertEquals("Two elements", 2, out.size());
189189
assertEquals("UUIDs are serialized at the moment", idLoc2, out.head());
@@ -195,7 +195,7 @@ public void scalaListSharedRef() throws Exception {
195195
var idLoc1 = new IdentifiedLocation(new Location(1, 5));
196196
var in = join(idLoc1, join(idLoc1, nil()));
197197

198-
var out = serde(List.class, in, 32);
198+
var out = serde(List.class, in, 40);
199199

200200
assertEquals("Two elements", 2, out.size());
201201
assertEquals("Head is equal to original", idLoc1, out.head());
@@ -288,7 +288,7 @@ public void hashMapIsLazy() throws Exception {
288288
in.put("World", s2);
289289

290290
LazySeq.forbidden = true;
291-
var out = serde(java.util.Map.class, in, 64);
291+
var out = serde(java.util.Map.class, in, 72);
292292

293293
assertEquals("Two pairs element", 2, out.size());
294294
assertEquals("Two keys", 2, out.keySet().size());
Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.enso.persist;
22

3+
import java.io.IOException;
34
import org.enso.persist.PerInputImpl.InputCache;
45
import org.enso.persist.Persistance.Reference;
56

@@ -8,23 +9,44 @@ final class PerBufferReference<T> extends Persistance.Reference<T> {
89
private final PerInputImpl.InputCache cache;
910
private final int offset;
1011

11-
private PerBufferReference(Persistance<T> p, PerInputImpl.InputCache buffer, int offset) {
12+
/**
13+
* References can be cached, or loaded again every time.
14+
*
15+
* <p>If {@code cached} is set to {@code this}, then the caching is disabled and {@link
16+
* #get(Class<V>)} will always load a new instance of the object. This is the mode one gets when
17+
* using an API method {@link Persistance.Input#readReference(Class<T>)}.
18+
*
19+
* <p>In other cases the {@code cached} value can be {@code null} meaning <em>not yet loaded</em>
20+
* or non-{@code null} holding the cached value to be returned from the {@link #get(Class<V>)}
21+
* method until this reference instance is GCed.
22+
*/
23+
private Object cached;
24+
25+
private PerBufferReference(
26+
Persistance<T> p, PerInputImpl.InputCache buffer, int offset, boolean allowCaching) {
1227
this.p = p;
1328
this.cache = buffer;
1429
this.offset = offset;
30+
this.cached = allowCaching ? null : this;
1531
}
1632

1733
@SuppressWarnings(value = "unchecked")
18-
final <T> T readObject(Class<T> clazz) {
34+
final <T> T readObject(Class<T> clazz) throws IOException {
35+
if (cached != this && clazz.isInstance(cached)) {
36+
return clazz.cast(cached);
37+
}
1938
if (p != null) {
2039
if (clazz.isAssignableFrom(p.clazz)) {
2140
clazz = (Class) p.clazz;
2241
} else {
2342
throw new ClassCastException();
2443
}
2544
}
26-
org.enso.persist.PerInputImpl in = new PerInputImpl(cache, offset);
45+
var in = new PerInputImpl(cache, offset);
2746
T obj = in.readInline(clazz);
47+
if (cached != this) {
48+
cached = obj;
49+
}
2850
return obj;
2951
}
3052

@@ -33,6 +55,10 @@ static <V> Reference<V> from(InputCache buffer, int offset) {
3355
}
3456

3557
static <V> Reference<V> from(Persistance<V> p, InputCache buffer, int offset) {
36-
return new PerBufferReference<>(p, buffer, offset);
58+
return new PerBufferReference<>(p, buffer, offset, false);
59+
}
60+
61+
static <V> Reference<V> cached(Persistance<V> p, InputCache buffer, int offset) {
62+
return new PerBufferReference<>(p, buffer, offset, true);
3763
}
3864
}

lib/java/persistance/src/main/java/org/enso/persist/PerGenerator.java

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
import org.slf4j.Logger;
1313

1414
final class PerGenerator {
15-
static final byte[] HEADER = new byte[] {0x0a, 0x0d, 0x03, 0x0f};
15+
static final byte[] HEADER = new byte[] {0x0a, 0x0d, 0x13, 0x0f};
1616
private final OutputStream main;
1717
private final Map<Object, Integer> knownObjects = new IdentityHashMap<>();
18+
private int countReferences = 1;
19+
private final Map<Object, Integer> pendingReferences = new IdentityHashMap<>();
1820
private final Histogram histogram;
1921
private final PerMap map;
20-
final Function<Object, Object> writeReplace;
22+
private final Function<Object, Object> writeReplace;
2123
private int position;
2224

2325
private PerGenerator(
@@ -39,19 +41,25 @@ static byte[] writeObject(Object obj, Function<Object, Object> writeReplace) thr
3941
data.writeInt(g.versionStamp());
4042
data.write(new byte[4]); // space
4143
data.flush();
42-
var at = g.writeObject(obj);
44+
45+
var at = g.writeObjectAndReferences(obj);
46+
4347
var arr = out.toByteArray();
44-
arr[8] = (byte) ((at >> 24) & 0xff);
45-
arr[9] = (byte) ((at >> 16) & 0xff);
46-
arr[10] = (byte) ((at >> 8) & 0xff);
47-
arr[11] = (byte) (at & 0xff);
48+
putIntToArray(arr, 8, at);
4849

4950
if (histogram != null) {
5051
histogram.dump(PerUtils.LOG, arr.length);
5152
}
5253
return arr;
5354
}
5455

56+
private static void putIntToArray(byte[] arr, int position, int value) {
57+
arr[position] = (byte) ((value >> 24) & 0xff);
58+
arr[position + 1] = (byte) ((value >> 16) & 0xff);
59+
arr[position + 2] = (byte) ((value >> 8) & 0xff);
60+
arr[position + 3] = (byte) (value & 0xff);
61+
}
62+
5563
final <T> int writeObject(T t) throws IOException {
5664
if (t == null) {
5765
return -1;
@@ -106,6 +114,63 @@ final int versionStamp() {
106114
return map.versionStamp;
107115
}
108116

117+
private int registerReference(Persistance.Reference<?> ref) {
118+
var obj = ref.get(Object.class);
119+
var existingId = pendingReferences.get(obj);
120+
if (existingId == null) {
121+
var currentSize = countReferences++;
122+
pendingReferences.put(obj, currentSize);
123+
return currentSize;
124+
} else {
125+
return existingId;
126+
}
127+
}
128+
129+
/**
130+
* Writes an object into the buffer. Writes also all {@link Persistance.Reference} that were left
131+
* pending during the serialization.
132+
*
133+
* @param obj the object to write down
134+
* @return location of the table {@code int size and then int[size]}
135+
*/
136+
private int writeObjectAndReferences(Object obj) throws IOException {
137+
pendingReferences.put(obj, 0);
138+
var objAt = writeObject(obj);
139+
140+
var refsOut = new ByteArrayOutputStream();
141+
var refsData = new DataOutputStream(refsOut);
142+
refsData.writeInt(-1); // space for size of references
143+
refsData.writeInt(objAt); // the main object
144+
var count = 1;
145+
for (; ; ) {
146+
var all = new ArrayList<>(pendingReferences.entrySet());
147+
all.sort(
148+
(e1, e2) -> {
149+
return e1.getValue() - e2.getValue();
150+
});
151+
var round = all.subList(count, all.size());
152+
if (round.isEmpty()) {
153+
break;
154+
}
155+
for (var entry : round) {
156+
count++;
157+
var at = writeObject(entry.getKey());
158+
assert count == entry.getValue() : "Expecting " + count + " got " + entry.getValue();
159+
refsData.writeInt(at);
160+
}
161+
}
162+
refsData.flush();
163+
var arr = refsOut.toByteArray();
164+
165+
putIntToArray(arr, 0, count);
166+
167+
var tableAt = this.position;
168+
this.main.write(arr);
169+
this.position += arr.length;
170+
171+
return tableAt;
172+
}
173+
109174
private static final class ReferenceOutput extends DataOutputStream
110175
implements Persistance.Output {
111176
private final PerGenerator generator;
@@ -117,6 +182,12 @@ private static final class ReferenceOutput extends DataOutputStream
117182

118183
@Override
119184
public <T> void writeInline(Class<T> clazz, T t) throws IOException {
185+
if (Persistance.Reference.class == clazz) {
186+
Persistance.Reference<?> ref = (Persistance.Reference<?>) t;
187+
var id = this.generator.registerReference(ref);
188+
writeInt(id);
189+
return;
190+
}
120191
var obj = generator.writeReplace.apply(t);
121192
var p = generator.map.forType(clazz);
122193
p.writeInline(obj, this);

0 commit comments

Comments
 (0)