Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[POC] FieldSerializer for Records #931

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9706cd6
Experimental FieldSerializer for Records
theigl Feb 21, 2022
0a73948
Experimental FieldSerializer for Records
theigl Feb 24, 2022
b945876
Experimental FieldSerializer for Records
theigl Nov 13, 2022
10681fc
Experimental FieldSerializer for Records
theigl Nov 13, 2022
e8563aa
Experimental FieldSerializer for Records
theigl Nov 13, 2022
57b919c
Experimental FieldSerializer for Records
theigl Nov 13, 2022
8093552
Experimental FieldSerializer for Records
theigl Nov 13, 2022
1d8c838
Merge remote-tracking branch 'origin/master' into record-field-serial…
theigl Dec 16, 2022
86eceb8
Adjust expected default serializer count
theigl Dec 16, 2022
5e1d921
Adjust GitHub workflow to use JDK17
theigl Dec 16, 2022
ca3fb98
Cleanup
theigl Dec 16, 2022
c41eb11
Require JDK 17
theigl Dec 16, 2022
78986e0
Remove registration for `RecordSerializer`
theigl Dec 16, 2022
b89a764
Cleanup
theigl Dec 16, 2022
62805b4
Cleanup
theigl Dec 16, 2022
2ecda65
Cleanup
theigl Dec 16, 2022
77bd150
Cleanup
theigl Dec 16, 2022
01d2026
Add tests for `TaggedFieldSerializer`
theigl Dec 17, 2022
0ca27e8
Cleanup
theigl Dec 17, 2022
bb13cd6
Merge remote-tracking branch 'origin/master' into record-field-serial…
theigl Dec 20, 2022
9ff145b
Remove unnecessary call to `kry.reference()` since records cannot con…
theigl Dec 20, 2022
3fdfef3
Merge remote-tracking branch 'origin/master' into record-field-serial…
theigl Dec 30, 2022
d8a67a5
Merge remote-tracking branch 'origin/master' into record-field-serial…
theigl Oct 16, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 2 additions & 16 deletions .github/workflows/pr-workflow.yml
Expand Up @@ -11,26 +11,12 @@ jobs:
- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: 11
java-version: 17
cache: 'maven'

- name: Build with JDK 11
- name: Build with JDK 17
run: mvn -B install --no-transfer-progress -DskipTests

- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: 8
- name: Test with JDK 8
run: mvn -v && mvn -B test

- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: 11
- name: Test with JDK 11
run: mvn -v && mvn -B test

