RFR: 8247532: Records deserialization is slow
Peter Levart
peter.levart at gmail.com
Sun Jun 21 12:49:15 UTC 2020
Hi Chris,
Good work on SerialByteStreamBuilder. A very useful tool.
What I noticed in the tests is that you usually follow the following
pattern:
- create a record instance r
- serialize it to bytes and deserialize it back to instance deser1
- create a stream with SerialByteStreamBuilder and deserialize it to
instance deser2
- compare deser1 with deser2
Now if there was a bug in deserialization logic it would be possible for
both deser1 and deser2 to be equal but still wrong. Adding another
assert to compare r with deser1 in each test could catch such bug.
In the following test:
245 public void testArrays() throws Exception {
246 out.println("\n---");
247 {
248 record IntArray(int[] ints, long[] longs) implements
Serializable { }
249 IntArray r = new IntArray(new int[] { 5, 4, 3,2, 1 },
new long[] { 9L });
250 IntArray deser1 = deserialize(serialize(r));
251
252 byte[] builderBytes = SerialByteStreamBuilder
253 .newBuilder(IntArray.class.getName())
254 .addField("ints", String[].class, new int[] {
5, 4, 3,2, 1 })
255 .addField("longs", String[].class, new long[]
{ 9L })
256 .build();
257
258 IntArray deser2 = deserialize(builderBytes);
259 assertEquals(deser2.ints(), deser1.ints());
260 assertEquals(deser2.longs(), deser1.longs());
261 }
...in lines 254, 255, you specify wrong types of fields (making the
method generic with a type parameter could prevent such mistakes). I
wonder why the test passes. In both variants (with or without the patch)
we have this check in java.io.ObjectStreamClass.RecordSupport:
for (int i = 0; i < fields.length; i++) {
ObjectStreamField f = fields[i];
String fName = f.getName();
if (!fName.equals(pName))
continue;
Class<?> fType = f.getField().getType();
if (!pType.isAssignableFrom(fType))
throw new InternalError(fName + " unassignable, pType:" + pType + ", fType:" + fType);
... which is actually checking types of reflected
java.lang.reflact.Field(s) with types of reflected
java.lang.reflect.RecordComponent(s) that match in name which always
match when record type is correctly constructed. And later when values
are deserialized and assigned to constructor parameters, we have the
right types of values in this "unusual" stream. In other words, changing
the field type from one reference type to another is always a compatible
change as far as stream format is concerned, deserialization then
succeeds if the value in the stream is of correct type for the local
type of field. Theoretically the change can be such that the reference
types are unrelated, because such types have one common value: null. In
this example the stream was unusual. The types were unrelated, but the
stream had the correct type of value != null.
Anyway, maybe the test should also contain a case where the type of
field does change compatibly (for example, Integer field serialized,
deserialized into Number field), and one or more where it changes
incompatibly. For the incompatible changes, I think some exceptions
would be emitted by MethodHandle adapters that try to adapt incompatible
reference types, other incompatible primitive field type changes would
be caught by java.io.ObjectStreamClass.matchFields() method...
I think that the following method in SerialByteStreamBuilder is also
incorrect:
363 private void writeObjectFieldDesc(DataOutputStream out)
throws IOException {
364 for (Map.Entry<NameAndType,Object> entry :
objectFields.entrySet()) {
365 Class<?> cl = entry.getKey().type();
366 assert !cl.isPrimitive();
367 // obj_typecode
368 if (cl.isArray()) {
369 out.writeByte('[');
370 } else {
371 out.writeByte('L');
372 }
373 writeUTF(out, entry.getKey().name());
374 out.writeByte(TC_STRING);
375 writeUTF(out, "L" + cl.getName().replace('.', '/')
+ ";");
376
377 }
378 }
Line 375 should probably be:
writeUTF(out,
(cl.isArray() ? cl.getName() : "L" + cl.getName()
+ ";")
.replace('.', '/'));
So I took the liberty and incorporated your work into my patch (both
will be authors of the patch) and addressed above concerns. The modified
test does not pass on JDK14. In testIncompatibleRefFieldTypeChange JDK
14 throws ClassCastException (because it is thrown from the binding of
arguments to method handle) while with the patched JDK16 it throws
InvalidObjectException, because ClassCastException is thrown from
invokeExact, which is then wrapped with InvalidObjectException and I
think is more correct:
http://cr.openjdk.java.net/~plevart/jdk-dev/RecordsDeserialization/webrev.04/
So this patch actually fixes a little bug too.
Regards, Peter
6/17/20 4:13 PM, Chris Hegarty wrote:
> Peter,
>
>> On 17 Jun 2020, at 09:23, Chris Hegarty <chris.hegarty at oracle.com> wrote:
>>
>>> On 17 Jun 2020, at 06:44, Peter Levart <peter.levart at gmail.com> wrote:
>>>>> ..
>>>> If there is a way for a test to compile several versions of the same (record) class and then produce byte streams with it, then we could simulate various class-evolution cases (field missing, surplus field, change of field type, etc...) without crafting the bytestream by hand. WDYT?
>>> I have a better idea. The test could contain several different classes with different names that would be used to generated variations of serialized stream. Such streams could then be "patched" so they would contain the common target class name and then used to (attempt to) deserialize the target class. I'll try to devise such test…
>>>
>> I think that could work. I’ll mock something up too, just for comparison purposes.
>
> Here is an initial version at a test that can be expanded to cover more of the stream-field shapes of serializable records.
>
> I quickly backed away from generating the byte streams using OOS alone, since it enforces strict ordering of the fields in the stream, beyond that of primitives followed by non-primitives. And I want to be able to do things like, for example, record Point(int x, int y) - stream of x:1 y:2, or y:2 x:1 - which will verify the nominality aspect of the caching implementation. So I opted for a basic byte-stream-builder approach.
>
> https://cr.openjdk.java.net/~chegar/serialFieldTest_webrev/test/jdk/java/io/Serializable/records/DifferentStreamFieldsTest.java.html
>
> I also would like a builder of serial byte streams anyway, as it will be useful for things beyond this change. Maybe it could even be expanded upon and used as a test library, at some future point. The above implementation can probably be easily broken if pushed hard with larger graphs of objects, but it should be good enough for what we need here.
>
> -Chris.
>
More information about the core-libs-dev
mailing list