2-- Module : Streamly.Benchmark.Data.Fold
3-- Copyright : (c) 2018 Composewell
5-- Maintainer : streamly@composewell.com
7{-# LANGUAGE FlexibleContexts #-}
9{-# LANGUAGE ScopedTypeVariables #-}
11#ifdef __HADDOCK_VERSION__
16{-# LANGUAGE TemplateHaskell #-}
17{-# OPTIONS_GHC -fplugin Test.Inspection.Plugin #-}
20module Main (main) where
22import Control.DeepSeq (NFData(..))
23import Control.Exception (SomeException, ErrorCall, try)
24import Streamly.Internal.Data.Unfold (Unfold)
25import System.IO (Handle, hClose)
26import System.Random (randomRIO)
28import qualified Prelude
29import qualified Streamly.FileSystem.Handle as FH
30import qualified Streamly.Internal.Data.Fold as FL
31import qualified Streamly.Internal.Data.Unfold as UF
32import qualified Streamly.Internal.Data.Stream.IsStream as S
33import qualified Streamly.Internal.Data.Stream.StreamD as D
34import qualified Streamly.Internal.Data.Stream.StreamK as K
35import qualified Streamly.Prelude as SP
37import Gauge hiding (env)
38import Prelude hiding (concat, take, filter, zipWith, map, mapM, takeWhile)
39import Streamly.Benchmark.Common
40import Streamly.Benchmark.Common.Handle
47benchIO :: (NFData b) => String -> (Int -> IO b) -> Benchmark
48benchIO name f = bench name $ nfIO $ randomRIO (1,1) >>= f
50-------------------------------------------------------------------------------
51-- Stream generation and elimination
52-------------------------------------------------------------------------------
54-- generate numbers up to the argument value
56source :: Monad m => Int -> Unfold m Int Int
57source n = UF.enumerateFromToIntegral n
59-------------------------------------------------------------------------------
61-------------------------------------------------------------------------------
63{-# INLINE drainGeneration #-}
64drainGeneration :: Monad m => Unfold m a b -> a -> m ()
65drainGeneration unf seed = UF.fold FL.drain unf seed
67{-# INLINE drainTransformation #-}
69 Monad m => Unfold m a b -> (Unfold m a b -> Unfold m c d) -> c -> m ()
70drainTransformation unf f seed = drainGeneration (f unf) seed
72{-# INLINE drainTransformationDefault #-}
73drainTransformationDefault ::
74 Monad m => Int -> (Unfold m Int Int -> Unfold m c d) -> c -> m ()
75drainTransformationDefault to =
76 drainTransformation (UF.enumerateFromToIntegral to)
78{-# INLINE drainProduct #-}
83 -> (Unfold m a b -> Unfold m c d -> Unfold m e f)
86drainProduct unf1 unf2 f seed = drainGeneration (f unf1 unf2) seed
88{-# INLINE drainProductDefault #-}
92 -> (Unfold m Int Int -> Unfold m Int Int -> Unfold m e f)
95drainProductDefault to = drainProduct src src
99 src = UF.enumerateFromToIntegral to
101-------------------------------------------------------------------------------
102-- Operations on input
103-------------------------------------------------------------------------------
106lmap :: Monad m => Int -> Int -> m ()
108 drainTransformationDefault (size + start) (UF.lmap (+ 1)) start
111lmapM :: Monad m => Int -> Int -> m ()
113 drainTransformationDefault (size + start) (UF.lmapM (return . (+) 1)) start
116supply :: Monad m => Int -> Int -> m ()
118 drainTransformationDefault (size + start) (UF.supply start) undefined
121{-# INLINE supplyFirst #-}
122supplyFirst :: Monad m => Int -> Int -> m ()
123supplyFirst size start =
125 (UF.take size UF.enumerateFromStepIntegral)
126 (UF.supplyFirst start)
129{-# INLINE supplySecond #-}
130supplySecond :: Monad m => Int -> Int -> m ()
131supplySecond size start =
133 (UF.take size UF.enumerateFromStepIntegral)
137{-# INLINE discardFirst #-}
138discardFirst :: Monad m => Int -> Int -> m ()
139discardFirst size start =
140 drainTransformationDefault (size + start) UF.discardFirst (start, start)
142{-# INLINE discardSecond #-}
143discardSecond :: Monad m => Int -> Int -> m ()
144discardSecond size start =
145 drainTransformationDefault (size + start) UF.discardSecond (start, start)
148swap :: Monad m => Int -> Int -> m ()
151 (UF.take size UF.enumerateFromStepIntegral)
155-------------------------------------------------------------------------------
157-------------------------------------------------------------------------------
159{-# INLINE fromStream #-}
160fromStream :: Int -> Int -> IO ()
161fromStream size start =
162 drainGeneration UF.fromStream (S.replicate size start :: S.SerialT IO Int)
164-- XXX INVESTIGATE: Although the performance of this should be equivalant to
165-- fromStream, this is considerably worse. More than 4x worse.
166{-# INLINE fromStreamK #-}
167fromStreamK :: Monad m => Int -> Int -> m ()
168fromStreamK size start = drainGeneration UF.fromStreamK (K.replicate size start)
170{-# INLINE fromStreamD #-}
171fromStreamD :: Monad m => Int -> Int -> m ()
172fromStreamD size start =
173 drainGeneration UF.fromStreamD (D.replicate size start)
176_nilM :: Monad m => Int -> Int -> m ()
177_nilM _ start = drainGeneration (UF.nilM return) start
180consM :: Monad m => Int -> Int -> m ()
182 drainTransformationDefault (size + start) (UF.consM return) start
184{-# INLINE _functionM #-}
185_functionM :: Monad m => Int -> Int -> m ()
186_functionM _ start = drainGeneration (UF.functionM return) start
188{-# INLINE _function #-}
189_function :: Monad m => Int -> Int -> m ()
190_function _ start = drainGeneration (UF.function id) start
192{-# INLINE _identity #-}
193_identity :: Monad m => Int -> Int -> m ()
194_identity _ start = drainGeneration UF.identity start
197_const :: Monad m => Int -> Int -> m ()
199 drainGeneration (UF.take size (UF.fromEffect (return start))) undefined
201{-# INLINE unfoldrM #-}
202unfoldrM :: Monad m => Int -> Int -> m ()
203unfoldrM size start = drainGeneration (UF.unfoldrM step) start
209 $ if i < start + size
213{-# INLINE fromList #-}
214fromList :: Monad m => Int -> Int -> m ()
215fromList size start = drainGeneration UF.fromList [start .. start + size]
217{-# INLINE fromListM #-}
218fromListM :: Monad m => Int -> Int -> m ()
219fromListM size start =
220 drainGeneration UF.fromListM (Prelude.map return [start .. start + size])
222{-# INLINE _fromSVar #-}
223_fromSVar :: Int -> Int -> m ()
226{-# INLINE _fromProducer #-}
227_fromProducer :: Int -> Int -> m ()
228_fromProducer = undefined
230{-# INLINE replicateM #-}
231replicateM :: Monad m => Int -> Int -> m ()
232replicateM size start = drainGeneration (UF.replicateM size) (return start)
234{-# INLINE repeatM #-}
235repeatM :: Monad m => Int -> Int -> m ()
236repeatM size start = drainGeneration (UF.take size UF.repeatM) (return start)
238{-# INLINE iterateM #-}
239iterateM :: Monad m => Int -> Int -> m ()
241 drainGeneration (UF.take size (UF.iterateM return)) (return start)
243{-# INLINE fromIndicesM #-}
244fromIndicesM :: Monad m => Int -> Int -> m ()
245fromIndicesM size start =
246 drainGeneration (UF.take size (UF.fromIndicesM return)) start
248{-# INLINE enumerateFromStepIntegral #-}
249enumerateFromStepIntegral :: Monad m => Int -> Int -> m ()
250enumerateFromStepIntegral size start =
251 drainGeneration (UF.take size UF.enumerateFromStepIntegral) (start, 1)
253{-# INLINE enumerateFromToIntegral #-}
254enumerateFromToIntegral :: Monad m => Int -> Int -> m ()
255enumerateFromToIntegral size start =
256 drainGeneration (UF.enumerateFromToIntegral (size + start)) start
258{-# INLINE enumerateFromIntegral #-}
259enumerateFromIntegral :: Monad m => Int -> Int -> m ()
260enumerateFromIntegral size start =
261 drainGeneration (UF.take size UF.enumerateFromIntegral) start
263{-# INLINE enumerateFromStepNum #-}
264enumerateFromStepNum :: Monad m => Int -> Int -> m ()
265enumerateFromStepNum size start =
266 drainGeneration (UF.take size (UF.enumerateFromStepNum 1)) start
268{-# INLINE numFrom #-}
269numFrom :: Monad m => Int -> Int -> m ()
270numFrom size start = drainGeneration (UF.take size UF.numFrom) start
272{-# INLINE enumerateFromToFractional #-}
273enumerateFromToFractional :: Monad m => Int -> Int -> m ()
274enumerateFromToFractional size start =
275 let intToDouble x = (fromInteger (fromIntegral x)) :: Double
277 (UF.enumerateFromToFractional (intToDouble $ start + size))
280-------------------------------------------------------------------------------
281-- Stream transformation
282-------------------------------------------------------------------------------
285map :: Monad m => Int -> Int -> m ()
286map size start = drainTransformationDefault (size + start) (UF.map (+1)) start
289mapM :: Monad m => Int -> Int -> m ()
291 drainTransformationDefault (size + start) (UF.mapM (return . (+) 1)) start
293{-# INLINE mapMWithInput #-}
294mapMWithInput :: Monad m => Int -> Int -> m ()
295mapMWithInput size start =
296 drainTransformationDefault
298 (UF.mapMWithInput (\a b -> return $ a + b))
301-------------------------------------------------------------------------------
303-------------------------------------------------------------------------------
305{-# INLINE takeWhileM #-}
306takeWhileM :: Monad m => Int -> Int -> m ()
307takeWhileM size start =
308 drainTransformationDefault
310 (UF.takeWhileM (\b -> return (b <= size + start)))
313{-# INLINE takeWhile #-}
314takeWhile :: Monad m => Int -> Int -> m ()
315takeWhile size start =
316 drainTransformationDefault
318 (UF.takeWhile (\b -> b <= size + start))
322take :: Monad m => Int -> Int -> m ()
323take size start = drainTransformationDefault (size + start) (UF.take size) start
326filter :: Monad m => Int -> Int -> m ()
328 drainTransformationDefault (size + start) (UF.filter (\_ -> True)) start
330{-# INLINE filterM #-}
331filterM :: Monad m => Int -> Int -> m ()
333 drainTransformationDefault
335 (UF.filterM (\_ -> (return True)))
338{-# INLINE _dropOne #-}
339_dropOne :: Monad m => Int -> Int -> m ()
341 drainTransformationDefault (size + start) (UF.drop 1) start
343{-# INLINE dropAll #-}
344dropAll :: Monad m => Int -> Int -> m ()
346 drainTransformationDefault (size + start) (UF.drop (size + 1)) start
348{-# INLINE dropWhileTrue #-}
349dropWhileTrue :: Monad m => Int -> Int -> m ()
350dropWhileTrue size start =
351 drainTransformationDefault
353 (UF.dropWhileM (\_ -> return True))
356{-# INLINE dropWhileFalse #-}
357dropWhileFalse :: Monad m => Int -> Int -> m ()
358dropWhileFalse size start =
359 drainTransformationDefault
361 (UF.dropWhileM (\_ -> return False))
364{-# INLINE dropWhileMTrue #-}
365dropWhileMTrue :: Monad m => Int -> Int -> m ()
366dropWhileMTrue size start =
367 drainTransformationDefault
369 (UF.dropWhileM (\_ -> return True))
372{-# INLINE dropWhileMFalse #-}
373dropWhileMFalse :: Monad m => Int -> Int -> m ()
374dropWhileMFalse size start =
375 drainTransformationDefault
377 (UF.dropWhileM (\_ -> return False))
380-------------------------------------------------------------------------------
382-------------------------------------------------------------------------------
384{-# INLINE zipWith #-}
385zipWith :: Monad m => Int -> Int -> m ()
387 drainProductDefault (size + start) (UF.zipWith (+)) start
389{-# INLINE zipWithM #-}
390zipWithM :: Monad m => Int -> Int -> m ()
394 (UF.zipWithM (\a b -> return $ a + b))
397{-# INLINE teeZipWith #-}
398teeZipWith :: Monad m => Int -> Int -> m ()
399teeZipWith size start =
400 drainProductDefault (size + start) (UF.zipWith (+)) start
402-------------------------------------------------------------------------------
404-------------------------------------------------------------------------------
406{-# INLINE toNullAp #-}
407toNullAp :: Monad m => Int -> Int -> m ()
408toNullAp value start =
409 let end = start + nthRoot 2 value
411 -- in UF.fold ((+) <$> s <*> s) FL.drain start
412 in UF.fold FL.drain ((+) `fmap` s `UF.apply` s) start
414{-# INLINE _apDiscardFst #-}
415_apDiscardFst :: Int -> Int -> m ()
416_apDiscardFst = undefined
418{-# INLINE _apDiscardSnd #-}
419_apDiscardSnd :: Int -> Int -> m ()
420_apDiscardSnd = undefined
422-------------------------------------------------------------------------------
424-------------------------------------------------------------------------------
426nthRoot :: Double -> Int -> Int
427nthRoot n value = round (fromIntegral value**(1/n))
429{-# INLINE concatMapM #-}
430concatMapM :: Monad m => Int -> Int -> m ()
431concatMapM value start =
432 val `seq` drainGeneration (UF.concatMapM unfoldInGen unfoldOut) start
436 val = nthRoot 2 value
437 unfoldInGen i = return (UF.enumerateFromToIntegral (i + val))
438 unfoldOut = UF.enumerateFromToIntegral (start + val)
441toNull :: Monad m => Int -> Int -> m ()
443 let end = start + nthRoot 2 value
451 u = src `UF.bind` \x ->
454 in UF.fold FL.drain u start
457{-# INLINE toNull3 #-}
458toNull3 :: Monad m => Int -> Int -> m ()
460 let end = start + nthRoot 3 value
469 u = src `UF.bind` \x ->
472 in UF.fold FL.drain u start
475toList :: Monad m => Int -> Int -> m [Int]
476toList value start = do
477 let end = start + nthRoot 2 value
485 u = src `UF.bind` \x ->
488 in UF.fold FL.toList u start
490{-# INLINE toListSome #-}
491toListSome :: Monad m => Int -> Int -> m [Int]
492toListSome value start = do
493 let end = start + nthRoot 2 value
501 u = src `UF.bind` \x ->
504 in UF.fold FL.toList (UF.take 1000 u) start
506{-# INLINE filterAllOut #-}
507filterAllOut :: Monad m => Int -> Int -> m ()
508filterAllOut value start = do
509 let end = start + nthRoot 2 value
516 u = src `UF.bind` \x ->
521 else UF.nilM (return . const ())
522 in UF.fold FL.drain u start
524{-# INLINE filterAllIn #-}
525filterAllIn :: Monad m => Int -> Int -> m ()
526filterAllIn value start = do
527 let end = start + nthRoot 2 value
534 u = src `UF.bind` \x ->
539 else UF.nilM (return . const ())
540 in UF.fold FL.drain u start
542{-# INLINE filterSome #-}
543filterSome :: Monad m => Int -> Int -> m ()
544filterSome value start = do
545 let end = start + nthRoot 2 value
552 u = src `UF.bind` \x ->
557 else UF.nilM (return . const ())
558 in UF.fold FL.drain u start
560{-# INLINE breakAfterSome #-}
561breakAfterSome :: Int -> Int -> IO ()
562breakAfterSome value start =
563 let end = start + nthRoot 2 value
570 u = src `UF.bind` \x ->
577 (_ :: Either ErrorCall ()) <- try $ UF.fold FL.drain u start
580-------------------------------------------------------------------------------
582-------------------------------------------------------------------------------
584-- n * (n + 1) / 2 == linearCount
585concatCount :: Int -> Int
586concatCount linearCount =
587 round (((1 + 8 * fromIntegral linearCount)**(1/2::Double) - 1) / 2)
590concat :: Monad m => Int -> Int -> m ()
591concat linearCount start = do
592 let end = start + concatCount linearCount
593 UF.fold FL.drain (UF.many (source end) (source end)) start
595-------------------------------------------------------------------------------
597-------------------------------------------------------------------------------
600moduleName = "Data.Unfold"
602o_1_space_transformation_input :: Int -> [Benchmark]
603o_1_space_transformation_input size =
605 "transformation/input"
606 [ benchIO "lmap" $ lmap size
607 , benchIO "lmapM" $ lmapM size
608 , benchIO "supply" $ supply size
609 , benchIO "supplyFirst" $ supplyFirst size
610 , benchIO "supplySecond" $ supplySecond size
611 , benchIO "discardFirst" $ discardFirst size
612 , benchIO "discardSecond" $ discardSecond size
613 , benchIO "swap" $ swap size
617o_1_space_generation :: Int -> [Benchmark]
618o_1_space_generation size =
621 [ benchIO "fromStream" $ fromStream size
622 , benchIO "fromStreamK" $ fromStreamK size
623 , benchIO "fromStreamD" $ fromStreamD size
624 -- Very small benchmarks, reporting in ns
625 -- , benchIO "nilM" $ nilM size
626 , benchIO "consM" $ consM size
627 -- , benchIO "functionM" $ functionM size
628 -- , benchIO "function" $ function size
629 -- , benchIO "identity" $ identity size
630 -- , benchIO "const" $ const size
631 , benchIO "unfoldrM" $ unfoldrM size
632 , benchIO "fromList" $ fromList size
633 , benchIO "fromListM" $ fromListM size
635 -- , benchIO "fromSVar" $ fromSVar size
636 -- , benchIO "fromProducer" $ fromProducer size
637 , benchIO "replicateM" $ replicateM size
638 , benchIO "repeatM" $ repeatM size
639 , benchIO "iterateM" $ iterateM size
640 , benchIO "fromIndicesM" $ fromIndicesM size
641 , benchIO "enumerateFromStepIntegral" $ enumerateFromStepIntegral size
642 , benchIO "enumerateFromToIntegral" $ enumerateFromToIntegral size
643 , benchIO "enumerateFromIntegral" $ enumerateFromIntegral size
644 , benchIO "enumerateFromStepNum" $ enumerateFromStepNum size
645 , benchIO "numFrom" $ numFrom size
646 , benchIO "enumerateFromToFractional" $ enumerateFromToFractional size
650o_1_space_transformation :: Int -> [Benchmark]
651o_1_space_transformation size =
654 [ benchIO "map" $ map size
655 , benchIO "mapM" $ mapM size
656 , benchIO "mapMWithInput" $ mapMWithInput size
660o_1_space_filtering :: Int -> [Benchmark]
661o_1_space_filtering size =
664 [ benchIO "takeWhileM" $ takeWhileM size
665 , benchIO "takeWhile" $ takeWhile size
666 , benchIO "take" $ take size
667 , benchIO "filter" $ filter size
668 , benchIO "filterM" $ filterM size
669 -- Very small benchmark, reporting in ns
670 -- , benchIO "dropOne" $ dropOne size
671 , benchIO "dropAll" $ dropAll size
672 , benchIO "dropWhileTrue" $ dropWhileTrue size
673 , benchIO "dropWhileFalse" $ dropWhileFalse size
674 , benchIO "dropWhileMTrue" $ dropWhileMTrue size
675 , benchIO "dropWhileMFalse" $ dropWhileMFalse size
679o_1_space_zip :: Int -> [Benchmark]
683 [ benchIO "zipWithM" $ zipWithM size
684 , benchIO "zipWith" $ zipWith size
685 , benchIO "teeZipWith" $ teeZipWith size
689o_1_space_nested :: Int -> [Benchmark]
690o_1_space_nested size =
693 [ benchIO "(<*>) (sqrt n x sqrt n)" $ toNullAp size
695 -- , benchIO "apDiscardFst" $ apDiscardFst size
696 -- , benchIO "apDiscardSnd" $ apDiscardSnd size
698 , benchIO "concatMapM (sqrt n x sqrt n)" $ concatMapM size
699 , benchIO "(>>=) (sqrt n x sqrt n)" $ toNull size
700 , benchIO "(>>=) (cubert n x cubert n x cubert n)" $ toNull3 size
701 , benchIO "breakAfterSome" $ breakAfterSome size
702 , benchIO "filterAllOut" $ filterAllOut size
703 , benchIO "filterAllIn" $ filterAllIn size
704 , benchIO "filterSome" $ filterSome size
706 , benchIO "concat" $ concat size
710o_n_space_nested :: Int -> [Benchmark]
711o_n_space_nested size =
714 [ benchIO "toList" $ toList size
715 , benchIO "toListSome" $ toListSome size
719-------------------------------------------------------------------------------
720-- Unfold Exception Benchmarks
721-------------------------------------------------------------------------------
722-- | Send the file contents to /dev/null with exception handling
723readWriteOnExceptionUnfold :: Handle -> Handle -> IO ()
724readWriteOnExceptionUnfold inh devNull =
725 let readEx = UF.onException (\_ -> hClose inh) FH.read
726 in SP.fold (FH.write devNull) $ SP.unfold readEx inh
729inspect $ hasNoTypeClasses 'readWriteOnExceptionUnfold
730-- inspect $ 'readWriteOnExceptionUnfold `hasNoType` ''Step
733-- | Send the file contents to /dev/null with exception handling
734readWriteHandleExceptionUnfold :: Handle -> Handle -> IO ()
735readWriteHandleExceptionUnfold inh devNull =
736 let handler (_e :: SomeException) = hClose inh >> return 10
737 readEx = UF.handle (UF.functionM handler) FH.read
738 in SP.fold (FH.write devNull) $ SP.unfold readEx inh
741inspect $ hasNoTypeClasses 'readWriteHandleExceptionUnfold
742-- inspect $ 'readWriteHandleExceptionUnfold `hasNoType` ''Step
745-- | Send the file contents to /dev/null with exception handling
746readWriteFinally_Unfold :: Handle -> Handle -> IO ()
747readWriteFinally_Unfold inh devNull =
748 let readEx = UF.finally_ (\_ -> hClose inh) FH.read
749 in SP.fold (FH.write devNull) $ SP.unfold readEx inh
752inspect $ hasNoTypeClasses 'readWriteFinally_Unfold
753-- inspect $ 'readWriteFinallyUnfold `hasNoType` ''Step
756readWriteFinallyUnfold :: Handle -> Handle -> IO ()
757readWriteFinallyUnfold inh devNull =
758 let readEx = UF.finally (\_ -> hClose inh) FH.read
759 in SP.fold (FH.write devNull) $ SP.unfold readEx inh
761-- | Send the file contents to /dev/null with exception handling
762readWriteBracket_Unfold :: Handle -> Handle -> IO ()
763readWriteBracket_Unfold inh devNull =
764 let readEx = UF.bracket_ return (\_ -> hClose inh) FH.read
765 in SP.fold (FH.write devNull) $ SP.unfold readEx inh
768inspect $ hasNoTypeClasses 'readWriteBracket_Unfold
769-- inspect $ 'readWriteBracketUnfold `hasNoType` ''Step
772readWriteBracketUnfold :: Handle -> Handle -> IO ()
773readWriteBracketUnfold inh devNull =
774 let readEx = UF.bracket return (\_ -> hClose inh) FH.read
775 in SP.fold (FH.write devNull) $ S.unfold readEx inh
777o_1_space_copy_read_exceptions :: BenchEnv -> [Benchmark]
778o_1_space_copy_read_exceptions env =
779 [ bgroup "exceptions"
780 [ mkBenchSmall "UF.onException" env $ \inh _ ->
781 readWriteOnExceptionUnfold inh (nullH env)
782 , mkBenchSmall "UF.handle" env $ \inh _ ->
783 readWriteHandleExceptionUnfold inh (nullH env)
784 , mkBenchSmall "UF.finally_" env $ \inh _ ->
785 readWriteFinally_Unfold inh (nullH env)
786 , mkBenchSmall "UF.finally" env $ \inh _ ->
787 readWriteFinallyUnfold inh (nullH env)
788 , mkBenchSmall "UF.bracket_" env $ \inh _ ->
789 readWriteBracket_Unfold inh (nullH env)
790 , mkBenchSmall "UF.bracket" env $ \inh _ ->
791 readWriteBracketUnfold inh (nullH env)
796-------------------------------------------------------------------------------
798-------------------------------------------------------------------------------
802 env <- mkHandleBenchEnv
803 runWithCLIOpts defaultStreamSize (allBenchmarks env)
807 allBenchmarks env size =
808 [ bgroup (o_1_space_prefix moduleName)
810 [ o_1_space_transformation_input size
811 , o_1_space_generation size
812 , o_1_space_transformation size
813 , o_1_space_filtering size
815 , o_1_space_nested size
816 , o_1_space_copy_read_exceptions env
818 , bgroup (o_n_space_prefix moduleName)
819 $ Prelude.concat [o_n_space_nested size]
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list