Browse Source

Improve Fortunes bench performance for Servant (#4553)

* Switch to `type-of-html` for better performance.

* Backport `b74090b` to hasql bench.

* Replace redundant alias with unqualified import.

https://github.com/TechEmpower/FrameworkBenchmarks/pull/4553#discussion_r266223443

* Move around files to re-use stack and dockerfile.

* Re-use docker image+layers to speed up builds.

- follow up to previous commit; actually update all refs to make them build.
- added little debug statement to manually test that the right executable is launched matching test name.
naushadh 6 years ago
parent
commit
dc82e002f8

+ 5 - 3
frameworks/Haskell/servant/benchmark_config.json

@@ -14,14 +14,15 @@
       "database": "Postgres",
       "database": "Postgres",
       "framework": "Servant",
       "framework": "Servant",
       "language": "Haskell",
       "language": "Haskell",
-      "flavor": "GHC710",
+      "flavor": "GHC863",
       "orm": "Raw",
       "orm": "Raw",
       "platform": "Wai",
       "platform": "Wai",
       "webserver": "Warp",
       "webserver": "Warp",
       "os": "Linux",
       "os": "Linux",
       "database_os": "Linux",
       "database_os": "Linux",
       "display_name": "servant+hasql",
       "display_name": "servant+hasql",
-      "notes": "Uses libpq system dependency."
+      "notes": "Uses libpq system dependency.",
+      "dockerfile": "servant.dockerfile"
     },
     },
     "mysql-haskell": {
     "mysql-haskell": {
       "json_url": "/json",
       "json_url": "/json",
@@ -43,7 +44,8 @@
       "os": "Linux",
       "os": "Linux",
       "database_os": "Linux",
       "database_os": "Linux",
       "display_name": "servant+mysql-haskell",
       "display_name": "servant+mysql-haskell",
-      "notes": "Pure Haskell."
+      "notes": "Pure Haskell.",
+      "dockerfile": "servant.dockerfile"
     }
     }
   }]
   }]
 }
 }

+ 5 - 6
frameworks/Haskell/servant/hasql/servant-bench.cabal → frameworks/Haskell/servant/hasql/servant-hasql.cabal

@@ -1,7 +1,7 @@
--- Initial servant-bench.cabal generated by cabal init.  For further
+-- Initial servant-hasql.cabal generated by cabal init.  For further
 -- documentation, see http://haskell.org/cabal/users-guide/
 -- documentation, see http://haskell.org/cabal/users-guide/
 
 
-name:                servant-bench
+name:                servant-hasql
 version:             0.1.0.0
 version:             0.1.0.0
 -- synopsis:
 -- synopsis:
 -- description:
 -- description:
@@ -22,8 +22,7 @@ library
   build-depends:       base >=4.8
   build-depends:       base >=4.8
                      , servant >= 0.7
                      , servant >= 0.7
                      , servant-server >= 0.7
                      , servant-server >= 0.7
-                     , servant-lucid >= 0.7
-                     , lucid
+                     , type-of-html
                      , aeson >= 0.11
                      , aeson >= 0.11
                      , hasql >= 0.19
                      , hasql >= 0.19
                      , hasql-pool >= 0.4
                      , hasql-pool >= 0.4
@@ -37,11 +36,11 @@ library
   hs-source-dirs:      src
   hs-source-dirs:      src
   default-language:    Haskell2010
   default-language:    Haskell2010
 
 
-executable servant-exe
+executable servant-hasql
   main-is:             Main.hs
   main-is:             Main.hs
   ghc-options:         -Wall -threaded -rtsopts -O2
   ghc-options:         -Wall -threaded -rtsopts -O2
   build-depends:       base
   build-depends:       base
-                     , servant-bench
+                     , servant-hasql
                      , bytestring
                      , bytestring
                      , hasql
                      , hasql
   hs-source-dirs:      driver
   hs-source-dirs:      driver

+ 59 - 16
frameworks/Haskell/servant/hasql/src/ServantBench.hs

@@ -4,6 +4,9 @@
 {-# LANGUAGE OverloadedStrings     #-}
 {-# LANGUAGE OverloadedStrings     #-}
 {-# LANGUAGE TemplateHaskell       #-}
 {-# LANGUAGE TemplateHaskell       #-}
 {-# LANGUAGE TypeOperators         #-}
 {-# LANGUAGE TypeOperators         #-}
+{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE UndecidableInstances  #-}
+
 module ServantBench (run) where
 module ServantBench (run) where
 
 
 import           Control.Exception          (bracket)
 import           Control.Exception          (bracket)
@@ -11,7 +14,7 @@ import           Control.Monad              (replicateM)
 import           Control.Monad.IO.Class     (liftIO)
 import           Control.Monad.IO.Class     (liftIO)
 import           Data.Aeson                 hiding (json)
 import           Data.Aeson                 hiding (json)
 import qualified Data.ByteString            as BS
 import qualified Data.ByteString            as BS
-import           Data.ByteString.Lazy
+import           Data.ByteString.Lazy       (ByteString)
 import qualified Data.ByteString.Lazy.Char8 as LBSC
 import qualified Data.ByteString.Lazy.Char8 as LBSC
 import           Data.Functor.Contravariant (contramap)
 import           Data.Functor.Contravariant (contramap)
 import           Data.Either                (fromRight, partitionEithers)
 import           Data.Either                (fromRight, partitionEithers)
@@ -20,6 +23,7 @@ import           Data.List                  (sortOn)
 import           Data.Maybe                 (maybe)
 import           Data.Maybe                 (maybe)
 import           Data.Monoid                ((<>))
 import           Data.Monoid                ((<>))
 import qualified Data.Text                  as Text
 import qualified Data.Text                  as Text
+import           Data.Text                  (Text)
 import           GHC.Exts                   (IsList (fromList))
 import           GHC.Exts                   (IsList (fromList))
 import           GHC.Generics               (Generic)
 import           GHC.Generics               (Generic)
 import qualified Hasql.Decoders             as HasqlDec
 import qualified Hasql.Decoders             as HasqlDec
@@ -27,19 +31,20 @@ import qualified Hasql.Encoders             as HasqlEnc
 import           Hasql.Pool                 (Pool, acquire, release, use)
 import           Hasql.Pool                 (Pool, acquire, release, use)
 import qualified Hasql.Statement            as HasqlStatement
 import qualified Hasql.Statement            as HasqlStatement
 import           Hasql.Session              (statement)
 import           Hasql.Session              (statement)
-import           Lucid
+import qualified Html
+import           Html ((#), type (#), type (>))
 import qualified Network.Wai.Handler.Warp   as Warp
 import qualified Network.Wai.Handler.Warp   as Warp
-import           Network.HTTP.Media         ((//))
+import           Network.HTTP.Media         ((//), (/:))
 import           Servant
 import           Servant
-import           Servant.HTML.Lucid         (HTML)
 import           System.Random.MWC          (GenIO, createSystemRandom,
 import           System.Random.MWC          (GenIO, createSystemRandom,
                                              uniformR)
                                              uniformR)
+import qualified Data.List.NonEmpty as NE
 
 
 type API =
 type API =
        "json" :> Get '[JSON] Value
        "json" :> Get '[JSON] Value
   :<|> "db" :> Get '[JSON] World
   :<|> "db" :> Get '[JSON] World
   :<|> "queries" :> QueryParam "queries" QueryId :> Get '[JSON] [World]
   :<|> "queries" :> QueryParam "queries" QueryId :> Get '[JSON] [World]
-  :<|> "fortune" :> Get '[HTML] (Html ())
+  :<|> "fortune" :> Get '[HTML] FortunesHtml
   :<|> "updates" :> QueryParam "queries" QueryId :> Get '[JSON] [World]
   :<|> "updates" :> QueryParam "queries" QueryId :> Get '[JSON] [World]
   :<|> "plaintext" :> Get '[Plain] ByteString
   :<|> "plaintext" :> Get '[Plain] ByteString
 
 
@@ -57,6 +62,7 @@ server pool gen =
 
 
 run :: Warp.Port -> BS.ByteString -> IO ()
 run :: Warp.Port -> BS.ByteString -> IO ()
 run port dbSettings = do
 run port dbSettings = do
+  putStrLn "Launching servant hasql"
   gen <- createSystemRandom
   gen <- createSystemRandom
   bracket (acquire settings) release $ \pool ->
   bracket (acquire settings) release $ \pool ->
     Warp.run port $ serve api $ server pool gen
     Warp.run port $ serve api $ server pool gen
@@ -99,6 +105,17 @@ instance Accept Plain where contentType _ = "text" // "plain"
 instance MimeRender Plain ByteString where
 instance MimeRender Plain ByteString where
   mimeRender _ = id
   mimeRender _ = id
   {-# INLINE mimeRender #-}
   {-# INLINE mimeRender #-}
+  
+-- * HTML
+-- TODO: package the following block of code into a library akin to 'servant-lucid'
+
+data HTML
+instance Accept HTML where
+    contentTypes _ =
+      "text" // "html" /: ("charset", "utf-8") NE.:|
+      ["text" // "html"]
+instance Html.Document a => MimeRender HTML a where
+    mimeRender _ = Html.renderByteString
 
 
 ------------------------------------------------------------------------------
 ------------------------------------------------------------------------------
 
 
@@ -148,6 +165,30 @@ multipleDb pool gen mQueryId = do
 
 
 -- * Test 4: Fortunes
 -- * Test 4: Fortunes
 
 
+type FortunesHtml
+  = (('Html.DOCTYPE Html.> ())
+  # ('Html.Html
+    > (('Html.Head > ('Html.Title > Html.Raw Text))
+      # ('Html.Body
+        > ('Html.Table
+          > (
+              ('Html.Tr
+              > ( ('Html.Th > Html.Raw Text)
+                # ('Html.Th > Html.Raw Text)
+                )
+              )
+            # ['Html.Tr
+              > ( ('Html.Td > Int)
+                # ('Html.Td > Text)
+                )
+              ]
+            )
+          )
+        )
+      )
+    )
+  )
+
 selectFortunes :: HasqlStatement.Statement () [Fortune]
 selectFortunes :: HasqlStatement.Statement () [Fortune]
 selectFortunes = HasqlStatement.Statement q encoder decoder True
 selectFortunes = HasqlStatement.Statement q encoder decoder True
   where
   where
@@ -157,23 +198,25 @@ selectFortunes = HasqlStatement.Statement q encoder decoder True
    decoder = HasqlDec.rowList $ Fortune <$> intValDec <*> HasqlDec.column HasqlDec.text
    decoder = HasqlDec.rowList $ Fortune <$> intValDec <*> HasqlDec.column HasqlDec.text
 {-# INLINE selectFortunes #-}
 {-# INLINE selectFortunes #-}
 
 
-fortunes :: Pool -> Handler (Html ())
+fortunes :: Pool -> Handler FortunesHtml
 fortunes pool = do
 fortunes pool = do
   r <- liftIO $ use pool (statement () selectFortunes)
   r <- liftIO $ use pool (statement () selectFortunes)
   case r of
   case r of
     Left e -> throwError err500 { errBody = LBSC.pack . show $ e }
     Left e -> throwError err500 { errBody = LBSC.pack . show $ e }
     Right fs -> return $ do
     Right fs -> return $ do
       let new = Fortune 0 "Additional fortune added at request time."
       let new = Fortune 0 "Additional fortune added at request time."
-      doctypehtml_ $ do
-        head_ $ title_ "Fortunes"
-        body_ $ do
-          table_ $ do
-            tr_ $ do
-              th_ "id"
-              th_ "message"
-            mapM_ (\f -> tr_ $ do
-              td_ (toHtml . show $ fId f)
-              td_ (toHtml $ fMessage f)) (sortOn fMessage (new : fs))
+      let header = Html.tr_ $ Html.th_ (Html.Raw "id") # Html.th_ (Html.Raw "message")
+      let mkRow f = Html.tr_ $ Html.td_ (fromIntegral $ fId f) # Html.td_ (fMessage f)
+      let rows = fmap mkRow $ sortOn fMessage (new : fs)
+      Html.doctype_ #
+        Html.html_ (
+          Html.head_ (
+            Html.title_ (Html.Raw "Fortunes")
+          ) #
+          Html.body_ ( Html.table_ $
+            header # rows
+          )
+        )
 {-# INLINE fortunes #-}
 {-# INLINE fortunes #-}
 
 
 
 

+ 0 - 7
frameworks/Haskell/servant/hasql/stack.yaml

@@ -1,7 +0,0 @@
-resolver: lts-13.10
-packages:
-- '.'
-
-# the following flags are meant for use with ../servant-mysql-haskell.dockerfile
-compiler: ghc-8.6.3 # this MUST match the resolver's GHC; minor hack to ensure GHC isn't downloaded into sandbox.
-allow-different-user: true # carryover from hasql sibling test dir

+ 1 - 2
frameworks/Haskell/servant/mysql-haskell/servant-mysql-haskell.cabal

@@ -21,8 +21,7 @@ executable servant-mysql-haskell
       base >= 4.7 && < 5
       base >= 4.7 && < 5
     , servant >= 0.7
     , servant >= 0.7
     , servant-server >= 0.7
     , servant-server >= 0.7
-    , servant-lucid >= 0.7
-    , lucid
+    , type-of-html
     , aeson >= 0.11
     , aeson >= 0.11
     , resource-pool
     , resource-pool
     , mysql-haskell
     , mysql-haskell

+ 56 - 15
frameworks/Haskell/servant/mysql-haskell/src/Main.hs

@@ -4,6 +4,8 @@
 {-# LANGUAGE OverloadedStrings     #-}
 {-# LANGUAGE OverloadedStrings     #-}
 {-# LANGUAGE TemplateHaskell       #-}
 {-# LANGUAGE TemplateHaskell       #-}
 {-# LANGUAGE TypeOperators         #-}
 {-# LANGUAGE TypeOperators         #-}
+{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE UndecidableInstances  #-}
 
 
 module Main (main) where
 module Main (main) where
 
 
@@ -25,21 +27,22 @@ import           GHC.Generics               (Generic)
 import qualified Data.Pool                  as Pool
 import qualified Data.Pool                  as Pool
 import qualified Database.MySQL.Base        as MySQL
 import qualified Database.MySQL.Base        as MySQL
 import qualified System.IO.Streams          as Streams
 import qualified System.IO.Streams          as Streams
-import qualified Lucid
+import qualified Html
+import           Html ((#), type (#), type (>))
 import qualified Network.Wai.Handler.Warp   as Warp
 import qualified Network.Wai.Handler.Warp   as Warp
-import           Network.HTTP.Media         ((//))
+import           Network.HTTP.Media         ((//), (/:))
 import           Servant
 import           Servant
-import           Servant.HTML.Lucid         (HTML)
 import           System.Random.MWC          (GenIO, createSystemRandom,
 import           System.Random.MWC          (GenIO, createSystemRandom,
                                              uniformR)
                                              uniformR)
 import qualified GHC.Conc
 import qualified GHC.Conc
 import           System.Environment (getArgs)
 import           System.Environment (getArgs)
+import qualified Data.List.NonEmpty as NE
 
 
 type API =
 type API =
        "json" :> Get '[JSON] Aeson.Value
        "json" :> Get '[JSON] Aeson.Value
   :<|> "db" :> Get '[JSON] World
   :<|> "db" :> Get '[JSON] World
   :<|> "queries" :> QueryParam "queries" Count :> Get '[JSON] [World]
   :<|> "queries" :> QueryParam "queries" Count :> Get '[JSON] [World]
-  :<|> "fortune" :> Get '[HTML] (Lucid.Html ())
+  :<|> "fortune" :> Get '[HTML] FortunesHtml
   :<|> "updates" :> QueryParam "queries" Count :> Get '[JSON] [World]
   :<|> "updates" :> QueryParam "queries" Count :> Get '[JSON] [World]
   :<|> "plaintext" :> Get '[Plain] LBS.ByteString
   :<|> "plaintext" :> Get '[Plain] LBS.ByteString
 
 
@@ -57,6 +60,7 @@ server pool gen =
 
 
 run :: Warp.Port -> MySQL.ConnectInfo -> IO ()
 run :: Warp.Port -> MySQL.ConnectInfo -> IO ()
 run port dbSettings = do
 run port dbSettings = do
+  putStrLn "Launching servant mysql-hasql"
   gen <- createSystemRandom
   gen <- createSystemRandom
   numCaps <- GHC.Conc.getNumCapabilities
   numCaps <- GHC.Conc.getNumCapabilities
   let mkPool = Pool.createPool (MySQL.connect dbSettings) MySQL.close numCaps 10 512
   let mkPool = Pool.createPool (MySQL.connect dbSettings) MySQL.close numCaps 10 512
@@ -130,6 +134,17 @@ instance Accept Plain where contentType _ = "text" // "plain"
 instance MimeRender Plain LBS.ByteString where
 instance MimeRender Plain LBS.ByteString where
   mimeRender _ = id
   mimeRender _ = id
   {-# INLINE mimeRender #-}
   {-# INLINE mimeRender #-}
+  
+-- * HTML
+-- TODO: package the following block of code into a library akin to 'servant-lucid'
+
+data HTML
+instance Accept HTML where
+    contentTypes _ =
+      "text" // "html" /: ("charset", "utf-8") NE.:|
+      ["text" // "html"]
+instance Html.Document a => MimeRender HTML a where
+    mimeRender _ = Html.renderByteString
 
 
 ------------------------------------------------------------------------------
 ------------------------------------------------------------------------------
 
 
@@ -204,23 +219,49 @@ selectFortunes conn = do
     _ -> Left err
     _ -> Left err
 {-# INLINE selectFortunes #-}
 {-# INLINE selectFortunes #-}
 
 
-fortunes :: DbPool -> Handler (Lucid.Html ())
+type FortunesHtml
+  = (('Html.DOCTYPE Html.> ())
+  # ('Html.Html
+    > (('Html.Head > ('Html.Title > Html.Raw Text))
+      # ('Html.Body
+        > ('Html.Table
+          > (
+              ('Html.Tr
+              > ( ('Html.Th > Html.Raw Text)
+                # ('Html.Th > Html.Raw Text)
+                )
+              )
+            # ['Html.Tr
+              > ( ('Html.Td > Int)
+                # ('Html.Td > Text)
+                )
+              ]
+            )
+          )
+        )
+      )
+    )
+  )
+
+fortunes :: DbPool -> Handler FortunesHtml
 fortunes pool = do
 fortunes pool = do
   r <- liftIO $ Pool.withResource pool selectFortunes
   r <- liftIO $ Pool.withResource pool selectFortunes
   case r of
   case r of
     Left e -> throwError err500 { errBody = LBS.fromStrict . TextEnc.encodeUtf8 . Text.pack . show $ e }
     Left e -> throwError err500 { errBody = LBS.fromStrict . TextEnc.encodeUtf8 . Text.pack . show $ e }
     Right fs -> return $ do
     Right fs -> return $ do
       let new = Fortune 0 "Additional fortune added at request time."
       let new = Fortune 0 "Additional fortune added at request time."
-      Lucid.doctypehtml_ $ do
-        Lucid.head_ $ Lucid.title_ "Fortunes"
-        Lucid.body_ $ do
-          Lucid.table_ $ do
-            Lucid.tr_ $ do
-              Lucid.th_ "id"
-              Lucid.th_ "message"
-            mapM_ (\f -> Lucid.tr_ $ do
-              Lucid.td_ (Lucid.toHtml . show $ fId f)
-              Lucid.td_ (Lucid.toHtml $ fMessage f)) (sortOn fMessage (new : fs))
+      let header = Html.tr_ $ Html.th_ (Html.Raw "id") # Html.th_ (Html.Raw "message")
+      let mkRow f = Html.tr_ $ Html.td_ (fromIntegral $ fId f) # Html.td_ (fMessage f)
+      let rows = fmap mkRow $ sortOn fMessage (new : fs)
+      Html.doctype_ #
+        Html.html_ (
+          Html.head_ (
+            Html.title_ (Html.Raw "Fortunes")
+          ) #
+          Html.body_ ( Html.table_ $
+            header # rows
+          )
+        )
 {-# INLINE fortunes #-}
 {-# INLINE fortunes #-}
 
 
 -- * Test 5: Updates
 -- * Test 5: Updates

+ 0 - 7
frameworks/Haskell/servant/mysql-haskell/stack.yaml

@@ -1,7 +0,0 @@
-resolver: lts-13.11
-packages:
-- '.'
-
-# the following flags are meant for use with ../servant-mysql-haskell.dockerfile
-compiler: ghc-8.6.3 # this MUST match the resolver's GHC; minor hack to ensure GHC isn't downloaded into sandbox.
-allow-different-user: true # carryover from hasql sibling test dir

+ 0 - 13
frameworks/Haskell/servant/servant-mysql-haskell.dockerfile

@@ -1,13 +0,0 @@
-FROM haskell:8.6.3
-
-WORKDIR /servant
-
-COPY ./mysql-haskell/stack.yaml .
-COPY ./mysql-haskell/servant-mysql-haskell.cabal .
-RUN stack setup
-RUN stack install --dependencies-only
-
-ADD ./mysql-haskell/ .
-RUN stack build --pedantic
-
-CMD stack exec servant-mysql-haskell -- tfb-database +RTS -A32m -N$(nproc)

+ 11 - 6
frameworks/Haskell/servant/servant.dockerfile

@@ -3,14 +3,19 @@ FROM haskell:8.6.3
 RUN apt update -yqq && apt install -yqq xz-utils make
 RUN apt update -yqq && apt install -yqq xz-utils make
 RUN apt install -yqq libpq-dev
 RUN apt install -yqq libpq-dev
 
 
-WORKDIR /servant
+WORKDIR /app
 
 
-COPY ./hasql/stack.yaml .
-COPY ./hasql/servant-bench.cabal .
+COPY ./stack.yaml .
+COPY ./hasql/servant-hasql.cabal ./hasql/
+COPY ./mysql-haskell/servant-mysql-haskell.cabal ./mysql-haskell/
 RUN stack setup
 RUN stack setup
 RUN stack install --dependencies-only
 RUN stack install --dependencies-only
 
 
-ADD ./hasql/ .
-RUN stack build --pedantic
+ADD ./hasql/ ./hasql/
+ADD ./mysql-haskell/ ./mysql-haskell/
+RUN stack build --pedantic --copy-bins
+RUN ln -s ~/.local/bin/servant-hasql ~/.local/bin/servant
 
 
-CMD stack --allow-different-user exec servant-exe -- tfb-database +RTS -A32m -N$(nproc)
+ARG TFB_TEST_NAME
+ENV TFB_TEST_NAME=${TFB_TEST_NAME}
+CMD stack exec $TFB_TEST_NAME -- tfb-database +RTS -A32m -N$(nproc)

+ 8 - 0
frameworks/Haskell/servant/stack.yaml

@@ -0,0 +1,8 @@
+resolver: lts-13.13
+packages:
+- './hasql'
+- './mysql-haskell'
+
+# the following flags are meant for use with servant.dockerfile
+compiler: ghc-8.6.3 # this MUST match the resolver's GHC; minor hack to ensure GHC isn't downloaded into sandbox.
+allow-different-user: true