From 535dc55ab33ad8807cb1c272768c64a32316f8c1 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Mon, 17 Jan 2022 20:45:07 +0100 Subject: [PATCH 1/2] Compare decoding speed with hermes-json --- benchmarks/aeson-benchmarks.cabal | 6 ++ benchmarks/bench/CompareWithHermes.hs | 69 +++++++++++++++++++++++ benchmarks/bench/Twitter/Hermes.hs | 50 ++++++++++++++++ benchmarks/bench/aeson-benchmark-suite.hs | 13 +++++ cabal.project | 2 + 5 files changed, 140 insertions(+) create mode 100644 benchmarks/bench/CompareWithHermes.hs create mode 100644 benchmarks/bench/Twitter/Hermes.hs diff --git a/benchmarks/aeson-benchmarks.cabal b/benchmarks/aeson-benchmarks.cabal index a841a7884..ff8275628 100644 --- a/benchmarks/aeson-benchmarks.cabal +++ b/benchmarks/aeson-benchmarks.cabal @@ -104,6 +104,12 @@ executable aeson-benchmark-suite other-modules: Compare.JsonBuilder build-depends: json-builder + if impl(ghc >=8.10) + build-depends: hermes-json >=0.2.0.1 + other-modules: + CompareWithHermes + Twitter.Hermes + if !flag(text2) -- buffer-builder might work with text-2 sometime build-depends: buffer-builder diff --git a/benchmarks/bench/CompareWithHermes.hs b/benchmarks/bench/CompareWithHermes.hs new file mode 100644 index 000000000..3f7c094be --- /dev/null +++ b/benchmarks/bench/CompareWithHermes.hs @@ -0,0 +1,69 @@ +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE OverloadedStrings #-} + +module CompareWithHermes (benchmark) where + +import Prelude.Compat +import Bench + +import Data.Maybe (fromMaybe) +import qualified Data.Aeson as A +import qualified Data.Aeson.Decoding as A.D +import qualified Data.Aeson.Parser.Internal as I +import qualified Data.ByteString as BS +import qualified Data.ByteString.Lazy as BL +import qualified Data.Hermes as H +import qualified Twitter as T +import Twitter.Manual () -- fair comparison with manual Hermes decoders +import Twitter.Hermes + +import Utils + +decode :: BL.ByteString -> T.Result +decode s = fromMaybe (error "fail to parse via Aeson") $ A.decode s + +decode' :: BL.ByteString -> T.Result +decode' s = fromMaybe (error "fail to parse via Aeson") $ A.decode' s + +decodeS :: BS.ByteString -> T.Result +decodeS s = fromMaybe (error "fail to parse via Aeson") $ A.decodeStrict' s + +decodeTS :: BS.ByteString -> T.Result +decodeTS s = fromMaybe (error "fail to parse via Aeson") $ A.D.decodeStrict s + +decodeIP :: BL.ByteString -> T.Result +decodeIP s = fromMaybe (error "fail to parse via Parser.decodeWith") $ + I.decodeWith I.jsonEOF A.fromJSON s + +decodeH :: BS.ByteString -> T.Result +decodeH s = case H.decodeEither twitterResultDecoder s of + Right result -> result + Left err -> error (show err) + +benchmark :: Benchmark +benchmark = + env (readL enFile) $ \enA -> + env (readS enFile) $ \enS -> + env (readL jpFile) $ \jpA -> + env (readS jpFile) $ \jpS -> + bgroup "compare-hermes" [ + bgroup "decode" [ + bgroup "en" [ + bench "aeson/lazy" $ nf decode enA + , bench "aeson/strict" $ nf decode' enA + , bench "aeson/stricter" $ nf decodeS enS + , bench "aeson/parser" $ nf decodeIP enA + , bench "aeson/tokens/strict" $ nf decodeTS enS + , bench "hermes" $ nf decodeH enS + ] + , bgroup "jp" [ + bench "aeson" $ nf decode jpA + , bench "aeson/stricter" $ nf decodeS jpS + , bench "aeson/tokens/strict" $ nf decodeTS jpS + , bench "hermes" $ nf decodeH jpS + ] + ] + ] + where + enFile = "twitter100.json" + jpFile = "jp100.json" diff --git a/benchmarks/bench/Twitter/Hermes.hs b/benchmarks/bench/Twitter/Hermes.hs new file mode 100644 index 000000000..290079a83 --- /dev/null +++ b/benchmarks/bench/Twitter/Hermes.hs @@ -0,0 +1,50 @@ +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE OverloadedStrings #-} +module Twitter.Hermes where + +import Prelude.Compat + +import Data.Int (Int64) + +import qualified Data.Hermes as H +import qualified Twitter as T + +twitterResultDecoder :: H.Value -> H.Decoder T.Result +twitterResultDecoder = H.withObject $ \obj -> + T.Result + <$> H.atKey "results" (H.list storyDecoder) obj + <*> H.atKey "max_id" int64 obj + <*> H.atKey "since_id" int64 obj + <*> H.atKey "refresh_url" H.text obj + <*> H.atKey "next_page" H.text obj + <*> H.atKey "results_per_page" H.int obj + <*> H.atKey "page" H.int obj + <*> H.atKey "completed_in" H.double obj + <*> H.atKey "since_id_str" H.text obj + <*> H.atKey "max_id_str" H.text obj + <*> H.atKey "query" H.text obj + +storyDecoder :: H.Value -> H.Decoder T.Story +storyDecoder = H.withObject $ \obj -> + T.Story + <$> H.atKey "from_user_id_str" H.text obj + <*> H.atKey "profile_image_url" H.text obj + <*> H.atKey "created_at" H.text obj + <*> H.atKey "from_user" H.text obj + <*> H.atKey "id_str" H.text obj + <*> H.atKey "metadata" metadataDecoder obj + <*> H.atKey "to_user_id" (H.nullable int64) obj + <*> H.atKey "text" H.text obj + <*> H.atKey "id" int64 obj + <*> H.atKey "from_user_id" int64 obj + <*> pure Nothing -- our bench corpus doesn't have any geolocated tweets + <*> H.atKey "iso_language_code" H.text obj + <*> H.atKey "to_user_id_str" (H.nullable H.text) obj + <*> H.atKey "source" H.text obj + +int64 :: H.Value -> H.Decoder Int64 +int64 = fmap fromIntegral <$> H.int + +metadataDecoder :: H.Value -> H.Decoder T.Metadata +metadataDecoder = H.withObject $ \obj -> + T.Metadata <$> H.atKey "result_type" H.text obj diff --git a/benchmarks/bench/aeson-benchmark-suite.hs b/benchmarks/bench/aeson-benchmark-suite.hs index 729a529f3..7d5ff9615 100644 --- a/benchmarks/bench/aeson-benchmark-suite.hs +++ b/benchmarks/bench/aeson-benchmark-suite.hs @@ -29,6 +29,12 @@ import qualified AesonMap import qualified AutoCompare import qualified Compare import qualified CompareWithJSON +<<<<<<< HEAD:benchmarks/bench/aeson-benchmark-suite.hs +======= +#ifdef MIN_VERSION_hermes_json +import qualified CompareWithHermes +#endif +>>>>>>> Use hermes only with GHC > 8.8:benchmarks/bench/Suite.hs import qualified Dates import qualified GitHub import qualified Issue673 @@ -42,6 +48,10 @@ import Utils import qualified UnescapePureText1 as Text1 #endif +#if __GLASGOW_HASKELL__ >=810 +import qualified CompareWithHermes +#endif + ------------------------------------------------------------------------------- -- Decode bench ------------------------------------------------------------------------------- @@ -136,3 +146,6 @@ main = do ] ++ Compare.benchmarks -- compares to different libs (encoding) ++ [ CompareWithJSON.benchmark ] +#if __GLASGOW_HASKELL__ >=810 + ++ [ CompareWithHermes.benchmark ] +#endif diff --git a/cabal.project b/cabal.project index 0fad60de7..ca7375fcb 100644 --- a/cabal.project +++ b/cabal.project @@ -6,5 +6,7 @@ packages: benchmarks tests: true benchmarks: true +allow-newer: hermes-json:attoparsec-iso8601 + -- packages: https://hackage.haskell.org/package/libperf-0.1/candidate/libperf-0.1.tar.gz -- packages: https://hackage.haskell.org/package/tasty-perfbench-0.1/candidate/tasty-perfbench-0.1.tar.gz From 3230b5e9e2981097e894416cea089f851f75fdf3 Mon Sep 17 00:00:00 2001 From: Oleg Grenrus Date: Sat, 11 Feb 2023 20:56:47 +0200 Subject: [PATCH 2/2] Add benchmark where we parse Aeson.Value with hermes --- .github/workflows/haskell-ci.yml | 11 +---------- benchmarks/aeson-benchmarks.cabal | 1 + benchmarks/bench/CompareWithHermes.hs | 11 +++++++++++ benchmarks/bench/Data/Hermes/Aeson.hs | 18 ++++++++++++++++++ benchmarks/bench/aeson-benchmark-suite.hs | 6 ------ cabal.haskell-ci | 3 +++ 6 files changed, 34 insertions(+), 16 deletions(-) create mode 100644 benchmarks/bench/Data/Hermes/Aeson.hs diff --git a/.github/workflows/haskell-ci.yml b/.github/workflows/haskell-ci.yml index 0d5a5f569..41752f87f 100644 --- a/.github/workflows/haskell-ci.yml +++ b/.github/workflows/haskell-ci.yml @@ -246,6 +246,7 @@ jobs: if [ $((HCNUMVER >= 80200)) -ne 0 ] ; then echo "package aeson-benchmarks" >> cabal.project ; fi if [ $((HCNUMVER >= 80200)) -ne 0 ] ; then echo " ghc-options: -Werror=missing-methods" >> cabal.project ; fi cat >> cabal.project <> cabal.project.local cat cabal.project @@ -260,10 +261,6 @@ jobs: key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }} path: ~/.cabal/store restore-keys: ${{ runner.os }}-${{ matrix.compiler }}- - - name: install dependencies - run: | - $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks --dependencies-only -j2 all - $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dependencies-only -j2 all - name: build w/o tests run: | $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all @@ -289,31 +286,25 @@ jobs: rm -f cabal.project.local - name: constraint set text-2.0 run: | - $CABAL v2-build $ARG_COMPILER --enable-tests --disable-benchmarks --constraint='text ^>=2.0' --dependencies-only -j2 all $CABAL v2-build $ARG_COMPILER --enable-tests --disable-benchmarks --constraint='text ^>=2.0' all $CABAL v2-test $ARG_COMPILER --enable-tests --disable-benchmarks --constraint='text ^>=2.0' all - name: constraint set text-1.2 run: | - if [ $((HCNUMVER < 90400)) -ne 0 ] ; then $CABAL v2-build $ARG_COMPILER --enable-tests --disable-benchmarks --constraint='text ^>=1.2.3.0' --dependencies-only -j2 all ; fi if [ $((HCNUMVER < 90400)) -ne 0 ] ; then $CABAL v2-build $ARG_COMPILER --enable-tests --disable-benchmarks --constraint='text ^>=1.2.3.0' all ; fi if [ $((HCNUMVER < 90400)) -ne 0 ] ; then $CABAL v2-test $ARG_COMPILER --enable-tests --disable-benchmarks --constraint='text ^>=1.2.3.0' all ; fi - name: constraint set bytestring-0.11.2.0 run: | - $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks --constraint='bytestring >= 0.11.2.0' --dependencies-only -j2 all $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks --constraint='bytestring >= 0.11.2.0' all - name: constraint set ordered-keymap-off run: | - $CABAL v2-build $ARG_COMPILER --enable-tests --disable-benchmarks --constraint='aeson -ordered-keymap' --dependencies-only -j2 all $CABAL v2-build $ARG_COMPILER --enable-tests --disable-benchmarks --constraint='aeson -ordered-keymap' all $CABAL v2-test $ARG_COMPILER --enable-tests --disable-benchmarks --constraint='aeson -ordered-keymap' all - name: constraint set ordered-keymap-on run: | - $CABAL v2-build $ARG_COMPILER --enable-tests --disable-benchmarks --constraint='aeson +ordered-keymap' --dependencies-only -j2 all $CABAL v2-build $ARG_COMPILER --enable-tests --disable-benchmarks --constraint='aeson +ordered-keymap' all $CABAL v2-test $ARG_COMPILER --enable-tests --disable-benchmarks --constraint='aeson +ordered-keymap' all - name: constraint set cffi run: | - if [ $((HCNUMVER < 90400)) -ne 0 ] ; then $CABAL v2-build $ARG_COMPILER --enable-tests --disable-benchmarks --constraint='aeson +cffi' --dependencies-only -j2 all ; fi if [ $((HCNUMVER < 90400)) -ne 0 ] ; then $CABAL v2-build $ARG_COMPILER --enable-tests --disable-benchmarks --constraint='aeson +cffi' all ; fi if [ $((HCNUMVER < 90400)) -ne 0 ] ; then $CABAL v2-test $ARG_COMPILER --enable-tests --disable-benchmarks --constraint='aeson +cffi' all ; fi - name: save cache diff --git a/benchmarks/aeson-benchmarks.cabal b/benchmarks/aeson-benchmarks.cabal index ff8275628..0a9538429 100644 --- a/benchmarks/aeson-benchmarks.cabal +++ b/benchmarks/aeson-benchmarks.cabal @@ -108,6 +108,7 @@ executable aeson-benchmark-suite build-depends: hermes-json >=0.2.0.1 other-modules: CompareWithHermes + Data.Hermes.Aeson Twitter.Hermes if !flag(text2) diff --git a/benchmarks/bench/CompareWithHermes.hs b/benchmarks/bench/CompareWithHermes.hs index 3f7c094be..876b813d5 100644 --- a/benchmarks/bench/CompareWithHermes.hs +++ b/benchmarks/bench/CompareWithHermes.hs @@ -13,6 +13,7 @@ import qualified Data.Aeson.Parser.Internal as I import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as BL import qualified Data.Hermes as H +import qualified Data.Hermes.Aeson as H.A import qualified Twitter as T import Twitter.Manual () -- fair comparison with manual Hermes decoders import Twitter.Hermes @@ -40,6 +41,14 @@ decodeH s = case H.decodeEither twitterResultDecoder s of Right result -> result Left err -> error (show err) +-- decode to A.Value, and then use A.FromJSON instance. +decodeHA :: BS.ByteString -> T.Result +decodeHA s = case H.decodeEither H.A.valueDecoder s of + Left err -> error (show err) + Right v -> case A.fromJSON v of + A.Success x -> x + A.Error err -> error (show err) + benchmark :: Benchmark benchmark = env (readL enFile) $ \enA -> @@ -55,12 +64,14 @@ benchmark = , bench "aeson/parser" $ nf decodeIP enA , bench "aeson/tokens/strict" $ nf decodeTS enS , bench "hermes" $ nf decodeH enS + , bench "hermes/aeson" $ nf decodeHA enS ] , bgroup "jp" [ bench "aeson" $ nf decode jpA , bench "aeson/stricter" $ nf decodeS jpS , bench "aeson/tokens/strict" $ nf decodeTS jpS , bench "hermes" $ nf decodeH jpS + , bench "hermes/aeson" $ nf decodeHA jpS ] ] ] diff --git a/benchmarks/bench/Data/Hermes/Aeson.hs b/benchmarks/bench/Data/Hermes/Aeson.hs new file mode 100644 index 000000000..6e08175d3 --- /dev/null +++ b/benchmarks/bench/Data/Hermes/Aeson.hs @@ -0,0 +1,18 @@ +module Data.Hermes.Aeson where + +import Control.Applicative ((<|>)) + +import qualified Data.Hermes as H +import qualified Data.Aeson as A +import qualified Data.Vector as V +import qualified Data.Aeson.Key as K +import qualified Data.Aeson.KeyMap as KM + +valueDecoder :: H.Value -> H.Decoder A.Value +valueDecoder v = + A.String <$> H.text v <|> + A.Number <$> H.scientific v <|> + A.Bool <$> H.bool v <|> + A.Object . KM.fromList <$> H.objectAsKeyValues (pure . K.fromText) valueDecoder v <|> + A.Array . V.fromList <$> H.list valueDecoder v <|> + A.Null <$ H.nullable (\_ -> fail "notnull") v diff --git a/benchmarks/bench/aeson-benchmark-suite.hs b/benchmarks/bench/aeson-benchmark-suite.hs index 7d5ff9615..f0c95b595 100644 --- a/benchmarks/bench/aeson-benchmark-suite.hs +++ b/benchmarks/bench/aeson-benchmark-suite.hs @@ -29,12 +29,6 @@ import qualified AesonMap import qualified AutoCompare import qualified Compare import qualified CompareWithJSON -<<<<<<< HEAD:benchmarks/bench/aeson-benchmark-suite.hs -======= -#ifdef MIN_VERSION_hermes_json -import qualified CompareWithHermes -#endif ->>>>>>> Use hermes only with GHC > 8.8:benchmarks/bench/Suite.hs import qualified Dates import qualified GitHub import qualified Issue673 diff --git a/cabal.haskell-ci b/cabal.haskell-ci index 111f65981..ab804c10c 100644 --- a/cabal.haskell-ci +++ b/cabal.haskell-ci @@ -2,6 +2,9 @@ branches: master cabal-check: False docspec: True +-- due hermes-json depending on attoparsec-iso8601 +install-dependencies: False + -- GADT docs haddock: >=8.6