Compose the Go XML serialization - #689
Merged
Merged
Conversation
Write-side dual of #688, as #687 was of #686 in C#. Measured on the AAS meta-model fixture, the serialization region of ``xmlization.go`` goes 11,080 -> 4,727 lines (-57%), and the file as a whole 16,692 -> 10,330. ``writeExtensionAsSequence`` and its six properties go 180 -> 69 lines. Everything is written through one shape, ``func(encoder, value T) error``. A stringification, a class's own ``write<X>AsSequence`` and the generated item, list and tuple writers all have it, so ``writeElement`` -- which frames an XML element around a value written that way -- is the only writer a property needs, whatever kind of value it holds. What is left per property is that one call and a decision on presence: err = finishProperty( "SemanticID()", writeOptionalInstance( encoder, "semanticId", that.SemanticID(), writeReferenceAsSequence, ), ) if err != nil { return } ``finishProperty`` takes the *result* of the write rather than the write itself -- the same trick as the read side's ``readOptional``, since Go evaluates the argument first -- so one function concludes every property, prepending the getter to the path of the error. Mind that the getter is spelled out as it is in Go, ``SemanticID()`` and not ``semanticId``, since a serialization error renders its path through ``aasreporting.ToGolangPath``, while the de-serialization prepends the XML name, its errors being XPaths. Go has no ``nameof``. The ``writeOptional*`` family has one member per way Go spells an optional, each named after that spelling, as the check for the presence is the only thing which differs between them: a pointer to the value (``writeOptionalPointer``: a scalar, an enumeration, a tuple), a value which is nil on its own (``writeOptionalInstance``: an instance, or a named union, which is a pointer to a struct) and a slice (``writeOptionalSlice``: a list, the bytes). They can not be collapsed into one, since Go decides nil-ness by the representation: a type parameter can not be compared against ``nil``, ``any(that) == nil`` is false for a nil *pointer* boxed in an ``any`` -- hence the comparison against the zero value in ``writeOptionalInstance``, which in turn panics on a slice, which is not comparable. Each of them writes nothing at all when the value is absent, which is what keeps the absence out of the generated code. No closure is allocated on the write path any more: every writer passed on is a top-level function or an instantiated generic, hence a static funcval, exactly as on the read side. A list and a tuple get one generated content writer as well, per item type resp. per tuple type -- ``writeListOfIReference`` and ``writeTupleOfStringLong`` and their kin -- since ``writeList`` and ``writeTupleN`` take a writer per item, and Go gives no partial application to bind those in without allocating. Finally, the flush. ``write<X>AsSequence`` used to flush the encoder after every property, and ``write<X>`` again at its end element, which defeated the buffering of ``xml.Encoder`` on every write; only the flush after the last token is needed, as ``EncodeToken`` deliberately leaves it to the caller. ``Marshal`` is therefore split into ``writeClass``, which dispatches on the model type, and ``Marshal`` itself, which calls it and flushes exactly once; ``writeInstance`` and ``writeUnion`` go to ``writeClass``, so a nested instance does not flush either. Serializing an instance holding 1,000 instances (42 KB of XML) from the ``list_of_classes`` fixture, the writes which actually reach the underlying writer go 2,003 -> 11, one per 4 KB of buffer instead of one per property and per instance; to an ``os.File`` that is 5.29 -> 0.76 ms per serialization, the median of five.
Coverage Report for CI Build 34778173263Coverage increased (+0.01%) to 85.286%Details
Uncovered Changes
Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Write-side dual of #688, as #687 was of #686 in C#.
Measured on the AAS meta-model fixture, the serialization region of
xmlization.gogoes 11,080 -> 4,727 lines (-57%), and the file as a whole 16,692 -> 10,330.writeExtensionAsSequenceand its six properties go 180 -> 69 lines.Everything is written through one shape,
func(encoder, value T) error. A stringification, a class's ownwrite<X>AsSequenceand the generated item, list and tuple writers all have it, sowriteElement-- which frames an XML element around a value written that way -- is the only writer a property needs, whatever kind of value it holds. What is left per property is that one call and a decision on presence:finishPropertytakes the result of the write rather than the write itself -- the same trick as the read side'sreadOptional, since Go evaluates the argument first -- so one function concludes every property, prepending the getter to the path of the error.Mind that the getter is spelled out as it is in Go,
SemanticID()and notsemanticId, since a serialization error renders its path throughaasreporting.ToGolangPath, while the de-serialization prepends the XML name, its errors being XPaths. Go has nonameof.The
writeOptional*family has one member per way Go spells an optional, each named after that spelling, as the check for the presence is the only thing which differs between them: a pointer to the value (writeOptionalPointer: a scalar, an enumeration, a tuple), a value which is nil on its own (writeOptionalInstance: an instance, or a named union, which is a pointer to a struct) and a slice (writeOptionalSlice: a list, the bytes). They can not be collapsed into one, since Go decides nil-ness by the representation: a type parameter can not be compared againstnil,any(that) == nilis false for a nil pointer boxed in anany-- hence the comparison against the zero value inwriteOptionalInstance, which in turn panics on a slice, which is not comparable. Each of them writes nothing at all when the value is absent, which is what keeps the absence out of the generated code.No closure is allocated on the write path any more: every writer passed on is a top-level function or an instantiated generic, hence a static funcval, exactly as on the read side. A list and a tuple get one generated content writer as well, per item type resp. per tuple type --
writeListOfIReferenceandwriteTupleOfStringLongand their kin -- sincewriteListandwriteTupleNtake a writer per item, and Go gives no partial application to bind those in without allocating.Finally, the flush.
write<X>AsSequenceused to flush the encoder after every property, andwrite<X>again at its end element, which defeated the buffering ofxml.Encoderon every write; only the flush after the last token is needed, asEncodeTokendeliberately leaves it to the caller.Marshalis therefore split intowriteClass, which dispatches on the model type, andMarshalitself, which calls it and flushes exactly once;writeInstanceandwriteUniongo towriteClass, so a nested instance does not flush either. Serializing an instance holding 1,000 instances (42 KB of XML) from thelist_of_classesfixture, the writes which actually reach the underlying writer go 2,003 -> 11, one per 4 KB of buffer instead of one per property and per instance; to anos.Filethat is 5.29 -> 0.76 ms per serialization, the median of five.