The following HelloWorld app was used as benchmark:
data HelloWorld = HelloWorld
mkYesod "HelloWorld" [parseRoutes|
/ HomeR GET
|]
instance Yesod HelloWorld where
-- makeSessionBackend = const $ return Nothing
getHomeR = return . RepPlain . toContent $ "Hello World!"
main :: IO ()
main = warp 8080 HelloWorld
The benchmark was tested with httperf under the following
environments:
[vanilla-nosession] Released yesod-core 1.1.1.1, but without sessions.
[vanilla-session] Released yesod-core 1.1.1.1 (with sessions).
[faster-session-1] With patch 065e33a, "Faster, leaner
implementation of putTime/getTime".
[faster-session-2] With this commit.
Performance results:
A) Testing with:
httperf --hog --client=0/1 --server=localhost \
--port=8080 --uri=/ --rate=1000 \
--send-buffer=4096 --recv-buffer=16384 \
--num-conns=100 --wsess=1000,60,1 \
--burst-length=20 --session-cookie
Results:
vanilla-nosession: 19187.7 req/s (0.1 ms/req)
vanilla-session: 2523.3 req/s (0.4 ms/req)
faster-session-1: 2933.5 req/s (0.3 ms/req)
faster-session-2: 2957.5 req/s (0.3 ms/req)
This test benchmarks both saving and loading the session.
Interestingly, this commit provides only a small performance
increase.
B) Testing with:
httperf --hog --client=0/1 --server=localhost \
--port=8080 --uri=/ --rate=1000 \
--send-buffer=4096 --recv-buffer=16384 \
--num-conns=100 --num-calls=1000 \
--burst-length=20
Results:
vanilla-nosession: 43548.7 req/s (0.0 ms/req)
vanilla-session: 3609.4 req/s (0.3 ms/req)
faster-session-1: 3454.9 req/s (0.3 ms/req)
faster-session-2: 3786.9 req/s (0.3 ms/req)
This test benchmarks only saving the session. Strangely,
faster-session-1 was worse than vanilla-session (while
isolated tests show that the new putTime should be +70x
faster). However, there is a non-negligible performance
increase on faster-session-2.
131 lines
4.1 KiB
Haskell
131 lines
4.1 KiB
Haskell
module Yesod.Internal.Session
|
|
( encodeClientSession
|
|
, decodeClientSession
|
|
, clientSessionDateCacher
|
|
, ClientSessionDateCache(..)
|
|
, BackendSession
|
|
, SaveSession
|
|
, SessionBackend(..)
|
|
) where
|
|
|
|
import Yesod.Internal (Header(..))
|
|
import qualified Web.ClientSession as CS
|
|
import Data.Int (Int64)
|
|
import Data.Serialize
|
|
import Data.Time
|
|
import Data.ByteString (ByteString)
|
|
import Control.Concurrent (forkIO, killThread, threadDelay)
|
|
import Control.Monad (forever, guard)
|
|
import Data.Text (Text, pack, unpack)
|
|
import Control.Arrow (first)
|
|
import Control.Applicative ((<$>))
|
|
|
|
import qualified Data.ByteString.Char8 as S8
|
|
import qualified Data.IORef as I
|
|
import qualified Network.Wai as W
|
|
|
|
type BackendSession = [(Text, S8.ByteString)]
|
|
|
|
type SaveSession = BackendSession -- ^ The session contents after running the handler
|
|
-> IO [Header]
|
|
|
|
newtype SessionBackend master = SessionBackend
|
|
{ sbLoadSession :: master
|
|
-> W.Request
|
|
-> IO (BackendSession, SaveSession) -- ^ Return the session data and a function to save the session
|
|
}
|
|
|
|
encodeClientSession :: CS.Key
|
|
-> CS.IV
|
|
-> ClientSessionDateCache -- ^ expire time
|
|
-> ByteString -- ^ remote host
|
|
-> [(Text, ByteString)] -- ^ session
|
|
-> ByteString -- ^ cookie value
|
|
encodeClientSession key iv date rhost session' =
|
|
CS.encrypt key iv $ encode $ SessionCookie expires rhost session'
|
|
where expires = Right (csdcExpiresSerialized date)
|
|
|
|
decodeClientSession :: CS.Key
|
|
-> ClientSessionDateCache -- ^ current time
|
|
-> ByteString -- ^ remote host field
|
|
-> ByteString -- ^ cookie value
|
|
-> Maybe [(Text, ByteString)]
|
|
decodeClientSession key date rhost encrypted = do
|
|
decrypted <- CS.decrypt key encrypted
|
|
SessionCookie (Left expire) rhost' session' <-
|
|
either (const Nothing) Just $ decode decrypted
|
|
guard $ expire > csdcNow date
|
|
guard $ rhost' == rhost
|
|
return session'
|
|
|
|
data SessionCookie = SessionCookie (Either UTCTime ByteString) ByteString [(Text, ByteString)]
|
|
deriving (Show, Read)
|
|
instance Serialize SessionCookie where
|
|
put (SessionCookie a b c) = do
|
|
either putTime putByteString a
|
|
put b
|
|
put (map (first unpack) c)
|
|
get = do
|
|
a <- getTime
|
|
b <- get
|
|
c <- map (first pack) <$> get
|
|
return $ SessionCookie (Left a) b c
|
|
|
|
|
|
----------------------------------------------------------------------
|
|
|
|
|
|
-- Mostly copied from Kazu's date-cache, but with modifications
|
|
-- that better suit our needs.
|
|
--
|
|
-- The cached date is updated every 10s, we don't need second
|
|
-- resolution for session expiration times.
|
|
|
|
data ClientSessionDateCache =
|
|
ClientSessionDateCache {
|
|
csdcNow :: !UTCTime
|
|
, csdcExpires :: !UTCTime
|
|
, csdcExpiresSerialized :: !ByteString
|
|
} deriving (Eq, Show)
|
|
|
|
clientSessionDateCacher ::
|
|
NominalDiffTime -- ^ Inactive session valitity.
|
|
-> IO (IO ClientSessionDateCache, IO ())
|
|
clientSessionDateCacher validity = do
|
|
ref <- getUpdated >>= I.newIORef
|
|
tid <- forkIO $ forever (doUpdate ref)
|
|
return $! (I.readIORef ref, killThread tid)
|
|
where
|
|
getUpdated = do
|
|
now <- getCurrentTime
|
|
let expires = validity `addUTCTime` now
|
|
expiresS = runPut (putTime expires)
|
|
return $! ClientSessionDateCache now expires expiresS
|
|
doUpdate ref = do
|
|
threadDelay 10000000 -- 10s
|
|
I.writeIORef ref =<< getUpdated
|
|
|
|
|
|
----------------------------------------------------------------------
|
|
|
|
|
|
putTime :: Putter UTCTime
|
|
putTime (UTCTime d t) =
|
|
let d' = fromInteger $ toModifiedJulianDay d
|
|
t' = fromIntegral $ fromEnum (t / diffTimeScale)
|
|
in put (d' * posixDayLength_int64 + min posixDayLength_int64 t')
|
|
|
|
getTime :: Get UTCTime
|
|
getTime = do
|
|
val <- get
|
|
let (d, t) = val `divMod` posixDayLength_int64
|
|
d' = ModifiedJulianDay $! fromIntegral d
|
|
t' = fromIntegral t
|
|
d' `seq` t' `seq` return (UTCTime d' t')
|
|
|
|
posixDayLength_int64 :: Int64
|
|
posixDayLength_int64 = 86400
|
|
|
|
diffTimeScale :: DiffTime
|
|
diffTimeScale = 1e12
|