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

Caching wrapper types #27

Open
andrewparmet opened this issue Apr 2, 2020 · 0 comments
Open

Caching wrapper types #27

andrewparmet opened this issue Apr 2, 2020 · 0 comments

Comments

@andrewparmet
Copy link
Collaborator

andrewparmet commented Apr 2, 2020

The Java protobuf implementation cleverly caches Strings and ByteStrings when calculating message size and serializing messages. This saves a string iteration - protokt has to iterate once to find the size of a String's byte array representation, and then once to write it.

We can cache similarly and even do this for wrapper types. In fact, we can do slightly better than protobuf-java on String fields by only copying lazily and never calling CodedInputStream.readString() at all - only convert to a String when requested.

This makes OptimizedSizeofConverter obsolete.

Here's a general idea of what the implementation might look like:

class CachingReference<S : Any, T : Any>(
    @Volatile private var ref: Any,
    private val converter: Converter<S, T>
) {
    val wrapped: S
        get() =
            ref.let {
                if (converter.wrapper.java.isAssignableFrom(it::class.java)) {
                    @Suppress("UNCHECKED_CAST")
                    it as S
                } else {
                    @Suppress("UNCHECKED_CAST")
                    val converted = converter.wrap(it as T)
                    ref = converted
                    converted
                }
            }

    val unwrapped: T
        get() =
            ref.let {
                if (converter.wrapped.java.isAssignableFrom(it::class.java)) {
                    @Suppress("UNCHECKED_CAST")
                    it as T
                } else {
                    @Suppress("UNCHECKED_CAST")
                    val converted = converter.unwrap(it as S)
                    ref = converted
                    converted
                }
            }
}
class CachingModel
private constructor(
    private val _name: CachingReference<String, ByteArray>,
    private val _uuid: CachingReference<java.util.UUID, ByteArray>,
    private val _instant: CachingReference<java.time.Instant, com.toasttab.protokt.Timestamp>,
    private val _duration: CachingReference<java.time.Duration, com.toasttab.protokt.Duration>?,
    val unknown: Map<Int, Unknown>
) : KtMessage {
    // Only generated if a wrapped type is used
    private constructor(
        name: Any?,
        uuid: java.util.UUID,
        instant: Any?,
        duration: Any?,
        unknown: Map<Int, Unknown>
    ) : this(
        CachingReference(name ?: "", StringConverter),
        CachingReference(uuid, UuidConverter),
        CachingReference(
            requireNotNull(instant) {
                "instant specified nonnull with (protokt.property).non_null but was null"
            },
            InstantConverter
        ),
        duration?.let { CachingReference(it, DurationConverter) },
        unknown
    )

    override val messageSize by lazy { sizeof() }

    val name: String
        get() = _name.wrapped

    val uuid: java.util.UUID
        get() = _uuid.wrapped

    val instant: java.time.Instant
        get() = _instant.wrapped

    val duration: java.time.Duration?
        get() = _duration?.wrapped

    override fun serialize(serializer: KtMessageSerializer) {
        if (_name.unwrapped.isNotEmpty()) {
            serializer.write(Tag(10)).write(_name.unwrapped)
        }
        if (_uuid.unwrapped.isNotEmpty()) {
            serializer.write(Tag(18)).write(_uuid.unwrapped)
        }
        serializer.write(Tag(50)).write(_instant.unwrapped)
        if (_duration != null) {
            serializer.write(Tag(58)).write(_duration.unwrapped)
        }
        if (unknown.isNotEmpty()) {
            serializer.writeUnknown(unknown)
        }
    }

    private fun sizeof(): Int {
        var res = 0
        if (_name.unwrapped.isNotEmpty()) {
            res += sizeof(Tag(1)) + sizeof(_name.unwrapped)
        }
        if (_uuid.unwrapped.isNotEmpty()) {
            res += sizeof(Tag(2)) + sizeof(_uuid.unwrapped)
        }
        res += sizeof(Tag(6)) + sizeof(_instant.unwrapped)
        if (_duration != null) {
            res += sizeof(Tag(7)) + sizeof(_duration.unwrapped)
        }
        res += unknown.values.sumBy { it.sizeof() }
        return res
    }

    override fun equals(other: Any?): Boolean =
        other is CachingModel &&
            other.name == name &&
            other.uuid == uuid &&
            other.instant == instant &&
            other.duration == duration &&
            other.unknown == unknown

    override fun hashCode(): Int {
        var result = unknown.hashCode()
        result = 31 * result + name.hashCode()
        result = 31 * result + uuid.hashCode()
        result = 31 * result + instant.hashCode()
        result = 31 * result + duration.hashCode()
        return result
    }

    override fun toString(): String =
        "CachingModel(" +
            "name=$name, " +
            "uuid=$uuid, " +
            "instant=$instant, " +
            "duration=$duration, " +
            "unknown=$unknown)"

    fun copy(dsl: CachingModelDsl.() -> Unit) =
        CachingModel4 {
            name = this@CachingModel.name
            uuid = this@CachingModel.uuid
            instant = this@CachingModel.instant
            duration = this@CachingModel.duration
            unknown = this@CachingModel.unknown
            dsl()
        }

    class CachingModelDsl {
        var name = ""
        var uuid: java.util.UUID? = null
        var instant: java.time.Instant? = null
        var duration: java.time.Duration? = null
        var unknown: Map<Int, Unknown> = emptyMap()
            set(newValue) { field = copyMap(newValue) }

        fun build() =
            CachingModel(
                name,
                requireNotNull(uuid) {
                    "uuid wrapped and not specified and has no default value"
                },
                instant,
                duration,
                unknown
            )
    }

    companion object Deserializer : KtDeserializer<CachingModel>, (CachingModelDsl.() -> Unit) -> CachingModel {
        override fun deserialize(deserializer: KtMessageDeserializer): CachingModel {
            var name: ByteArray? = null
            var uuid: ByteArray? = null
            var instant: com.toasttab.protokt.Timestamp? = null
            var duration: com.toasttab.protokt.Duration? = null
            var unknown: MutableMap<Int, Unknown>? = null

            while (true) {
                when (deserializer.readTag()) {
                    0 ->
                        return CachingModel(
                            name,
                            // Has to be done eagerly in case no default is possible and it would be illegal
                            UuidConverter.wrap(uuid ?: Bytes.empty.bytes),
                            instant,
                            duration,
                            finishMap(unknown)
                        )
                    10 -> name = deserializer.readByteArray()
                    18 -> uuid = deserializer.readByteArray()
                    50 -> instant =
                        deserializer.readMessage(com.toasttab.protokt.Timestamp)
                    58 -> duration =
                        deserializer.readMessage(com.toasttab.protokt.Duration)
                    else -> unknown =
                        (unknown ?: mutableMapOf()).also {
                            processUnknown(deserializer, it)
                        }
                }
            }
        }

        override fun invoke(dsl: CachingModelDsl.() -> Unit) =
            CachingModelDsl().apply(dsl).build()
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant