Skip to content

Bounded map/array support (io.avaje.json.JsonWriter/JsonReader) #472

Description

@mechite

(on the topic of writing MessagePack, CBOR implementations using avaje's great
generation etc).

I want the ability to pre-determine the size of maps and arrays.

That is, it can be more of an optimization offering in the API, and not used
by JSON, or anything other unbounded formats.

It would look something like this (same goes for arrays):

private static final class ThrowableAdapter
        implements JsonAdapter<Throwable> {
        ...
        @Override
        public void toJson(JsonWriter writer, Throwable value) {

            // I want to hard code the number of fields, in this map
            writer.beginObject();

            // writer.object(2);
            //
            // You don't need to end the object
            //
            // 1. JSON parsers can forcefully close it after 2 insertions in
            //    this example, but they have to track the state of this.
            //
            //    JSON can just throw an UnsupportedOperationException instead.
            //
            // 2. Binary formats that have these bounded headers, can utilize
            //    this, but support the other approach using "lazy evaluation".
            //
            //    That is very slow and wastes a lot of memory.
            //    The entire contents being written is buffered, that way.
            //
            // 3. Binary formats can fail on the other approach, then.
            //
            // 4. avaje-jsonb generates something that can use this feature,
            //    using a boolean comparison at runtime, in the constructor
            //    of the JsonAdapter.
            //
            //    "Are you using a bounded format?"
            //    Otherwise, I see no way of making it so "pluggable" where one
            //    can just swap an application with zero re-compilation.

            writer.name("type");
            writer.value(value.getClass().toString());

            writer.name("message");
            writer.value(value.getMessage());

            // This is only needed if you used `begin*`
            writer.endObject();
        }
        ...
}

I was originally thinking that maybe my JsonAdapter can store state, and I can
count how many times that someone inserted fields, etc, and then just write
headers based on what we expect toJson to do, etc.

This feature is not needed at all, and it goes against the very name of this
project - avaje jsonb.


I just wanted to start a discussion on what I can do with generation / writing,
for a non-JSON format, while potentially getting the pluggability which the
Jackson implementations provide.

This is where it sucks that an annotation processor cannot really
"have dependencies" in any nicely supported way.

It would be really much easier, if avaje-jsonb was pluggable, and I can supply
an optimized path for MessagePack serialization, some interface & member methods
toMessagePack/fromMessagePack.

For Avaje in that case, it could have been "as simple" as opening up some parts
of the generator, to allow me to just plug-in my own methods, implements
clauses, etc.


Thinking about the problem, there are many ways I can think about entering the
issue, and I think mainly it would just be nice to offer a very fast
serialization mechanism while staying pluggable and making it "as easy" as
removing some SPI implementation JARs at runtime.


With the new FFM API, not using it for foreign-memory but using it for unsafe
memory operations, ala sun.misc.Unsafe, one is able to implement a binary
format like MessagePack, with zero-copy readers, etc:

0x83 [map of 3 elements]
	  name -> John Doe       <-- we have the ability to just point to this,
	                             and give a `String`, meaning we never copied
	                             the buffer (from the network stream, etc).
	  age -> 0x14

This works if we have access to a ByteBuffer or MemorySegment to feed our
data directly into, our reader is fed the data in a "streaming" way later, and
can just return pointers, zero-copy.

The buffer can be shared across an application, and then there is absolutely
zero GC pressure at all for serialization. We completely take this factor out,
and store basically no data "dynamically" on the heap.

In a virtual-thread world - we ask for the thread-local buffer upon connection,
we load our data into it, run the parser over to access what we need - there are
no copies, and we only ever allocated the single buffer - combine that with the
lightweight thread, and we have almost no short-lived objects.

Everything there is request scoped, or you could even re-use a buffer for
multiple requests if synchronization was faster than allocation (I doubt).

The allocation of avaje's "meta" objects, such as the json adapter classes,
this is ny-on free honestly, especially stateless objects that may be the "same
every time".


Writing FFM-based parsers that can really utilize the JIT to full potential and
give us the best of all worlds? It's simple enough to actually do this, if it
would work.

It'd be cooler to just try a zero-copy JSON project, before combining this with
binary protocols to try and squeeze every drop out of our serialization.

With the efficiency of binary protocols over the network, we can get
tiny payloads + really quick computation - compressed JSON is small too but it
will not be as simple to parse, if binary is done right.

Protobuf & Flatbuffers are examples where we just drop the human-readable
aspect, and completely denote an entire model statically. This is annoying to
maintain compared to JSON+XML formats that have stood the test of time, just
due to their "implementation agnosticism" for being human-readable.

I am not super knowledgeable about any of this and the above is just my
gatherings from "thinking about the problem" to try and see how far we can push
performance.

I see no motivation to use an "agnostic" binary format, other than performance,
and if gzip+JSON could match or surpass performance no matter what, then the
idea can go into the trash-bin.

In theory, declarative/static stuff like Protobuf can be really fast, and the
binary sizes would be half of msgpack/cbor/etc, even smaller than JSON etc.
Bad "Schema evolution" for me is the biggest negative side-effect.


TL:DR; adding the feature described at the very top, and some extra notes about
zero-copy and "potential".

CC @rbygrave @SentryMan, feel free to close if you are not enthusiastic

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions