2-- Module : Streamly.Benchmark.Data.Stream.StreamK
3-- Copyright : (c) 2018 Composewell Technologies
6-- Maintainer : streamly@composewell.com
9{-# LANGUAGE FlexibleContexts #-}
10{-# LANGUAGE ScopedTypeVariables #-}
12#ifdef __HADDOCK_VERSION__
17{-# LANGUAGE TemplateHaskell #-}
18{-# OPTIONS_GHC -fplugin Test.Inspection.Plugin #-}
21module Main (main) where
23import Control.Monad (when)
24import Data.Maybe (isJust)
25import System.Random (randomRIO)
27 (tail, mapM_, foldl, last, map, mapM, concatMap, zip, init)
29import qualified Prelude as P
30import qualified Data.List as List
32import qualified Streamly.Internal.Data.Stream.StreamK as S
33import qualified Streamly.Internal.Data.SVar as S
35import Gauge (bench, nfIO, bgroup, Benchmark, defaultMain)
37import Streamly.Benchmark.Common
43value, value2, value3, value16, maxValue :: Int
45value2 = round (P.fromIntegral value**(1/2::P.Double)) -- double nested loop
46value3 = round (P.fromIntegral value**(1/3::P.Double)) -- triple nested loop
47value16 = round (P.fromIntegral value**(1/16::P.Double)) -- triple nested loop
50-------------------------------------------------------------------------------
52-------------------------------------------------------------------------------
56{-# INLINE nullTail #-}
57{-# INLINE headTail #-}
59toNull, uncons, nullTail, headTail, zip
61 => Stream m Int -> m ()
64toList :: Monad m => Stream m Int -> m [Int]
66foldl :: Monad m => Stream m Int -> m Int
68last :: Monad m => Stream m Int -> m (Maybe Int)
70-------------------------------------------------------------------------------
71-- Stream generation and elimination
72-------------------------------------------------------------------------------
74type Stream m a = S.Stream m a
76{-# INLINE sourceUnfoldr #-}
77sourceUnfoldr :: Int -> Stream m Int
78sourceUnfoldr n = S.unfoldr step n
83 else Just (cnt, cnt + 1)
85{-# INLINE sourceUnfoldrN #-}
86sourceUnfoldrN :: Int -> Int -> Stream m Int
87sourceUnfoldrN m n = S.unfoldr step n
92 else Just (cnt, cnt + 1)
94{-# INLINE sourceUnfoldrM #-}
95sourceUnfoldrM :: S.MonadAsync m => Int -> Stream m Int
96sourceUnfoldrM n = S.unfoldrM step n
101 else return (Just (cnt, cnt + 1))
103{-# INLINE sourceUnfoldrMN #-}
104sourceUnfoldrMN :: S.MonadAsync m => Int -> Int -> Stream m Int
105sourceUnfoldrMN m n = S.unfoldrM step n
110 else return (Just (cnt, cnt + 1))
112{-# INLINE sourceFromFoldable #-}
113sourceFromFoldable :: Int -> Stream m Int
114sourceFromFoldable n = S.fromFoldable [n..n+value]
116{-# INLINE sourceFromFoldableM #-}
117sourceFromFoldableM :: S.MonadAsync m => Int -> Stream m Int
118sourceFromFoldableM n =
119 Prelude.foldr S.consM S.nil (Prelude.fmap return [n..n+value])
121{-# INLINABLE concatMapFoldableWith #-}
122concatMapFoldableWith :: (S.IsStream t, Foldable f)
123 => (t m b -> t m b -> t m b) -> (a -> t m b) -> f a -> t m b
124concatMapFoldableWith f g = Prelude.foldr (f . g) S.nil
126{-# INLINE sourceFoldMapWith #-}
127sourceFoldMapWith :: Int -> Stream m Int
128sourceFoldMapWith n = concatMapFoldableWith S.serial S.yield [n..n+value]
130{-# INLINE sourceFoldMapWithM #-}
131sourceFoldMapWithM :: Monad m => Int -> Stream m Int
132sourceFoldMapWithM n =
133 concatMapFoldableWith S.serial (S.yieldM . return) [n..n+value]
135-------------------------------------------------------------------------------
137-------------------------------------------------------------------------------
139{-# INLINE runStream #-}
140runStream :: Monad m => Stream m a -> m ()
142-- runStream = S.mapM_ (\_ -> return ())
145mapM_ :: Monad m => Stream m a -> m ()
146mapM_ = S.mapM_ (\_ -> return ())
153 Just (_, t) -> uncons t
156init :: (Monad m, S.IsStream t) => t m a -> m ()
162tail :: (Monad m, S.IsStream t) => t m a -> m ()
163tail s = S.tail s >>= P.mapM_ tail
167 when (not r) $ S.tail s >>= P.mapM_ nullTail
171 when (isJust h) $ S.tail s >>= P.mapM_ headTail
174foldl = S.foldl' (+) 0
177-------------------------------------------------------------------------------
179-------------------------------------------------------------------------------
181{-# INLINE transform #-}
182transform :: Monad m => Stream m a -> m ()
185{-# INLINE composeN #-}
188 => Int -> (Stream m Int -> Stream m Int) -> Stream m Int -> m ()
192 2 -> transform . f . f
193 3 -> transform . f . f . f
194 4 -> transform . f . f . f . f
200{-# INLINE filterEven #-}
201{-# INLINE filterAllOut #-}
202{-# INLINE filterAllIn #-}
203{-# INLINE _takeOne #-}
204{-# INLINE takeAll #-}
205{-# INLINE takeWhileTrue #-}
206{-# INLINE dropOne #-}
207{-# INLINE dropAll #-}
208{-# INLINE dropWhileTrue #-}
209{-# INLINE dropWhileFalse #-}
212scan, map, fmapK, filterEven, filterAllOut,
213 filterAllIn, _takeOne, takeAll, takeWhileTrue, dropAll, dropOne,
214 dropWhileTrue, dropWhileFalse, foldrS, foldlS
216 => Int -> Stream m Int -> m ()
219{-# INLINE mapMSerial #-}
220{-# INLINE intersperse #-}
221mapM, mapMSerial, intersperse
222 :: S.MonadAsync m => Int -> Stream m Int -> m ()
224scan n = composeN n $ S.scanl' (+) 0
225map n = composeN n $ P.fmap (+1)
226fmapK n = composeN n $ P.fmap (+1)
227mapM n = composeN n $ S.mapM return
228mapMSerial n = composeN n $ S.mapMSerial return
229filterEven n = composeN n $ S.filter even
230filterAllOut n = composeN n $ S.filter (> maxValue)
231filterAllIn n = composeN n $ S.filter (<= maxValue)
232_takeOne n = composeN n $ S.take 1
233takeAll n = composeN n $ S.take maxValue
234takeWhileTrue n = composeN n $ S.takeWhile (<= maxValue)
235dropOne n = composeN n $ S.drop 1
236dropAll n = composeN n $ S.drop maxValue
237dropWhileTrue n = composeN n $ S.dropWhile (<= maxValue)
238dropWhileFalse n = composeN n $ S.dropWhile (<= 1)
239foldrS n = composeN n $ S.foldrS S.cons S.nil
240foldlS n = composeN n $ S.foldlS (flip S.cons) S.nil
241intersperse n = composeN n $ S.intersperse maxValue
243-------------------------------------------------------------------------------
245-------------------------------------------------------------------------------
247iterStreamLen, maxIters :: Int
251{-# INLINE iterateSource #-}
254 => (Stream m Int -> Stream m Int) -> Int -> Int -> Stream m Int
255iterateSource g i n = f i (sourceUnfoldrMN iterStreamLen n)
258 f x m = g (f (x P.- 1) m)
260{-# INLINE iterateMapM #-}
261{-# INLINE iterateScan #-}
262{-# INLINE iterateFilterEven #-}
263{-# INLINE iterateTakeAll #-}
264{-# INLINE iterateDropOne #-}
265{-# INLINE iterateDropWhileFalse #-}
266{-# INLINE iterateDropWhileTrue #-}
267iterateMapM, iterateScan, iterateFilterEven, iterateTakeAll, iterateDropOne,
268 iterateDropWhileFalse, iterateDropWhileTrue
270 => Int -> Stream m Int
273iterateScan = iterateSource (S.scanl' (+) 0) (maxIters `div` 10)
274iterateDropWhileFalse = iterateSource (S.dropWhile (> maxValue))
277iterateMapM = iterateSource (S.mapM return) maxIters
278iterateFilterEven = iterateSource (S.filter even) maxIters
279iterateTakeAll = iterateSource (S.take maxValue) maxIters
280iterateDropOne = iterateSource (S.drop 1) maxIters
281iterateDropWhileTrue = iterateSource (S.dropWhile (<= maxValue)) maxIters
283-------------------------------------------------------------------------------
285-------------------------------------------------------------------------------
287zip src = transform $ S.zipWith (,) src src
289-------------------------------------------------------------------------------
291-------------------------------------------------------------------------------
293{-# INLINE scanMap #-}
294{-# INLINE dropMap #-}
295{-# INLINE dropScan #-}
296{-# INLINE takeDrop #-}
297{-# INLINE takeScan #-}
298{-# INLINE takeMap #-}
299{-# INLINE filterDrop #-}
300{-# INLINE filterTake #-}
301{-# INLINE filterScan #-}
302{-# INLINE filterMap #-}
303scanMap, dropMap, dropScan, takeDrop, takeScan, takeMap, filterDrop,
304 filterTake, filterScan, filterMap
305 :: Monad m => Int -> Stream m Int -> m ()
307scanMap n = composeN n $ S.map (subtract 1) . S.scanl' (+) 0
308dropMap n = composeN n $ S.map (subtract 1) . S.drop 1
309dropScan n = composeN n $ S.scanl' (+) 0 . S.drop 1
310takeDrop n = composeN n $ S.drop 1 . S.take maxValue
311takeScan n = composeN n $ S.scanl' (+) 0 . S.take maxValue
312takeMap n = composeN n $ S.map (subtract 1) . S.take maxValue
313filterDrop n = composeN n $ S.drop 1 . S.filter (<= maxValue)
314filterTake n = composeN n $ S.take maxValue . S.filter (<= maxValue)
315filterScan n = composeN n $ S.scanl' (+) 0 . S.filter (<= maxBound)
316filterMap n = composeN n $ S.map (subtract 1) . S.filter (<= maxValue)
318-------------------------------------------------------------------------------
320-------------------------------------------------------------------------------
322-- concatMap unfoldrM/unfoldrM
324{-# INLINE concatMap #-}
325concatMap :: Int -> Int -> Int -> IO ()
326concatMap outer inner n =
327 S.drain $ S.concatMap
328 (\_ -> sourceUnfoldrMN inner n)
329 (sourceUnfoldrMN outer n)
332inspect $ hasNoTypeClasses 'concatMap
335-- concatMap unfoldr/unfoldr
337{-# INLINE concatMapPure #-}
338concatMapPure :: Int -> Int -> Int -> IO ()
339concatMapPure outer inner n =
340 S.drain $ S.concatMap
341 (\_ -> sourceUnfoldrN inner n)
342 (sourceUnfoldrN outer n)
345inspect $ hasNoTypeClasses 'concatMapPure
348-- concatMap replicate/unfoldrM
350{-# INLINE concatMapRepl #-}
351concatMapRepl :: Int -> Int -> Int -> IO ()
352concatMapRepl outer inner n =
353 S.drain $ S.concatMap (S.replicate inner) (sourceUnfoldrMN outer n)
356inspect $ hasNoTypeClasses 'concatMapRepl
361{-# INLINE sourceConcatMapId #-}
362sourceConcatMapId :: Monad m
363 => Int -> Int -> Stream m (Stream m Int)
364sourceConcatMapId val n =
365 S.fromFoldable $ fmap (S.yieldM . return) [n..n+val]
367{-# INLINE concatStreamsWith #-}
368concatStreamsWith :: Int -> Int -> Int -> IO ()
369concatStreamsWith outer inner n =
370 S.drain $ S.concatMapBy S.serial
371 (sourceUnfoldrMN inner)
372 (sourceUnfoldrMN outer n)
374-------------------------------------------------------------------------------
376-------------------------------------------------------------------------------
378{-# INLINE toNullApNested #-}
379toNullApNested :: Monad m => Stream m Int -> m ()
380toNullApNested s = runStream $ do
383{-# INLINE toNullNested #-}
384toNullNested :: Monad m => Stream m Int -> m ()
385toNullNested s = runStream $ do
390{-# INLINE toNullNested3 #-}
391toNullNested3 :: Monad m => Stream m Int -> m ()
392toNullNested3 s = runStream $ do
398{-# INLINE filterAllOutNested #-}
401 => Stream m Int -> m ()
402filterAllOutNested str = runStream $ do
410{-# INLINE filterAllInNested #-}
413 => Stream m Int -> m ()
414filterAllInNested str = runStream $ do
422-------------------------------------------------------------------------------
423-- Nested Composition Pure lists
424-------------------------------------------------------------------------------
426{-# INLINE sourceUnfoldrList #-}
427sourceUnfoldrList :: Int -> Int -> [Int]
428sourceUnfoldrList maxval n = List.unfoldr step n
433 else Just (cnt, cnt + 1)
435{-# INLINE toNullApNestedList #-}
436toNullApNestedList :: [Int] -> [Int]
437toNullApNestedList s = (+) <$> s <*> s
439{-# INLINE toNullNestedList #-}
440toNullNestedList :: [Int] -> [Int]
441toNullNestedList s = do
446{-# INLINE toNullNestedList3 #-}
447toNullNestedList3 :: [Int] -> [Int]
448toNullNestedList3 s = do
454{-# INLINE filterAllOutNestedList #-}
455filterAllOutNestedList :: [Int] -> [Int]
456filterAllOutNestedList str = do
464{-# INLINE filterAllInNestedList #-}
465filterAllInNestedList :: [Int] -> [Int]
466filterAllInNestedList str = do
474-------------------------------------------------------------------------------
476-------------------------------------------------------------------------------
479moduleName = "Data.Stream.StreamK"
481o_1_space :: [Benchmark]
483 [ bgroup (o_1_space_prefix moduleName)
484 [ bgroup "generation"
485 [ benchFold "unfoldr" toNull sourceUnfoldr
486 , benchFold "unfoldrM" toNull sourceUnfoldrM
488 , benchFold "fromFoldable" toNull sourceFromFoldable
489 , benchFold "fromFoldableM" toNull sourceFromFoldableM
492 , benchFold "concatMapFoldableWith" toNull sourceFoldMapWith
493 , benchFold "concatMapFoldableWithM" toNull sourceFoldMapWithM
495 , bgroup "elimination"
496 [ benchFold "toNull" toNull sourceUnfoldrM
497 , benchFold "mapM_" mapM_ sourceUnfoldrM
498 , benchFold "uncons" uncons sourceUnfoldrM
499 , benchFold "init" init sourceUnfoldrM
500 , benchFold "foldl'" foldl sourceUnfoldrM
501 , benchFold "last" last sourceUnfoldrM
504 [ benchFold "toNullAp" toNullApNested (sourceUnfoldrMN value2)
505 , benchFold "toNull" toNullNested (sourceUnfoldrMN value2)
506 , benchFold "toNull3" toNullNested3 (sourceUnfoldrMN value3)
507 , benchFold "filterAllIn" filterAllInNested (sourceUnfoldrMN value2)
508 , benchFold "filterAllOut" filterAllOutNested (sourceUnfoldrMN value2)
509 , benchFold "toNullApPure" toNullApNested (sourceUnfoldrN value2)
510 , benchFold "toNullPure" toNullNested (sourceUnfoldrN value2)
511 , benchFold "toNull3Pure" toNullNested3 (sourceUnfoldrN value3)
512 , benchFold "filterAllInPure" filterAllInNested (sourceUnfoldrN value2)
513 , benchFold "filterAllOutPure" filterAllOutNested (sourceUnfoldrN value2)
515 , bgroup "transformation"
516 [ benchFold "foldrS" (foldrS 1) sourceUnfoldrM
517 , benchFold "scan" (scan 1) sourceUnfoldrM
518 , benchFold "map" (map 1) sourceUnfoldrM
519 , benchFold "fmap" (fmapK 1) sourceUnfoldrM
520 , benchFold "mapM" (mapM 1) sourceUnfoldrM
521 , benchFold "mapMSerial" (mapMSerial 1) sourceUnfoldrM
523 , bgroup "transformationX4"
524 [ benchFold "scan" (scan 4) sourceUnfoldrM
525 , benchFold "map" (map 4) sourceUnfoldrM
526 , benchFold "fmap" (fmapK 4) sourceUnfoldrM
527 , benchFold "mapM" (mapM 4) sourceUnfoldrM
528 , benchFold "mapMSerial" (mapMSerial 4) sourceUnfoldrM
529 -- XXX this is horribly slow
530 -- , benchFold "concatMap" (concatMap 4) (sourceUnfoldrMN value16)
533 [ benchIOSrc1 "concatMapPure (n of 1)"
534 (concatMapPure value 1)
535 , benchIOSrc1 "concatMapPure (sqrt n of sqrt n)"
536 (concatMapPure value2 value2)
537 , benchIOSrc1 "concatMapPure (1 of n)"
538 (concatMapPure 1 value)
540 , benchIOSrc1 "concatMap (n of 1)"
542 , benchIOSrc1 "concatMap (sqrt n of sqrt n)"
543 (concatMap value2 value2)
544 , benchIOSrc1 "concatMap (1 of n)"
547 , benchIOSrc1 "concatMapRepl (sqrt n of sqrt n)"
548 (concatMapRepl value2 value2)
550 -- This is for comparison with concatMapFoldableWith
551 , benchIOSrc1 "concatMapWithId (n of 1) (fromFoldable)"
552 (S.drain . S.concatMapBy S.serial id . sourceConcatMapId value)
554 , benchIOSrc1 "concatMapWith (n of 1)"
555 (concatStreamsWith value 1)
556 , benchIOSrc1 "concatMapWith (sqrt n of sqrt n)"
557 (concatStreamsWith value2 value2)
558 , benchIOSrc1 "concatMapWith (1 of n)"
559 (concatStreamsWith 1 value)
562 [ benchFold "filter-even" (filterEven 1) sourceUnfoldrM
563 , benchFold "filter-all-out" (filterAllOut 1) sourceUnfoldrM
564 , benchFold "filter-all-in" (filterAllIn 1) sourceUnfoldrM
565 , benchFold "take-all" (takeAll 1) sourceUnfoldrM
566 , benchFold "takeWhile-true" (takeWhileTrue 1) sourceUnfoldrM
567 , benchFold "drop-one" (dropOne 1) sourceUnfoldrM
568 , benchFold "drop-all" (dropAll 1) sourceUnfoldrM
569 , benchFold "dropWhile-true" (dropWhileTrue 1) sourceUnfoldrM
570 , benchFold "dropWhile-false" (dropWhileFalse 1) sourceUnfoldrM
572 , bgroup "filteringX4"
573 [ benchFold "filter-even" (filterEven 4) sourceUnfoldrM
574 , benchFold "filter-all-out" (filterAllOut 4) sourceUnfoldrM
575 , benchFold "filter-all-in" (filterAllIn 4) sourceUnfoldrM
576 , benchFold "take-all" (takeAll 4) sourceUnfoldrM
577 , benchFold "takeWhile-true" (takeWhileTrue 4) sourceUnfoldrM
578 , benchFold "drop-one" (dropOne 4) sourceUnfoldrM
579 , benchFold "drop-all" (dropAll 4) sourceUnfoldrM
580 , benchFold "dropWhile-true" (dropWhileTrue 4) sourceUnfoldrM
581 , benchFold "dropWhile-false" (dropWhileFalse 4) sourceUnfoldrM
584 [ benchFold "zip" zip sourceUnfoldrM
587 [ benchFold "scan-map" (scanMap 1) sourceUnfoldrM
588 , benchFold "drop-map" (dropMap 1) sourceUnfoldrM
589 , benchFold "drop-scan" (dropScan 1) sourceUnfoldrM
590 , benchFold "take-drop" (takeDrop 1) sourceUnfoldrM
591 , benchFold "take-scan" (takeScan 1) sourceUnfoldrM
592 , benchFold "take-map" (takeMap 1) sourceUnfoldrM
593 , benchFold "filter-drop" (filterDrop 1) sourceUnfoldrM
594 , benchFold "filter-take" (filterTake 1) sourceUnfoldrM
595 , benchFold "filter-scan" (filterScan 1) sourceUnfoldrM
596 , benchFold "filter-map" (filterMap 1) sourceUnfoldrM
599 [ benchFold "scan-map" (scanMap 2) sourceUnfoldrM
600 , benchFold "drop-map" (dropMap 2) sourceUnfoldrM
601 , benchFold "drop-scan" (dropScan 2) sourceUnfoldrM
602 , benchFold "take-drop" (takeDrop 2) sourceUnfoldrM
603 , benchFold "take-scan" (takeScan 2) sourceUnfoldrM
604 , benchFold "take-map" (takeMap 2) sourceUnfoldrM
605 , benchFold "filter-drop" (filterDrop 2) sourceUnfoldrM
606 , benchFold "filter-take" (filterTake 2) sourceUnfoldrM
607 , benchFold "filter-scan" (filterScan 2) sourceUnfoldrM
608 , benchFold "filter-map" (filterMap 2) sourceUnfoldrM
611 [ benchFold "scan-map" (scanMap 4) sourceUnfoldrM
612 , benchFold "drop-map" (dropMap 4) sourceUnfoldrM
613 , benchFold "drop-scan" (dropScan 4) sourceUnfoldrM
614 , benchFold "take-drop" (takeDrop 4) sourceUnfoldrM
615 , benchFold "take-scan" (takeScan 4) sourceUnfoldrM
616 , benchFold "take-map" (takeMap 4) sourceUnfoldrM
617 , benchFold "filter-drop" (filterDrop 4) sourceUnfoldrM
618 , benchFold "filter-take" (filterTake 4) sourceUnfoldrM
619 , benchFold "filter-scan" (filterScan 4) sourceUnfoldrM
620 , benchFold "filter-map" (filterMap 4) sourceUnfoldrM
625o_n_heap :: [Benchmark]
627 [ bgroup (o_n_heap_prefix moduleName)
628 [ bgroup "transformation"
629 [ benchFold "foldlS" (foldlS 1) sourceUnfoldrM
635benchK :: P.String -> (Int -> Stream P.IO Int) -> Benchmark
636benchK name f = bench name $ nfIO $ randomRIO (1,1) >>= toNull . f
638o_n_stack :: [Benchmark]
640 [ bgroup (o_n_stack_prefix moduleName)
641 [ bgroup "elimination"
642 [ benchFold "tail" tail sourceUnfoldrM
643 , benchFold "nullTail" nullTail sourceUnfoldrM
644 , benchFold "headTail" headTail sourceUnfoldrM
646 , bgroup "transformation"
648 -- XXX why do these need so much stack
649 benchFold "intersperse" (intersperse 1) (sourceUnfoldrMN value2)
650 , benchFold "interspersePure" (intersperse 1) (sourceUnfoldrN value2)
652 , bgroup "transformationX4"
654 benchFold "intersperse" (intersperse 4) (sourceUnfoldrMN value16)
657 [ benchK "mapM" iterateMapM
658 , benchK "scan(1/10)" iterateScan
659 , benchK "filterEven" iterateFilterEven
660 , benchK "takeAll" iterateTakeAll
661 , benchK "dropOne" iterateDropOne
662 , benchK "dropWhileFalse(1/10)" iterateDropWhileFalse
663 , benchK "dropWhileTrue" iterateDropWhileTrue
668o_n_space :: [Benchmark]
670 [ bgroup (o_n_space_prefix moduleName)
671 [ bgroup "elimination"
672 [ benchFold "toList" toList sourceUnfoldrM
677{-# INLINE benchList #-}
678benchList :: P.String -> ([Int] -> [Int]) -> (Int -> [Int]) -> Benchmark
679benchList name run f = bench name $ nfIO $ randomRIO (1,1) >>= return . run . f
681o_1_space_list :: [Benchmark]
684 [ bgroup "elimination"
685 [ benchList "last" (\xs -> [List.last xs]) (sourceUnfoldrList value)
688 [ benchList "toNullAp" toNullApNestedList (sourceUnfoldrList value2)
689 , benchList "toNull" toNullNestedList (sourceUnfoldrList value2)
690 , benchList "toNull3" toNullNestedList3 (sourceUnfoldrList value3)
691 , benchList "filterAllIn" filterAllInNestedList (sourceUnfoldrList value2)
692 , benchList "filterAllOut" filterAllOutNestedList (sourceUnfoldrList value2)
698main = defaultMain $ concat
700 , [bgroup (o_1_space_prefix moduleName) o_1_space_list]
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