1{-# LANGUAGE ConstraintKinds #-}
2{-# LANGUAGE FlexibleContexts #-}
3{-# LANGUAGE FlexibleInstances #-}
4{-# LANGUAGE LambdaCase #-}
5{-# LANGUAGE MultiParamTypeClasses #-}
6{-# LANGUAGE RankNTypes #-}
7{-# LANGUAGE UndecidableInstances #-} -- XXX
10-- Module : Streamly.Core
11-- Copyright : (c) 2017 Harendra Kumar
14-- Maintainer : harendra.kumar@gmail.com
15-- Stability : experimental
31 -- * Semigroup Style Composition
40 -- * Concurrent Stream Vars (SVars)
54import Control.Applicative (Alternative (..))
55import Control.Concurrent (ThreadId, forkIO,
56 myThreadId, threadDelay)
57import Control.Concurrent.MVar (MVar, newEmptyMVar, tryTakeMVar,
59import Control.Exception (SomeException (..))
60import qualified Control.Exception.Lifted as EL
61import Control.Monad (MonadPlus(..), mzero, when)
62import Control.Monad.Base (MonadBase (..), liftBaseDefault)
63import Control.Monad.Catch (MonadThrow, throwM)
64import Control.Monad.Error.Class (MonadError(..))
65import Control.Monad.IO.Class (MonadIO(..))
66import Control.Monad.Reader.Class (MonadReader(..))
67import Control.Monad.State.Class (MonadState(..))
68import Control.Monad.Trans.Class (MonadTrans (lift))
69import Control.Monad.Trans.Control (MonadBaseControl, liftBaseWith)
70import Data.Atomics (atomicModifyIORefCAS,
71 atomicModifyIORefCAS_)
72import Data.Concurrent.Queue.MichaelScott (LinkedQueue, newQ, pushL,
74import Data.Functor (void)
75import Data.IORef (IORef, modifyIORef, newIORef,
77import Data.Maybe (isNothing)
78import Data.Semigroup (Semigroup(..))
80import qualified Data.Set as S
81import Prelude hiding (repeat)
83------------------------------------------------------------------------------
84-- Parent child thread communication type
85------------------------------------------------------------------------------
87-- | Events that a child thread may send to a parent thread.
90 | ChildStop ThreadId (Maybe SomeException)
92------------------------------------------------------------------------------
93-- State threaded around the monad for thread management
94------------------------------------------------------------------------------
96-- | Conjunction is used for monadic/product style composition. Disjunction is
97-- used for fold/sum style composition. We need to distinguish the two types of
98-- SVars so that the scheduling of the two is independent.
99data SVarTag = Conjunction | Disjunction deriving Eq
101-- | For fairly interleaved parallel composition the sched policy is FIFO
102-- whereas for left biased parallel composition it is LIFO.
103data SVarSched = LIFO | FIFO deriving Eq
105-- | Identify the type of the SVar. Two computations using the same style can
106-- be scheduled on the same SVar.
107data SVarStyle = SVarStyle SVarTag SVarSched deriving Eq
109-- | An SVar or a Stream Var is a conduit to the output from multiple streams
110-- running concurrently and asynchronously. An SVar can be thought of as an
111-- asynchronous IO handle. We can write any number of streams to an SVar in a
112-- non-blocking manner and then read them back at any time at any pace. The
113-- SVar would run the streams asynchronously and accumulate results. An SVar
114-- may not really execute the stream completely and accumulate all the results.
115-- However, it ensures that the reader can read the results at whatever paces
116-- it wants to read. The SVar monitors and adapts to the consumer's pace.
118-- An SVar is a mini scheduler, it has an associated runqueue that holds the
119-- stream tasks to be picked and run by a pool of worker threads. It has an
120-- associated output queue where the output stream elements are placed by the
121-- worker threads. A doorBell is used by the worker threads to intimate the
122-- consumer thread about availability of new results in the output queue. More
123-- workers are added to the SVar by 'fromStreamVar' on demand if the output
124-- produced is not keeping pace with the consumer. On bounded SVars, workers
125-- block on the output queue to provide throttling of the producer when the
126-- consumer is not pulling fast enough. The number of workers may even get
127-- reduced depending on the consuming pace.
129-- New work is enqueued either at the time of creation of the SVar or as a
130-- result of executing the parallel combinators i.e. '<|' and '<|>' when the
131-- already enqueued computations get evaluated. See 'joinStreamVar2'.
134 SVar { outputQueue :: IORef [ChildEvent a]
135 , doorBell :: MVar Bool -- wakeup mechanism for outQ
136 , enqueue :: Stream m a -> IO ()
138 , runningThreads :: IORef (Set ThreadId)
139 , queueEmpty :: m Bool
140 , svarStyle :: SVarStyle
143------------------------------------------------------------------------------
145------------------------------------------------------------------------------
147-- TBD use a functor instead of the bare type a?
149-- | The type 'Stream m a' represents a monadic stream of values of type 'a'
150-- constructed using actions in monad 'm'. It uses a stop continuation and a
151-- yield continuation. You can consider it a rough equivalent of direct style
154-- data Stream m a = Stop | Yield a (Maybe (Stream m a))
156-- Our goal is to be able to represent finite as well as infinite streams and
157-- being able to compose a large number of small streams efficiently. In
158-- addition we want to compose streams in parallel, to facilitate that we
159-- maintain a local state in an SVar that is shared across and is used for
160-- synchronization of the streams being composed.
162-- Using this type, there are two ways to indicate the end of a stream, one is
163-- by calling the stop continuation and the other one is by yielding the last
164-- value along with 'Nothing' as the rest of the stream.
166-- Why do we have this redundancy? Why can't we use (a -> Stream m a -> m r) as
167-- the type of the yield continuation and always use the stop continuation to
168-- indicate the end of the stream? The reason is that when we compose a large
169-- number of short or singleton streams then using the stop continuation
170-- becomes expensive, just to know that there is no next element we have to
171-- call the continuation, introducing an indirection, it seems when using CPS
172-- GHC is not able to optimize this out as efficiently as it can be in direct
173-- style because of the function call involved. In direct style it will just be
174-- a constructor check and a memory access instead of a function call. So we
177-- data Stream m a = Stop | Yield a (Stream m a)
179-- In CPS style, when we use the 'Maybe' argument of yield to indicate the end
180-- then just like direct style we can figure out that there is no next element
181-- without a function call.
183-- Then why not get rid of the stop continuation and use only yield to indicate
184-- the end of stream? The answer is, in that case to indicate the end of the
185-- stream we would have to yield at least one element so there is no way to
186-- represent an empty stream.
188-- Whenever we make a singleton stream or in general when we build a stream
189-- strictly i.e. when we know all the elements of the stream in advance we can
190-- use the last yield to indicate the end of the stream, because we know in
191-- advance at the time of the last yield that the stream is ending. We build
192-- singleton streams in the implementation of 'pure' for Applicative and Monad,
193-- and in 'lift' for MonadTrans, in these places we use yield with 'Nothing' to
194-- indicate the end of the stream. Note that, the only advantage of Maybe is
195-- when we have to build a large number of singleton or short streams. For
196-- larger streams anyway the overhead of a separate stop continuation is not
197-- significant. This could be significant when we breakdown a large stream into
198-- its elements, process them in some way and then recompose it from the
199-- pieces. Zipping streams is one such example. Zipping with streamly is the
200-- fastest among all streaming libraries.
202-- However in a lazy computation we cannot know in advance that the stream is
203-- ending therefore we cannot use 'Maybe', we use the stop continuation in that
204-- case. For example when building a stream from a lazy container using a right
209 runStream :: forall r.
210 Maybe (SVar m a) -- local state
212 -> (a -> Maybe (Stream m a) -> m r) -- yield
216-- | A monad that can perform asynchronous/concurrent IO operations. Streams
217-- that can be composed concurrently require the underlying monad to be
219type MonadAsync m = (MonadIO m, MonadBaseControl IO m, MonadThrow m)
221cons :: a -> Maybe (Stream m a) -> Stream m a
222cons a r = Stream $ \_ _ yld -> yld a r
224repeat :: a -> Stream m a
225repeat a = let x = cons a (Just x) in x
228nil = Stream $ \_ stp _ -> stp
230------------------------------------------------------------------------------
232------------------------------------------------------------------------------
234-- Streams can be composed sequentially or in parallel; in product style
235-- compositions (monadic bind multiplies streams in a ListT fashion) or in sum
236-- style compositions like 'Semigroup', 'Monoid', 'Alternative' or variants of
239------------------------------------------------------------------------------
241------------------------------------------------------------------------------
243-- | Concatenates two streams sequentially i.e. the first stream is
244-- exhausted completely before yielding any element from the second stream.
245serial :: Stream m a -> Stream m a -> Stream m a
248 go (Stream m) = Stream $ \_ stp yld ->
249 let stop = (runStream m2) Nothing stp yld
250 yield a Nothing = yld a (Just m2)
251 yield a (Just r) = yld a (Just (go r))
252 in m Nothing stop yield
254instance Semigroup (Stream m a) where
257------------------------------------------------------------------------------
259------------------------------------------------------------------------------
261instance Monoid (Stream m a) where
265------------------------------------------------------------------------------
267------------------------------------------------------------------------------
269coserial :: Stream m a -> Stream m a -> Stream m a
270coserial m1 m2 = Stream $ \_ stp yld -> do
271 let stop = (runStream m2) Nothing stp yld
272 yield a Nothing = yld a (Just m2)
273 yield a (Just r) = yld a (Just (coserial m2 r))
274 (runStream m1) Nothing stop yield
276------------------------------------------------------------------------------
277-- Spawning threads and collecting result in streamed fashion
278------------------------------------------------------------------------------
281doFork :: MonadBaseControl IO m
283 -> (SomeException -> m ())
285doFork action exHandler =
286 EL.mask $ \restore ->
287 liftBaseWith $ \runInIO -> forkIO $ do
288 -- XXX test the exception handling
289 _ <- runInIO $ EL.catch (restore action) exHandler
290 -- XXX restore state here?
293-- XXX exception safety of all atomic/MVar operations
296send :: MonadIO m => SVar m a -> ChildEvent a -> m ()
297send sv msg = liftIO $ do
298 atomicModifyIORefCAS_ (outputQueue sv) $ \es -> msg : es
299 -- XXX need a memory barrier? The wake up must happen only after the
300 -- store has finished otherwise we can have lost wakeup problems.
301 void $ tryPutMVar (doorBell sv) True
303{-# INLINE sendStop #-}
304sendStop :: MonadIO m => SVar m a -> m ()
305sendStop sv = liftIO myThreadId >>= \tid -> send sv (ChildStop tid Nothing)
307-- Note: Left associated compositions can grow this queue to a large size
308{-# INLINE enqueueLIFO #-}
309enqueueLIFO :: IORef [Stream m a] -> Stream m a -> IO ()
310enqueueLIFO q m = atomicModifyIORefCAS_ q $ \ ms -> m : ms
312runqueueLIFO :: MonadIO m => SVar m a -> IORef [Stream m a] -> m ()
313runqueueLIFO sv q = run
320 Nothing -> sendStop sv
321 Just m -> (runStream m) (Just sv) run yield
323 sendit a = send sv (ChildYield a)
324 yield a Nothing = sendit a >> run
325 yield a (Just r) = sendit a >> (runStream r) (Just sv) run yield
327 dequeue = liftIO $ atomicModifyIORefCAS q $ \case
329 x : xs -> (xs, Just x)
331{-# INLINE enqueueFIFO #-}
332enqueueFIFO :: LinkedQueue (Stream m a) -> Stream m a -> IO ()
335runqueueFIFO :: MonadIO m => SVar m a -> LinkedQueue (Stream m a) -> m ()
336runqueueFIFO sv q = run
343 Nothing -> sendStop sv
344 Just m -> (runStream m) (Just sv) run yield
346 dequeue = liftIO $ tryPopR q
347 sendit a = send sv (ChildYield a)
348 yield a Nothing = sendit a >> run
349 yield a (Just r) = sendit a >> liftIO (enqueueFIFO q r) >> run
351-- Thread tracking is needed for two reasons:
353-- 1) Killing threads on exceptions. Threads may not be allowed to go away by
354-- themselves because they may run for significant times before going away or
355-- worse they may be stuck in IO and never go away.
357-- 2) To know when all threads are done.
359{-# NOINLINE addThread #-}
360addThread :: MonadIO m => SVar m a -> ThreadId -> m ()
362 liftIO $ modifyIORef (runningThreads sv) (S.insert tid)
364{-# INLINE delThread #-}
365delThread :: MonadIO m => SVar m a -> ThreadId -> m ()
367 liftIO $ modifyIORef (runningThreads sv) $ (\s -> S.delete tid s)
369{-# INLINE allThreadsDone #-}
370allThreadsDone :: MonadIO m => SVar m a -> m Bool
371allThreadsDone sv = liftIO $ S.null <$> readIORef (runningThreads sv)
373{-# NOINLINE handleChildException #-}
374handleChildException :: MonadIO m => SVar m a -> SomeException -> m ()
375handleChildException sv e = do
376 tid <- liftIO myThreadId
377 send sv (ChildStop tid (Just e))
379{-# NOINLINE pushWorker #-}
380pushWorker :: MonadAsync m => SVar m a -> m ()
382 doFork (runqueue sv) (handleChildException sv) >>= addThread sv
384-- XXX When the queue is LIFO we can put a limit on the number of dispatches.
385-- Also, if a worker blocks on the output queue we can decide if we want to
386-- block or make it go away entirely, depending on the number of workers and
387-- the type of the queue.
388{-# INLINE sendWorkerWait #-}
389sendWorkerWait :: MonadAsync m => SVar m a -> m ()
390sendWorkerWait sv = do
392 SVarStyle _ LIFO -> liftIO $ threadDelay 200
393 SVarStyle _ FIFO -> liftIO $ threadDelay 0
395 output <- liftIO $ readIORef (outputQueue sv)
396 when (null output) $ do
397 done <- queueEmpty sv
399 then pushWorker sv >> sendWorkerWait sv
400 else void (liftIO $ takeMVar (doorBell sv))
402-- | Pull a stream from an SVar.
403{-# NOINLINE fromStreamVar #-}
404fromStreamVar :: MonadAsync m => SVar m a -> Stream m a
405fromStreamVar sv = Stream $ \_ stp yld -> do
406 -- XXX if reading the IORef is costly we can use a flag in the SVar to
407 -- indicate we are done.
408 done <- allThreadsDone sv
412 res <- liftIO $ tryTakeMVar (doorBell sv)
413 when (isNothing res) $ sendWorkerWait sv
414 list <- liftIO $ atomicModifyIORefCAS (outputQueue sv) $ \x -> ([], x)
415 -- To avoid lock overhead we read all events at once instead of reading
416 -- one at a time. We just reverse the list to process the events in the
417 -- order they arrived. Maybe we can use a queue instead?
418 (runStream $ processEvents (reverse list)) Nothing stp yld
422 handleException e tid = do
424 -- XXX implement kill async exception handling
425 -- liftIO $ readIORef (runningThreads sv) >>= mapM_ killThread
428 {-# INLINE processEvents #-}
429 processEvents [] = Stream $ \_ stp yld -> do
430 done <- allThreadsDone sv
432 then (runStream (fromStreamVar sv)) Nothing stp yld
435 processEvents (ev : es) = Stream $ \_ stp yld -> do
436 let continue = (runStream (processEvents es)) Nothing stp yld
437 yield a = yld a (Just (processEvents es))
440 ChildYield a -> yield a
443 Nothing -> delThread sv tid >> continue
444 Just ex -> handleException ex tid
446getFifoSVar :: MonadIO m => SVarStyle -> IO (SVar m a)
447getFifoSVar ctype = do
449 outQMv <- newEmptyMVar
450 running <- newIORef S.empty
453 SVar { outputQueue = outQ
455 , runningThreads = running
456 , runqueue = runqueueFIFO sv q
458 , queueEmpty = liftIO $ nullQ q
463getLifoSVar :: MonadIO m => SVarStyle -> IO (SVar m a)
464getLifoSVar ctype = do
466 outQMv <- newEmptyMVar
467 running <- newIORef S.empty
469 let checkEmpty = null <$> liftIO (readIORef q)
471 SVar { outputQueue = outQ
473 , runningThreads = running
474 , runqueue = runqueueLIFO sv q
475 , enqueue = enqueueLIFO q
476 , queueEmpty = checkEmpty
481-- | Create a new empty SVar.
482newEmptySVar :: MonadAsync m => SVarStyle -> m (SVar m a)
483newEmptySVar style = do
486 SVarStyle _ FIFO -> getFifoSVar style
487 SVarStyle _ LIFO -> getLifoSVar style
489-- | Create a new SVar and enqueue one stream computation on it.
490newStreamVar1 :: MonadAsync m => SVarStyle -> Stream m a -> m (SVar m a)
491newStreamVar1 style m = do
492 sv <- newEmptySVar style
493 -- Note: We must have all the work on the queue before sending the
494 -- pushworker, otherwise the pushworker may exit before we even get a
496 liftIO $ (enqueue sv) m
500-- | Create a new SVar and enqueue two stream computations on it.
501newStreamVar2 :: MonadAsync m
502 => SVarStyle -> Stream m a -> Stream m a -> m (SVar m a)
503newStreamVar2 style m1 m2 = do
504 -- Note: We must have all the work on the queue before sending the
505 -- pushworker, otherwise the pushworker may exit before we even get a
509 SVarStyle _ FIFO -> do
510 c <- getFifoSVar style
511 (enqueue c) m1 >> (enqueue c) m2
513 SVarStyle _ LIFO -> do
514 c <- getLifoSVar style
515 (enqueue c) m2 >> (enqueue c) m1
520-- | Write a stream to an 'SVar' in a non-blocking manner. The stream can then
521-- be read back from the SVar using 'fromSVar'.
522toStreamVar :: MonadAsync m => SVar m a -> Stream m a -> m ()
524 liftIO $ (enqueue sv) m
525 done <- allThreadsDone sv
526 -- XXX there may be a race here unless we are running in the consumer
527 -- thread. This is safe only when called from the consumer thread or when
528 -- no consumer is present.
529 when done $ pushWorker sv
531------------------------------------------------------------------------------
532-- Running streams concurrently
533------------------------------------------------------------------------------
535-- Concurrency rate control. Our objective is to create more threads on demand
536-- if the consumer is running faster than us. As soon as we encounter an
537-- Alternative composition we create a push pull pair of threads. We use a
538-- channel for communication between the consumer pulling from the channel and
539-- the producer who is pushing to the channel. The producer creates more threads
540-- if no output is seen on the channel, that is the consumer is running faster.
541-- However this mechanism can be problematic if the initial production latency
542-- is high, we may end up creating too many threads. So we need some way to
543-- monitor and use the latency as well.
545-- TBD We may run computations at the lower level of the composition tree
546-- serially even if they are composed using a parallel combinator. We can use
547-- <> in place of <| and <=> in place of <|>. If we find that a parallel
548-- channel immediately above a computation becomes empty we can switch to
549-- parallelizing the computation. For that we can use a state flag to fork the
550-- rest of the computation at any point of time inside the Monad bind operation
551-- if the consumer is running at a faster speed.
553-- TBD the alternative composition allows us to dispatch a chunkSize of only 1.
554-- If we have to dispatch in arbitrary chunksizes we will need to compose the
555-- parallel actions using a data constructor (Free Alternative) instead so that
556-- we can divide it in chunks of arbitrary size before dispatching. If the stream
557-- is composed of hierarchically composed grains of different sizes then we can
558-- always switch to a desired granularity depending on the consumer speed.
560-- TBD for pure work (when we are not in the IO monad) we can divide it into
561-- just the number of CPUs.
563{-# NOINLINE withNewSVar2 #-}
564withNewSVar2 :: MonadAsync m
565 => SVarStyle -> Stream m a -> Stream m a -> Stream m a
566withNewSVar2 style m1 m2 = Stream $ \_ stp yld -> do
567 sv <- newStreamVar2 style m1 m2
568 (runStream (fromStreamVar sv)) Nothing stp yld
570-- | Join two computations on the currently running 'SVar' queue for concurrent
571-- execution. The 'SVarStyle' required by the current composition context is
572-- passed as one of the parameters. If the style does not match with the style
573-- of the current 'SVar' we create a new 'SVar' and schedule the computations
574-- on that. The newly created SVar joins as one of the computations on the
575-- current SVar queue.
577-- When we are using parallel composition, an SVar is passed around as a state
578-- variable. We try to schedule a new parallel computation on the SVar passed
579-- to us. The first time, when no SVar exists, a new SVar is created.
580-- Subsequently, 'joinStreamVar2' may get called when a computation already
581-- scheduled on the SVar is further evaluated. For example, when (a \<|> b) is
582-- evaluated it calls a 'joinStreamVar2' to put 'a' and 'b' on the current scheduler
583-- queue. However, if the scheduling and composition style of the new
584-- computation being scheduled is different than the style of the current SVar,
585-- then we create a new SVar and schedule it on that.
589-- * (x \<|> y) \<|> (t \<|> u) -- all of them get scheduled on the same SVar
590-- * (x \<|> y) \<|> (t \<| u) -- @t@ and @u@ get scheduled on a new child SVar
591-- because of the scheduling policy change.
592-- * if we 'adapt' a stream of type 'AsyncT' to a stream of type
593-- 'ParallelT', we create a new SVar at the transitioning bind.
594-- * When the stream is switching from disjunctive composition to conjunctive
595-- composition and vice-versa we create a new SVar to isolate the scheduling
598{-# INLINE joinStreamVar2 #-}
599joinStreamVar2 :: MonadAsync m
600 => SVarStyle -> Stream m a -> Stream m a -> Stream m a
601joinStreamVar2 style m1 m2 = Stream $ \st stp yld ->
603 Just sv | svarStyle sv == style ->
604 liftIO ((enqueue sv) m2) >> (runStream m1) st stp yld
605 _ -> (runStream (withNewSVar2 style m1 m2)) Nothing stp yld
607------------------------------------------------------------------------------
608-- Semigroup and Monoid style compositions for parallel actions
609------------------------------------------------------------------------------
611{-# INLINE coparallel #-}
612coparallel :: MonadAsync m => Stream m a -> Stream m a -> Stream m a
613coparallel = joinStreamVar2 (SVarStyle Disjunction LIFO)
615{-# INLINE parallel #-}
616parallel :: MonadAsync m => Stream m a -> Stream m a -> Stream m a
617parallel = joinStreamVar2 (SVarStyle Disjunction FIFO)
619-------------------------------------------------------------------------------
620-- Instances (only used for deriving newtype instances)
621-------------------------------------------------------------------------------
623-- Stream type is not exposed, these instances are only for deriving instances
624-- for the newtype wrappers based on Stream.
626-- Dummy Instances, defined to enable the definition of other instances that
627-- require a Monad constraint. Must be defined by the newtypes.
629instance Monad m => Functor (Stream m) where
632instance Monad m => Applicative (Stream m) where
636instance Monad m => Monad (Stream m) where
640------------------------------------------------------------------------------
641-- Alternative & MonadPlus
642------------------------------------------------------------------------------
644alt :: Stream m a -> Stream m a -> Stream m a
645alt m1 m2 = Stream $ \_ stp yld ->
646 let stop = runStream m2 Nothing stp yld
648 in runStream m1 Nothing stop yield
650instance MonadAsync m => Alternative (Stream m) where
654instance MonadAsync m => MonadPlus (Stream m) where
658-------------------------------------------------------------------------------
660-------------------------------------------------------------------------------
662instance MonadTrans Stream where
663 lift mx = Stream $ \_ _ yld -> mx >>= (\a -> (yld a Nothing))
665instance (MonadBase b m, Monad m) => MonadBase b (Stream m) where
666 liftBase = liftBaseDefault
668------------------------------------------------------------------------------
669-- Standard transformer instances
670------------------------------------------------------------------------------
672instance MonadIO m => MonadIO (Stream m) where
673 liftIO = lift . liftIO
675instance MonadThrow m => MonadThrow (Stream m) where
676 throwM = lift . throwM
678-- XXX handle and test cross thread state transfer
679instance MonadError e m => MonadError e (Stream m) where
680 throwError = lift . throwError
681 catchError m h = Stream $ \st stp yld ->
682 let handle r = r `catchError` \e -> (runStream (h e)) st stp yld
683 yield a Nothing = yld a Nothing
684 yield a (Just r) = yld a (Just (catchError r h))
685 in handle $ (runStream m) st stp yield
687instance MonadReader r m => MonadReader r (Stream m) where
689 local f m = Stream $ \st stp yld ->
690 let yield a Nothing = local f $ yld a Nothing
691 yield a (Just r) = local f $ yld a (Just (local f r))
692 in (runStream m) st (local f stp) yield
694instance MonadState s m => MonadState s (Stream m) where
697 state k = lift (state k)
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