This isn't super exciting, but I've been doing lots of custom decoders and so I started working on some better functions for composing decoders. I realised I could either make many decoders inside each other, or one decoder with many decodes, but I wondered if there was much performance difference between the two. So I made a benchmark to compare:
Turns out there was no difference whatsoever, which is nice because as you can see, one API is much nicer than the other
benchmark.roc
app"time"packages{json:"https://github.com/lukewilliamboswell/roc-json/releases/download/0.6.3/_2Dh4Eju2v_tFtZeMq8aZ9qw2outG04NbkmKpFhXS_4.tar.br",pf:"https://github.com/roc-lang/basic-cli/releases/download/0.8.1/x8URkvfyi9I0QhmVG98roKBUs_AZRkLFwFJVJ3942YA.tar.br"}imports[pf.Stdout,pf.Task,pf.Utc,pf.Sleep,json.Core,]provides[main]topfbenchmark=\func,name,times->start<-Utc.now|>Task.await_<-runBenchmarkfunctimes|>Task.awaitfinish<-Utc.now|>Task.awaitduration=(Utc.deltaAsMillisstartfinish)|>Num.toStrStdout.line"$(name): Completed in $(duration)ms"runBenchmark=\func,times->Task.looptimes\state->ifstate!=0then_<-(benchLoopfunc)|>Task.awaitTask.ok(Step(state-1))elseTask.ok(Done{})#TightlooptoreducetaskoverheadbenchLoop=\fn->List.repeat010000|>List.map\_->_=fn{}{}|>Task.okinput=""" {"other9":9} """|>Str.toUtf8main={}<-(\_->Decode.decodeWithinputcontentChangeDecodeCore.json)|>benchmark"single Decoder"20|>Task.await{}<-(\_->Decode.decodeWithinputcontentChangeDecode2Core.json)|>benchmark"multi Decoder"20|>Task.awaitTask.ok{}PartialChangeEvent:{text:Str,}FullChangeEvent:{range:U8,rangeLength:U64,text:Str,}Other:{text:Str,other:U8,}Other2:{text:Str,other2:U8,}Other3:{text:Str,other3:U8,}Other4:{text:Str,other4:U8,}Other5:{text:Str,other5:U8,}Other6:{text:Str,other6:U8,}Other7:{text:Str,other7:U8,}Other8:{text:Str,other8:U8,}Other9:{text:Str,other9:U8,}TextDocumentContentChangeEvent:=[PartialChangePartialChangeEvent,FullChangeFullChangeEvent,OtherOther,Other2Other2,Other3Other3,Other4Other4,Other5Other5,Other6Other6,Other7Other7,Other8Other8,Other9Other9,]implements[Decoding{decoder:contentChangeDecode},Encoding{toEncoder:contentChangeEncoder},]contentChangeEncoder=\@TextDocumentContentChangeEventchange->whenchangeisPartialChangea->Encode.toEncoderaFullChangea->Encode.toEncoderaOthera->Encode.toEncoderaOther2a->Encode.toEncoderaOther3a->Encode.toEncoderaOther4a->Encode.toEncodera_->Encode.toEncoder1##TryanotherdecodeifthelastdecodefailedtryOnErr:DecodeResult_,_->_tryOnErr=\decoded,try->whendecoded.resultisErre->try{}Okres->decodedcontentChangeDecode=Decode.custom\bytes,fmt->Decode.fromBytesPartialbytesfmt|>Decode.mapResultPartialChange|>tryOnErr\_->Decode.fromBytesPartialbytesfmt|>Decode.mapResultFullChange|>tryOnErr\_->Decode.fromBytesPartialbytesfmt|>Decode.mapResultOther|>tryOnErr\_->Decode.fromBytesPartialbytesfmt|>Decode.mapResultOther2|>tryOnErr\_->Decode.fromBytesPartialbytesfmt|>Decode.mapResultOther3|>tryOnErr\_->Decode.fromBytesPartialbytesfmt|>Decode.mapResultOther4|>tryOnErr\_->Decode.fromBytesPartialbytesfmt|>Decode.mapResultOther5|>tryOnErr\_->Decode.fromBytesPartialbytesfmt|>Decode.mapResultOther6|>tryOnErr\_->Decode.fromBytesPartialbytesfmt|>Decode.mapResultOther7|>tryOnErr\_->Decode.fromBytesPartialbytesfmt|>Decode.mapResultOther8|>tryOnErr\_->Decode.fromBytesPartialbytesfmt|>Decode.mapResultOther9|>Decode.mapResult@TextDocumentContentChangeEvent##Makesadecoderthatdecodestheinputandthenwrapsitusingthewrapper##UsefulfordecodingOpaquetypesoranydecodewhereyouwishtodecodetoanintermediateandthefurthurpassthatintermediate##eg:stringenumtotags,intenumtotagswrapDecode=\wrapper->Decode.custom\bytes,fmt->Decode.fromBytesPartialbytesfmt|>Decode.mapResultwrapper##makesadecoderthattriestodecodeusingtheprovededfirstDecoder##IfthefirstDecoderfails,itattemptsanewdecodewithanewwrapper##UseLike:##```roc##contentChangeDecode=##wrapDecodePartialChange##|>wrapOnErrFullChange##|>wrapSuccess@TextDocumentContentChangeEvent##``wrapOnErr=\firstDecoder,wrapper->Decode.custom\bytes,fmt->Decode.decodeWithbytesfirstDecoderfmt|>tryOnErr\_->(Decode.fromBytesPartialbytesfmt|>Decode.mapResultwrapper)##Takesadecoderandwrapsit'soutputusingtheprovidedwrapper##UseLike:##```roc##contentChangeDecode=##wrapDecodePartialChange##|>wrapOnErrFullChange##|>wrapSuccess@TextDocumentContentChangeEvent##``wrapSuccess=\decoder,wrapper->Decode.custom\bytes,fmt->Decode.decodeWithbytesdecoderfmt|>Decode.mapResultwrappercontentChangeDecode2=wrapDecodePartialChange|>wrapOnErrFullChange|>wrapOnErrOther|>wrapOnErrOther2|>wrapOnErrOther3|>wrapOnErrOther4|>wrapOnErrOther5|>wrapOnErrOther6|>wrapOnErrOther7|>wrapOnErrOther8|>wrapOnErrOther9|>wrapSuccess@TextDocumentContentChangeEvent
Okay, I updated it to use the roc-json package. Everything is running off local branches because of this null decoding business :man_facepalming: Thanks for the catch @Brendan Hansknecht
@Brendan Hansknecht I have benchmarks!
I did some testing of decoding a large json file (24MB):
JsonVal is recursive tag union that can represent all the json primatives for dynamic json decoding. jsonVal: Completed 1 runs in 1950ms
This test takes on a single field from each item in the long json list json id only: Completed 1 runs in 1054ms
Dotnet:
| Method | Mean | Error | StdDev | Median |
|------------------- |----------:|---------:|---------:|----------:|
| IdOnly | 93.16 ms | 1.728 ms | 4.239 ms | 91.63 ms |
| JsonVal | 184.21 ms | 1.518 ms | 1.420 ms | 184.50 ms |
So we are around 10x slower than the inbuilt decoder in dotnet
Okay, I have some answers. Thankyou Luke!:
20% of the time is spent just doing List.get! So i guess I was correct to have concerns there @Brendan Hansknecht :)
Well I exposed getUnchecked, and tried to make more stuff tail recursive and it's a little better but not that much. About 20-25%... Not really the 10x improvement I'm looking for.