2-- Module : Stream.Transform
3-- Copyright : (c) 2018 Composewell Technologies
4-- License : BSD-3-Clause
5-- Maintainer : streamly@composewell.com
8{-# LANGUAGE FlexibleContexts #-}
9{-# LANGUAGE FlexibleInstances #-}
10{-# LANGUAGE ScopedTypeVariables #-}
11{-# LANGUAGE RankNTypes #-}
13{-# OPTIONS_GHC -Wno-orphans #-}
15{-# OPTIONS_GHC -Wno-deprecations #-}
18#ifdef __HADDOCK_VERSION__
23{-# LANGUAGE TemplateHaskell #-}
24{-# OPTIONS_GHC -fplugin Test.Inspection.Plugin #-}
27module Stream.Transform (benchmarks) where
29import Control.Monad.IO.Class (MonadIO(..))
31import System.Random (randomRIO)
33import qualified Streamly.Internal.Data.Fold as FL
35import qualified Stream.Common as Common
36import qualified Streamly.Internal.Data.Unfold as Unfold
39import Control.DeepSeq (NFData(..))
40import Data.Functor.Identity (Identity(..))
41import qualified Prelude
42import qualified Streamly.Internal.Data.Fold as Fold
43import qualified Streamly.Internal.Data.Stream.IsStream as Stream
44import Streamly.Internal.Data.Time.Units
46import Streamly.Internal.Data.Stream (Stream)
47import qualified Streamly.Internal.Data.Stream as Stream
48#ifndef USE_STREAMLY_CORE
49import qualified Streamly.Internal.Data.Stream.Time as Stream
52import Control.DeepSeq (NFData(..))
53import Data.Functor.Identity (Identity(..))
54import qualified Prelude
55import qualified Streamly.Internal.Data.Fold as Fold
56import Streamly.Internal.Data.StreamK (StreamK)
57import qualified Streamly.Internal.Data.StreamK as StreamK
62import Stream.Common hiding (scanl')
63import Streamly.Benchmark.Common
64import Prelude hiding (sequence, mapM)
67type Stream = Stream.SerialT
70-------------------------------------------------------------------------------
71-- Pipelines (stream-to-stream transformations)
72-------------------------------------------------------------------------------
74-------------------------------------------------------------------------------
75-- one-to-one transformations
76-------------------------------------------------------------------------------
78-------------------------------------------------------------------------------
79-- Traversable Instance
80-------------------------------------------------------------------------------
83{-# INLINE traversableTraverse #-}
84traversableTraverse :: StreamK Identity Int -> IO (StreamK Identity Int)
85traversableTraverse = traverse return
87{-# INLINE traversableSequenceA #-}
88traversableSequenceA :: StreamK Identity Int -> IO (StreamK Identity Int)
89traversableSequenceA = sequenceA . Prelude.fmap return
91{-# INLINE traversableMapM #-}
92traversableMapM :: StreamK Identity Int -> IO (StreamK Identity Int)
93traversableMapM = Prelude.mapM return
95{-# INLINE traversableSequence #-}
96traversableSequence :: StreamK Identity Int -> IO (StreamK Identity Int)
97traversableSequence = Prelude.sequence . Prelude.fmap return
99{-# INLINE benchPureSinkIO #-}
102 => Int -> String -> (StreamK Identity Int -> IO b) -> Benchmark
103benchPureSinkIO value name f =
105 $ nfIO $ randomRIO (1, 1) >>= f . fromStream . sourceUnfoldr value
107instance NFData a => NFData (StreamK Identity a) where
111 $ Stream.fold (Fold.foldl' (\_ x -> rnf x) ()) (toStream xs)
113o_n_space_traversable :: Int -> [Benchmark]
114o_n_space_traversable value =
115 -- Buffering operations using heap proportional to number of elements.
116 [ bgroup "traversable"
117 -- Traversable instance
118 [ benchPureSinkIO value "traverse" traversableTraverse
119 , benchPureSinkIO value "sequenceA" traversableSequenceA
120 , benchPureSinkIO value "mapM" traversableMapM
121 , benchPureSinkIO value "sequence" traversableSequence
126-------------------------------------------------------------------------------
128-------------------------------------------------------------------------------
132scanl' :: MonadIO m => Int -> Stream m Int -> m ()
133scanl' n = composeN n $ Stream.scanl' (+) 0
135{-# INLINE scanlM' #-}
136scanlM' :: MonadIO m => Int -> Stream m Int -> m ()
137scanlM' n = composeN n $ Stream.scanlM' (\b a -> return $ b + a) (return 0)
139{-# INLINE scanl1' #-}
140scanl1' :: MonadIO m => Int -> Stream m Int -> m ()
141scanl1' n = composeN n $ Stream.scanl1' (+)
143{-# INLINE scanl1M' #-}
144scanl1M' :: MonadIO m => Int -> Stream m Int -> m ()
145scanl1M' n = composeN n $ Stream.scanl1M' (\b a -> return $ b + a)
149scan :: MonadIO m => Int -> Stream m Int -> m ()
150scan n = composeN n $ Stream.scan FL.sum
153{-# INLINE postscanl' #-}
154postscanl' :: MonadIO m => Int -> Stream m Int -> m ()
155postscanl' n = composeN n $ Stream.postscanl' (+) 0
157{-# INLINE postscanlM' #-}
158postscanlM' :: MonadIO m => Int -> Stream m Int -> m ()
159postscanlM' n = composeN n $ Stream.postscanlM' (\b a -> return $ b + a) (return 0)
162{-# INLINE postscan #-}
163postscan :: MonadIO m => Int -> Stream m Int -> m ()
164postscan n = composeN n $ Stream.postscan FL.sum
166{-# INLINE sequence #-}
167sequence :: MonadAsync m => Stream m (m Int) -> m ()
168sequence = Common.drain . Stream.sequence
171tap :: MonadIO m => Int -> Stream m Int -> m ()
172tap n = composeN n $ Stream.tap FL.sum
175{-# INLINE pollCounts #-}
176pollCounts :: Int -> Stream IO Int -> IO ()
178 composeN n (Stream.pollCounts (const True) f)
182 f = Stream.drain . Stream.rollingMap2 (-) . Stream.delayPost 1
184{-# INLINE timestamped #-}
185timestamped :: (MonadAsync m) => Stream m Int -> m ()
186timestamped = Stream.drain . Stream.timestamped
191foldrS :: MonadIO m => Int -> Stream m Int -> m ()
193 composeN n (toStream . StreamK.foldrS StreamK.cons StreamK.nil . fromStream)
195{-# INLINE foldrSMap #-}
196foldrSMap :: MonadIO m => Int -> Stream m Int -> m ()
200 . StreamK.foldrS (\x xs -> x + 1 `StreamK.cons` xs) StreamK.nil
207foldrT :: MonadIO m => Int -> Stream m Int -> m ()
208foldrT n = composeN n (unCrossStream . Stream.foldrT cns (CrossStream Stream.nil))
210 where cns x (CrossStream xs) = CrossStream (Stream.cons x xs)
212{-# INLINE foldrTMap #-}
213foldrTMap :: MonadIO m => Int -> Stream m Int -> m ()
214foldrTMap n = composeN n $ Stream.foldrT (\x xs -> x + 1 `Stream.cons` xs) Stream.nil
218trace :: MonadAsync m => Int -> Stream m Int -> m ()
219trace n = composeN n $ Stream.trace return
221o_1_space_mapping :: Int -> [Benchmark]
222o_1_space_mapping value =
228 benchIOSink value "foldrS" (foldrS 1)
229 , benchIOSink value "foldrSMap" (foldrSMap 1)
232 -- , benchIOSink value "foldrT" (foldrT 1)
233 -- , benchIOSink value "foldrTMap" (foldrTMap 1)
236 benchIOSink value "map" (mapN 1)
237 , bench "sequence" $ nfIO $ randomRIO (1, 1000) >>= \n ->
238 sequence (sourceUnfoldrAction value n)
239 , benchIOSink value "mapM" (mapM 1)
240 , benchIOSink value "tap" (tap 1)
242 , benchIOSink value "pollCounts 1 second" (pollCounts 1)
243 , benchIOSink value "timestamped" timestamped
246 , benchIOSink value "scanl'" (scanl' 1)
247 , benchIOSink value "scanl1'" (scanl1' 1)
248 , benchIOSink value "scanlM'" (scanlM' 1)
249 , benchIOSink value "scanl1M'" (scanl1M' 1)
250 , benchIOSink value "postscanl'" (postscanl' 1)
251 , benchIOSink value "postscanlM'" (postscanlM' 1)
253 , benchIOSink value "scan" (scan 1)
254 , benchIOSink value "postscan" (postscan 1)
258o_1_space_mappingX4 :: Int -> [Benchmark]
259o_1_space_mappingX4 value =
261 [ benchIOSink value "map" (mapN 4)
262 , benchIOSink value "mapM" (mapM 4)
263 , benchIOSink value "trace" (trace 4)
266 , benchIOSink value "scanl'" (scanl' 4)
267 , benchIOSink value "scanl1'" (scanl1' 4)
268 , benchIOSink value "scanlM'" (scanlM' 4)
269 , benchIOSink value "scanl1M'" (scanl1M' 4)
270 , benchIOSink value "postscanl'" (postscanl' 4)
271 , benchIOSink value "postscanlM'" (postscanlM' 4)
276{-# INLINE sieveScan #-}
277sieveScan :: Monad m => Stream m Int -> Stream m Int
280 . Stream.scan (FL.foldlM' (\(primes, _) n -> do
282 let ps = takeWhile (\p -> p * p <= n) primes
283 in if all (\p -> n `mod` p /= 0) ps
284 then (primes ++ [n], Just n)
285 else (primes, Nothing)) (return ([2], Just 2)))
287o_n_space_mapping :: Int -> [Benchmark]
288o_n_space_mapping value =
290 [ benchIO "naive prime sieve"
291 (\n -> Stream.fold FL.sum $ sieveScan $ Stream.enumerateFromTo 2 (value + n))
295-------------------------------------------------------------------------------
297-------------------------------------------------------------------------------
299o_1_space_functor :: Int -> [Benchmark]
300o_1_space_functor value =
302 [ benchIOSink value "fmap" (mapN 1)
303 , benchIOSink value "fmap x 4" (mapN 4)
307-------------------------------------------------------------------------------
308-- Iteration/looping utilities
309-------------------------------------------------------------------------------
311{-# INLINE iterateN #-}
312iterateN :: (Int -> a -> a) -> a -> Int -> a
313iterateN g initial count = f count initial
318 f i x = f (i - 1) (g i x)
321-- Iterate a transformation over a singleton stream
322{-# INLINE iterateSingleton #-}
323iterateSingleton :: Applicative m =>
324 (Int -> StreamK m Int -> StreamK m Int)
328iterateSingleton g count n = toStream $ iterateN g (StreamK.fromPure n) count
330-- Iterate a transformation over a singleton stream
331{-# INLINE iterateSingleton #-}
332iterateSingleton :: Applicative m =>
333 (Int -> Stream m Int -> Stream m Int)
337iterateSingleton g count n = iterateN g (Stream.fromPure n) count
341-- XXX need to check why this is slower than the explicit recursion above, even
342-- if the above code is written in a foldr like head recursive way. We also
343-- need to try this with foldlM' once #150 is fixed.
344-- However, it is perhaps best to keep the iteration benchmarks independent of
345-- foldrM and any related fusion issues.
346{-# INLINE _iterateSingleton #-}
349 => (Int -> Stream m Int -> Stream m Int)
353_iterateSingleton g value n = S.foldrM g (return n) $ sourceIntFromTo value n
356o_n_space_iterated :: Int -> [Benchmark]
357o_n_space_iterated value =
359 [ benchIO "(+) (n times) (baseline)" $ \i0 ->
360 iterateN (\i acc -> acc >>= \n -> return $ i + n) (return i0) value
361 , benchIOSrc "(<$) (n times)" $
362 iterateSingleton (<$) value
363 , benchIOSrc "fmap (n times)" $
364 iterateSingleton (fmap . (+)) value
366 , benchIOSrc fromSerial "_(<$) (n times)" $
367 _iterateSingleton (<$) value
368 , benchIOSrc fromSerial "_fmap (n times)" $
369 _iterateSingleton (fmap . (+)) value
374-------------------------------------------------------------------------------
375-- Size reducing transformations (filtering)
376-------------------------------------------------------------------------------
378{-# INLINE filterEven #-}
379filterEven :: MonadIO m => Int -> Stream m Int -> m ()
380filterEven n = composeN n $ Stream.filter even
382{-# INLINE filterAllOut #-}
383filterAllOut :: MonadIO m => Int -> Int -> Stream m Int -> m ()
384filterAllOut value n = composeN n $ Stream.filter (> (value + 1))
386{-# INLINE filterAllIn #-}
387filterAllIn :: MonadIO m => Int -> Int -> Stream m Int -> m ()
388filterAllIn value n = composeN n $ Stream.filter (<= (value + 1))
390{-# INLINE filterMEven #-}
391filterMEven :: MonadIO m => Int -> Stream m Int -> m ()
392filterMEven n = composeN n $ Stream.filterM (return . even)
394{-# INLINE filterMAllOut #-}
395filterMAllOut :: MonadIO m => Int -> Int -> Stream m Int -> m ()
396filterMAllOut value n = composeN n $ Stream.filterM (\x -> return $ x > (value + 1))
398{-# INLINE filterMAllIn #-}
399filterMAllIn :: MonadIO m => Int -> Int -> Stream m Int -> m ()
400filterMAllIn value n = composeN n $ Stream.filterM (\x -> return $ x <= (value + 1))
402{-# INLINE _takeOne #-}
403_takeOne :: MonadIO m => Int -> Stream m Int -> m ()
404_takeOne n = composeN n $ Stream.take 1
406{-# INLINE takeAll #-}
407takeAll :: MonadIO m => Int -> Int -> Stream m Int -> m ()
408takeAll value n = composeN n $ Stream.take (value + 1)
410{-# INLINE takeWhileTrue #-}
411takeWhileTrue :: MonadIO m => Int -> Int -> Stream m Int -> m ()
412takeWhileTrue value n = composeN n $ Stream.takeWhile (<= (value + 1))
414{-# INLINE takeWhileMTrue #-}
415takeWhileMTrue :: MonadIO m => Int -> Int -> Stream m Int -> m ()
416takeWhileMTrue value n = composeN n $ Stream.takeWhileM (return . (<= (value + 1)))
418#if !defined(USE_STREAMLY_CORE) && !defined(USE_PRELUDE)
419{-# INLINE takeInterval #-}
420takeInterval :: Double -> Int -> Stream IO Int -> IO ()
421takeInterval i n = composeN n (Stream.takeInterval i)
423-- Inspection testing is disabled for takeInterval
424-- Enable it when looking at it throughly
426-- inspect $ hasNoType 'takeInterval ''SPEC
427-- inspect $ hasNoTypeClasses 'takeInterval
428-- inspect $ 'takeInterval `hasNoType` ''D.Step
431{-# INLINE dropInterval #-}
432dropInterval :: Double -> Int -> Stream IO Int -> IO ()
433dropInterval i n = composeN n (Stream.dropInterval i)
435-- Inspection testing is disabled for dropInterval
436-- Enable it when looking at it throughly
438-- inspect $ hasNoTypeClasses 'dropInterval
439-- inspect $ 'dropInterval `hasNoType` ''D.Step
443{-# INLINE dropOne #-}
444dropOne :: MonadIO m => Int -> Stream m Int -> m ()
445dropOne n = composeN n $ Stream.drop 1
447{-# INLINE dropAll #-}
448dropAll :: MonadIO m => Int -> Int -> Stream m Int -> m ()
449dropAll value n = composeN n $ Stream.drop (value + 1)
451{-# INLINE dropWhileTrue #-}
452dropWhileTrue :: MonadIO m => Int -> Int -> Stream m Int -> m ()
453dropWhileTrue value n = composeN n $ Stream.dropWhile (<= (value + 1))
455{-# INLINE dropWhileMTrue #-}
456dropWhileMTrue :: MonadIO m => Int -> Int -> Stream m Int -> m ()
457dropWhileMTrue value n = composeN n $ Stream.dropWhileM (return . (<= (value + 1)))
459{-# INLINE dropWhileFalse #-}
460dropWhileFalse :: MonadIO m => Int -> Int -> Stream m Int -> m ()
461dropWhileFalse value n = composeN n $ Stream.dropWhile (> (value + 1))
464-- XXX Decide on the time interval
465{-# INLINE _intervalsOfSum #-}
466_intervalsOfSum :: MonadAsync m => Double -> Int -> Stream m Int -> m ()
467_intervalsOfSum i n = composeN n (Stream.intervalsOf i FL.sum)
470{-# INLINE findIndices #-}
471findIndices :: MonadIO m => Int -> Int -> Stream m Int -> m ()
472findIndices value n = composeN n $ Stream.findIndices (== (value + 1))
474{-# INLINE elemIndices #-}
475elemIndices :: MonadIO m => Int -> Int -> Stream m Int -> m ()
476elemIndices value n = composeN n $ Stream.elemIndices (value + 1)
478{-# INLINE deleteBy #-}
479deleteBy :: MonadIO m => Int -> Int -> Stream m Int -> m ()
480deleteBy value n = composeN n $ Stream.deleteBy (>=) (value + 1)
482-- uniq . uniq == uniq, composeN 2 ~ composeN 1
484uniq :: MonadIO m => Int -> Stream m Int -> m ()
485uniq n = composeN n Stream.uniq
487{-# INLINE mapMaybe #-}
488mapMaybe :: MonadIO m => Int -> Stream m Int -> m ()
497{-# INLINE mapMaybeM #-}
498mapMaybeM :: MonadAsync m => Int -> Stream m Int -> m ()
505 else return $ Just x)
507o_1_space_filtering :: Int -> [Benchmark]
508o_1_space_filtering value =
510 [ benchIOSink value "filter-even" (filterEven 1)
511 , benchIOSink value "filter-all-out" (filterAllOut value 1)
512 , benchIOSink value "filter-all-in" (filterAllIn value 1)
514 , benchIOSink value "filterM-even" (filterMEven 1)
515 , benchIOSink value "filterM-all-out" (filterMAllOut value 1)
516 , benchIOSink value "filterM-all-in" (filterMAllIn value 1)
519 , benchIOSink value "take-all" (takeAll value 1)
520 , benchIOSink value "takeWhile-true" (takeWhileTrue value 1)
521 -- , benchIOSink value "takeWhileM-true" (_takeWhileMTrue value 1)
522 , benchIOSink value "drop-one" (dropOne 1)
523 , benchIOSink value "drop-all" (dropAll value 1)
524#if !defined(USE_STREAMLY_CORE) && !defined(USE_PRELUDE)
525 , benchIOSink value "takeInterval-all" (takeInterval 10000 1)
526 , benchIOSink value "dropInterval-all" (dropInterval 10000 1)
528 , benchIOSink value "dropWhile-true" (dropWhileTrue value 1)
529 -- , benchIOSink value "dropWhileM-true" (_dropWhileMTrue value 1)
533 (dropWhileFalse value 1)
534 , benchIOSink value "deleteBy" (deleteBy value 1)
536 , benchIOSink value "uniq" (uniq 1)
539 , benchIOSink value "mapMaybe" (mapMaybe 1)
540 , benchIOSink value "mapMaybeM" (mapMaybeM 1)
542 -- Searching (stateful map and filter)
543 , benchIOSink value "findIndices" (findIndices value 1)
544 , benchIOSink value "elemIndices" (elemIndices value 1)
548o_1_space_filteringX4 :: Int -> [Benchmark]
549o_1_space_filteringX4 value =
550 [ bgroup "filteringX4"
551 [ benchIOSink value "filter-even" (filterEven 4)
552 , benchIOSink value "filter-all-out" (filterAllOut value 4)
553 , benchIOSink value "filter-all-in" (filterAllIn value 4)
555 , benchIOSink value "filterM-even" (filterMEven 4)
556 , benchIOSink value "filterM-all-out" (filterMAllOut value 4)
557 , benchIOSink value "filterM-all-in" (filterMAllIn value 4)
560 , benchIOSink value "take-all" (takeAll value 4)
561 , benchIOSink value "takeWhile-true" (takeWhileTrue value 4)
562 , benchIOSink value "takeWhileM-true" (takeWhileMTrue value 4)
563 , benchIOSink value "drop-one" (dropOne 4)
564 , benchIOSink value "drop-all" (dropAll value 4)
565 , benchIOSink value "dropWhile-true" (dropWhileTrue value 4)
566 , benchIOSink value "dropWhileM-true" (dropWhileMTrue value 4)
570 (dropWhileFalse value 4)
571 , benchIOSink value "deleteBy" (deleteBy value 4)
573 , benchIOSink value "uniq" (uniq 4)
576 , benchIOSink value "mapMaybe" (mapMaybe 4)
577 , benchIOSink value "mapMaybeM" (mapMaybeM 4)
580 , benchIOSink value "findIndices" (findIndices value 4)
581 , benchIOSink value "elemIndices" (elemIndices value 4)
585-------------------------------------------------------------------------------
586-- Size increasing transformations (insertions)
587-------------------------------------------------------------------------------
589{-# INLINE intersperse #-}
590intersperse :: MonadAsync m => Int -> Int -> Stream m Int -> m ()
591intersperse value n = composeN n $ Stream.intersperse (value + 1)
593{-# INLINE intersperseM #-}
594intersperseM :: MonadAsync m => Int -> Int -> Stream m Int -> m ()
595intersperseM value n = composeN n $ Stream.intersperseM (return $ value + 1)
597{-# INLINE insertBy #-}
598insertBy :: MonadIO m => Int -> Int -> Stream m Int -> m ()
599insertBy value n = composeN n $ Stream.insertBy compare (value + 1)
601{-# INLINE interposeSuffix #-}
602interposeSuffix :: Monad m => Int -> Int -> Stream m Int -> m ()
603interposeSuffix value n =
604 composeN n $ Stream.interposeSuffix (value + 1) Unfold.identity
606{-# INLINE intercalateSuffix #-}
607intercalateSuffix :: Monad m => Int -> Int -> Stream m Int -> m ()
608intercalateSuffix value n =
609 composeN n $ Stream.intercalateSuffix Unfold.identity (value + 1)
611o_1_space_inserting :: Int -> [Benchmark]
612o_1_space_inserting value =
614 [ benchIOSink value "intersperse" (intersperse value 1)
615 , benchIOSink value "intersperseM" (intersperseM value 1)
616 , benchIOSink value "insertBy" (insertBy value 1)
617 , benchIOSink value "interposeSuffix" (interposeSuffix value 1)
618 , benchIOSink value "intercalateSuffix" (intercalateSuffix value 1)
622o_1_space_insertingX4 :: Int -> [Benchmark]
623o_1_space_insertingX4 value =
624 [ bgroup "insertingX4"
625 [ benchIOSink value "intersperse" (intersperse value 4)
626 , benchIOSink value "insertBy" (insertBy value 4)
630-------------------------------------------------------------------------------
632-------------------------------------------------------------------------------
634{-# INLINE indexed #-}
635indexed :: MonadIO m => Int -> Stream m Int -> m ()
636indexed n = composeN n (fmap snd . Stream.indexed)
638{-# INLINE indexedR #-}
639indexedR :: MonadIO m => Int -> Int -> Stream m Int -> m ()
640indexedR value n = composeN n (fmap snd . Stream.indexedR value)
642o_1_space_indexing :: Int -> [Benchmark]
643o_1_space_indexing value =
645 [ benchIOSink value "indexed" (indexed 1)
646 , benchIOSink value "indexedR" (indexedR value 1)
650o_1_space_indexingX4 :: Int -> [Benchmark]
651o_1_space_indexingX4 value =
652 [ bgroup "indexingx4"
653 [ benchIOSink value "indexed" (indexed 4)
654 , benchIOSink value "indexedR" (indexedR value 4)
658-------------------------------------------------------------------------------
660-------------------------------------------------------------------------------
662-- In addition to gauge options, the number of elements in the stream can be
663-- passed using the --stream-size option.
665benchmarks :: String -> Int -> [Benchmark]
666benchmarks moduleName size =
667 [ bgroup (o_1_space_prefix moduleName) $ Prelude.concat
668 [ o_1_space_functor size
669 , o_1_space_mapping size
670 , o_1_space_mappingX4 size
671 , o_1_space_filtering size
672 , o_1_space_filteringX4 size
673 , o_1_space_inserting size
674 , o_1_space_insertingX4 size
675 , o_1_space_indexing size
676 , o_1_space_indexingX4 size
678 , bgroup (o_n_space_prefix moduleName) $ Prelude.concat
681 o_n_space_traversable size
684 o_n_space_mapping size
685 , o_n_space_iterated 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