yesod/yesod-core/Yesod/Internal/Session.hs
Felipe Lessa b2a9beba3c Use a cache for session cookie's expiration time.
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.
2012-09-05 01:35:10 -03:00

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