on master help sign in
Merge pull request #236 from iphydf/chunked-processing

perf: Process chunks of bracketed pastes a linear number of times.

commit1f4926369eac21657888aaf64cad20006d6c68d6 authorJonathan Daugherty <cygnus@foobox.com> dateThu 10 Mar 2022, 11:56 (5 years ago) parent2590f05547ab19
8 files changed+170−107
src/Graphics/Vty/Input/Classify.hs+53−29 src/Graphics/Vty/Input/Classify/Parse.hs+10−7 src/Graphics/Vty/Input/Classify/Types.hs+6−1 src/Graphics/Vty/Input/Focus.hs+15−13 src/Graphics/Vty/Input/Loop.hs+35−17 src/Graphics/Vty/Input/Mouse.hs+17−15 src/Graphics/Vty/Input/Paste.hs+15−16 src/Graphics/Vty/Output/XTermColor.hs+19−9
src/Graphics/Vty/Input/Classify.hs+53−29
@@ -1,10 +1,11 @@
11{-# OPTIONS_HADDOCK hide #-}
2−-- This makes a kind of tri. Has space efficiency issues with large
2+-- This makes a kind of trie. Has space efficiency issues with large
33-- input blocks. Likely building a parser and just applying that would
44-- be better.
55module Graphics.Vty.Input.Classify
66 ( classify
77 , KClass(..)
8+ , ClassifierState(..)
89 )
910where
1011
@@ -16,25 +17,39 @@ import Graphics.Vty.Input.Classify.Types
1617
1718import Codec.Binary.UTF8.Generic (decode)
1819
19−import Data.List (inits)
20+import Control.Arrow (first)
2021import qualified Data.Map as M( fromList, lookup )
2122import Data.Maybe ( mapMaybe )
2223import qualified Data.Set as S( fromList, member )
2324
24−import Data.Char
2525import Data.Word
2626
27−compile :: ClassifyMap -> String -> KClass
27+import qualified Data.ByteString as BS
28+import qualified Data.ByteString.Char8 as BS8
29+import Data.ByteString.Char8 (ByteString)
30+
31+-- | Whether the classifier is currently processing a chunked format.
32+-- Currently, only bracketed pastes use this.
33+data ClassifierState
34+ = ClassifierStart
35+ -- ^ Not processing a chunked format.
36+ | ClassifierInChunk ByteString [ByteString]
37+ -- ^ Currently processing a chunked format. The initial chunk is in the
38+ -- first argument and a reversed remainder of the chunks is collected in
39+ -- the second argument. At the end of the processing, the chunks are
40+ -- reversed and concatenated with the final chunk.
41+
42+compile :: ClassifyMap -> ByteString -> KClass
2843compile table = cl' where
2944 -- take all prefixes and create a set of these
30− prefixSet = S.fromList $ concatMap (init . inits . fst) table
45+ prefixSet = S.fromList $ concatMap (init . BS.inits . BS8.pack . fst) table
3146 maxValidInputLength = maximum (map (length . fst) table)
32− eventForInput = M.fromList table
33− cl' [] = Prefix
47+ eventForInput = M.fromList $ map (first BS8.pack) table
48+ cl' inputBlock | BS8.null inputBlock = Prefix
3449 cl' inputBlock = case M.lookup inputBlock eventForInput of
3550 -- if the inputBlock is exactly what is expected for an
3651 -- event then consume the whole block and return the event
37− Just e -> Valid e []
52+ Just e -> Valid e BS8.empty
3853 Nothing -> case S.member inputBlock prefixSet of
3954 True -> Prefix
4055 -- look up progressively smaller tails of the input
@@ -45,40 +60,49 @@ compile table = cl' where
4560 -- H: There will always be one match. The prefixSet
4661 -- contains, by definition, all prefixes of an event.
4762 False ->
48− let inputPrefixes = reverse $ take maxValidInputLength $ tail $ inits inputBlock
63+ let inputPrefixes = reverse . take maxValidInputLength . tail . BS8.inits $ inputBlock
4964 in case mapMaybe (\s -> (,) s `fmap` M.lookup s eventForInput) inputPrefixes of
50− (s,e) : _ -> Valid e (drop (length s) inputBlock)
65+ (s,e) : _ -> Valid e (BS8.drop (BS8.length s) inputBlock)
5166 -- neither a prefix or a full event.
5267 [] -> Invalid
5368
54−classify :: ClassifyMap -> String -> KClass
55−classify table =
56− let standardClassifier = compile table
57− in \s -> case s of
58− _ | bracketedPasteStarted s ->
69+classify :: ClassifyMap -> ClassifierState -> ByteString -> KClass
70+classify table = process
71+ where
72+ standardClassifier = compile table
73+
74+ process ClassifierStart s =
75+ case BS.uncons s of
76+ _ | bracketedPasteStarted s ->
77+ if bracketedPasteFinished s
78+ then parseBracketedPaste s
79+ else Chunk
80+ _ | isMouseEvent s -> classifyMouseEvent s
81+ _ | isFocusEvent s -> classifyFocusEvent s
82+ Just (c,cs) | c >= 0xC2 -> classifyUtf8 c cs
83+ _ -> standardClassifier s
84+
85+ process (ClassifierInChunk p ps) s | bracketedPasteStarted p =
5986 if bracketedPasteFinished s
60− then parseBracketedPaste s
61− else Prefix
62− _ | isMouseEvent s -> classifyMouseEvent s
63− _ | isFocusEvent s -> classifyFocusEvent s
64− c:cs | ord c >= 0xC2 -> classifyUtf8 c cs
65− _ -> standardClassifier s
87+ then parseBracketedPaste $ BS.concat $ p:reverse (s:ps)
88+ else Chunk
89+ process ClassifierInChunk{} _ = Invalid
6690
67−classifyUtf8 :: Char -> String -> KClass
91+classifyUtf8 :: Word8 -> ByteString -> KClass
6892classifyUtf8 c cs =
69− let n = utf8Length (ord c)
70− (codepoint,rest) = splitAt n (c:cs)
93+ let n = utf8Length c
94+ (codepoint,rest) = BS8.splitAt (n - 1) cs
7195
7296 codepoint8 :: [Word8]
73− codepoint8 = map (fromIntegral . ord) codepoint
97+ codepoint8 = c:BS.unpack codepoint
7498
7599 in case decode codepoint8 of
76− _ | n < length codepoint -> Prefix
77− Just (unicodeChar, _) -> Valid (EvKey (KChar unicodeChar) []) rest
100+ _ | n < BS.length codepoint + 1 -> Prefix
101+ Just (unicodeChar, _) -> Valid (EvKey (KChar unicodeChar) []) rest
78102 -- something bad happened; just ignore and continue.
79− Nothing -> Invalid
103+ Nothing -> Invalid
80104
81−utf8Length :: (Num t, Ord a, Num a) => a -> t
105+utf8Length :: Word8 -> Int
82106utf8Length c
83107 | c < 0x80 = 1
84108 | c < 0xE0 = 2
src/Graphics/Vty/Input/Classify/Parse.hs+10−7
@@ -16,12 +16,15 @@ import Graphics.Vty.Input.Classify.Types
1616import Control.Monad.Trans.Maybe
1717import Control.Monad.State
1818
19−type Parser a = MaybeT (State String) a
19+import qualified Data.ByteString.Char8 as BS8
20+import Data.ByteString.Char8 (ByteString)
21+
22+type Parser a = MaybeT (State ByteString) a
2023
2124-- | Run a parser on a given input string. If the parser fails, return
2225-- 'Invalid'. Otherwise return the valid event ('Valid') and the
2326-- remaining unparsed characters.
24−runParser :: String -> Parser Event -> KClass
27+runParser :: ByteString -> Parser Event -> KClass
2528runParser s parser =
2629 case runState (runMaybeT parser) s of
2730 (Nothing, _) -> Invalid
@@ -36,9 +39,9 @@ failParse = fail "invalid parse"
3639-- return '123' and consume those characters.
3740readInt :: Parser Int
3841readInt = do
39− s <- get
42+ s <- BS8.unpack <$> get
4043 case (reads :: ReadS Int) s of
41− [(i, rest)] -> put rest >> return i
44+ [(i, rest)] -> put (BS8.pack rest) >> return i
4245 _ -> failParse
4346
4447-- | Read a character from the input stream. If one cannot be read (e.g.
@@ -46,9 +49,9 @@ readInt = do
4649readChar :: Parser Char
4750readChar = do
4851 s <- get
49− case s of
50− c:rest -> put rest >> return c
51− _ -> failParse
52+ case BS8.uncons s of
53+ Just (c,rest) -> put rest >> return c
54+ Nothing -> failParse
5255
5356-- | Read a character from the input stream and fail parsing if it is
5457-- not the specified character.
src/Graphics/Vty/Input/Classify/Types.hs+6−1
@@ -8,8 +8,10 @@ where
88
99import Graphics.Vty.Input.Events
1010
11+import Data.ByteString.Char8 (ByteString)
12+
1113data KClass
12− = Valid Event String
14+ = Valid Event ByteString
1315 -- ^ A valid event was parsed. Any unused characters from the input
1416 -- stream are also provided.
1517 | Invalid
@@ -17,4 +19,7 @@ data KClass
1719 | Prefix
1820 -- ^ The input characters form the prefix of a valid event character
1921 -- sequence.
22+ | Chunk
23+ -- ^ The input characters are either start of a bracketed paste chunk
24+ -- or in the middle of a bracketed paste chunk.
2025 deriving(Show, Eq)
src/Graphics/Vty/Input/Focus.hs+15−13
@@ -11,30 +11,32 @@ import Graphics.Vty.Input.Classify.Types
1111import Graphics.Vty.Input.Classify.Parse
1212
1313import Control.Monad.State
14−import Data.List (isPrefixOf)
14+
15+import qualified Data.ByteString.Char8 as BS8
16+import Data.ByteString.Char8 (ByteString)
1517
1618-- | These sequences set xterm-based terminals to send focus event
1719-- sequences.
18−requestFocusEvents :: String
19−requestFocusEvents = "\ESC[?1004h"
20+requestFocusEvents :: ByteString
21+requestFocusEvents = BS8.pack "\ESC[?1004h"
2022
2123-- | These sequences disable focus events.
22−disableFocusEvents :: String
23−disableFocusEvents = "\ESC[?1004l"
24+disableFocusEvents :: ByteString
25+disableFocusEvents = BS8.pack "\ESC[?1004l"
2426
2527-- | Does the specified string begin with a focus event?
26−isFocusEvent :: String -> Bool
27−isFocusEvent s = isPrefixOf focusIn s ||
28− isPrefixOf focusOut s
28+isFocusEvent :: ByteString -> Bool
29+isFocusEvent s = BS8.isPrefixOf focusIn s ||
30+ BS8.isPrefixOf focusOut s
2931
30−focusIn :: String
31−focusIn = "\ESC[I"
32+focusIn :: ByteString
33+focusIn = BS8.pack "\ESC[I"
3234
33−focusOut :: String
34−focusOut = "\ESC[O"
35+focusOut :: ByteString
36+focusOut = BS8.pack "\ESC[O"
3537
3638-- | Attempt to classify an input string as a focus event.
37−classifyFocusEvent :: String -> KClass
39+classifyFocusEvent :: ByteString -> KClass
3840classifyFocusEvent s = runParser s $ do
3941 when (not $ isFocusEvent s) failParse
4042
src/Graphics/Vty/Input/Loop.hs+35−17
@@ -33,12 +33,15 @@ import Control.Monad.Trans.State (StateT(..), evalStateT)
3333import Control.Monad.State.Class (MonadState, modify)
3434import Control.Monad.Trans.Reader (ReaderT(..))
3535
36−import Data.Char
36+import qualified Data.ByteString.Char8 as BS8
37+import qualified Data.ByteString as BS
38+import Data.ByteString.Char8 (ByteString)
3739import Data.IORef
3840import Data.Word (Word8)
3941
40−import Foreign ( allocaArray, peekArray, Ptr )
42+import Foreign (allocaArray)
4143import Foreign.C.Types (CInt(..))
44+import Foreign.Ptr (Ptr, castPtr)
4245
4346import System.IO
4447import System.Posix.IO (fdReadBuf, setFdOption, FdOption(..))
@@ -76,10 +79,11 @@ data InputBuffer = InputBuffer
7679makeLenses ''InputBuffer
7780
7881data InputState = InputState
79− { _unprocessedBytes :: String
82+ { _unprocessedBytes :: ByteString
83+ , _classifierState :: ClassifierState
8084 , _appliedConfig :: Config
8185 , _inputBuffer :: InputBuffer
82− , _classifier :: String -> KClass
86+ , _classifier :: ClassifierState -> ByteString -> KClass
8387 }
8488
8589makeLenses ''InputState
@@ -104,7 +108,7 @@ loopInputProcessor = do
104108 dropInvalid
105109 loopInputProcessor
106110
107−addBytesToProcess :: String -> InputM ()
111+addBytesToProcess :: ByteString -> InputM ()
108112addBytesToProcess block = unprocessedBytes <>= block
109113
110114emit :: Event -> InputM ()
@@ -117,7 +121,7 @@ emit event = do
117121--
118122-- Precondition: Under the threaded runtime. Only current use is from a
119123-- forkOS thread. That case satisfies precondition.
120−readFromDevice :: InputM String
124+readFromDevice :: InputM ByteString
121125readFromDevice = do
122126 newConfig <- view configRef >>= liftIO . readIORef
123127 oldConfig <- use appliedConfig
@@ -137,9 +141,10 @@ readFromDevice = do
137141 threadWaitRead fd
138142 bytesRead <- fdReadBuf fd bufferPtr (fromIntegral maxBytes)
139143 if bytesRead > 0
140− then map (chr . fromIntegral) <$> peekArray (fromIntegral bytesRead) bufferPtr
141− else return []
142− when (not $ null stringRep) $ logMsg $ "input bytes: " ++ show stringRep
144+ then BS.packCStringLen (castPtr bufferPtr, fromIntegral bytesRead)
145+ else return BS.empty
146+ when (not $ BS.null stringRep) $
147+ logMsg $ "input bytes: " ++ show (BS8.unpack stringRep)
143148 return stringRep
144149
145150applyConfig :: Fd -> Config -> IO ()
@@ -150,30 +155,43 @@ applyConfig _ _ = fail "(vty) applyConfig was not provided a complete configurat
150155parseEvent :: InputM Event
151156parseEvent = do
152157 c <- use classifier
158+ s <- use classifierState
153159 b <- use unprocessedBytes
154− case c b of
160+ case c s b of
155161 Valid e remaining -> do
156162 logMsg $ "valid parse: " ++ show e
157163 logMsg $ "remaining: " ++ show remaining
164+ classifierState .= ClassifierStart
158165 unprocessedBytes .= remaining
159166 return e
160− _ -> mzero
167+ _ -> mzero
161168
162169dropInvalid :: InputM ()
163170dropInvalid = do
164171 c <- use classifier
172+ s <- use classifierState
165173 b <- use unprocessedBytes
166− when (c b == Invalid) $ do
167− logMsg "dropping input bytes"
168− unprocessedBytes .= []
174+ case c s b of
175+ Chunk -> do
176+ classifierState .=
177+ case s of
178+ ClassifierStart -> ClassifierInChunk b []
179+ ClassifierInChunk p bs -> ClassifierInChunk p (b:bs)
180+ unprocessedBytes .= BS8.empty
181+ Invalid -> do
182+ logMsg "dropping input bytes"
183+ classifierState .= ClassifierStart
184+ unprocessedBytes .= BS8.empty
185+ _ -> return ()
169186
170187runInputProcessorLoop :: ClassifyMap -> Input -> IO ()
171188runInputProcessorLoop classifyTable input = do
172189 let bufferSize = 1024
173190 allocaArray bufferSize $ \(bufferPtr :: Ptr Word8) -> do
174− s0 <- InputState [] <$> readIORef (_configRef input)
175− <*> pure (InputBuffer bufferPtr bufferSize)
176− <*> pure (classify classifyTable)
191+ s0 <- InputState BS8.empty ClassifierStart
192+ <$> readIORef (_configRef input)
193+ <*> pure (InputBuffer bufferPtr bufferSize)
194+ <*> pure (classify classifyTable)
177195 runReaderT (evalStateT loopInputProcessor s0) input
178196
179197-- | Construct two IO actions: one to configure the terminal for Vty and
src/Graphics/Vty/Input/Mouse.hs+17−15
@@ -15,10 +15,12 @@ import Graphics.Vty.Input.Classify.Types
1515import Graphics.Vty.Input.Classify.Parse
1616
1717import Control.Monad.State
18−import Data.List (isPrefixOf)
1918import Data.Maybe (catMaybes)
2019import Data.Bits ((.&.))
2120
21+import qualified Data.ByteString.Char8 as BS8
22+import Data.ByteString.Char8 (ByteString)
23+
2224-- A mouse event in SGR extended mode is
2325--
2426-- '\ESC' '[' '<' B ';' X ';' Y ';' ('M'|'m')
@@ -32,28 +34,28 @@ import Data.Bits ((.&.))
3234
3335-- | These sequences set xterm-based terminals to send mouse event
3436-- sequences.
35−requestMouseEvents :: String
36−requestMouseEvents = "\ESC[?1000h\ESC[?1002h\ESC[?1006h"
37+requestMouseEvents :: ByteString
38+requestMouseEvents = BS8.pack "\ESC[?1000h\ESC[?1002h\ESC[?1006h"
3739
3840-- | These sequences disable mouse events.
39−disableMouseEvents :: String
40−disableMouseEvents = "\ESC[?1000l\ESC[?1002l\ESC[?1006l"
41+disableMouseEvents :: ByteString
42+disableMouseEvents = BS8.pack "\ESC[?1000l\ESC[?1002l\ESC[?1006l"
4143
4244-- | Does the specified string begin with a mouse event?
43−isMouseEvent :: String -> Bool
45+isMouseEvent :: ByteString -> Bool
4446isMouseEvent s = isSGREvent s || isNormalEvent s
4547
46−isSGREvent :: String -> Bool
47−isSGREvent = isPrefixOf sgrPrefix
48+isSGREvent :: ByteString -> Bool
49+isSGREvent = BS8.isPrefixOf sgrPrefix
4850
49−sgrPrefix :: String
50−sgrPrefix = "\ESC[M"
51+sgrPrefix :: ByteString
52+sgrPrefix = BS8.pack "\ESC[M"
5153
52−isNormalEvent :: String -> Bool
53−isNormalEvent = isPrefixOf normalPrefix
54+isNormalEvent :: ByteString -> Bool
55+isNormalEvent = BS8.isPrefixOf normalPrefix
5456
55−normalPrefix :: String
56−normalPrefix = "\ESC[<"
57+normalPrefix :: ByteString
58+normalPrefix = BS8.pack "\ESC[<"
5759
5860-- Modifier bits:
5961shiftBit :: Int
@@ -88,7 +90,7 @@ hasBitSet :: Int -> Int -> Bool
8890hasBitSet val bit = val .&. bit > 0
8991
9092-- | Attempt to lassify an input string as a mouse event.
91−classifyMouseEvent :: String -> KClass
93+classifyMouseEvent :: ByteString -> KClass
9294classifyMouseEvent s = runParser s $ do
9395 when (not $ isMouseEvent s) failParse
9496
src/Graphics/Vty/Input/Paste.hs+15−16
@@ -9,34 +9,33 @@ module Graphics.Vty.Input.Paste
99where
1010
1111import qualified Data.ByteString.Char8 as BS8
12+import Data.ByteString.Char8 (ByteString)
1213
1314import Graphics.Vty.Input.Events
1415import Graphics.Vty.Input.Classify.Types
1516
16−import Data.List (isPrefixOf, isInfixOf)
17+bracketedPasteStart :: ByteString
18+bracketedPasteStart = BS8.pack "\ESC[200~"
1719
18−bracketedPasteStart :: String
19−bracketedPasteStart = "\ESC[200~"
20−
21−bracketedPasteEnd :: String
22−bracketedPasteEnd = "\ESC[201~"
20+bracketedPasteEnd :: ByteString
21+bracketedPasteEnd = BS8.pack "\ESC[201~"
2322
2423-- | Does the input start a bracketed paste?
25−bracketedPasteStarted :: String -> Bool
26−bracketedPasteStarted = isPrefixOf bracketedPasteStart
24+bracketedPasteStarted :: ByteString -> Bool
25+bracketedPasteStarted = BS8.isPrefixOf bracketedPasteStart
2726
2827-- | Does the input contain a complete bracketed paste?
29−bracketedPasteFinished :: String -> Bool
30−bracketedPasteFinished = isInfixOf bracketedPasteEnd
28+bracketedPasteFinished :: ByteString -> Bool
29+bracketedPasteFinished = BS8.isInfixOf bracketedPasteEnd
3130
3231-- | Parse a bracketed paste. This should only be called on a string if
3332-- both 'bracketedPasteStarted' and 'bracketedPasteFinished' return
3433-- 'True'.
35−parseBracketedPaste :: String -> KClass
34+parseBracketedPaste :: ByteString -> KClass
3635parseBracketedPaste s =
37− Valid (EvPaste p) (BS8.unpack $ BS8.drop (BS8.length end) rest')
36+ Valid (EvPaste p) (BS8.drop endLen rest')
3837 where
39− start = BS8.pack bracketedPasteStart
40− end = BS8.pack bracketedPasteEnd
41− (_, rest ) = BS8.breakSubstring start . BS8.pack $ s
42− (p, rest') = BS8.breakSubstring end . BS8.drop (BS8.length start) $ rest
38+ startLen = BS8.length bracketedPasteStart
39+ endLen = BS8.length bracketedPasteEnd
40+ (_, rest ) = BS8.breakSubstring bracketedPasteStart s
41+ (p, rest') = BS8.breakSubstring bracketedPasteEnd . BS8.drop startLen $ rest
src/Graphics/Vty/Output/XTermColor.hs+19−9
@@ -15,13 +15,17 @@ import qualified Graphics.Vty.Output.TerminfoBased as TerminfoBased
1515import Blaze.ByteString.Builder (writeToByteString)
1616import Blaze.ByteString.Builder.Word (writeWord8)
1717
18+import qualified Data.ByteString.Char8 as BS8
19+import Data.ByteString.Char8 (ByteString)
20+import Foreign.Ptr (castPtr)
21+
1822import Control.Monad (void, when)
1923import Control.Monad.Trans
2024import Data.Char (toLower)
2125import Data.IORef
2226
23−import System.Posix.IO (fdWrite)
24−import System.Posix.Types (Fd)
27+import System.Posix.IO (fdWriteBuf)
28+import System.Posix.Types (ByteCount, Fd)
2529import System.Posix.Env (getEnv)
2630
2731import Data.List (isInfixOf)
@@ -31,6 +35,12 @@ import Data.Maybe (catMaybes)
3135import Data.Monoid ((<>))
3236#endif
3337
38+-- | Write a 'ByteString' to an 'Fd'.
39+fdWrite :: Fd -> ByteString -> IO ByteCount
40+fdWrite fd s =
41+ BS8.useAsCStringLen s $ \(buf,len) -> do
42+ fdWriteBuf fd (castPtr buf) (fromIntegral len)
43+
3444-- | Construct an Xterm output driver. Initialize the display to UTF-8.
3545reserveTerminal :: ( Applicative m, MonadIO m ) => String -> Fd -> m Output
3646reserveTerminal variant outFd = liftIO $ do
@@ -100,19 +110,19 @@ utf8Active = do
100110
101111-- | Enable bracketed paste mode:
102112-- http://cirw.in/blog/bracketed-paste
103−enableBracketedPastes :: String
104−enableBracketedPastes = "\ESC[?2004h"
113+enableBracketedPastes :: ByteString
114+enableBracketedPastes = BS8.pack "\ESC[?2004h"
105115
106116-- | Disable bracketed paste mode:
107−disableBracketedPastes :: String
108−disableBracketedPastes = "\ESC[?2004l"
117+disableBracketedPastes :: ByteString
118+disableBracketedPastes = BS8.pack "\ESC[?2004l"
109119
110120-- | These sequences set xterm based terminals to UTF-8 output.
111121--
112122-- There is no known terminfo capability equivalent to this.
113−setUtf8CharSet, setDefaultCharSet :: String
114−setUtf8CharSet = "\ESC%G"
115−setDefaultCharSet = "\ESC%@"
123+setUtf8CharSet, setDefaultCharSet :: ByteString
124+setUtf8CharSet = BS8.pack "\ESC%G"
125+setDefaultCharSet = BS8.pack "\ESC%@"
116126
117127xtermInlineHack :: Output -> IO ()
118128xtermInlineHack t = do