- uses: actions/setup-java@v3
with:
distribution: 'temurin'
Expand Down
@@ -0,0 +1,99 @@
/* Copyright (c) 2008-2022, Nathan Sweet
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided with the distribution.
* - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

package com.esotericsoftware.kryo.benchmarks;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.Serializer;
import com.esotericsoftware.kryo.SerializerFactory.CompatibleFieldSerializerFactory;
import com.esotericsoftware.kryo.SerializerFactory.TaggedFieldSerializerFactory;
import com.esotericsoftware.kryo.benchmarks.data.Image;
import com.esotericsoftware.kryo.benchmarks.data.Image.Size;
import com.esotericsoftware.kryo.benchmarks.data.Media;
import com.esotericsoftware.kryo.benchmarks.data.Media.Player;
import com.esotericsoftware.kryo.benchmarks.data.MediaContent;
import com.esotericsoftware.kryo.benchmarks.data.Sample;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.esotericsoftware.kryo.serializers.CollectionSerializer;
import com.esotericsoftware.kryo.serializers.FieldSerializer;
import com.esotericsoftware.kryo.serializers.RecordSerializer;
import com.esotericsoftware.kryo.serializers.VersionFieldSerializer;

import java.util.ArrayList;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;

public class RecordSerializerBenchmark {
@Benchmark
public void field (FieldSerializerState state) {
state.roundTrip();
}

@Benchmark
public void record (RecordSerializerState state) {
state.roundTrip();
}

@State(Scope.Thread)
static public abstract class BenchmarkState {
@Param({"true", "false"}) public boolean references;

final Kryo kryo = new Kryo();
final Output output = new Output(1024 * 512);
final Input input = new Input(output.getBuffer());
Object object;

@Setup(Level.Trial)
public void setup () {
object = new RecordRectangle("2134324", 10, 10L, 20D);
kryo.setReferences(references);
}

public void roundTrip () {
output.setPosition(0);
kryo.writeObject(output, object);
input.setPosition(0);
input.setLimit(output.position());
kryo.readObject(input, object.getClass());
}
}

public record RecordRectangle (String height, int width, long x, double y) { }

static public class FieldSerializerState extends BenchmarkState {
public void setup () {
kryo.setDefaultSerializer(FieldSerializer.class);
kryo.register(RecordRectangle.class);
super.setup();
}
}

static public class RecordSerializerState extends BenchmarkState {
public void setup () {
kryo.register(RecordRectangle.class, new RecordSerializer<>());
super.setup();
}
}
}
2 changes: 1 addition & 1 deletion pom.xml
Expand Up @@ -49,7 +49,7 @@
<properties>
<kryo.root>${basedir}</kryo.root>
<kryo.major.version>5</kryo.major.version>
<javac.target>1.8</javac.target>
<javac.target>17</javac.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>5.10.0</junit.version>
<kotlin.version>1.9.10</kotlin.version>
Expand Down
16 changes: 0 additions & 16 deletions src/com/esotericsoftware/kryo/Kryo.java
Expand Up @@ -81,7 +81,6 @@
import com.esotericsoftware.kryo.serializers.ImmutableCollectionsSerializers;
import com.esotericsoftware.kryo.serializers.MapSerializer;
import com.esotericsoftware.kryo.serializers.OptionalSerializers;
import com.esotericsoftware.kryo.serializers.RecordSerializer;
import com.esotericsoftware.kryo.serializers.TimeSerializers;
import com.esotericsoftware.kryo.util.DefaultClassResolver;
import com.esotericsoftware.kryo.util.DefaultGenerics;
Expand Down Expand Up @@ -230,10 +229,6 @@ public Kryo (ClassResolver classResolver, ReferenceResolver referenceResolver) {
OptionalSerializers.addDefaultSerializers(this);
TimeSerializers.addDefaultSerializers(this);
ImmutableCollectionsSerializers.addDefaultSerializers(this);
// Add RecordSerializer if JDK 14+ available
if (isClassAvailable("java.lang.Record")) {
addDefaultSerializer("java.lang.Record", RecordSerializer.class);
}
lowPriorityDefaultSerializerCount = defaultSerializers.size();

// Primitives and string. Primitive wrappers automatically use the same registration as primitives.
Expand Down Expand Up @@ -284,17 +279,6 @@ public void addDefaultSerializer (Class type, SerializerFactory serializerFactor
insertDefaultSerializer(type, serializerFactory);
}

/** Instances with the specified class name will use the specified serializer when {@link #register(Class)} or
* {@link #register(Class, int)} are called.
* @see #setDefaultSerializer(Class) */
private void addDefaultSerializer (String className, Class<? extends Serializer> serializer) {
try {
addDefaultSerializer(Class.forName(className), serializer);
} catch (ClassNotFoundException e) {
throw new KryoException("default serializer cannot be added: " + className);
}
}

