on master help sign in
Merge remote-tracking branch 'erikd/master'
commitc8749fb6fb48c88081d509b13d300bdf81bf9084 authorMichael Snoyman <michael@snoyman.com> dateTue 24 Jan 2012, 12:18 (15 years ago) parent92cd327f4f26d2
4 files changed+257−34
warp/Network/Wai/Handler/Warp.hs+34−34 warp/ReadInt.hs+58 warp/test/readInt.hs+164 warp/warp.cabal+1
warp/Network/Wai/Handler/Warp.hs+34−34
@@ -3,6 +3,7 @@
33{-# LANGUAGE OverloadedStrings #-}
44{-# LANGUAGE CPP #-}
55{-# LANGUAGE BangPatterns #-}
6+{-# LANGUAGE MagicHash #-}
67---------------------------------------------------------
78--
89-- Module : Network.Wai.Handler.Warp
@@ -83,8 +84,7 @@ import Control.Exception
8384 , bracketOnError
8485 )
8586import Control.Concurrent (forkIO)
86−import qualified Data.Char as C
87−import Data.Maybe (fromMaybe)
87+import Data.Maybe (fromMaybe, isJust)
8888
8989import Data.Typeable (Typeable)
9090
@@ -113,6 +113,8 @@ import Control.Monad (forever, when)
113113import qualified Network.HTTP.Types as H
114114import qualified Data.CaseInsensitive as CI
115115import System.IO (hPutStrLn, stderr)
116+import ReadInt (readInt64)
117+
116118
117119#if WINDOWS
118120import Control.Concurrent (threadDelay)
@@ -176,7 +178,7 @@ bindPort p s = do
176178 addr = if null addrs' then head addrs else head addrs'
177179 bracketOnError
178180 (Network.Socket.socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr))
179− (sClose)
181+ sClose
180182 (\sock -> do
181183 setSocketOption sock ReuseAddr 1
182184 bindSocket sock (addrAddress addr)
@@ -207,7 +209,7 @@ runSettings set =
207209 bracket
208210 (bindPort (settingsPort set) (settingsHost set))
209211 sClose .
210− (flip (runSettingsSocket set))
212+ flip (runSettingsSocket set)
211213#endif
212214
213215type Port = Int
@@ -219,7 +221,7 @@ type Port = Int
219221-- Note that the 'settingsPort' will still be passed to 'Application's via the
220222-- 'serverPort' record.
221223runSettingsSocket :: Settings -> Socket -> Application -> IO ()
222−runSettingsSocket set socket app = do
224+runSettingsSocket set socket app =
223225 runSettingsConnection set getter app
224226 where
225227 getter = do
@@ -244,7 +246,7 @@ serveConnection :: Settings
244246 -> T.Handle
245247 -> (SomeException -> IO ())
246248 -> Port -> Application -> Connection -> SockAddr -> IO ()
247−serveConnection settings th onException port app conn remoteHost' = do
249+serveConnection settings th onException port app conn remoteHost' =
248250 catch
249251 (finally
250252 (runResourceT serveConnection')
@@ -269,7 +271,7 @@ serveConnection settings th onException port app conn remoteHost' = do
269271
270272 liftIO $ T.resume th
271273 keepAlive <- sendResponse th env conn res
272− if keepAlive then serveConnection'' fromClient else return ()
274+ when keepAlive $ serveConnection'' fromClient
273275 Just intercept -> do
274276 liftIO $ T.pause th
275277 intercept fromClient conn
@@ -304,18 +306,16 @@ parseRequest' :: Port
304306parseRequest' _ [] _ _ = throwIO $ NotEnoughLines []
305307parseRequest' port (firstLine:otherLines) remoteHost' src = do
306308 (method, rpath', gets, httpversion) <- parseFirst firstLine
307− let (host',rpath) =
308− if S.null rpath'
309− then ("","/")
310− else if "http://" `S.isPrefixOf` rpath'
311− then S.breakByte 47 $ S.drop 7 rpath' -- '/'
312− else ("", rpath')
309+ let (host',rpath)
310+ | S.null rpath' = ("", "/")
311+ | "http://" `S.isPrefixOf` rpath' = S.breakByte 47 $ S.drop 7 rpath'
312+ | otherwise = ("", rpath')
313313 let heads = map parseHeaderNoAttr otherLines
314314 let host = fromMaybe host' $ lookup "host" heads
315315 let len =
316316 case lookup "content-length" heads of
317317 Nothing -> 0
318− Just bs -> fromIntegral $ B.foldl' (\i c -> i * 10 + C.digitToInt c) 0 $ B.takeWhile C.isDigit bs
318+ Just bs -> readInt bs
319319 let serverName' = takeUntil 58 host -- ':'
320320 -- FIXME isolate takes an Integer instead of Int or Int64. If this is a
321321 -- performance penalty, we may need our own version.
@@ -349,7 +349,7 @@ takeUntil c bs =
349349
350350parseFirst :: ByteString
351351 -> ResourceT IO (ByteString, ByteString, ByteString, H.HttpVersion)
352−parseFirst s =
352+parseFirst s =
353353 case S.split 32 s of -- ' '
354354 [method, query, http'] -> do
355355 let (hfirst, hsecond) = B.splitAt 5 http'
@@ -375,8 +375,8 @@ colonSpaceBuilder = copyByteString ": "
375375headers :: H.HttpVersion -> H.Status -> H.ResponseHeaders -> Bool -> Builder
376376headers !httpversion !status !responseHeaders !isChunked' = {-# SCC "headers" #-}
377377 let !start = httpBuilder
378− `mappend` (copyByteString $
379− case httpversion of
378+ `mappend` copyByteString
379+ (case httpversion of
380380 H.HttpVersion 1 1 -> "1.1"
381381 _ -> "1.0")
382382 `mappend` spaceBuilder
@@ -415,7 +415,7 @@ isChunked :: H.HttpVersion -> Bool
415415isChunked = (==) H.http11
416416
417417hasBody :: H.Status -> Request -> Bool
418−hasBody s req = s /= (H.Status 204 "") && s /= H.status304 &&
418+hasBody s req = s /= H.Status 204 "" && s /= H.status304 &&
419419 H.statusCode s >= 200 && requestMethod req /= "HEAD"
420420
421421sendResponse :: T.Handle
@@ -427,7 +427,7 @@ sendResponse th req conn r = sendResponse' r
427427 isChunked' = isChunked version
428428 needsChunked hs = isChunked' && not (hasLength hs)
429429 isKeepAlive hs = isPersist && (isChunked' || hasLength hs)
430− hasLength hs = lookup "content-length" hs /= Nothing
430+ hasLength hs = isJust $ lookup "content-length" hs
431431
432432 sendResponse' :: Response -> ResourceT IO Bool
433433 sendResponse' (ResponseFile s hs fp mpart) = liftIO $ do
@@ -471,10 +471,10 @@ sendResponse th req conn r = sendResponse' r
471471 headers' = headers version s hs
472472 needsChunked' = needsChunked hs
473473 body = if needsChunked'
474− then (headers' needsChunked')
474+ then headers' needsChunked'
475475 `mappend` chunkedTransferEncoding b
476476 `mappend` chunkedTransferTerminator
477− else (headers' False) `mappend` b
477+ else headers' False `mappend` b
478478
479479 sendResponse' (ResponseSource s hs body) =
480480 response
@@ -497,7 +497,7 @@ sendResponse th req conn r = sendResponse' r
497497 return $ isKeepAlive hs
498498 needsChunked' = needsChunked hs
499499 chunk :: C.Conduit Builder IO Builder
500− chunk = C.Conduit $ return $ C.PreparedConduit
500+ chunk = C.Conduit $ return C.PreparedConduit
501501 { C.conduitPush = push
502502 , C.conduitClose = close
503503 }
@@ -529,17 +529,18 @@ connSource Connection { connRecv = recv } th = C.PreparedSource
529529
530530-- | Use 'connSendAll' to send this data while respecting timeout rules.
531531connSink :: Connection -> T.Handle -> C.Sink B.ByteString IO ()
532−connSink Connection { connSendAll = send } th = C.Sink $ return $ C.SinkData
532+connSink Connection { connSendAll = send } th = C.Sink $ return C.SinkData
533533 { C.sinkPush = push
534534 , C.sinkClose = close
535535 }
536536 where
537537 close = liftIO (T.resume th)
538538 push x = do
539− liftIO $ T.resume th
540− liftIO $ send x
541− liftIO $ T.pause th
542− return $ C.Processing
539+ liftIO $ do
540+ T.resume th
541+ send x
542+ T.pause th
543+ return C.Processing
543544 -- We pause timeouts before passing control back to user code. This ensures
544545 -- that a timeout will only ever be executed when Warp is in control. We
545546 -- also make sure to resume the timeout after the completion of user code
@@ -573,9 +574,8 @@ defaultSettings = Settings
573574 case fromException e of
574575 Just x -> go x
575576 Nothing ->
576− if go' $ fromException e
577− then hPutStrLn stderr $ show e
578− else return ()
577+ when (go' $ fromException e) $
578+ hPutStrLn stderr $ show e
579579 , settingsTimeout = 30
580580 , settingsIntercept = const Nothing
581581 , settingsManager = Nothing
@@ -649,10 +649,10 @@ checkCR bs pos =
649649 else pos
650650{-# INLINE checkCR #-}
651651
652−-- Note: This function produces garbage on invalid input. But serving an
653−-- invalid content-length is a bad idea, mkay?
654−readInt :: S.ByteString -> Integer
655−readInt = S.foldl' (\x w -> x * 10 + fromIntegral w - 48) 0
652+readInt :: Integral a => ByteString -> a
653+readInt bs = fromIntegral $ readInt64 bs
654+{-# INLINE readInt #-}
655+
656656
657657-- | Call the inner function with a timeout manager.
658658withManager :: Int -- ^ timeout in microseconds
warp/ReadInt.hsnew file+58
@@ -0,0 +1,58 @@
1+{-# LANGUAGE OverloadedStrings, MagicHash, BangPatterns #-}
2+
3+-- Copyright : Erik de Castro Lopo <erikd@mega-nerd.com>
4+-- License : BSD3
5+
6+module ReadInt (readInt64) where
7+
8+-- This function lives in its own file because the MagicHash pragma interacts
9+-- poorly with the CPP pragma.
10+
11+import Data.ByteString (ByteString)
12+import Data.Int (Int64)
13+import GHC.Prim
14+import GHC.Types
15+
16+import qualified Data.ByteString.Char8 as B
17+import qualified Data.Char as C
18+
19+-- This function is used to parse the Content-Length field of HTTP headers and
20+-- is a performance hot spot. It should only be replaced with something
21+-- significantly and provably faster.
22+--
23+-- It needs to be able work correctly on 32 bit CPUs for file sizes > 2G so we
24+-- use Int64 here and then make a generic 'readInt' that allows conversion to
25+-- Int and Integer.
26+
27+readInt64 :: ByteString -> Int64
28+readInt64 bs =
29+ B.foldl' (\i c -> i * 10 + fromIntegral (mhDigitToInt c)) 0
30+ $ B.takeWhile C.isDigit bs
31+{- NOINLINE readInt64MH #-}
32+
33+data Table = Table !Addr#
34+
35+{- NOINLINE mhDigitToInt #-}
36+mhDigitToInt :: Char -> Int
37+mhDigitToInt (C# i) = I# (word2Int# $ indexWord8OffAddr# addr (ord# i))
38+ where
39+ !(Table addr) = table
40+ table :: Table
41+ table = Table
42+ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
43+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
44+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
45+ \\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x00\x00\x00\x00\x00\x00\
46+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
47+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
48+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
49+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
50+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
51+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
52+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
53+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
54+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
55+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
56+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
57+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"#
58+
warp/test/readInt.hsnew file+164
@@ -0,0 +1,164 @@
1+{-# LANGUAGE OverloadedStrings, MagicHash, BangPatterns #-}
2+
3+-- Copyright : Erik de Castro Lopo <erikd@mega-nerd.com>
4+-- License : BSD3
5+
6+-- A program to QuickCheck and benchmark a function used in the Warp web server
7+-- and elsewhere to read the Content-Length field of HTTP headers.
8+--
9+-- Compile and run as:
10+-- ghc -Wall -O3 --make readInt.hs -o readInt && ./readInt
11+
12+import Criterion.Main
13+import Data.ByteString (ByteString)
14+import Data.Int (Int64)
15+import Data.Maybe (fromMaybe)
16+
17+import qualified Data.ByteString as S
18+import qualified Data.ByteString.Char8 as B
19+import qualified Data.Char as C
20+import qualified Numeric as N
21+import qualified Test.QuickCheck as QC
22+
23+-- ReadInt is a module internal to Warp.
24+import qualified ReadInt as RI
25+
26+
27+import GHC.Prim
28+import GHC.Types
29+
30+-- This is the absolute mimimal solution. It will return garbage if the
31+-- imput string contains anything other than ASCI digits.
32+readIntOrig :: ByteString -> Integer
33+readIntOrig =
34+ S.foldl' (\x w -> x * 10 + fromIntegral w - 48) 0
35+
36+
37+-- Using Numeric.readDec which works on String, so the ByteString has to be
38+-- unpacked first.
39+readDec :: ByteString -> Integer
40+readDec s =
41+ case N.readDec (B.unpack s) of
42+ [] -> 0
43+ (x, _):_ -> x
44+
45+
46+-- Use ByteString's readInteger function (which requires some extra stuff to
47+-- slow us down).
48+readIntegerBS :: ByteString -> Integer
49+readIntegerBS bs = fst $ fromMaybe (0, "") $ B.readInteger bs
50+
51+
52+-- No checking for non-digits. Will overflow at 2^31 on 32 bit CPUs.
53+readIntRaw :: ByteString -> Int
54+readIntRaw =
55+ B.foldl' (\i c -> i * 10 + C.digitToInt c) 0
56+
57+
58+-- The best solution.
59+readIntTC :: Integral a => ByteString -> a
60+readIntTC bs = fromIntegral
61+ $ B.foldl' (\i c -> i * 10 + C.digitToInt c) 0 $ B.takeWhile C.isDigit bs
62+
63+
64+-- Three specialisations of readIntTC.
65+readInt :: ByteString -> Int
66+readInt = readIntTC
67+
68+readInt64 :: ByteString -> Int64
69+readInt64 = readIntTC
70+
71+readInteger :: ByteString -> Integer
72+readInteger = readIntTC
73+
74+
75+-- MagicHash version suggested by Vincent Hanquez.
76+readIntMH :: Integral a => ByteString -> a
77+readIntMH s = fromIntegral $ ireadInt64MH s
78+ where
79+ ireadInt64MH :: ByteString -> Int64
80+ ireadInt64MH bs =
81+ B.foldl' (\i c -> i * 10 + fromIntegral (mhDigitToInt c)) 0
82+ $ B.takeWhile C.isDigit bs
83+
84+readInt64MH :: ByteString -> Int64
85+readInt64MH = readIntMH
86+
87+readIntegerMH :: ByteString -> Integer
88+readIntegerMH = readIntMH
89+
90+data Table = Table !Addr#
91+
92+mhDigitToInt :: Char -> Int
93+mhDigitToInt (C# i) = I# (word2Int# $ indexWord8OffAddr# addr (ord# i))
94+ where
95+ !(Table addr) = table
96+ table :: Table
97+ table = Table
98+ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
99+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
100+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
101+ \\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x00\x00\x00\x00\x00\x00\
102+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
103+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
104+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
105+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
106+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
107+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
108+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
109+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
110+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
111+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
112+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
113+ \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"#
114+
115+
116+-- This is the one Warp actually uses.
117+readIntWarp :: ByteString -> Integer
118+readIntWarp s = fromIntegral $ RI.readInt64 s
119+
120+
121+-- A QuickCheck property. Test that for a number >= 0, converting it to
122+-- a string using show and then reading the value back with the function
123+-- under test returns the original value.
124+-- The functions under test only work on Natural numbers (the Conent-Length
125+-- field in a HTTP header is always >= 0) so we check the absolute value of
126+-- the value that QuickCheck generates for us.
127+prop_read_show_idempotent :: Integral a => (ByteString -> a) -> a -> Bool
128+prop_read_show_idempotent freader x =
129+ let px = abs x
130+ in px == freader (B.pack $ show px)
131+
132+
133+runQuickCheckTests :: IO ()
134+runQuickCheckTests = do
135+ QC.quickCheck (prop_read_show_idempotent readInt)
136+ QC.quickCheck (prop_read_show_idempotent readInt64)
137+ QC.quickCheck (prop_read_show_idempotent readInteger)
138+ QC.quickCheck (prop_read_show_idempotent readInt64MH)
139+ QC.quickCheck (prop_read_show_idempotent readIntegerMH)
140+ QC.quickCheck (prop_read_show_idempotent readIntWarp)
141+
142+
143+runCriterionTests :: ByteString -> IO ()
144+runCriterionTests number =
145+ defaultMain
146+ [ bench "readIntOrig" $ nf readIntOrig number
147+ , bench "readDec" $ nf readDec number
148+ , bench "readIntegerBS" $ nf readIntegerBS number
149+ , bench "readRaw" $ nf readIntRaw number
150+ , bench "readInt" $ nf readInt number
151+ , bench "readInt64" $ nf readInt64 number
152+ , bench "readInteger" $ nf readInteger number
153+ , bench "readInt64MH" $ nf readInt64MH number
154+ , bench "readIntegerMH" $ nf readIntegerMH number
155+ , bench "readIntWarp" $ nf readIntWarp number
156+ ]
157+
158+main :: IO ()
159+main = do
160+ putStrLn "Quickcheck tests."
161+ runQuickCheckTests
162+ putStrLn "Criterion tests."
163+ runCriterionTests "1234567898765432178979128361238162386182"
164+
warp/warp.cabal+1
@@ -29,6 +29,7 @@ Library
2929 , http-types >= 0.6 && < 0.7
3030 , case-insensitive >= 0.2
3131 , unix-compat >= 0.2
32+ , ghc-prim
3233 if flag(network-bytestring)
3334 build-depends: network >= 2.2.1.5 && < 2.2.3
3435 , network-bytestring >= 0.1.3 && < 0.1.4