/** Instances of the specified class will use the specified serializer when {@link #register(Class)} or
* {@link #register(Class, int)} are called. Serializer instances are created as needed via
* {@link ReflectionSerializerFactory#newSerializer(Kryo, Class, Class)}. By default, the following classes have a default
Expand Down
43 changes: 43 additions & 0 deletions src/com/esotericsoftware/kryo/serializers/AsmField.java
Expand Up @@ -74,6 +74,13 @@ public void read (Input input, Object object) {
access.setInt(object, accessIndex, input.readInt());
}

public Object read(Input input) {
if (varEncoding)
return input.readVarInt(false);
else
return input.readInt();
}

public void copy (Object original, Object copy) {
access.setInt(copy, accessIndex, access.getInt(original, accessIndex));
}
Expand All @@ -92,6 +99,10 @@ public void read (Input input, Object object) {
access.setFloat(object, accessIndex, input.readFloat());
}

public Object read(Input input) {
return input.readFloat();
}

public void copy (Object original, Object copy) {
access.setFloat(copy, accessIndex, access.getFloat(original, accessIndex));
}
Expand All @@ -110,6 +121,10 @@ public void read (Input input, Object object) {
access.setShort(object, accessIndex, input.readShort());
}

public Object read(Input input) {
return input.readShort();
}

public void copy (Object original, Object copy) {
access.setShort(copy, accessIndex, access.getShort(original, accessIndex));
}
Expand All @@ -128,6 +143,10 @@ public void read (Input input, Object object) {
access.setByte(object, accessIndex, input.readByte());
}

public Object read(Input input) {
return input.readByte();
}

public void copy (Object original, Object copy) {
access.setByte(copy, accessIndex, access.getByte(original, accessIndex));
}
Expand All @@ -146,6 +165,10 @@ public void read (Input input, Object object) {
access.setBoolean(object, accessIndex, input.readBoolean());
}

public Object read(Input input) {
return input.readBoolean();
}

public void copy (Object original, Object copy) {
access.setBoolean(copy, accessIndex, access.getBoolean(original, accessIndex));
}
Expand All @@ -164,6 +187,10 @@ public void read (Input input, Object object) {
access.setChar(object, accessIndex, input.readChar());
}

public Object read(Input input) {
return input.readChar();
}

public void copy (Object original, Object copy) {
access.setChar(copy, accessIndex, access.getChar(original, accessIndex));
}
Expand All @@ -188,6 +215,14 @@ public void read (Input input, Object object) {
access.setLong(object, accessIndex, input.readLong());
}

public Object read(Input input) {
if (varEncoding) {
return input.readVarLong(false);
} else {
return input.readLong();
}
}

public void copy (Object original, Object copy) {
access.setLong(copy, accessIndex, access.getLong(original, accessIndex));
}
Expand All @@ -206,6 +241,10 @@ public void read (Input input, Object object) {
access.setDouble(object, accessIndex, input.readDouble());
}

public Object read(Input input) {
return input.readDouble();
}

public void copy (Object original, Object copy) {
access.setDouble(copy, accessIndex, access.getDouble(original, accessIndex));
}
Expand All @@ -224,6 +263,10 @@ public void read (Input input, Object object) {
access.set(object, accessIndex, input.readString());
}

public Object read(Input input) {
return input.readString();
}

public void copy (Object original, Object copy) {
access.set(copy, accessIndex, access.getString(original, accessIndex));
}
Expand Down
19 changes: 16 additions & 3 deletions src/com/esotericsoftware/kryo/serializers/CachedFields.java
Expand Up @@ -62,6 +62,7 @@

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.RecordComponent;
import java.security.AccessControlException;
import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -135,24 +136,25 @@ private void addField (Field field, boolean asm, ArrayList<CachedField> fields,
boolean isTransient = Modifier.isTransient(modifiers);
if (isTransient && !config.serializeTransient && !config.copyTransient) return;

Class type = serializer.type;
Class declaringClass = field.getDeclaringClass();
GenericType genericType = new GenericType(declaringClass, serializer.type, field.getGenericType());
GenericType genericType = new GenericType(declaringClass, type, field.getGenericType());
Class fieldClass = genericType.getType() instanceof Class ? (Class)genericType.getType() : field.getType();
int accessIndex = -1;
if (asm //
&& !Modifier.isFinal(modifiers) //
&& Modifier.isPublic(modifiers) //
&& Modifier.isPublic(fieldClass.getModifiers())) {
try {
if (access == null) access = FieldAccess.get(serializer.type);
if (access == null) access = FieldAccess.get(type);
accessIndex = ((FieldAccess)access).getIndex(field);
} catch (RuntimeException | LinkageError ex) {
if (DEBUG) debug("kryo", "Unable to use ReflectASM.", ex);
}
}

CachedField cachedField;
if (unsafe)
if (unsafe && !type.isRecord())
cachedField = newUnsafeField(field, fieldClass, genericType);
else if (accessIndex != -1) {
cachedField = newAsmField(field, fieldClass, genericType);
Expand Down Expand Up @@ -183,6 +185,17 @@ else if (accessIndex != -1) {
"Cached " + fieldClass.getSimpleName() + " field: " + field.getName() + " (" + className(declaringClass) + ")");
}

final RecordComponent[] recordComponents = type.getRecordComponents();
if (recordComponents != null) {
for (int i = 0; i < recordComponents.length; i++) {
RecordComponent recordComponent = recordComponents[i];
if (recordComponent.getName().equals(field.getName())) {
cachedField.index = i;
break;
}
}
}

applyAnnotations(cachedField);

if (isTransient) {
Expand Down
Expand Up @@ -114,8 +114,12 @@ public void write (Kryo kryo, Output output, T object) {
public T read (Kryo kryo, Input input, Class<? extends T> type) {
int pop = pushTypeVariables();

T object = create(kryo, input, type);
kryo.reference(object);
T object = null;
final boolean isRecord = type.isRecord();
if (!isRecord) {
object = create(kryo, input, type);
kryo.reference(object);
}

CachedField[] fields = (CachedField[])kryo.getGraphContext().get(this);
if (fields == null) fields = readFields(kryo, input);
Expand All @@ -127,6 +131,7 @@ public T read (Kryo kryo, Input input, Class<? extends T> type) {
fieldInput = inputChunked = new InputChunked(input, config.chunkSize);
else
fieldInput = input;
Object[] values = null;
for (int i = 0, n = fields.length; i < n; i++) {
CachedField cachedField = fields[i];

Expand Down Expand Up @@ -182,10 +187,19 @@ public T read (Kryo kryo, Input input, Class<? extends T> type) {
}

if (TRACE) log("Read", cachedField, input.position());
cachedField.read(fieldInput, object);
if (object != null) {
cachedField.read(fieldInput, object);
} else {
if (values == null) values = new Object[cachedFields.fields.length];
values[cachedField.index] = cachedField.read(fieldInput);
}
if (chunked) inputChunked.nextChunk();
}

if (isRecord) {
object = invokeCanonicalConstructor(type, cachedFields.fields, values);
}

popTypeVariables(pop);
return object;
}
Expand Down